Compare commits

...

179 Commits

Author SHA1 Message Date
Angela P Wen
1314a36ba4 v1.6.5 (#1314)
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2022-04-25 09:42:44 -07:00
shati-patel
2b8b621298 10% nicer way of wrapping code lines 😄
+ update test data to contain a single-line example
2022-04-25 12:42:10 +01:00
shati-patel
aed4c9fc58 MRVA: Make markdown code snippets look nicer
Remove some extraneous newlines
2022-04-25 12:42:10 +01:00
shati-patel
1a03c0e4ac Attempt to fix tests 2022-04-22 14:52:15 +01:00
shati-patel
a8c54b7640 MRVA: Don't display excessive error/warning pop-ups if user doesn't select a repo list 2022-04-22 14:52:15 +01:00
shati-patel
9bb60c9474 Link to workflow + fix incorrect comment 2022-04-22 13:01:54 +01:00
shati-patel
0b2ce7a071 MRVA: Display available results, even if some jobs are cancelled 2022-04-22 13:01:54 +01:00
Angela P Wen
dac7881ca3 Bug fix for show eval log and show eval log summary commands in query history view (#1304) 2022-04-21 08:11:58 -07:00
Charis Kyriakou
31bd927959 Fix max-width for code paths (#1309) 2022-04-21 13:12:40 +00:00
shati-patel
908a862dd1 Tidy up test 2022-04-21 09:57:23 +01:00
shati-patel
6676ba99d0 Add initial test data for problem query 2022-04-21 09:57:23 +01:00
shati-patel
6d3c6e598f Change folder structure to have separate folders for path-problem and problem queries 2022-04-21 09:57:23 +01:00
shati-patel
e1a10fc827 Markdown results: Highlight snippets with "<strong>" 2022-04-21 09:17:31 +01:00
shati-patel
a74dfea08b Use HTML code blocks
This is so that we can highlight code snippets using `<strong>` tags
2022-04-20 10:32:24 +01:00
Andrew Eisenberg
44ff380c86 Merge pull request #1295 from github/aeisenberg/result-log
Add better error messages for partial failing variant analysis
2022-04-19 17:55:31 -07:00
Andrew Eisenberg
0a41713253 Add new test
And rename test file.
2022-04-19 17:45:17 -07:00
Andrew Eisenberg
f5a5675da4 Merge pull request #1298 from github/aeisenberg/no-results-mixing
Avoid loading wrong results into an open window
2022-04-19 16:02:14 -07:00
Andrew Eisenberg
7a8cf55090 Merge pull request #1294 from github/aeisenberg/db-name-github
Display nicer names for github-downloaded databases
2022-04-19 16:01:04 -07:00
Andrew Eisenberg
7932de3b7d Merge pull request #1299 from github/aeisenberg/remove-jsonc 2022-04-18 09:06:13 -07:00
Andrew Eisenberg
c8ba967a54 Remove jsonc dependency
This dependency was only used to parse package.json and
this can be just as easily parsed by regular JSON object.

jsonc can also parse JSON with comments, but there are no
comments in package.json.
2022-04-14 15:45:24 -07:00
Andrew Eisenberg
f5d2f0e0ca Merge pull request #1263 from github/dependabot/npm_and_yarn/extensions/ql-vscode/zip-a-folder-1.1.3
Bump zip-a-folder from 0.0.12 to 1.1.3 in /extensions/ql-vscode
2022-04-14 15:36:44 -07:00
Andrew Eisenberg
2c7e2f4b7f Avoid loading wrong results into an open window
This fixes a bug where an open results view will accumulate results from
other queries who have their results downloaded while this view is open.

The fix is to ensure that the results view for the query is open when
some results are downloaded.
2022-04-14 14:54:42 -07:00
dependabot[bot]
ee3ebe687b Bump zip-a-folder from 0.0.12 to 1.1.3 in /extensions/ql-vscode
Bumps [zip-a-folder](https://github.com/maugenst/zip-a-folder) from 0.0.12 to 1.1.3.
- [Release notes](https://github.com/maugenst/zip-a-folder/releases)
- [Commits](https://github.com/maugenst/zip-a-folder/commits)

---
updated-dependencies:
- dependency-name: zip-a-folder
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-14 21:09:55 +00:00
Andrew Eisenberg
77024f0757 Merge pull request #1297 from github/dependabot/npm_and_yarn/extensions/ql-vscode/async-2.6.4
Bump async from 2.6.3 to 2.6.4 in /extensions/ql-vscode
2022-04-14 14:08:46 -07:00
Andrew Eisenberg
c0e39886eb Add unit tests for remote queries in logs
Also, change text slightly.
2022-04-14 13:39:36 -07:00
dependabot[bot]
6339e7897d Bump async from 2.6.3 to 2.6.4 in /extensions/ql-vscode
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-14 19:41:37 +00:00
Andrew Eisenberg
783a8a8772 Merge pull request #1290 from github/aeisenberg/remote-history-label-editing
Allow remote query items to have their labels edited
2022-04-14 12:40:50 -07:00
Andrew Eisenberg
8f2d865999 Display nicer names for github-downloaded databases
This will now name databases downloaded from github based on their nwo.

Also, this adds a new button to suggest downloading from github in an
empty databases view.
2022-04-14 12:36:43 -07:00
Andrew Eisenberg
d6d0825926 Merge branch 'main' into aeisenberg/remote-history-label-editing 2022-04-14 12:30:08 -07:00
Andrew Eisenberg
37de2e7f52 Add better error messages for partial failing variant analysis
Two scenarios handled:

1. no database for existing repo
2. repo does not exits (or no access rights for current user)

In either case, an error message is sent to the logs, with a notificaiton
in a popup.
2022-04-13 16:32:13 -07:00
Andrew Eisenberg
800c9e0c93 Remove deprecated comments
Also, change interpolation of result count. For Remote queries, this
value will be empty. For local queries, use the label `X results`, where
`X` is the number of results for this query.
2022-04-13 14:08:44 -07:00
shati-patel
a1bc7eb4d5 Capitalize! 2022-04-13 17:00:17 +01:00
shati-patel
8ff45d2aee Split handling of highlighted code lines into helper function 2022-04-13 17:00:17 +01:00
Andrew Eisenberg
8ec19777b5 Merge pull request #1291 from github/aeisenberg/handle-remote-cancel
Handle cancelling of remote queries
2022-04-13 06:59:14 -07:00
Andrew Eisenberg
3e388fedeb Merge pull request #1292 from github/aeisenberg/rename-remote-queries
Rename remote queries -> variant analysis
2022-04-13 06:41:33 -07:00
Andrew Eisenberg
83ffba2f08 Rename remote queries -> variant analysis
In some user facing text.
2022-04-12 13:16:44 -07:00
Andrew Eisenberg
f1c4fef8ba Allow remote query items to have their labels edited
The labels for remote query items are interpolated using the same
strategy as local queries with two caveats:

1. There is no easy way to get the result count without reading files,
   so, this value is kept empty.
2. There is no database name for remote queries. Instead, use the
   nwo of the controller repo.

Also, adds tests for the history item label provider.
2022-04-12 12:37:31 -07:00
Andrew Eisenberg
eec506a209 Introduce history-item-label-provider
The label provider is the instance that performs the logic for
generating labels for history items, using string interpolation when
necessary.

This commit creates the label provider and uses it with local queries.
Remote queries will be changed in the next commit.
2022-04-12 12:35:01 -07:00
Andrew Eisenberg
2ca0060c6a Remove references to 'remote query' in user-facing text
(Only in recently introduced locations. More work still needs to be
done.)

Also:

- Change error to info
- Create credentials directly, don't use a callback.
2022-04-12 12:20:39 -07:00
shati-patel
8b2d79a7f7 Formatting fixes and code tidy-up 2022-04-12 12:32:45 +01:00
shati-patel
c4db8b6d4b Create markdown summary file for sharing MRVA results 2022-04-12 12:32:45 +01:00
Andrew Eisenberg
61d4305593 Handle cancelling of remote queries
This change issues a cancel request when the user clicks on "cancel" for
a remote query.

The cancel can take quite a while to complete, so a message is popped up
to let the user know.
2022-04-11 19:05:00 -07:00
Andrew Eisenberg
542e1d24aa Allow remote query items to have their labels edited
The labels for remote query items are interpolated using the same
strategy as local queries with two caveats:

1. There is no easy way to get the result count without reading files,
   so, this value is kept empty.
2. There is no database name for remote queries. Instead, use the 
   nwo of the controller repo.
2022-04-11 14:20:57 -07:00
shati-patel
47ec074cfb Tidy-up and address review comments 2022-04-11 15:24:08 +01:00
shati-patel
e44835e795 Make line endings consistent? 2022-04-11 15:24:08 +01:00
shati-patel
2e28146a58 Create markdown files for sharing results 2022-04-11 15:24:08 +01:00
Andrew Eisenberg
85e051a76d Merge pull request #1285 from github/aeisenberg/reenable-openvsx
Reenable publishing to open-vsx
2022-04-08 09:40:40 -07:00
Andrew Eisenberg
7027a61e63 Update changelog 2022-04-07 14:01:28 -07:00
Andrew Eisenberg
e8c5b27d92 Reenable publishing to open-vsx
The extension ms-vscode.test-adapter-converter is now available on
open-vsx, but under a different name.

Fixes https://github.com/github/vscode-codeql/issues/1085

I have verified that I can publish and install the extension by
manually publishing v1.6.4.
2022-04-07 13:58:16 -07:00
Andrew Eisenberg
a3deec7875 Merge pull request #1280 from febuiles/patch-2
Update dependency-review.yml
2022-04-07 08:39:47 -07:00
Andrew Eisenberg
6282a462c8 Merge pull request #1283 from github/bump-cli 2022-04-07 07:44:29 -07:00
Shati Patel
dac5952e96 Bump CLI version used in integration tests 2022-04-07 15:30:41 +01:00
Federico Builes
ada6fcb908 Try using workflow_dispatch. 2022-04-07 13:36:57 +02:00
Andrew Eisenberg
8d2f902420 Merge pull request #1282 from github/version/bump-to-v1.6.5
Bump version to v1.6.5
2022-04-07 02:11:28 -07:00
aeisenberg
fc3fe7a81e Bump version to v1.6.5 2022-04-06 22:39:04 +00:00
Andrew Eisenberg
426cc95e9f Merge pull request #1281 from github/v1.6.4
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
v1.6.4
2022-04-06 15:36:05 -07:00
Andrew Eisenberg
9e40043fe0 v1.6.4 2022-04-06 14:54:56 -07:00
Federico Builes
14608fe5f7 Update dependency-review.yml 2022-04-06 15:17:40 +02:00
Charis Kyriakou
22ed090685 Add support for system defined repository lists (#1271) 2022-04-06 09:05:22 +01:00
Charis Kyriakou
2ca4097daf Move remote queries test files to be under remote-queries dir (#1270) 2022-04-05 08:40:10 +01:00
github-actions[bot]
f1d16015bf Bump version to v1.6.4 (#1278)
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-04-04 23:44:55 +00:00
Andrew Eisenberg
9a81ad05ed Merge pull request #1277 from github/v1.6.3
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
v1.6.3
2022-04-04 12:39:27 -07:00
Andrew Eisenberg
76e983d19c v1.6.3
Also adds a step in our release process to manually test the new
extension build.
2022-04-04 12:30:41 -07:00
Andrew Eisenberg
a3015c0fa3 Merge pull request #1276 from github/aeisenberg/dev-dependencies
Move source-map-support to dependencies
2022-04-04 12:27:09 -07:00
Andrew Eisenberg
88d0bda049 Move source-map-support to dependencies 2022-04-04 12:15:57 -07:00
Andrew Eisenberg
d2ec54e89e Merge pull request #1273 from github/version/bump-to-v1.6.3
Bump version to v1.6.3
2022-04-04 09:10:52 -07:00
edoardopirovano
4559c5a38d Bump version to v1.6.3 2022-04-04 15:28:36 +00:00
Edoardo Pirovano
16bd106abc v1.6.2
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2022-04-04 08:25:23 -07:00
Charis Kyriakou
e5dcec8d8e Move repository selection code to own module (#1269) 2022-04-04 11:03:53 +01:00
Charis Kyriakou
ad3565d3ad Use the repos defined in the query result instead of the query (#1268) 2022-04-04 11:03:05 +01:00
Andrew Eisenberg
5fe12ecd74 Merge pull request #1265 from github/aeisenberg/pat-instructions-update
Move vscode marketplace pat isntructions to internal docs
2022-03-31 12:24:51 -07:00
Andrew Eisenberg
318214642f Merge pull request #1249 from github/dependabot/npm_and_yarn/extensions/ql-vscode/ts-node-10.7.0
Bump ts-node from 8.10.2 to 10.7.0 in /extensions/ql-vscode
2022-03-31 12:15:43 -07:00
Andrew Eisenberg
227fe3ee6b Fix typo
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2022-03-31 12:12:57 -07:00
dependabot[bot]
978a82dd1a Bump ts-node from 8.10.2 to 10.7.0 in /extensions/ql-vscode
Bumps [ts-node](https://github.com/TypeStrong/ts-node) from 8.10.2 to 10.7.0.
- [Release notes](https://github.com/TypeStrong/ts-node/releases)
- [Commits](https://github.com/TypeStrong/ts-node/compare/v8.10.2...v10.7.0)

---
updated-dependencies:
- dependency-name: ts-node
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-31 18:51:38 +00:00
Andrew Eisenberg
04f72a7da9 Merge pull request #1260 from github/aeisenberg/source-map-support
Add source map support and clean test dependencies
2022-03-31 11:42:22 -07:00
Andrew Eisenberg
a0954a1dc0 Move vscode marketplace pat isntructions to internal docs 2022-03-31 10:22:33 -07:00
Angela P Wen
cc1bf74370 Print end-of-query summary logs to Query Server Console (#1264)
* Log new end summary file to query server console

* Change supported CLI version to 2.9.0
2022-03-31 16:26:13 +00:00
Andrew Eisenberg
2f7908773a Merge pull request #1253 from github/aeisenberg/codeSnippet-handling 2022-03-31 07:19:44 -07:00
Andrew Eisenberg
0efd02979e Merge pull request #1242 from github/aeisenberg/analysis-results-on-restart 2022-03-31 07:19:02 -07:00
shati-patel
bd9776c4b7 Variant analysis: Remove handling of invalid repos
This is now done automatically on the API side
2022-03-31 15:15:16 +01:00
Andrew Eisenberg
35e9da83ec Add source map support and clean test dependencies
1. Source map support means that stack traces will point to the *.ts
   file instead of the generated *.js file
2. Cleaning test dependencies means moving all mocha and chai
   registration into the respective index files and removing unnecessary
   imports.
2022-03-30 12:30:18 -07:00
Andrew Eisenberg
4f5ca0bca9 Merge pull request #1261 from github/aeisenberg/dependabot-changes
Run dependabot updates weekly
2022-03-30 12:05:06 -07:00
Andrew Eisenberg
43f314b2b5 Change missing code snippet handling in UI
Also, simplify sarif tests.
2022-03-30 12:02:19 -07:00
Andrew Eisenberg
4bdf579ce2 Merge branch 'aeisenberg/analysis-results-on-restart' into aeisenberg/codeSnippet-handling 2022-03-30 11:57:24 -07:00
Andrew Eisenberg
aba3039eef Merge pull request #1257 from github/dependabot/npm_and_yarn/extensions/ql-vscode/sinon-13.0.1
Bump sinon from 9.0.2 to 13.0.1 in /extensions/ql-vscode
2022-03-30 11:48:11 -07:00
Andrew Eisenberg
bbff791c65 Merge pull request #1258 from github/dependabot/npm_and_yarn/extensions/ql-vscode/gulp-sourcemaps-3.0.0
Bump gulp-sourcemaps from 2.6.5 to 3.0.0 in /extensions/ql-vscode
2022-03-30 11:47:20 -07:00
Andrew Eisenberg
1ed50b3081 Run dependabot updates weekly
Daily is too noisy.
2022-03-30 11:45:39 -07:00
Andrew Eisenberg
67336a24e7 Simplify checking for downloaded analyses
And some renaming.
2022-03-30 11:30:10 -07:00
Andrew Eisenberg
48174c327d Merge pull request #1246 from github/aeisenberg/repo-filter
Add repositories search box
2022-03-30 11:14:27 -07:00
Andrew Eisenberg
43f2539b42 Remove unused css class 2022-03-30 10:54:14 -07:00
dependabot[bot]
462a7a722a Bump gulp-sourcemaps from 2.6.5 to 3.0.0 in /extensions/ql-vscode
Bumps [gulp-sourcemaps](https://github.com/gulp-sourcemaps/gulp-sourcemaps) from 2.6.5 to 3.0.0.
- [Release notes](https://github.com/gulp-sourcemaps/gulp-sourcemaps/releases)
- [Commits](https://github.com/gulp-sourcemaps/gulp-sourcemaps/compare/v2.6.5...v3.0.0)

---
updated-dependencies:
- dependency-name: gulp-sourcemaps
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-30 13:04:47 +00:00
dependabot[bot]
4101bb252e Bump sinon from 9.0.2 to 13.0.1 in /extensions/ql-vscode
Bumps [sinon](https://github.com/sinonjs/sinon) from 9.0.2 to 13.0.1.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v9.0.2...v13.0.1)

---
updated-dependencies:
- dependency-name: sinon
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-30 13:04:31 +00:00
Shati Patel
4ff4e4827e Bump CLI version in integration tests 2022-03-30 12:03:16 +01:00
Andrew Eisenberg
8daa92ad49 Merge branch 'main' into aeisenberg/analysis-results-on-restart 2022-03-29 16:04:35 -07:00
Andrew Eisenberg
371e83bff9 Merge branch 'aeisenberg/analysis-results-on-restart' into aeisenberg/codeSnippet-handling 2022-03-29 15:30:08 -07:00
Andrew Eisenberg
6fa0227a1e Merge branch 'main' into aeisenberg/codeSnippet-handling 2022-03-29 15:08:17 -07:00
Andrew Eisenberg
c38e4ce265 Merge pull request #1252 from github/aeisenberg/settings
Prevent cli path from being synced across remote instances
2022-03-29 14:23:51 -07:00
Andrew Eisenberg
de06ed148d Merge branch 'main' into aeisenberg/analysis-results-on-restart 2022-03-29 14:21:15 -07:00
Andrew Eisenberg
21bcd62ba8 Merge pull request #1239 from github/dependabot/npm_and_yarn/extensions/ql-vscode/types/gulp-replace-1.1.0
Bump @types/gulp-replace from 0.0.31 to 1.1.0 in /extensions/ql-vscode
2022-03-29 14:21:06 -07:00
Andrew Eisenberg
76c034f79a Merge branch 'main' into aeisenberg/repo-filter 2022-03-29 14:15:31 -07:00
Andrew Eisenberg
d8d394ce40 Use new version of gulp-replace 2022-03-29 14:09:01 -07:00
Andrew Eisenberg
213f4ce92f Merge branch 'main' into aeisenberg/settings 2022-03-29 13:54:41 -07:00
Andrew Eisenberg
2d1726763f Merge pull request #1254 from github/aeisenberg/fix-main
Fix duplication import
2022-03-29 13:54:00 -07:00
Andrew Eisenberg
abfd9b3cbd Fix duplication import 2022-03-29 13:21:08 -07:00
Andrew Eisenberg
6114f6a7fd Merge branch 'main' into aeisenberg/analysis-results-on-restart 2022-03-29 13:18:13 -07:00
Andrew Eisenberg
61e674e9f6 Allow for undefined codeSnippets
This reverts commit 006cc8c52a.
2022-03-29 13:10:28 -07:00
Andrew Eisenberg
006cc8c52a Undo sarif-processing change
Will move to a different PR.
2022-03-29 13:07:56 -07:00
Andrew Eisenberg
ffe7fdcb46 Rename methods and address comments 2022-03-29 13:04:00 -07:00
Andrew Eisenberg
49cceffe1b Merge pull request #1235 from github/aeisenberg/history-sort
Add query history sorting for remote queries
2022-03-29 11:13:35 -07:00
Andrew Eisenberg
011782395a Merge pull request #1250 from github/dependabot/npm_and_yarn/extensions/ql-vscode/types/webpack-5.28.0
Bump @types/webpack from 4.41.21 to 5.28.0 in /extensions/ql-vscode
2022-03-29 11:13:00 -07:00
Andrew Eisenberg
558009543f Update changelog 2022-03-29 11:11:44 -07:00
Andrew Eisenberg
aaef5bde2c Prevent cli path from being synced across remote instances
This will fix a problem where settings sync will cause the cli not
to be found on codespaces.
2022-03-29 11:08:31 -07:00
Andrew Eisenberg
f52f595d56 Add max-width for remote queries results page 2022-03-29 11:05:22 -07:00
dependabot[bot]
50196d8430 Bump @types/webpack from 4.41.21 to 5.28.0 in /extensions/ql-vscode
Bumps [@types/webpack](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/webpack) from 4.41.21 to 5.28.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/webpack)

---
updated-dependencies:
- dependency-name: "@types/webpack"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-29 17:17:18 +00:00
Andrew Eisenberg
2ecfbfbb42 Merge pull request #1244 from github/aeisenberg/webpack-watch
Add webpack watch gulp task
2022-03-29 10:16:18 -07:00
Andrew Eisenberg
9508dffe6d Merge pull request #1236 from github/dependabot/npm_and_yarn/extensions/ql-vscode/fs-extra-10.0.1
Bump fs-extra from 9.0.1 to 10.0.1 in /extensions/ql-vscode
2022-03-29 10:15:13 -07:00
Andrew Eisenberg
b4a72bbcab Merge pull request #1238 from github/dependabot/npm_and_yarn/extensions/ql-vscode/through2-4.0.2
Bump through2 from 3.0.2 to 4.0.2 in /extensions/ql-vscode
2022-03-29 10:08:32 -07:00
Andrew Eisenberg
4ceaaf92cc Merge pull request #1237 from github/dependabot/npm_and_yarn/extensions/ql-vscode/vsce-2.7.0
Bump vsce from 1.88.0 to 2.7.0 in /extensions/ql-vscode
2022-03-29 10:07:04 -07:00
Andrew Eisenberg
ef28c9531b Update extensions/ql-vscode/gulpfile.ts/webpack.ts 2022-03-29 08:50:42 -07:00
Shati Patel
c86c602e39 Allow GitHub URL as well as NWO (#1241) 2022-03-29 12:45:46 +01:00
Angela P Wen
3bee2905e5 Gate show eval log and summary commands behind CLI v2.8.4 (#1243) 2022-03-29 05:30:31 -04:00
Edoardo Pirovano
9ac8a15cd5 Address review comments from @aeisenberg 2022-03-29 05:30:31 -04:00
Edoardo Pirovano
81b8104064 Expose per-query structured evaluator logs 2022-03-29 05:30:31 -04:00
Andrew Eisenberg
65f58b1f98 Add repositories search box
A simple, webview-only search box for filtering repositories from
the remote queries results view.
2022-03-28 17:01:11 -07:00
Andrew Eisenberg
7e872aa6d6 Add webpack watch gulp task
Now, when running `npm run watch`, both the regular tsc command
and the webpack command will be run in watch mode.

The raw gulp tasks are now:

- `gulp watchView` to watch webpack compilation.
- `gulp watchCss` to watch for css changes.
- `gulp compileView` to compile the webpack once and exit.

However, stats are no longer being printed out. Not sure why.
2022-03-28 15:43:35 -07:00
Andrew Eisenberg
0383a91a68 Display proper download state in remote results view
Before displaying any results for a remote query, ensure that all
downloaded results are in memory. This ensures the proper download icon
is displayed alongside each NWO.
2022-03-28 12:38:13 -07:00
Andrew Eisenberg
bb6ebe5750 Handle query directory not existing
Also, fix some changelog notes.
2022-03-28 10:55:02 -07:00
Andrew Eisenberg
71aa3d145f Update changelog 2022-03-25 14:30:01 -07:00
dependabot[bot]
2f1f80029b Bump @types/gulp-replace from 0.0.31 to 1.1.0 in /extensions/ql-vscode
Bumps [@types/gulp-replace](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/gulp-replace) from 0.0.31 to 1.1.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/gulp-replace)

---
updated-dependencies:
- dependency-name: "@types/gulp-replace"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-25 21:28:00 +00:00
dependabot[bot]
ad18cfa284 Bump through2 from 3.0.2 to 4.0.2 in /extensions/ql-vscode
Bumps [through2](https://github.com/rvagg/through2) from 3.0.2 to 4.0.2.
- [Release notes](https://github.com/rvagg/through2/releases)
- [Commits](https://github.com/rvagg/through2/compare/v3.0.2...v4.0.2)

---
updated-dependencies:
- dependency-name: through2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-25 21:27:45 +00:00
dependabot[bot]
92ed1c6ac9 Bump vsce from 1.88.0 to 2.7.0 in /extensions/ql-vscode
Bumps [vsce](https://github.com/Microsoft/vsce) from 1.88.0 to 2.7.0.
- [Release notes](https://github.com/Microsoft/vsce/releases)
- [Commits](https://github.com/Microsoft/vsce/compare/v1.88.0...v2.7.0)

---
updated-dependencies:
- dependency-name: vsce
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-25 21:27:25 +00:00
dependabot[bot]
e71e04a8f1 Bump fs-extra from 9.0.1 to 10.0.1 in /extensions/ql-vscode
Bumps [fs-extra](https://github.com/jprichardson/node-fs-extra) from 9.0.1 to 10.0.1.
- [Release notes](https://github.com/jprichardson/node-fs-extra/releases)
- [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jprichardson/node-fs-extra/compare/9.0.1...10.0.1)

---
updated-dependencies:
- dependency-name: fs-extra
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-25 21:27:03 +00:00
Andrew Eisenberg
ef127c279c Merge pull request #1233 from github/aeisenberg/dependabot
Add dependabot configuration
2022-03-25 14:26:02 -07:00
Andrew Eisenberg
4afac5fa4d Add query history sorting for remote queries
Also, fix two smaller issues:

- Ensure the `Open Query Directory` command opens inside the specified
  directory.
- Ensure label changes are saved across restarts.
2022-03-25 14:25:07 -07:00
Andrew Eisenberg
29ae97aa82 Add actions to dependabot config 2022-03-25 13:18:46 -07:00
Andrew Eisenberg
9319d7e8ef Add dependabot configuration 2022-03-25 12:21:10 -07:00
dependabot[bot]
689db3713b Bump minimist from 1.2.5 to 1.2.6 in /extensions/ql-vscode
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-25 18:48:40 +00:00
Andrew Eisenberg
0b9fcb884b Merge pull request #1202 from github/aeisenberg/update-tsc
Update tsc to 4.5.5
2022-03-25 11:33:37 -07:00
Andrew Eisenberg
23e29a1fdc Update tsc to 4.5.5
The default version of tsc in vscode is now 4.5.4. This version
has changed the type of the variable in the catch block.
Previously, it was `any`. Now it is `unknown`.

This change updates vscode so that it can build with 4.5.4.

Previously, this had been a bit of a pain since sometimes running
a compile task in vscode will use the global default version of
tsc.
2022-03-25 09:48:51 -07:00
Shati Patel
90d636a026 Download databases from GitHub (#1229) 2022-03-25 15:24:09 +00:00
Andrew Eisenberg
3e3e12afb9 Merge pull request #1230 from github/aeisenberg/astviewer-uri
Fix invalid file comparison for changing ast viewer location
2022-03-25 08:21:05 -07:00
Andrew Eisenberg
421f5d23ec Update changelog 2022-03-24 12:39:11 -07:00
Andrew Eisenberg
0fa91f32cb Fix invalid file comparison for changing ast viewer location
This fixes a bug where the ast viewer was not updating its source
location when a user clicks on different parts of a file.

The problem was that the file name of the AST viewer was being stored as
a base name, which was getting compared with the full URI string of the
current file.

This fixes the comparison to ensure that the full URI strings are always
being compared.
2022-03-24 12:36:17 -07:00
shati-patel
3d21b203be Make "promptForLanguage" more general
(so we can use it for downloading a GH database as well)
2022-03-21 16:37:51 +00:00
shati-patel
3972b8f4c1 Rename LGTM-specific function 2022-03-21 16:37:51 +00:00
Tobias Speicher
2d1707db00 refactor: replace deprecated String.prototype.substr()
.substr() is deprecated so we replace it with .slice() which works similarily but isn't deprecated
Signed-off-by: Tobias Speicher <rootcommander@gmail.com>
2022-03-21 14:16:54 +00:00
Robert
72aa4f0561 Merge pull request #1226 from github/robertbrignull/allow-custom-action-branch
Allow a custom branch name in settings file
2022-03-21 10:52:21 +00:00
Robert
fd57cc95e9 Remove unnnecessary function 2022-03-21 10:38:00 +00:00
Robert
04c392be7e Allow a custom branch name in settings file 2022-03-18 16:26:06 +00:00
github-actions[bot]
38da598214 Bump version to v1.6.2 (#1221)
Co-authored-by: charisk <charisk@users.noreply.github.com>
2022-03-17 12:47:33 +00:00
Charis Kyriakou
3f2c9b647c v1.6.1 (#1220)
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2022-03-17 12:04:37 +00:00
Shati Patel
7d5b4369c1 Fix highlighting issues (#1219) 2022-03-17 11:45:31 +00:00
Shati Patel
aade33fa88 Minor webview fixes (#1217) 2022-03-17 11:12:50 +00:00
Shati Patel
2a8a90bdfc Change public occurrences of "remote queries" (#1215) 2022-03-17 10:14:32 +00:00
Shati Patel
f36048cc95 Use variable for highlighting code (#1216) 2022-03-17 10:08:42 +00:00
Charis Kyriakou
517feeca21 Remove SARIF viewer support (#1213) 2022-03-16 14:39:52 +00:00
Charis Kyriakou
9436a49118 Remove helper command for working on the Remote Query results view (#1214) 2022-03-16 14:19:19 +00:00
Charis Kyriakou
0e02cb08fd Enable viewing of analyses results (#1212) 2022-03-16 14:15:43 +00:00
Shati Patel
26244efc50 Create remote file links to GitHub URL (#1209)
Co-authored-by: Charis Kyriakou <charisk@github.com>
2022-03-16 14:11:17 +00:00
Charis Kyriakou
6339eeffe5 Minor styling fix for raw results (#1211) 2022-03-16 11:44:51 +00:00
Charis Kyriakou
8cc2f598eb Fix highlight region end column calculation (#1210) 2022-03-16 09:47:09 +00:00
Charis Kyriakou
46a1dd57f4 Minor style fixes around result rendering (#1208) 2022-03-15 14:43:24 +00:00
shati-patel
9d99fc521e Get database sha from result index 2022-03-15 10:30:01 +00:00
Shati Patel
bcf79354ee Bump CLI version in integration tests 2022-03-15 10:22:18 +00:00
Charis Kyriakou
27a8636bac Deal with non-printable characters when rendering raw results (#1203) 2022-03-14 11:25:33 +00:00
Charis Kyriakou
92a99938c9 Add support for remote queries raw results (#1198) 2022-03-14 08:18:43 +00:00
Charis Kyriakou
ed61eb0a95 Deal with analysis messages that have links to locations (#1195) 2022-03-14 08:14:09 +00:00
Andrew Eisenberg
50d495b522 Merge pull request #1201 from mrysav/patch-1
Install Dependency Review Action
2022-03-11 10:40:06 -08:00
Andrew Eisenberg
526d5c2c44 Apply suggestions from code review 2022-03-11 10:29:02 -08:00
Charis Kyriakou
1720f9201e Update Primer React to v35 (#1199) 2022-03-10 20:24:12 +00:00
Mitchell Rysavy
e62de1ca22 Create dependency-review.yml 2022-03-10 14:48:06 -05:00
Charis Kyriakou
d052ddb742 Rename analysis alert results (#1197) 2022-03-10 07:56:05 +00:00
Andrew Eisenberg
af53a02ea5 Merge pull request #1192 from github/aeisenberg/disable-openvsx-deploy
Disable the open-vsx-publish job
2022-03-09 09:27:17 -08:00
Charis Kyriakou
8e2d18da8c Rename ColumnValue to CellValue (#1196) 2022-03-09 16:44:15 +00:00
Charis Kyriakou
2c5004387d Add support for showing code flows (#1187) 2022-03-09 09:15:45 +00:00
Charis Kyriakou
3fc3b259ba Add pre-push hook check to block leftover .only()s (#1189) 2022-03-08 09:32:18 +00:00
Andrew Eisenberg
cd95f68692 Merge pull request #1191 from github/version/bump-to-v1.6.1
Bump version to v1.6.1
2022-03-07 10:25:23 -08:00
Andrew Eisenberg
59c3b1ba2f Disable the open-vsx-publish job
It is failing, blocked on #1085
2022-03-07 10:19:42 -08:00
aeisenberg
fa85865fe5 Bump version to v1.6.1 2022-03-07 18:04:29 +00:00
125 changed files with 6225 additions and 2236 deletions

22
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "extensions/ql-vscode"
schedule:
interval: "weekly"
day: "thursday" # Thursday is arbitrary
labels:
- "Update dependencies"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: ".github"
schedule:
interval: "weekly"
day: "thursday" # Thursday is arbitrary
labels:
- "Update dependencies"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]

16
.github/workflows/dependency-review.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: 'Dependency Review'
on:
- pull_request
- workflow_dispatch
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v1

View File

@@ -135,7 +135,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.3', 'v2.7.6', 'v2.8.2', 'nightly']
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.3', 'v2.7.6', 'v2.8.5', 'nightly']
env:
CLI_VERSION: ${{ matrix.version }}
NIGHTLY_URL: ${{ needs.find-nightly.outputs.url }}

View File

@@ -124,6 +124,7 @@ From inside of VSCode, open the `launch.json` file and in the _Launch Integratio
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.
@@ -143,12 +144,7 @@ To regenerate the Open VSX token:
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:
1. Follow the instructions on [getting a PAT for Azure DevOps](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token).
1. Update the secret in the `publish-vscode-marketplace` environment in the project settings.
Not that Azure DevOps PATs expire yearly and must be regenerated.
To regenerate the VSCode Marketplace token, please see our internal documentation. Note that Azure DevOps PATs expire every 90 days and must be regenerated.
## Resources

View File

@@ -1,5 +1,25 @@
# CodeQL for Visual Studio Code: Changelog
## 1.6.5 - 25 April 2022
- Re-enable publishing to open-vsx. [#1285](https://github.com/github/vscode-codeql/pull/1285)
## 1.6.4 - 6 April 2022
No user facing changes.
## 1.6.3 - 4 April 2022
- Fix a bug where the AST viewer was not synchronizing its selected node when the editor selection changes. [#1230](https://github.com/github/vscode-codeql/pull/1230)
- Avoid synchronizing the `codeQL.cli.executablePath` setting. [#1252](https://github.com/github/vscode-codeql/pull/1252)
- Open the directory in the finder/explorer (instead of just highlighting it) when running the "Open query directory" command from the query history view. [#1235](https://github.com/github/vscode-codeql/pull/1235)
- Ensure query label in the query history view changes are persisted across restarts. [#1235](https://github.com/github/vscode-codeql/pull/1235)
- Prints end-of-query evaluator log summaries to the Query Server Console. [#1264](https://github.com/github/vscode-codeql/pull/1264)
## 1.6.1 - 17 March 2022
No user facing changes.
## 1.6.0 - 7 March 2022
- Fix a bug where database upgrades could not be resolved if some of the target pack's dependencies are outside of the workspace. [#1138](https://github.com/github/vscode-codeql/pull/1138)
@@ -7,6 +27,7 @@
- Fix a bug where queries took a long time to run if there are no folders in the workspace. [#1157](https://github.com/github/vscode-codeql/pull/1157)
- [BREAKING CHANGE] The `codeQL.runningQueries.customLogDirectory` setting is deprecated and no longer has any function. Instead, all query log files will be stored in the query history directory, next to the query results. [#1178](https://github.com/github/vscode-codeql/pull/1178)
- Add a _Open query directory_ command for query items. This command opens the directory containing all artifacts for a query. [#1179](https://github.com/github/vscode-codeql/pull/1179)
- Add options to display evaluator logs for a given query run. Some information that was previously found in the query server output may now be found here. [#1186](https://github.com/github/vscode-codeql/pull/1186)
## 1.5.11 - 10 February 2022

View File

@@ -1,5 +1,6 @@
import * as gulp from 'gulp';
import * as replace from 'gulp-replace';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const replace = require('gulp-replace');
/** Inject the application insights key into the telemetry file */
export function injectAppInsightsKey() {

View File

@@ -1,5 +1,4 @@
import * as fs from 'fs-extra';
import * as jsonc from 'jsonc-parser';
import * as path from 'path';
export interface DeployedPackage {
@@ -28,7 +27,7 @@ async function copyPackage(sourcePath: string, destPath: string): Promise<void>
export async function deployPackage(packageJsonPath: string): Promise<DeployedPackage> {
try {
const packageJson: any = jsonc.parse(await fs.readFile(packageJsonPath, 'utf8'));
const packageJson: any = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
// Default to development build; use flag --release to indicate release build.
const isDevBuild = !process.argv.includes('--release');

View File

@@ -1,8 +1,8 @@
import * as gulp from 'gulp';
import { compileTypeScript, watchTypeScript, copyViewCss, cleanOutput } from './typescript';
import { compileTypeScript, watchTypeScript, copyViewCss, cleanOutput, watchCss } from './typescript';
import { compileTextMateGrammar } from './textmate';
import { copyTestData } from './tests';
import { compileView } from './webpack';
import { compileView, watchView } from './webpack';
import { packageExtension } from './package';
import { injectAppInsightsKey } from './appInsights';
@@ -14,5 +14,15 @@ export const buildWithoutPackage =
)
);
export { cleanOutput, compileTextMateGrammar, watchTypeScript, compileTypeScript, copyTestData, injectAppInsightsKey };
export {
cleanOutput,
compileTextMateGrammar,
watchTypeScript,
watchView,
compileTypeScript,
copyTestData,
injectAppInsightsKey,
compileView,
watchCss
};
export default gulp.series(buildWithoutPackage, injectAppInsightsKey, packageExtension);

View File

@@ -16,7 +16,8 @@
"noImplicitReturns": true,
"experimentalDecorators": true,
"noUnusedLocals": true,
"noUnusedParameters": true
"noUnusedParameters": true,
"esModuleInterop": true
},
"include": ["*.ts"]
}

View File

@@ -40,6 +40,10 @@ export function watchTypeScript() {
gulp.watch('src/**/*.ts', compileTypeScript);
}
export function watchCss() {
gulp.watch('src/**/*.css', copyViewCss);
}
/** Copy CSS files for the results view into the output directory. */
export function copyViewCss() {
return gulp.src('src/**/view/*.css')

View File

@@ -2,7 +2,23 @@ import * as webpack from 'webpack';
import { config } from './webpack.config';
export function compileView(cb: (err?: Error) => void) {
webpack(config).run((error, stats) => {
doWebpack(config, true, cb);
}
export function watchView(cb: (err?: Error) => void) {
const watchConfig = {
...config,
watch: true,
watchOptions: {
aggregateTimeout: 200,
poll: 1000,
}
};
doWebpack(watchConfig, false, cb);
}
function doWebpack(internalConfig: webpack.Configuration, failOnError: boolean, cb: (err?: Error) => void) {
const resultCb = (error: Error | undefined, stats?: webpack.Stats) => {
if (error) {
cb(error);
}
@@ -20,11 +36,16 @@ export function compileView(cb: (err?: Error) => void) {
errors: true
}));
if (stats.hasErrors()) {
cb(new Error('Compilation errors detected.'));
return;
if (failOnError) {
cb(new Error('Compilation errors detected.'));
return;
} else {
console.error('Compilation errors detected.');
}
}
cb();
}
};
cb();
});
webpack(internalConfig, resultCb);
}

View File

@@ -0,0 +1,4 @@
<!-- From https://github.com/microsoft/vscode-icons -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97553 0C3.57186 0 0 3.57186 0 7.97553C0 11.4985 2.29969 14.4832 5.43119 15.5596C5.82263 15.6086 5.96942 15.3639 5.96942 15.1682C5.96942 14.9725 5.96942 14.4832 5.96942 13.7982C3.76758 14.2875 3.27829 12.7217 3.27829 12.7217C2.93578 11.792 2.39755 11.5474 2.39755 11.5474C1.66361 11.0581 2.44648 11.0581 2.44648 11.0581C3.22936 11.107 3.66972 11.8899 3.66972 11.8899C4.40367 13.1131 5.52905 12.7706 5.96942 12.5749C6.01835 12.0367 6.263 11.6942 6.45872 11.4985C4.69725 11.3028 2.83792 10.6177 2.83792 7.53517C2.83792 6.65443 3.1315 5.96942 3.66972 5.38226C3.62079 5.23547 3.32722 4.40367 3.76758 3.32722C3.76758 3.32722 4.4526 3.1315 5.96942 4.15902C6.6055 3.9633 7.29052 3.91437 7.97553 3.91437C8.66055 3.91437 9.34557 4.01223 9.98165 4.15902C11.4985 3.1315 12.1835 3.32722 12.1835 3.32722C12.6239 4.40367 12.3303 5.23547 12.2813 5.43119C12.7706 5.96942 13.1131 6.70336 13.1131 7.5841C13.1131 10.6667 11.2538 11.3028 9.49235 11.4985C9.78593 11.7431 10.0306 12.2324 10.0306 12.9664C10.0306 14.0428 10.0306 14.8746 10.0306 15.1682C10.0306 15.3639 10.1774 15.6086 10.5688 15.5596C13.7492 14.4832 16 11.4985 16 7.97553C15.9511 3.57186 12.3792 0 7.97553 0Z" fill="#C5C5C5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,11 @@
<!-- From https://github.com/microsoft/vscode-icons -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97578 0C3.57211 0 0.000244141 3.57186 0.000244141 7.97553C0.000244141 11.4985 2.29994 14.4832 5.43144 15.5596C5.82287 15.6086 5.96966 15.3639 5.96966 15.1682C5.96966 14.9725 5.96966 14.4832 5.96966 13.7982C3.76783 14.2875 3.27853 12.7217 3.27853 12.7217C2.93602 11.792 2.3978 11.5474 2.3978 11.5474C1.66385 11.0581 2.44673 11.0581 2.44673 11.0581C3.2296 11.107 3.66997 11.8899 3.66997 11.8899C4.40391 13.1131 5.5293 12.7706 5.96966 12.5749C6.01859 12.0367 6.26324 11.6942 6.45896 11.4985C4.69749 11.3028 2.83816 10.6177 2.83816 7.53517C2.83816 6.65443 3.13174 5.96942 3.66997 5.38226C3.62104 5.23547 3.32746 4.40367 3.76783 3.32722C3.76783 3.32722 4.45284 3.1315 5.96966 4.15902C6.60575 3.9633 7.29076 3.91437 7.97578 3.91437C8.66079 3.91437 9.34581 4.01223 9.98189 4.15902C11.4987 3.1315 12.1837 3.32722 12.1837 3.32722C12.6241 4.40367 12.3305 5.23547 12.2816 5.43119C12.7709 5.96942 13.1134 6.70336 13.1134 7.5841C13.1134 10.6667 11.2541 11.3028 9.4926 11.4985C9.78618 11.7431 10.0308 12.2324 10.0308 12.9664C10.0308 14.0428 10.0308 14.8746 10.0308 15.1682C10.0308 15.3639 10.1776 15.6086 10.5691 15.5596C13.7495 14.4832 16.0002 11.4985 16.0002 7.97553C15.9513 3.57186 12.3794 0 7.97578 0Z" fill="#424242"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white" transform="translate(0.000244141)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.6.0",
"version": "1.6.5",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -14,15 +14,14 @@
},
"engines": {
"vscode": "^1.59.0",
"node": "^14.17.1",
"npm": "^7.20.6"
"node": ">=14.17.1",
"npm": ">=7.20.6"
},
"categories": [
"Programming Languages"
],
"extensionDependencies": [
"hbenl.vscode-test-explorer",
"ms-vscode.test-adapter-converter"
"hbenl.vscode-test-explorer"
],
"capabilities": {
"untrustedWorkspaces": {
@@ -45,6 +44,7 @@
"onCommand:codeQLDatabases.chooseDatabaseFolder",
"onCommand:codeQLDatabases.chooseDatabaseArchive",
"onCommand:codeQLDatabases.chooseDatabaseInternet",
"onCommand:codeQLDatabases.chooseDatabaseGithub",
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
"onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQL.viewAst",
@@ -54,6 +54,7 @@
"onCommand:codeQL.chooseDatabaseFolder",
"onCommand:codeQL.chooseDatabaseArchive",
"onCommand:codeQL.chooseDatabaseInternet",
"onCommand:codeQL.chooseDatabaseGithub",
"onCommand:codeQL.chooseDatabaseLgtm",
"onCommand:codeQLDatabases.chooseDatabase",
"onCommand:codeQLDatabases.setCurrentDatabase",
@@ -134,7 +135,7 @@
"title": "CodeQL",
"properties": {
"codeQL.cli.executablePath": {
"scope": "window",
"scope": "machine-overridable",
"type": "string",
"default": "",
"markdownDescription": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. If empty, the extension will look for a CodeQL executable on your shell PATH, or if CodeQL is not on your PATH, download and manage its own CodeQL executable."
@@ -223,7 +224,7 @@
},
"codeQL.queryHistory.format": {
"type": "string",
"default": "%q on %d - %s, %r result count [%t]",
"default": "%q on %d - %s, %r [%t]",
"markdownDescription": "Default string for how to label query history items.\n* %t is the time of the query\n* %q is the human-readable query name\n* %f is the query file name\n* %d is the database name\n* %r is the number of results\n* %s is a status string"
},
"codeQL.queryHistory.ttl": {
@@ -258,7 +259,7 @@
"scope": "application",
"description": "Specifies whether or not to write telemetry events to the extension log."
},
"codeQL.remoteQueries.repositoryLists": {
"codeQL.variantAnalysis.repositoryLists": {
"type": [
"object",
null
@@ -272,14 +273,14 @@
}
},
"default": null,
"markdownDescription": "[For internal use only] Lists of GitHub repositories that you want to query remotely. This should be a JSON object where each key is a user-specified name for this repository list, and the value is an array of GitHub repositories (of the form `<owner>/<repo>`)."
"markdownDescription": "[For internal use only] Lists of GitHub repositories that you want to run variant analysis against. This should be a JSON object where each key is a user-specified name for this repository list, and the value is an array of GitHub repositories (of the form `<owner>/<repo>`)."
},
"codeQL.remoteQueries.controllerRepo": {
"codeQL.variantAnalysis.controllerRepo": {
"type": "string",
"default": "",
"pattern": "^$|^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+/[a-zA-Z0-9-_]+$",
"patternErrorMessage": "Please enter a valid GitHub repository",
"markdownDescription": "[For internal use only] The name of the GitHub repository where you can view the progress and results of the \"Run Remote query\" command. The repository should be of the form `<owner>/<repo>`)."
"markdownDescription": "[For internal use only] The name of the GitHub repository where you can view the progress and results of the \"Run Variant Analysis\" command. The repository should be of the form `<owner>/<repo>`)."
}
}
},
@@ -297,12 +298,8 @@
"title": "CodeQL: Run Query on Multiple Databases"
},
{
"command": "codeQL.runRemoteQuery",
"title": "CodeQL: Run Remote Query"
},
{
"command": "codeQL.showFakeRemoteQueryResults",
"title": "CodeQL: [Internal] Show fake remote query results"
"command": "codeQL.runVariantAnalysis",
"title": "CodeQL: Run Variant Analysis"
},
{
"command": "codeQL.runQueries",
@@ -360,6 +357,14 @@
"dark": "media/dark/cloud-download.svg"
}
},
{
"command": "codeQLDatabases.chooseDatabaseGithub",
"title": "Download Database from GitHub",
"icon": {
"light": "media/light/github.svg",
"dark": "media/dark/github.svg"
}
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"title": "Download from LGTM",
@@ -432,6 +437,10 @@
"command": "codeQL.chooseDatabaseInternet",
"title": "CodeQL: Download Database"
},
{
"command": "codeQL.chooseDatabaseGithub",
"title": "CodeQL: Download Database from GitHub"
},
{
"command": "codeQL.chooseDatabaseLgtm",
"title": "CodeQL: Download Database from LGTM"
@@ -512,6 +521,14 @@
"command": "codeQLQueryHistory.openQueryDirectory",
"title": "Open query directory"
},
{
"command": "codeQLQueryHistory.showEvalLog",
"title": "Show Evaluator Log (Raw)"
},
{
"command": "codeQLQueryHistory.showEvalLogSummary",
"title": "Show Evaluator Log (Summary)"
},
{
"command": "codeQLQueryHistory.cancel",
"title": "Cancel"
@@ -546,7 +563,7 @@
},
{
"command": "codeQLQueryHistory.openOnGithub",
"title": "Open Remote Query on GitHub"
"title": "Open Variant Analysis on GitHub"
},
{
"command": "codeQLQueryResults.nextPathStep",
@@ -608,6 +625,11 @@
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLDatabases.chooseDatabaseGithub",
"when": "config.codeQL.canary && view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "view == codeQLDatabases",
@@ -710,6 +732,16 @@
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory && !hasRemoteServer"
},
{
"command": "codeQLQueryHistory.showEvalLog",
"group": "9_qlCommands",
"when": "codeql.supportsEvalLog && viewItem == rawResultsItem || codeql.supportsEvalLog && viewItem == interpretedResultsItem || codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showEvalLogSummary",
"group": "9_qlCommands",
"when": "codeql.supportsEvalLog && viewItem == rawResultsItem || codeql.supportsEvalLog && viewItem == interpretedResultsItem || codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showQueryText",
"group": "9_qlCommands",
@@ -802,13 +834,9 @@
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runRemoteQuery",
"command": "codeQL.runVariantAnalysis",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.showFakeRemoteQueryResults",
"when": "config.codeQL.canary"
},
{
"command": "codeQL.runQueries",
"when": "false"
@@ -837,6 +865,10 @@
"command": "codeQL.viewCfg",
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
},
{
"command": "codeQL.chooseDatabaseGithub",
"when": "config.codeQL.canary"
},
{
"command": "codeQLDatabases.setCurrentDatabase",
"when": "false"
@@ -881,6 +913,10 @@
"command": "codeQLDatabases.chooseDatabaseInternet",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseGithub",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "false"
@@ -905,6 +941,14 @@
"command": "codeQLQueryHistory.showQueryLog",
"when": "false"
},
{
"command": "codeQLQueryHistory.showEvalLog",
"when": "false"
},
{
"command": "codeQLQueryHistory.showEvalLogSummary",
"when": "false"
},
{
"command": "codeQLQueryHistory.openQueryDirectory",
"when": "false"
@@ -984,7 +1028,7 @@
"when": "editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runRemoteQuery",
"command": "codeQL.runVariantAnalysis",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
},
{
@@ -1053,6 +1097,8 @@
"build": "gulp",
"watch": "npm-run-all -p watch:*",
"watch:extension": "tsc --watch",
"watch:webpack": "gulp watchView",
"watch:css": "gulp watchCss",
"test": "mocha --exit -r ts-node/register test/pure-tests/**/*.ts",
"preintegration": "rm -rf ./out/vscode-tests && gulp",
"integration": "node ./out/vscode-tests/run-integration-tests.js no-workspace,minimal-workspace",
@@ -1065,21 +1111,22 @@
"dependencies": {
"@octokit/rest": "^18.5.6",
"@primer/octicons-react": "^16.3.0",
"@primer/react": "^34.3.0",
"@primer/react": "^35.0.0",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"d3": "^6.3.1",
"d3-graphviz": "^2.6.1",
"fs-extra": "^9.0.1",
"fs-extra": "^10.0.1",
"glob-promise": "^3.4.0",
"js-yaml": "^3.14.0",
"minimist": "~1.2.5",
"minimist": "~1.2.6",
"nanoid": "^3.2.0",
"node-fetch": "~2.6.7",
"path-browserify": "^1.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"semver": "~7.3.2",
"source-map-support": "^0.5.21",
"stream": "^0.0.2",
"stream-chain": "~2.2.4",
"stream-json": "~1.7.3",
@@ -1093,21 +1140,21 @@
"vscode-languageclient": "^6.1.3",
"vscode-test-adapter-api": "~1.7.0",
"vscode-test-adapter-util": "~0.7.0",
"zip-a-folder": "~0.0.12"
"zip-a-folder": "~1.1.3"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/chai-as-promised": "~7.1.2",
"@types/child-process-promise": "^2.2.1",
"@types/classnames": "~2.2.9",
"@types/del": "^4.0.0",
"@types/d3": "^6.2.0",
"@types/d3-graphviz": "^2.6.6",
"@types/del": "^4.0.0",
"@types/fs-extra": "^9.0.6",
"@types/glob": "^7.1.1",
"@types/google-protobuf": "^3.2.7",
"@types/gulp": "^4.0.9",
"@types/gulp-replace": "0.0.31",
"@types/gulp-replace": "^1.1.0",
"@types/gulp-sourcemaps": "0.0.32",
"@types/js-yaml": "^3.12.5",
"@types/jszip": "~3.1.6",
@@ -1128,7 +1175,7 @@
"@types/tmp": "^0.1.0",
"@types/unzipper": "~0.10.1",
"@types/vscode": "^1.59.0",
"@types/webpack": "^4.32.1",
"@types/webpack": "^5.28.0",
"@types/xml2js": "~0.4.4",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
@@ -1142,27 +1189,26 @@
"eslint-plugin-react": "~7.19.0",
"glob": "^7.1.4",
"gulp": "^4.0.2",
"gulp-replace": "^1.0.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-replace": "^1.1.3",
"gulp-sourcemaps": "^3.0.0",
"gulp-typescript": "^5.0.1",
"husky": "~4.2.5",
"jsonc-parser": "^2.3.0",
"lint-staged": "~10.2.2",
"mocha": "^9.1.3",
"mocha-sinon": "~2.1.2",
"npm-run-all": "^4.1.5",
"prettier": "~2.0.5",
"proxyquire": "~2.1.3",
"sinon": "~9.0.0",
"sinon": "~13.0.1",
"sinon-chai": "~3.5.0",
"style-loader": "~0.23.1",
"through2": "^3.0.1",
"through2": "^4.0.2",
"ts-loader": "^8.1.0",
"ts-node": "^8.3.0",
"ts-node": "^10.7.0",
"ts-protoc-gen": "^0.9.0",
"typescript": "^4.3.2",
"typescript": "^4.5.5",
"typescript-formatter": "^7.2.2",
"vsce": "^1.65.0",
"vsce": "^2.7.0",
"vscode-test": "^1.4.0",
"webpack": "^5.28.0",
"webpack-cli": "^4.6.0"
@@ -1170,11 +1216,11 @@
"husky": {
"hooks": {
"pre-commit": "npm run format-staged",
"pre-push": "npm run lint"
"pre-push": "npm run lint && scripts/forbid-mocha-only"
}
},
"lint-staged": {
"./**/*.{json,css,scss,md}": [
"./**/*.{json,css,scss}": [
"prettier --write"
],
"./**/*.{ts,tsx}": [

View File

@@ -0,0 +1,6 @@
if grep -rq --include '*.test.ts' 'it.only\|describe.only' './test' './src'; then
echo 'There is a .only() in the tests. Please remove it.'
exit 1;
else
exit 0;
fi

View File

@@ -10,7 +10,8 @@ import {
TextEditorSelectionChangeEvent,
TextEditorSelectionChangeKind,
Location,
Range
Range,
Uri
} from 'vscode';
import * as path from 'path';
@@ -104,7 +105,7 @@ class AstViewerDataProvider extends DisposableObject implements TreeDataProvider
export class AstViewer extends DisposableObject {
private treeView: TreeView<AstItem>;
private treeDataProvider: AstViewerDataProvider;
private currentFile: string | undefined;
private currentFileUri: Uri | undefined;
constructor() {
super();
@@ -125,12 +126,12 @@ export class AstViewer extends DisposableObject {
this.push(window.onDidChangeTextEditorSelection(this.updateTreeSelection, this));
}
updateRoots(roots: AstItem[], db: DatabaseItem, fileName: string) {
updateRoots(roots: AstItem[], db: DatabaseItem, fileUri: Uri) {
this.treeDataProvider.roots = roots;
this.treeDataProvider.db = db;
this.treeDataProvider.refresh();
this.treeView.message = `AST for ${path.basename(fileName)}`;
this.currentFile = fileName;
this.treeView.message = `AST for ${path.basename(fileUri.fsPath)}`;
this.currentFileUri = fileUri;
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(roots[0], { focus: false })?.then(
@@ -174,7 +175,7 @@ export class AstViewer extends DisposableObject {
if (
this.treeView.visible &&
e.textEditor.document.uri.fsPath === this.currentFile &&
e.textEditor.document.uri.fsPath === this.currentFileUri?.fsPath &&
e.selections.length === 1
) {
const selection = e.selections[0];
@@ -199,6 +200,6 @@ export class AstViewer extends DisposableObject {
this.treeDataProvider.db = undefined;
this.treeDataProvider.refresh();
this.treeView.message = undefined;
this.currentFile = undefined;
this.currentFileUri = undefined;
}
}

View File

@@ -7,7 +7,7 @@ const GITHUB_AUTH_PROVIDER_ID = 'github';
// https://docs.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps
const SCOPES = ['repo'];
/**
/**
* Handles authentication to GitHub, using the VS Code [authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication).
*/
export class Credentials {
@@ -18,6 +18,15 @@ export class Credentials {
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() { }
/**
* Initializes an instance of credentials with an octokit instance.
*
* Do not call this method until you know you actually need an instance of credentials.
* since calling this method will require the user to log in.
*
* @param context The extension context.
* @returns An instance of credentials.
*/
static async initialize(context: vscode.ExtensionContext): Promise<Credentials> {
const c = new Credentials();
c.registerListeners(context);

View File

@@ -1,6 +1,7 @@
import * as semver from 'semver';
import { runCodeQlCliCommand } from './cli';
import { Logger } from './logging';
import { getErrorMessage } from './pure/helpers-pure';
/**
* Get the version of a CodeQL CLI.
@@ -18,7 +19,7 @@ export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): P
} catch (e) {
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted.
// Either way, we can't determine compatibility.
void logger.log(`Failed to run 'codeql version'. Reason: ${e.message}`);
void logger.log(`Failed to run 'codeql version'. Reason: ${getErrorMessage(e)}`);
return undefined;
}
}

View File

@@ -8,12 +8,12 @@ import { Readable } from 'stream';
import { StringDecoder } from 'string_decoder';
import * as tk from 'tree-kill';
import { promisify } from 'util';
import { CancellationToken, Disposable, Uri } from 'vscode';
import { CancellationToken, commands, Disposable, Uri } from 'vscode';
import { BQRSInfo, DecodedBqrsChunk } from './pure/bqrs-cli-types';
import { CliConfig } from './config';
import { DistributionProvider, FindDistributionResultKind } from './distribution';
import { assertNever } from './pure/helpers-pure';
import { assertNever, getErrorMessage, getErrorStack } from './pure/helpers-pure';
import { QueryMetadata, SortDirection } from './pure/interface-types';
import { Logger, ProgressReporter } from './logging';
import { CompilationMessage } from './pure/messages';
@@ -346,7 +346,7 @@ export class CodeQLCliServer implements Disposable {
stderrBuffers.length == 0
? new Error(`${description} failed: ${err}`)
: new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString('utf8')}`);
newError.stack += (err.stack || '');
newError.stack += getErrorStack(err);
throw newError;
} finally {
void this.logger.log(Buffer.concat(stderrBuffers).toString('utf8'));
@@ -448,7 +448,7 @@ export class CodeQLCliServer implements Disposable {
try {
yield JSON.parse(event) as EventType;
} catch (err) {
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
throw new Error(`Parsing output of ${description} failed: ${(err as any).stderr || getErrorMessage(err)}`);
}
}
}
@@ -503,7 +503,7 @@ export class CodeQLCliServer implements Disposable {
try {
return JSON.parse(result) as OutputType;
} catch (err) {
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
throw new Error(`Parsing output of ${description} failed: ${(err as any).stderr || getErrorMessage(err)}`);
}
}
@@ -665,6 +665,26 @@ export class CodeQLCliServer implements Disposable {
return await this.runCodeQlCliCommand(['generate', 'query-help'], subcommandArgs, `Generating qhelp in markdown format at ${outputDirectory}`);
}
/**
* Generate a summary of an evaluation log.
* @param endSummaryPath The path to write only the end of query part of the human-readable summary to.
* @param inputPath The path of an evaluation event log.
* @param outputPath The path to write a human-readable summary of it to.
*/
async generateLogSummary(
inputPath: string,
outputPath: string,
endSummaryPath: string,
): Promise<string> {
const subcommandArgs = [
'--format=text',
`--end-summary=${endSummaryPath}`,
inputPath,
outputPath
];
return await this.runCodeQlCliCommand(['generate', 'log-summary'], subcommandArgs, 'Generating log summary');
}
/**
* Gets the results from a bqrs.
* @param bqrsPath The path to the bqrs.
@@ -751,7 +771,7 @@ export class CodeQLCliServer implements Disposable {
const dot = await this.readDotFiles(interpretedResultsPath);
return dot;
} catch (err) {
throw new Error(`Reading output of interpretation failed: ${err.stderr || err}`);
throw new Error(`Reading output of interpretation failed: ${getErrorMessage(err)}`);
}
}
@@ -940,6 +960,10 @@ export class CodeQLCliServer implements Disposable {
public async getVersion() {
if (!this._version) {
this._version = await this.refreshVersion();
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
await commands.executeCommand(
'setContext', 'codeql.supportsEvalLog', await this.cliConstraints.supportsPerQueryEvalLog()
);
}
return this._version;
}
@@ -1050,7 +1074,7 @@ export async function runCodeQlCliCommand(
void logger.log('CLI command succeeded.');
return result.stdout;
} catch (err) {
throw new Error(`${description} failed: ${err.stderr || err}`);
throw new Error(`${description} failed: ${(err as any).stderr || getErrorMessage(err)}`);
}
}
@@ -1106,8 +1130,8 @@ class SplitBuffer {
while (this.searchIndex <= (this.buffer.length - this.maxSeparatorLength)) {
for (const separator of this.separators) {
if (SplitBuffer.startsWith(this.buffer, separator, this.searchIndex)) {
const line = this.buffer.substr(0, this.searchIndex);
this.buffer = this.buffer.substr(this.searchIndex + separator.length);
const line = this.buffer.slice(0, this.searchIndex);
this.buffer = this.buffer.slice(this.searchIndex + separator.length);
this.searchIndex = 0;
return line;
}
@@ -1231,7 +1255,7 @@ export class CliVersionConstraint {
public static CLI_VERSION_WITH_NO_PRECOMPILE = new SemVer('2.7.1');
/**
* CLI version where remote queries are supported.
* CLI version where remote queries (variant analysis) are supported.
*/
public static CLI_VERSION_REMOTE_QUERIES = new SemVer('2.6.3');
@@ -1256,6 +1280,17 @@ export class CliVersionConstraint {
*/
public static CLI_VERSION_WITH_STRUCTURED_EVAL_LOG = new SemVer('2.8.2');
/**
* CLI version that supports rotating structured logs to produce one per query.
*
* Note that 2.8.4 supports generating the evaluation logs and summaries,
* but 2.9.0 includes a new option to produce the end-of-query summary logs to
* the query server console. For simplicity we gate all features behind 2.9.0,
* but if a user is tied to the 2.8 release, we can enable evaluator logs
* and summaries for them.
*/
public static CLI_VERSION_WITH_PER_QUERY_EVAL_LOG = new SemVer('2.9.0');
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1315,4 +1350,8 @@ export class CliVersionConstraint {
async supportsStructuredEvalLog() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_STRUCTURED_EVAL_LOG);
}
async supportsPerQueryEvalLog() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG);
}
}

View File

@@ -8,6 +8,7 @@ import {
} from 'vscode';
import { showAndLogErrorMessage, showAndLogWarningMessage } from './helpers';
import { logger } from './logging';
import { getErrorMessage, getErrorStack } from './pure/helpers-pure';
import { telemetryListener } from './telemetry';
export class UserCancellationException extends Error {
@@ -121,8 +122,9 @@ export function commandRunner(
try {
return await task(...args);
} catch (e) {
error = e;
const errorMessage = `${e.message || e} (${commandId})`;
const errorMessage = `${getErrorMessage(e) || e} (${commandId})`;
error = e instanceof Error ? e : new Error(errorMessage);
const errorStack = getErrorStack(e);
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
@@ -132,8 +134,8 @@ export function commandRunner(
}
} else {
// Include the full stack in the error log only.
const fullMessage = e.stack
? `${errorMessage}\n${e.stack}`
const fullMessage = errorStack
? `${errorMessage}\n${errorStack}`
: errorMessage;
void showAndLogErrorMessage(errorMessage, {
fullMessage
@@ -173,8 +175,9 @@ export function commandRunnerWithProgress<R>(
try {
return await withProgress(progressOptionsWithDefaults, task, ...args);
} catch (e) {
error = e;
const errorMessage = `${e.message || e} (${commandId})`;
const errorMessage = `${getErrorMessage(e) || e} (${commandId})`;
error = e instanceof Error ? e : new Error(errorMessage);
const errorStack = getErrorStack(e);
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
@@ -184,8 +187,8 @@ export function commandRunnerWithProgress<R>(
}
} else {
// Include the full stack in the error log only.
const fullMessage = e.stack
? `${errorMessage}\n${e.stack}`
const fullMessage = errorStack
? `${errorMessage}\n${errorStack}`
: errorMessage;
void showAndLogErrorMessage(errorMessage, {
outputLogger,

View File

@@ -21,6 +21,8 @@ import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli-types';
import resultsDiff from './resultsDiff';
import { CompletedLocalQueryInfo } from '../query-results';
import { getErrorMessage } from '../pure/helpers-pure';
import { HistoryItemLabelProvider } from '../history-item-label-provider';
interface ComparePair {
from: CompletedLocalQueryInfo;
@@ -38,6 +40,7 @@ export class CompareInterfaceManager extends DisposableObject {
private databaseManager: DatabaseManager,
private cliServer: CodeQLCliServer,
private logger: Logger,
private labelProvider: HistoryItemLabelProvider,
private showQueryResultsCallback: (
item: CompletedLocalQueryInfo
) => Promise<void>
@@ -70,7 +73,7 @@ export class CompareInterfaceManager extends DisposableObject {
try {
rows = this.compareResults(fromResultSet, toResultSet);
} catch (e) {
message = e.message;
message = getErrorMessage(e);
}
await this.postMessage({
@@ -80,12 +83,12 @@ export class CompareInterfaceManager extends DisposableObject {
// since we split the description into several rows
// only run interpolation if the label is user-defined
// otherwise we will wind up with duplicated rows
name: from.getShortLabel(),
name: this.labelProvider.getShortLabel(from),
status: from.completedQuery.statusString,
time: from.startTime,
},
toQuery: {
name: to.getShortLabel(),
name: this.labelProvider.getShortLabel(to),
status: to.completedQuery.statusString,
time: to.startTime,
},

View File

@@ -322,11 +322,11 @@ export function isCanary() {
*/
export const NO_CACHE_AST_VIEWER = new Setting('disableCache', AST_VIEWER_SETTING);
// Settings for remote queries
const REMOTE_QUERIES_SETTING = new Setting('remoteQueries', ROOT_SETTING);
// Settings for variant analysis
const REMOTE_QUERIES_SETTING = new Setting('variantAnalysis', ROOT_SETTING);
/**
* Lists of GitHub repositories that you want to query remotely via the "Run Remote query" command.
* Lists of GitHub repositories that you want to query remotely via the "Run Variant Analysis" command.
* Note: This command is only available for internal users.
*
* This setting should be a JSON object where each key is a user-specified name (string),
@@ -343,7 +343,7 @@ export async function setRemoteRepositoryLists(lists: Record<string, string[]> |
}
/**
* The name of the "controller" repository that you want to use with the "Run Remote query" command.
* The name of the "controller" repository that you want to use with the "Run Variant Analysis" command.
* Note: This command is only available for internal users.
*
* This setting should be a GitHub repository of the form `<owner>/<repo>`.
@@ -357,3 +357,14 @@ export function getRemoteControllerRepo(): string | undefined {
export async function setRemoteControllerRepo(repo: string | undefined) {
await REMOTE_CONTROLLER_REPO.updateValue(repo, ConfigurationTarget.Global);
}
/**
* The branch of "github/codeql-variant-analysis-action" to use with the "Run Variant Analysis" command.
* Default value is "main".
* Note: This command is only available for internal users.
*/
const ACTION_BRANCH = new Setting('actionBranch', REMOTE_QUERIES_SETTING);
export function getActionBranch(): string {
return ACTION_BRANCH.getValue<string>() || 'main';
}

View File

@@ -4,6 +4,7 @@ import { DecodedBqrsChunk, BqrsId, EntityValue } from '../pure/bqrs-cli-types';
import { DatabaseItem } from '../databases';
import { ChildAstItem, AstItem } from '../astViewer';
import fileRangeFromURI from './fileRangeFromURI';
import { Uri } from 'vscode';
/**
* A class that wraps a tree of QL results from a query that
@@ -17,7 +18,7 @@ export default class AstBuilder {
queryResults: QueryWithResults,
private cli: CodeQLCliServer,
public db: DatabaseItem,
public fileName: string
public fileName: Uri
) {
this.bqrsPath = queryResults.query.resultsPaths.resultsPath;
}

View File

@@ -10,7 +10,6 @@ import {
TextDocument,
Uri
} from 'vscode';
import * as path from 'path';
import { decodeSourceArchiveUri, encodeArchiveBasePath, zipArchiveScheme } from '../archive-filesystem-provider';
import { CodeQLCliServer } from '../cli';
@@ -160,7 +159,7 @@ export class TemplatePrintAstProvider {
return new AstBuilder(
query, this.cli,
this.dbm.findDatabaseItem(dbUri)!,
path.basename(fileUri.fsPath),
fileUri,
);
}

View File

@@ -21,6 +21,8 @@ import {
} from './commandRunner';
import { logger } from './logging';
import { tmpDir } from './helpers';
import { Credentials } from './authentication';
import { REPO_REGEX, getErrorMessage } from './pure/helpers-pure';
/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
@@ -46,8 +48,10 @@ export async function promptImportInternetDatabase(
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
undefined,
progress,
token,
cli
@@ -61,6 +65,82 @@ export async function promptImportInternetDatabase(
}
/**
* Prompts a user to fetch a database from GitHub.
* User enters a GitHub repository and then the user is asked which language
* to download (if there is more than one)
*
* @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database.
*/
export async function promptImportGithubDatabase(
databaseManager: DatabaseManager,
storagePath: string,
credentials: Credentials,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer
): Promise<DatabaseItem | undefined> {
progress({
message: 'Choose repository',
step: 1,
maxStep: 2
});
const githubRepo = await window.showInputBox({
title: 'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
placeHolder: 'https://github.com/<owner>/<repo> or <owner>/<repo>',
ignoreFocusOut: true,
});
if (!githubRepo) {
return;
}
if (!looksLikeGithubRepo(githubRepo)) {
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
}
const result = await convertGithubNwoToDatabaseUrl(githubRepo, credentials, progress);
if (!result) {
return;
}
const { databaseUrl, name, owner } = result;
const octokit = await credentials.getOctokit();
/**
* The 'token' property of the token object returned by `octokit.auth()`.
* The object is undocumented, but looks something like this:
* {
* token: 'xxxx',
* tokenType: 'oauth',
* type: 'token',
* }
* We only need the actual token string.
*/
const octokitToken = (await octokit.auth() as { token: string })?.token;
if (!octokitToken) {
// Just print a generic error message for now. Ideally we could show more debugging info, like the
// octokit object, but that would expose a user token.
throw new Error('Unable to get GitHub token.');
}
const item = await databaseArchiveFetcher(
databaseUrl,
{ 'Accept': 'application/zip', 'Authorization': `Bearer ${octokitToken}` },
databaseManager,
storagePath,
`${owner}/${name}`,
progress,
token,
cli
);
if (item) {
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database downloaded and imported successfully.');
return item;
}
return;
}
/**
* Prompts a user to fetch a database from lgtm.
* User enters a project url and then the user is asked which language
@@ -90,12 +170,14 @@ export async function promptImportLgtmDatabase(
}
if (looksLikeLgtmUrl(lgtmUrl)) {
const databaseUrl = await convertToDatabaseUrl(lgtmUrl, progress);
const databaseUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progress);
if (databaseUrl) {
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
undefined,
progress,
token,
cli
@@ -140,8 +222,10 @@ export async function importArchiveDatabase(
try {
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
undefined,
progress,
token,
cli
@@ -152,7 +236,7 @@ export async function importArchiveDatabase(
}
return item;
} catch (e) {
if (e.message.includes('unexpected end of file')) {
if (getErrorMessage(e).includes('unexpected end of file')) {
throw new Error('Database is corrupt or too large. Try unzipping outside of VS Code and importing the unzipped folder instead.');
} else {
// delegate
@@ -166,15 +250,19 @@ export async function importArchiveDatabase(
* or in the local filesystem.
*
* @param databaseUrl URL from which to grab the database
* @param requestHeaders Headers to send with the request
* @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database.
* @param nameOverride a name for the database that overrides the default
* @param progress callback to send progress messages to
* @param token cancellation token
*/
async function databaseArchiveFetcher(
databaseUrl: string,
requestHeaders: { [key: string]: string },
databaseManager: DatabaseManager,
storagePath: string,
nameOverride: string | undefined,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer,
@@ -193,7 +281,7 @@ async function databaseArchiveFetcher(
if (isFile(databaseUrl)) {
await readAndUnzip(databaseUrl, unzipPath, cli, progress);
} else {
await fetchAndUnzip(databaseUrl, unzipPath, cli, progress);
await fetchAndUnzip(databaseUrl, requestHeaders, unzipPath, cli, progress);
}
progress({
@@ -216,7 +304,7 @@ async function databaseArchiveFetcher(
});
await ensureZippedSourceLocation(dbPath);
const item = await databaseManager.openDatabase(progress, token, Uri.file(dbPath));
const item = await databaseManager.openDatabase(progress, token, Uri.file(dbPath), nameOverride);
await databaseManager.setCurrentDatabaseItem(item);
return item;
} else {
@@ -292,6 +380,7 @@ async function readAndUnzip(
async function fetchAndUnzip(
databaseUrl: string,
requestHeaders: { [key: string]: string },
unzipPath: string,
cli?: CodeQLCliServer,
progress?: ProgressCallback
@@ -310,7 +399,10 @@ async function fetchAndUnzip(
step: 1,
});
const response = await checkForFailingResponse(await fetch(databaseUrl), 'Error downloading database');
const response = await checkForFailingResponse(
await fetch(databaseUrl, { headers: requestHeaders }),
'Error downloading database'
);
const archiveFileStream = fs.createWriteStream(archivePath);
const contentLength = response.headers.get('content-length');
@@ -325,7 +417,6 @@ async function fetchAndUnzip(
await readAndUnzip(Uri.file(archivePath).toString(true), unzipPath, cli, progress);
// remove archivePath eagerly since these archives can be large.
await fs.remove(archivePath);
}
@@ -381,6 +472,89 @@ export async function findDirWithFile(
return;
}
/**
* The URL pattern is https://github.com/{owner}/{name}/{subpages}.
*
* This function accepts any URL that matches the pattern above. It also accepts just the
* name with owner (NWO): `<owner>/<repo>`.
*
* @param githubRepo The GitHub repository URL or NWO
*
* @return true if this looks like a valid GitHub repository URL or NWO
*/
export function looksLikeGithubRepo(
githubRepo: string | undefined
): githubRepo is string {
if (!githubRepo) {
return false;
}
if (REPO_REGEX.test(githubRepo) || convertGitHubUrlToNwo(githubRepo)) {
return true;
}
return false;
}
/**
* Converts a GitHub repository URL to the corresponding NWO.
* @param githubUrl The GitHub repository URL
* @return The corresponding NWO, or undefined if the URL is not valid
*/
function convertGitHubUrlToNwo(githubUrl: string): string | undefined {
try {
const uri = Uri.parse(githubUrl, true);
if (uri.scheme !== 'https') {
return;
}
if (uri.authority !== 'github.com' && uri.authority !== 'www.github.com') {
return;
}
const paths = uri.path.split('/').filter((segment: string) => segment);
const nwo = `${paths[0]}/${paths[1]}`;
if (REPO_REGEX.test(nwo)) {
return nwo;
}
return;
} catch (e) {
// Ignore the error here, since we catch failures at a higher level.
// In particular: returning undefined leads to an error in 'promptImportGithubDatabase'.
return;
}
}
export async function convertGithubNwoToDatabaseUrl(
githubRepo: string,
credentials: Credentials,
progress: ProgressCallback): Promise<{
databaseUrl: string,
owner: string,
name: string
} | undefined> {
try {
const nwo = convertGitHubUrlToNwo(githubRepo) || githubRepo;
const [owner, repo] = nwo.split('/');
const octokit = await credentials.getOctokit();
const response = await octokit.request('GET /repos/:owner/:repo/code-scanning/codeql/databases', { owner, repo });
const languages = response.data.map((db: any) => db.language);
const language = await promptForLanguage(languages, progress);
if (!language) {
return;
}
return {
databaseUrl: `https://api.github.com/repos/${owner}/${repo}/code-scanning/codeql/databases/${language}`,
owner,
name: repo
};
} catch (e) {
void logger.log(`Error: ${getErrorMessage(e)}`);
throw new Error(`Unable to get database for '${githubRepo}'`);
}
}
/**
* The URL pattern is https://lgtm.com/projects/{provider}/{org}/{name}/{irrelevant-subpages}.
* There are several possibilities for the provider: in addition to GitHub.com (g),
@@ -416,7 +590,7 @@ export function looksLikeLgtmUrl(lgtmUrl: string | undefined): lgtmUrl is string
return false;
}
const paths = uri.path.split('/').filter((segment) => segment);
const paths = uri.path.split('/').filter((segment: string) => segment);
return paths.length >= 4 && paths[0] === 'projects';
} catch (e) {
return false;
@@ -446,7 +620,7 @@ function extractProjectSlug(lgtmUrl: string): string | undefined {
}
// exported for testing
export async function convertToDatabaseUrl(
export async function convertLgtmUrlToDatabaseUrl(
lgtmUrl: string,
progress: ProgressCallback) {
try {
@@ -467,7 +641,9 @@ export async function convertToDatabaseUrl(
}
}
const language = await promptForLanguage(projectJson, progress);
const languages = projectJson?.languages?.map((lang: { language: string }) => lang.language) || [];
const language = await promptForLanguage(languages, progress);
if (!language) {
return;
}
@@ -479,7 +655,7 @@ export async function convertToDatabaseUrl(
language,
].join('/')}`;
} catch (e) {
void logger.log(`Error: ${e.message}`);
void logger.log(`Error: ${getErrorMessage(e)}`);
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
}
}
@@ -487,7 +663,7 @@ export async function convertToDatabaseUrl(
async function downloadLgtmProjectMetadata(lgtmUrl: string): Promise<any> {
const uri = Uri.parse(lgtmUrl, true);
const paths = ['api', 'v1.0'].concat(
uri.path.split('/').filter((segment) => segment)
uri.path.split('/').filter((segment: string) => segment)
).slice(0, 6);
const projectUrl = `https://lgtm.com/${paths.join('/')}`;
const projectResponse = await fetch(projectUrl);
@@ -495,7 +671,7 @@ async function downloadLgtmProjectMetadata(lgtmUrl: string): Promise<any> {
}
async function promptForLanguage(
projectJson: any,
languages: string[],
progress: ProgressCallback
): Promise<string | undefined> {
progress({
@@ -503,17 +679,19 @@ async function promptForLanguage(
step: 2,
maxStep: 2
});
if (!projectJson?.languages?.length) {
return;
if (!languages.length) {
throw new Error('No databases found');
}
if (projectJson.languages.length === 1) {
return projectJson.languages[0].language;
if (languages.length === 1) {
return languages[0];
}
return await window.showQuickPick(
projectJson.languages.map((lang: { language: string }) => lang.language), {
placeHolder: 'Select the database language to download:'
}
languages,
{
placeHolder: 'Select the database language to download:',
ignoreFocusOut: true,
}
);
}

View File

@@ -33,11 +33,13 @@ import * as qsClient from './queryserver-client';
import { upgradeDatabaseExplicit } from './upgrades';
import {
importArchiveDatabase,
promptImportGithubDatabase,
promptImportInternetDatabase,
promptImportLgtmDatabase,
} from './databaseFetcher';
import { CancellationToken } from 'vscode';
import { asyncFilter } from './pure/helpers-pure';
import { asyncFilter, getErrorMessage } from './pure/helpers-pure';
import { Credentials } from './authentication';
type ThemableIconPath = { light: string; dark: string } | string;
@@ -219,7 +221,8 @@ export class DatabaseUI extends DisposableObject {
private databaseManager: DatabaseManager,
private readonly queryServer: qsClient.QueryServerClient | undefined,
private readonly storagePath: string,
readonly extensionPath: string
readonly extensionPath: string,
private readonly getCredentials: () => Promise<Credentials>
) {
super();
@@ -291,6 +294,20 @@ export class DatabaseUI extends DisposableObject {
}
)
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseGithub',
async (
progress: ProgressCallback,
token: CancellationToken
) => {
const credentials = await this.getCredentials();
await this.handleChooseDatabaseGithub(credentials, progress, token);
},
{
title: 'Adding database from GitHub',
})
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseLgtm',
@@ -376,7 +393,7 @@ export class DatabaseUI extends DisposableObject {
try {
return await this.chooseAndSetDatabase(true, progress, token);
} catch (e) {
void showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(getErrorMessage(e));
return undefined;
}
};
@@ -444,7 +461,7 @@ export class DatabaseUI extends DisposableObject {
try {
return await this.chooseAndSetDatabase(false, progress, token);
} catch (e) {
void showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(getErrorMessage(e));
return undefined;
}
};
@@ -462,6 +479,21 @@ export class DatabaseUI extends DisposableObject {
);
};
handleChooseDatabaseGithub = async (
credentials: Credentials,
progress: ProgressCallback,
token: CancellationToken
): Promise<DatabaseItem | undefined> => {
return await promptImportGithubDatabase(
this.databaseManager,
this.storagePath,
credentials,
progress,
token,
this.queryServer?.cliServer
);
};
handleChooseDatabaseLgtm = async (
progress: ProgressCallback,
token: CancellationToken
@@ -590,8 +622,7 @@ export class DatabaseUI extends DisposableObject {
} catch (e) {
// rethrow and let this be handled by default error handling.
throw new Error(
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${e.message
}`
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${getErrorMessage(e)}`
);
}
};

View File

@@ -19,6 +19,7 @@ import { DisposableObject } from './pure/disposable-object';
import { Logger, logger } from './logging';
import { registerDatabases, Dataset, deregisterDatabases } from './pure/messages';
import { QueryServerClient } from './queryserver-client';
import { getErrorMessage } from './pure/helpers-pure';
/**
* databases.ts
@@ -147,7 +148,7 @@ export async function findSourceArchive(
}
async function resolveDatabase(
databasePath: string
databasePath: string,
): Promise<DatabaseContents> {
const name = path.basename(databasePath);
@@ -169,7 +170,9 @@ async function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {
return await glob('*.dbscheme', { cwd: dbDirectory });
}
async function resolveDatabaseContents(uri: vscode.Uri): Promise<DatabaseContents> {
async function resolveDatabaseContents(
uri: vscode.Uri,
): Promise<DatabaseContents> {
if (uri.scheme !== 'file') {
throw new Error(`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`);
}
@@ -359,7 +362,7 @@ export class DatabaseItemImpl implements DatabaseItem {
}
catch (e) {
this._contents = undefined;
this._error = e;
this._error = e instanceof Error ? e : new Error(String(e));
throw e;
}
}
@@ -568,14 +571,15 @@ export class DatabaseManager extends DisposableObject {
progress: ProgressCallback,
token: vscode.CancellationToken,
uri: vscode.Uri,
displayName?: string
): Promise<DatabaseItem> {
const contents = await resolveDatabaseContents(uri);
// Ignore the source archive for QLTest databases by default.
const isQLTestDatabase = path.extname(uri.fsPath) === '.testproj';
const fullOptions: FullDatabaseOptions = {
ignoreSourceArchive: isQLTestDatabase,
// displayName is only set if a user explicitly renames a database
displayName: undefined,
// If a displayName is not passed in, the basename of folder containing the database is used.
displayName,
dateAdded: Date.now(),
language: await this.getPrimaryLanguage(uri.fsPath)
};
@@ -726,7 +730,7 @@ export class DatabaseManager extends DisposableObject {
}
} catch (e) {
// database list had an unexpected type - nothing to be done?
void showAndLogErrorMessage(`Database list loading failed: ${e.message}`);
void showAndLogErrorMessage(`Database list loading failed: ${getErrorMessage(e)}`);
}
});
}
@@ -841,7 +845,7 @@ export class DatabaseManager extends DisposableObject {
void logger.log('Deleting database from filesystem.');
fs.remove(item.databaseUri.fsPath).then(
() => void logger.log(`Deleted '${item.databaseUri.fsPath}'`),
e => void logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${e.message}`));
e => void logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${getErrorMessage(e)}`));
}
// note that we use undefined as the item in order to reset the entire tree

View File

@@ -1,3 +1,4 @@
import 'source-map-support/register';
import {
CancellationToken,
CancellationTokenSource,
@@ -65,7 +66,7 @@ import {
showInformationMessageWithAction,
tmpDir
} from './helpers';
import { assertNever } from './pure/helpers-pure';
import { asError, assertNever, getErrorMessage } from './pure/helpers-pure';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager } from './interface';
import { WebviewReveal } from './interface-utils';
@@ -93,11 +94,9 @@ import { Credentials } from './authentication';
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
import { RemoteQueryResult } from './remote-queries/remote-query-result';
import { URLSearchParams } from 'url';
import { RemoteQueriesInterfaceManager } from './remote-queries/remote-queries-interface';
import * as sampleData from './remote-queries/sample-data';
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
import { AnalysesResultsManager } from './remote-queries/analyses-results-manager';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
import { HistoryItemLabelProvider } from './history-item-label-provider';
/**
* extension.ts
@@ -436,7 +435,8 @@ async function activateWithInstalledDistribution(
dbm,
qs,
getContextStoragePath(ctx),
ctx.extensionPath
ctx.extensionPath,
() => Credentials.initialize(ctx),
);
databaseUI.init();
ctx.subscriptions.push(databaseUI);
@@ -448,6 +448,7 @@ async function activateWithInstalledDistribution(
showResultsForCompletedQuery(item, WebviewReveal.Forced);
const queryStorageDir = path.join(ctx.globalStorageUri.fsPath, 'queries');
await fs.ensureDir(queryStorageDir);
const labelProvider = new HistoryItemLabelProvider(queryHistoryConfigurationListener);
void logger.log('Initializing query history.');
const qhm = new QueryHistoryManager(
@@ -456,6 +457,7 @@ async function activateWithInstalledDistribution(
queryStorageDir,
ctx,
queryHistoryConfigurationListener,
labelProvider,
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
showResultsForComparison(from, to),
);
@@ -467,8 +469,9 @@ async function activateWithInstalledDistribution(
});
ctx.subscriptions.push(qhm);
void logger.log('Initializing results panel interface.');
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger, labelProvider);
ctx.subscriptions.push(intm);
void logger.log('Initializing compare panel interface.');
@@ -477,6 +480,7 @@ async function activateWithInstalledDistribution(
dbm,
cliServer,
queryServerLogger,
labelProvider,
showResults
);
ctx.subscriptions.push(cmpm);
@@ -491,7 +495,7 @@ async function activateWithInstalledDistribution(
try {
await cmpm.showResults(from, to);
} catch (e) {
void showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(getErrorMessage(e));
}
}
@@ -526,7 +530,7 @@ async function activateWithInstalledDistribution(
token.onCancellationRequested(() => source.cancel());
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
const item = new LocalQueryInfo(initialInfo, queryHistoryConfigurationListener, source);
const item = new LocalQueryInfo(initialInfo, source);
qhm.addQuery(item);
try {
const completedQueryInfo = await compileAndRunQueryAgainstDatabase(
@@ -537,14 +541,17 @@ async function activateWithInstalledDistribution(
queryStorageDir,
progress,
source.token,
undefined,
item,
);
item.completeThisQuery(completedQueryInfo);
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.NotForced);
// 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) {
e.message = `Error running query: ${e.message}`;
item.failureReason = e.message;
const err = asError(e);
err.message = `Error running query: ${err.message}`;
item.failureReason = err.message;
throw e;
} finally {
await qhm.refreshTreeView();
@@ -569,11 +576,11 @@ async function activateWithInstalledDistribution(
try {
await cliServer.generateQueryHelp(pathToQhelp, absolutePathToMd);
await commands.executeCommand('markdown.showPreviewToSide', uri);
} catch (err) {
const errorMessage = err.message.includes('Generating qhelp in markdown') ? (
} catch (e) {
const errorMessage = getErrorMessage(e).includes('Generating qhelp in markdown') ? (
`Could not generate markdown from ${pathToQhelp}: Bad formatting in .qhelp file.`
) : `Could not open a preview of the generated file (${absolutePathToMd}).`;
void showAndLogErrorMessage(errorMessage, { fullMessage: `${errorMessage}\n${err}` });
void showAndLogErrorMessage(errorMessage, { fullMessage: `${errorMessage}\n${e}` });
}
}
@@ -696,9 +703,9 @@ async function activateWithInstalledDistribution(
for (const item of quickpick) {
try {
await compileAndRunQuery(false, uri, progress, token, item.databaseItem);
} catch (error) {
} catch (e) {
skippedDatabases.push(item.label);
errors.push(error.message);
errors.push(getErrorMessage(e));
}
}
if (skippedDatabases.length > 0) {
@@ -836,7 +843,7 @@ async function activateWithInstalledDistribution(
)
);
void logger.log('Initializing remote queries interface.');
void logger.log('Initializing variant analysis results view.');
const rqm = new RemoteQueriesManager(ctx, cliServer, qhm, queryStorageDir, logger);
ctx.subscriptions.push(rqm);
@@ -847,9 +854,9 @@ async function activateWithInstalledDistribution(
registerRemoteQueryTextProvider();
// The "runRemoteQuery" command is internal-only.
// The "runVariantAnalysis" command is internal-only.
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.runRemoteQuery', async (
commandRunnerWithProgress('codeQL.runVariantAnalysis', async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
@@ -866,10 +873,10 @@ async function activateWithInstalledDistribution(
token
);
} else {
throw new Error('Remote queries require the CodeQL Canary version to run.');
throw new Error('Variant analysis requires the CodeQL Canary version to run.');
}
}, {
title: 'Run Remote Query',
title: 'Run Variant Analysis',
cancellable: true
})
);
@@ -888,17 +895,6 @@ async function activateWithInstalledDistribution(
await rqm.autoDownloadRemoteQueryResults(queryResult, token);
}));
ctx.subscriptions.push(
commandRunner('codeQL.showFakeRemoteQueryResults', async () => {
const analysisResultsManager = new AnalysesResultsManager(ctx, queryStorageDir, logger);
const rqim = new RemoteQueriesInterfaceManager(ctx, logger, analysisResultsManager);
await rqim.showResults(sampleData.sampleRemoteQuery, sampleData.sampleRemoteQueryResult);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage1);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage2);
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage3);
}));
ctx.subscriptions.push(
commandRunner(
'codeQL.openReferencedFile',
@@ -945,6 +941,18 @@ async function activateWithInstalledDistribution(
title: 'Choose a Database from an Archive'
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseGithub', async (
progress: ProgressCallback,
token: CancellationToken
) => {
const credentials = await Credentials.initialize(ctx);
await databaseUI.handleChooseDatabaseGithub(credentials, progress, token);
},
{
title: 'Adding database from GitHub',
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', (
progress: ProgressCallback,

View File

@@ -76,9 +76,10 @@ export async function showAndLogWarningMessage(message: string, {
*/
export async function showAndLogInformationMessage(message: string, {
outputLogger = logger,
items = [] as string[]
items = [] as string[],
fullMessage = ''
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showInformationMessage);
return internalShowAndLog(message, items, outputLogger, Window.showInformationMessage, fullMessage);
}
type ShowMessageFn = (message: string, ...items: string[]) => Thenable<string | undefined>;

View File

@@ -0,0 +1,82 @@
import { env } from 'vscode';
import * as path from 'path';
import { QueryHistoryConfig } from './config';
import { LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
interface InterpolateReplacements {
t: string; // Start time
q: string; // Query name
d: string; // Database/Controller repo name
r: string; // Result count/Empty
s: string; // Status
f: string; // Query file name
'%': '%'; // Percent sign
}
export class HistoryItemLabelProvider {
constructor(private config: QueryHistoryConfig) {
/**/
}
getLabel(item: QueryHistoryInfo) {
const replacements = item.t === 'local'
? this.getLocalInterpolateReplacements(item)
: this.getRemoteInterpolateReplacements(item);
const rawLabel = item.userSpecifiedLabel ?? (this.config.format || '%q');
return this.interpolate(rawLabel, replacements);
}
/**
* If there is a user-specified label for this query, interpolate and use that.
* Otherwise, use the raw name of this query.
*
* @returns the name of the query, unless there is a custom label for this query.
*/
getShortLabel(item: QueryHistoryInfo): string {
return item.userSpecifiedLabel
? this.getLabel(item)
: item.t === 'local'
? item.getQueryName()
: item.remoteQuery.queryName;
}
private interpolate(rawLabel: string, replacements: InterpolateReplacements): string {
return rawLabel.replace(/%(.)/g, (match, key: keyof InterpolateReplacements) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
private getLocalInterpolateReplacements(item: LocalQueryInfo): InterpolateReplacements {
const { resultCount = 0, statusString = 'in progress' } = item.completedQuery || {};
return {
t: item.startTime,
q: item.getQueryName(),
d: item.initialInfo.databaseInfo.name,
r: `${resultCount} results`,
s: statusString,
f: item.getQueryFileName(),
'%': '%',
};
}
private getRemoteInterpolateReplacements(item: RemoteQueryHistoryItem): InterpolateReplacements {
return {
t: new Date(item.remoteQuery.executionStartTime).toLocaleString(env.language),
q: item.remoteQuery.queryName,
// There is no database name for remote queries. Instead use the controller repository name.
d: `${item.remoteQuery.controllerRepository.owner}/${item.remoteQuery.controllerRepository.name}`,
// There is no synchronous way to get the results count.
r: '',
s: item.status,
f: path.basename(item.remoteQuery.queryFilePath),
'%': '%'
};
}
}

View File

@@ -15,7 +15,7 @@ import * as cli from './cli';
import { CodeQLCliServer } from './cli';
import { DatabaseEventKind, DatabaseItem, DatabaseManager } from './databases';
import { showAndLogErrorMessage, tmpDir } from './helpers';
import { assertNever } from './pure/helpers-pure';
import { assertNever, getErrorMessage, getErrorStack } from './pure/helpers-pure';
import {
FromResultsViewMsg,
Interpretation,
@@ -49,6 +49,7 @@ import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-type
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
import { PAGE_SIZE } from './config';
import { CompletedLocalQueryInfo } from './query-results';
import { HistoryItemLabelProvider } from './history-item-label-provider';
/**
* interface.ts
@@ -136,7 +137,8 @@ export class InterfaceManager extends DisposableObject {
public ctx: vscode.ExtensionContext,
private databaseManager: DatabaseManager,
public cliServer: CodeQLCliServer,
public logger: Logger
public logger: Logger,
private labelProvider: HistoryItemLabelProvider
) {
super();
this.push(this._diagnosticCollection);
@@ -353,8 +355,8 @@ export class InterfaceManager extends DisposableObject {
assertNever(msg);
}
} catch (e) {
void showAndLogErrorMessage(e.message, {
fullMessage: e.stack
void showAndLogErrorMessage(getErrorMessage(e), {
fullMessage: getErrorStack(e)
});
}
}
@@ -416,7 +418,7 @@ export class InterfaceManager extends DisposableObject {
// more asynchronous message to not so abruptly interrupt
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const queryName = fullQuery.getShortLabel();
const queryName = this.labelProvider.getShortLabel(fullQuery);
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''
}.`,
@@ -483,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
database: fullQuery.initialInfo.databaseInfo,
shouldKeepOldResultsWhileRendering,
metadata: fullQuery.completedQuery.query.metadata,
queryName: fullQuery.label,
queryName: this.labelProvider.getLabel(fullQuery),
queryPath: fullQuery.initialInfo.queryPath
});
}
@@ -516,7 +518,7 @@ export class InterfaceManager extends DisposableObject {
resultSetNames,
pageSize: interpretedPageSize(this._interpretation),
numPages: numInterpretedPages(this._interpretation),
queryName: this._displayedQuery.label,
queryName: this.labelProvider.getLabel(this._displayedQuery),
queryPath: this._displayedQuery.initialInfo.queryPath
});
}
@@ -601,7 +603,7 @@ export class InterfaceManager extends DisposableObject {
database: results.initialInfo.databaseInfo,
shouldKeepOldResultsWhileRendering: false,
metadata: results.completedQuery.query.metadata,
queryName: results.label,
queryName: this.labelProvider.getLabel(results),
queryPath: results.initialInfo.queryPath
});
}
@@ -729,7 +731,7 @@ export class InterfaceManager extends DisposableObject {
// If interpretation fails, accept the error and continue
// trying to render uninterpreted results anyway.
void showAndLogErrorMessage(
`Showing raw results instead of interpreted ones due to an error. ${e.message}`
`Showing raw results instead of interpreted ones due to an error. ${getErrorMessage(e)}`
);
}
}
@@ -768,9 +770,8 @@ export class InterfaceManager extends DisposableObject {
try {
await this.showProblemResultsAsDiagnostics(interpretation, database);
} catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
void this.logger.log(
`Exception while computing problem results as diagnostics: ${msg}`
`Exception while computing problem results as diagnostics: ${getErrorMessage(e)}`
);
this._diagnosticCollection.clear();
}

View File

@@ -79,11 +79,11 @@ export interface WholeFileLocation {
export type ResolvableLocationValue = WholeFileLocation | LineColumnLocation;
export type UrlValue = ResolvableLocationValue | string;
export type UrlValue = ResolvableLocationValue | string;
export type ColumnValue = EntityValue | number | string | boolean;
export type CellValue = EntityValue | number | string | boolean;
export type ResultRow = ColumnValue[];
export type ResultRow = CellValue[];
export interface RawResultSet {
readonly schema: ResultSetSchema;
@@ -104,6 +104,6 @@ export function transformBqrsResultSet(
}
export interface DecodedBqrsChunk {
tuples: ColumnValue[][];
tuples: CellValue[][];
next?: number;
}

View File

@@ -4,6 +4,7 @@ import {
LineColumnLocation,
WholeFileLocation
} from './bqrs-cli-types';
import { createRemoteFileRef } from './location-link-utils';
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
@@ -93,3 +94,30 @@ export function isWholeFileLoc(loc: UrlValue): loc is WholeFileLocation {
export function isStringLoc(loc: UrlValue): loc is string {
return typeof loc === 'string';
}
export function tryGetRemoteLocation(
loc: UrlValue | undefined,
fileLinkPrefix: string
): string | undefined {
const resolvableLocation = tryGetResolvableLocation(loc);
if (!resolvableLocation) {
return undefined;
}
// Remote locations have the following format:
// file:/home/runner/work/<repo>/<repo/relative/path/to/file
// So we need to drop the first 6 parts of the path.
// TODO: We can make this more robust to other path formats.
const locationParts = resolvableLocation.uri.split('/');
const trimmedLocation = locationParts.slice(6, locationParts.length).join('/');
const fileLink = {
fileLinkPrefix,
filePath: trimmedLocation,
};
return createRemoteFileRef(
fileLink,
resolvableLocation.startLine,
resolvableLocation.endLine);
}

View File

@@ -35,3 +35,22 @@ export const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
export const ONE_HOUR_IN_MS = 1000 * 60 * 60;
export const TWO_HOURS_IN_MS = 1000 * 60 * 60 * 2;
export const THREE_HOURS_IN_MS = 1000 * 60 * 60 * 3;
/**
* This regex matches strings of the form `owner/repo` where:
* - `owner` is made up of alphanumeric characters or single hyphens, starting and ending in an alphanumeric character
* - `repo` is made up of alphanumeric characters, hyphens, or underscores
*/
export const REPO_REGEX = /^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+\/[a-zA-Z0-9-_]+$/;
export function getErrorMessage(e: any) {
return e instanceof Error ? e.message : String(e);
}
export function getErrorStack(e: any) {
return e instanceof Error ? e.stack ?? '' : '';
}
export function asError(e: any): Error {
return e instanceof Error ? e : new Error(String(e));
}

View File

@@ -394,8 +394,7 @@ export type FromRemoteQueriesMessage =
| OpenFileMsg
| OpenVirtualFileMsg
| RemoteQueryDownloadAnalysisResultsMessage
| RemoteQueryDownloadAllAnalysesResultsMessage
| RemoteQueryViewAnalysisResultsMessage;
| RemoteQueryDownloadAllAnalysesResultsMessage;
export type ToRemoteQueriesMessage =
| SetRemoteQueryResultMessage
@@ -430,7 +429,3 @@ export interface RemoteQueryDownloadAllAnalysesResultsMessage {
analysisSummaries: AnalysisSummary[];
}
export interface RemoteQueryViewAnalysisResultsMessage {
t: 'remoteQueryViewAnalysisResults';
analysisSummary: AnalysisSummary
}

View File

@@ -0,0 +1,15 @@
import { FileLink } from '../remote-queries/shared/analysis-result';
export function createRemoteFileRef(
fileLink: FileLink,
startLine?: number,
endLine?: number
): string {
if (startLine && endLine) {
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}#L${startLine}-L${endLine}`;
} else if (startLine) {
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}#L${startLine}`;
} else {
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}`;
}
}

View File

@@ -646,6 +646,35 @@ export interface ClearCacheParams {
*/
dryRun: boolean;
}
/**
* Parameters to start a new structured log
*/
export interface StartLogParams {
/**
* The dataset for which we want to start a new structured log
*/
db: Dataset;
/**
* The path where we want to place the new structured log
*/
logPath: string;
}
/**
* Parameters to terminate a structured log
*/
export interface EndLogParams {
/**
* The dataset for which we want to terminated the log
*/
db: Dataset;
/**
* The path of the log to terminate, will be a no-op if we aren't logging here
*/
logPath: string;
}
/**
* Parameters for trimming the cache of a dataset
*/
@@ -682,6 +711,26 @@ export interface ClearCacheResult {
deletionMessage: string;
}
/**
* The result of starting a new structured log.
*/
export interface StartLogResult {
/**
* A user friendly message saying what happened.
*/
outcomeMessage: string;
}
/**
* The result of terminating a structured log.
*/
export interface EndLogResult {
/**
* A user friendly message saying what happened.
*/
outcomeMessage: string;
}
/**
* Parameters for running a set of queries
*/
@@ -1018,6 +1067,16 @@ export const compileUpgrade = new rpc.RequestType<WithProgressId<CompileUpgradeP
*/
export const compileUpgradeSequence = new rpc.RequestType<WithProgressId<CompileUpgradeSequenceParams>, CompileUpgradeSequenceResult, void, void>('compilation/compileUpgradeSequence');
/**
* Start a new structured log in the evaluator, terminating the previous one if it exists
*/
export const startLog = new rpc.RequestType<WithProgressId<StartLogParams>, StartLogResult, void, void>('evaluation/startLog');
/**
* Terminate a structured log in the evaluator. Is a no-op if we aren't logging to the given location
*/
export const endLog = new rpc.RequestType<WithProgressId<EndLogParams>, EndLogResult, void, void>('evaluation/endLog');
/**
* Clear the cache of a dataset
*/

View File

@@ -1,4 +1,5 @@
import * as Sarif from 'sarif';
import { HighlightedRegion } from '../remote-queries/shared/analysis-result';
import { ResolvableLocationValue } from './bqrs-cli-types';
export interface SarifLink {
@@ -127,35 +128,111 @@ export function parseSarifLocation(
userVisibleFile
} as ParsedSarifLocation;
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const startLine = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const endLine = region.endLine === undefined ? startLine : region.endLine;
const startColumn = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const endColumn = region.endColumn! - 1;
const region = parseSarifRegion(physicalLocation.region);
return {
uri: effectiveLocation,
userVisibleFile,
startLine,
startColumn,
endLine,
endColumn,
...region
};
}
}
export function parseSarifRegion(
region: Sarif.Region
): {
startLine: number,
endLine: number,
startColumn: number,
endColumn: number
} {
// The SARIF we're given should have a startLine, but we
// fall back to 1, just in case something has gone wrong.
const startLine = region.startLine ?? 1;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const endLine = region.endLine === undefined ? startLine : region.endLine;
const startColumn = region.startColumn === undefined ? 1 : region.startColumn;
// Our tools should always supply `endColumn` field, which is fortunate, since
// the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code. We fall back to 1,
// just in case something has gone wrong.
//
// It is off by one with respect to the way vscode counts columns in selections.
const endColumn = (region.endColumn ?? 1) - 1;
return {
startLine,
startColumn,
endLine,
endColumn
};
}
export function isNoLocation(loc: ParsedSarifLocation): loc is NoLocation {
return 'hint' in loc;
}
// Some helpers for highlighting specific regions from a SARIF code snippet
/**
* Checks whether a particular line (determined by its line number in the original file)
* is part of the highlighted region of a SARIF code snippet.
*/
export function shouldHighlightLine(
lineNumber: number,
highlightedRegion: HighlightedRegion
): boolean {
if (lineNumber < highlightedRegion.startLine) {
return false;
}
if (highlightedRegion.endLine == undefined) {
return lineNumber == highlightedRegion.startLine;
}
return lineNumber <= highlightedRegion.endLine;
}
/**
* A line of code split into: plain text before the highlighted section, the highlighted
* text itself, and plain text after the highlighted section.
*/
export interface PartiallyHighlightedLine {
plainSection1: string;
highlightedSection: string;
plainSection2: string;
}
/**
* Splits a line of code into the highlighted and non-highlighted sections.
*/
export function parseHighlightedLine(
line: string,
lineNumber: number,
highlightedRegion: HighlightedRegion
): PartiallyHighlightedLine {
const isSingleLineHighlight = highlightedRegion.endLine === undefined;
const isFirstHighlightedLine = lineNumber === highlightedRegion.startLine;
const isLastHighlightedLine = lineNumber === highlightedRegion.endLine;
const highlightStartColumn = isSingleLineHighlight
? highlightedRegion.startColumn
: isFirstHighlightedLine
? highlightedRegion.startColumn
: 0;
const highlightEndColumn = isSingleLineHighlight
? highlightedRegion.endColumn
: isLastHighlightedLine
? highlightedRegion.endColumn
: line.length + 1;
const plainSection1 = line.substring(0, highlightStartColumn - 1);
const highlightedSection = line.substring(highlightStartColumn - 1, highlightEndColumn - 1);
const plainSection2 = line.substring(highlightEndColumn - 1, line.length);
return { plainSection1, highlightedSection, plainSection2 };
}

View File

@@ -28,12 +28,17 @@ import { URLSearchParams } from 'url';
import { QueryServerClient } from './queryserver-client';
import { DisposableObject } from './pure/disposable-object';
import { commandRunner } from './commandRunner';
import { assertNever, ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from './pure/helpers-pure';
import { assertNever, ONE_HOUR_IN_MS, TWO_HOURS_IN_MS, getErrorMessage, getErrorStack } from './pure/helpers-pure';
import { CompletedLocalQueryInfo, LocalQueryInfo as LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { DatabaseManager } from './databases';
import { registerQueryHistoryScubber } from './query-history-scrubber';
import { QueryStatus } from './query-status';
import { slurpQueryHistory, splatQueryHistory } from './query-serialization';
import * as fs from 'fs-extra';
import { CliVersionConstraint } from './cli';
import { HistoryItemLabelProvider } from './history-item-label-provider';
import { Credentials } from './authentication';
import { cancelRemoteQuery } from './remote-queries/gh-actions-api-client';
/**
* query-history.ts
@@ -119,7 +124,10 @@ export class HistoryTreeDataProvider extends DisposableObject {
private current: QueryHistoryInfo | undefined;
constructor(extensionPath: string) {
constructor(
extensionPath: string,
private readonly labelProvider: HistoryItemLabelProvider,
) {
super();
this.failedIconPath = path.join(
extensionPath,
@@ -136,13 +144,13 @@ export class HistoryTreeDataProvider extends DisposableObject {
}
async getTreeItem(element: QueryHistoryInfo): Promise<TreeItem> {
const treeItem = new TreeItem(element.label);
const treeItem = new TreeItem(this.labelProvider.getLabel(element));
treeItem.command = {
title: 'Query History Item',
command: 'codeQLQueryHistory.itemClicked',
arguments: [element],
tooltip: element.failureReason || element.label
tooltip: element.failureReason || this.labelProvider.getLabel(element)
};
// Populate the icon and the context value. We use the context value to
@@ -181,38 +189,48 @@ export class HistoryTreeDataProvider extends DisposableObject {
): ProviderResult<QueryHistoryInfo[]> {
return element ? [] : this.history.sort((h1, h2) => {
// TODO remote queries are not implemented yet.
if (h1.t !== 'local' && h2.t !== 'local') {
return 0;
}
if (h1.t !== 'local') {
return -1;
}
if (h2.t !== 'local') {
return 1;
}
const h1Label = this.labelProvider.getLabel(h1).toLowerCase();
const h2Label = this.labelProvider.getLabel(h2).toLowerCase();
const resultCount1 = h1.completedQuery?.resultCount ?? -1;
const resultCount2 = h2.completedQuery?.resultCount ?? -1;
const h1Date = h1.t === 'local'
? h1.initialInfo.start.getTime()
: h1.remoteQuery?.executionStartTime;
const h2Date = h2.t === 'local'
? h2.initialInfo.start.getTime()
: h2.remoteQuery?.executionStartTime;
// result count for remote queries is not available here.
const resultCount1 = h1.t === 'local'
? h1.completedQuery?.resultCount ?? -1
: -1;
const resultCount2 = h2.t === 'local'
? h2.completedQuery?.resultCount ?? -1
: -1;
switch (this.sortOrder) {
case SortOrder.NameAsc:
return h1.label.localeCompare(h2.label, env.language);
return h1Label.localeCompare(h2Label, env.language);
case SortOrder.NameDesc:
return h2.label.localeCompare(h1.label, env.language);
return h2Label.localeCompare(h1Label, env.language);
case SortOrder.DateAsc:
return h1.initialInfo.start.getTime() - h2.initialInfo.start.getTime();
return h1Date - h2Date;
case SortOrder.DateDesc:
return h2.initialInfo.start.getTime() - h1.initialInfo.start.getTime();
return h2Date - h1Date;
case SortOrder.CountAsc:
// If the result counts are equal, sort by name.
return resultCount1 - resultCount2 === 0
? h1.label.localeCompare(h2.label, env.language)
? h1Label.localeCompare(h2Label, env.language)
: resultCount1 - resultCount2;
case SortOrder.CountDesc:
// If the result counts are equal, sort by name.
return resultCount2 - resultCount1 === 0
? h2.label.localeCompare(h1.label, env.language)
? h2Label.localeCompare(h1Label, env.language)
: resultCount2 - resultCount1;
default:
assertNever(this.sortOrder);
@@ -301,12 +319,13 @@ export class QueryHistoryManager extends DisposableObject {
._onWillOpenQueryItem.event;
constructor(
private qs: QueryServerClient,
private dbm: DatabaseManager,
private queryStorageDir: string,
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
private doCompareCallback: (
private readonly qs: QueryServerClient,
private readonly dbm: DatabaseManager,
private readonly queryStorageDir: string,
private readonly ctx: ExtensionContext,
private readonly queryHistoryConfigListener: QueryHistoryConfig,
private readonly labelProvider: HistoryItemLabelProvider,
private readonly doCompareCallback: (
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo
) => Promise<void>
@@ -320,7 +339,8 @@ export class QueryHistoryManager extends DisposableObject {
this.queryMetadataStorageLocation = path.join((ctx.storageUri || ctx.globalStorageUri).fsPath, WORKSPACE_QUERY_HISTORY_FILE);
this.treeDataProvider = this.push(new HistoryTreeDataProvider(
ctx.extensionPath
ctx.extensionPath,
this.labelProvider
));
this.treeView = this.push(window.createTreeView('codeQLQueryHistory', {
treeDataProvider: this.treeDataProvider,
@@ -406,6 +426,18 @@ export class QueryHistoryManager extends DisposableObject {
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.cancel',
@@ -488,6 +520,10 @@ export class QueryHistoryManager extends DisposableObject {
this.registerQueryHistoryScrubber(queryHistoryConfigListener, ctx);
}
private getCredentials() {
return Credentials.initialize(this.ctx);
}
/**
* Register and create the history scrubber.
*/
@@ -507,7 +543,7 @@ export class QueryHistoryManager extends DisposableObject {
async readQueryHistory(): Promise<void> {
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
const history = await slurpQueryHistory(this.queryMetadataStorageLocation);
this.treeDataProvider.allHistory = history;
this.treeDataProvider.allHistory.forEach((item) => {
this._onDidAddQueryItem.fire(item);
@@ -575,9 +611,9 @@ export class QueryHistoryManager extends DisposableObject {
// Remote queries can be removed locally, but not remotely.
// The user must cancel the query on GitHub Actions explicitly.
this.treeDataProvider.remove(item);
void logger.log(`Deleted ${item.label}.`);
void logger.log(`Deleted ${this.labelProvider.getLabel(item)}.`);
if (item.status === QueryStatus.InProgress) {
void logger.log('The remote query is still running on GitHub Actions. To cancel there, you must go to the query run in your browser.');
void logger.log('The variant analysis is still running on GitHub Actions. To cancel there, you must go to the workflow run in your browser.');
}
this._onDidRemoveQueryItem.fire(item);
@@ -622,21 +658,21 @@ export class QueryHistoryManager extends DisposableObject {
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
// TODO will support remote queries
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
if (!this.assertSingleQuery(finalMultiSelect)) {
return;
}
const response = await window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
value: finalSingleItem.label,
placeHolder: `(use default: ${this.queryHistoryConfigListener.format})`,
value: finalSingleItem.userSpecifiedLabel ?? '',
title: 'Set query label',
prompt: 'Set the query history item label. See the description of the codeQL.queryHistory.format setting for more information.',
});
// undefined response means the user cancelled the dialog; don't change anything
if (response !== undefined) {
// Interpret empty string response as 'go back to using default'
finalSingleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
this.treeDataProvider.refresh();
finalSingleItem.userSpecifiedLabel = response === '' ? undefined : response;
await this.refreshTreeView();
}
}
@@ -663,7 +699,7 @@ export class QueryHistoryManager extends DisposableObject {
await this.doCompareCallback(from as CompletedLocalQueryInfo, to as CompletedLocalQueryInfo);
}
} catch (e) {
void showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(getErrorMessage(e));
}
}
@@ -727,21 +763,74 @@ export class QueryHistoryManager extends DisposableObject {
return;
}
let p: string | undefined;
let externalFilePath: string | undefined;
if (finalSingleItem.t === 'local') {
if (finalSingleItem.completedQuery) {
p = finalSingleItem.completedQuery.query.querySaveDir;
externalFilePath = path.join(finalSingleItem.completedQuery.query.querySaveDir, 'timestamp');
}
} else if (finalSingleItem.t === 'remote') {
p = path.join(this.queryStorageDir, finalSingleItem.queryId);
externalFilePath = path.join(this.queryStorageDir, finalSingleItem.queryId, 'timestamp');
}
if (p) {
try {
await commands.executeCommand('revealFileInOS', Uri.file(p));
} catch (e) {
throw new Error(`Failed to open ${p}: ${e.message}`);
if (externalFilePath) {
if (!(await fs.pathExists(externalFilePath))) {
// timestamp file is missing (manually deleted?) try selecting the parent folder.
// It's less nice, but at least it will work.
externalFilePath = path.dirname(externalFilePath);
if (!(await fs.pathExists(externalFilePath))) {
throw new Error(`Query directory does not exist: ${externalFilePath}`);
}
}
try {
await commands.executeCommand('revealFileInOS', Uri.file(externalFilePath));
} catch (e) {
throw new Error(`Failed to open ${externalFilePath}: ${getErrorMessage(e)}`);
}
}
}
private warnNoEvalLog() {
void showAndLogWarningMessage('No evaluator log is available for this run. Perhaps it failed before evaluation, or you are running with a version of CodeQL before ' + CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG + '?');
}
private warnNoEvalLogSummary() {
void showAndLogWarningMessage(`No evaluator log summary is available for this run. Perhaps it failed before evaluation, or you are running with a version of CodeQL before ${CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG}?`);
}
async handleShowEvalLog(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
// Only applicable to an individual local query
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local') {
return;
}
if (finalSingleItem.evalLogLocation) {
await this.tryOpenExternalFile(finalSingleItem.evalLogLocation);
} else {
this.warnNoEvalLog();
}
}
async handleShowEvalLogSummary(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
// Only applicable to an individual local query
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local') {
return;
}
if (finalSingleItem.evalLogSummaryLocation) {
await this.tryOpenExternalFile(finalSingleItem.evalLogSummaryLocation);
} else {
this.warnNoEvalLogSummary();
}
}
@@ -753,11 +842,20 @@ export class QueryHistoryManager extends DisposableObject {
// In the future, we may support cancelling remote queries, but this is not a short term plan.
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
(finalMultiSelect || [finalSingleItem]).forEach((item) => {
if (item.status === QueryStatus.InProgress && item.t === 'local') {
item.cancel();
const selected = finalMultiSelect || [finalSingleItem];
const results = selected.map(async item => {
if (item.status === QueryStatus.InProgress) {
if (item.t === 'local') {
item.cancel();
} else if (item.t === 'remote') {
void showAndLogInformationMessage('Cancelling variant analysis. This may take a while.');
const credentials = await this.getCredentials();
await cancelRemoteQuery(credentials, item.remoteQuery);
}
}
});
await Promise.all(results);
}
async handleShowQueryText(
@@ -803,7 +901,7 @@ export class QueryHistoryManager extends DisposableObject {
query.resultsPaths.interpretedResultsPath
);
} else {
const label = finalSingleItem.label;
const label = this.labelProvider.getLabel(finalSingleItem);
void showAndLogInformationMessage(
`Query ${label} has no interpreted results.`
);
@@ -920,11 +1018,12 @@ export class QueryHistoryManager extends DisposableObject {
try {
await window.showTextDocument(uri, { preview: false });
} catch (e) {
const msg = getErrorMessage(e);
if (
e.message.includes(
msg.includes(
'Files above 50MB cannot be synchronized with extensions'
) ||
e.message.includes('too large to open')
msg.includes('too large to open')
) {
const res = await showBinaryChoiceDialog(
`VS Code does not allow extensions to open files >50MB. This file
@@ -937,13 +1036,13 @@ the file in the file explorer and dragging it into the workspace.`
try {
await commands.executeCommand('revealFileInOS', uri);
} catch (e) {
void showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(getErrorMessage(e));
}
}
} else {
void showAndLogErrorMessage(`Could not open file ${fileLocation}`);
void logger.log(e.message);
void logger.log(e.stack);
void logger.log(getErrorMessage(e));
void logger.log(getErrorStack(e));
}
}
}
@@ -991,7 +1090,7 @@ the file in the file explorer and dragging it into the workspace.`
otherQuery.initialInfo.databaseInfo.name === dbName
)
.map((item) => ({
label: item.label,
label: this.labelProvider.getLabel(item),
description: (item as CompletedLocalQueryInfo).initialInfo.databaseInfo.name,
detail: (item as CompletedLocalQueryInfo).completedQuery.statusString,
query: item as CompletedLocalQueryInfo,

View File

@@ -14,7 +14,6 @@ import {
SarifInterpretationData,
GraphInterpretationData
} from './pure/interface-types';
import { QueryHistoryConfig } from './config';
import { DatabaseInfo } from './pure/interface-types';
import { QueryStatus } from './query-status';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
@@ -216,7 +215,8 @@ export class LocalQueryInfo {
public failureReason: string | undefined;
public completedQuery: CompletedQueryInfo | undefined;
private config: QueryHistoryConfig | undefined;
public evalLogLocation: string | undefined;
public evalLogSummaryLocation: string | undefined;
/**
* Note that in the {@link slurpQueryHistory} method, we create a FullQueryInfo instance
@@ -224,11 +224,8 @@ export class LocalQueryInfo {
*/
constructor(
public readonly initialInfo: InitialQueryInfo,
config: QueryHistoryConfig,
private cancellationSource?: CancellationTokenSource // used to cancel in progress queries
) {
this.setConfig(config);
}
) { /**/ }
cancel() {
this.cancellationSource?.cancel();
@@ -241,43 +238,12 @@ export class LocalQueryInfo {
return this.initialInfo.start.toLocaleString(env.language);
}
interpolate(template: string): string {
const { resultCount = 0, statusString = 'in progress' } = this.completedQuery || {};
const replacements: { [k: string]: string } = {
t: this.startTime,
q: this.getQueryName(),
d: this.initialInfo.databaseInfo.name,
r: resultCount.toString(),
s: statusString,
f: this.getQueryFileName(),
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
get userSpecifiedLabel() {
return this.initialInfo.userSpecifiedLabel;
}
/**
* Returns a label for this query that includes interpolated values.
*/
get label(): string {
return this.interpolate(
this.initialInfo.userSpecifiedLabel ?? this.config?.format ?? ''
);
}
/**
* Avoids getting the default label for the query.
* If there is a custom label for this query, interpolate and use that.
* Otherwise, use the name of the query.
*
* @returns the name of the query, unless there is a custom label for this query.
*/
getShortLabel(): string {
return this.initialInfo.userSpecifiedLabel
? this.interpolate(this.initialInfo.userSpecifiedLabel)
: this.getQueryName();
set userSpecifiedLabel(label: string | undefined) {
this.initialInfo.userSpecifiedLabel = label;
}
/**
@@ -340,21 +306,4 @@ export class LocalQueryInfo {
return QueryStatus.Failed;
}
}
/**
* The `config` property must not be serialized since it contains a listerner
* for global configuration changes. Instead, It should be set when the query
* is deserialized.
*
* @param config the global query history config object
*/
setConfig(config: QueryHistoryConfig) {
// avoid serializing config property
Object.defineProperty(this, 'config', {
enumerable: false,
writable: false,
configurable: true,
value: config
});
}
}

View File

@@ -1,13 +1,12 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { QueryHistoryConfig } from './config';
import { showAndLogErrorMessage } from './helpers';
import { asyncFilter } from './pure/helpers-pure';
import { asyncFilter, getErrorMessage, getErrorStack } from './pure/helpers-pure';
import { CompletedQueryInfo, LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { QueryEvaluationInfo } from './run-queries';
export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConfig): Promise<QueryHistoryInfo[]> {
export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInfo[]> {
try {
if (!(await fs.pathExists(fsPath))) {
return [];
@@ -29,10 +28,6 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
if (q.t === 'local') {
Object.setPrototypeOf(q, LocalQueryInfo.prototype);
// The config object is a global, se we need to set it explicitly
// and ensure it is not serialized to JSON.
q.setConfig(config);
// Date instances are serialized as strings. Need to
// convert them back to Date instances.
(q.initialInfo as any).start = new Date(q.initialInfo.start);
@@ -64,7 +59,7 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
});
} catch (e) {
void showAndLogErrorMessage('Error loading query history.', {
fullMessage: ['Error loading query history.', e.stack].join('\n'),
fullMessage: ['Error loading query history.', getErrorStack(e)].join('\n'),
});
// since the query history is invalid, it should be deleted so this error does not happen on next startup.
await fs.remove(fsPath);
@@ -94,6 +89,6 @@ export async function splatQueryHistory(queries: QueryHistoryInfo[], fsPath: str
}, null, 2);
await fs.writeFile(fsPath, data);
} catch (e) {
throw new Error(`Error saving query history to ${fsPath}: ${e.message}`);
throw new Error(`Error saving query history to ${fsPath}: ${getErrorMessage(e)}`);
}
}

View File

@@ -146,7 +146,7 @@ export class QueryServerClient extends DisposableObject {
args.push('--require-db-registration');
}
if (await this.cliServer.cliConstraints.supportsOldEvalStats()) {
if (await this.cliServer.cliConstraints.supportsOldEvalStats() && !(await this.cliServer.cliConstraints.supportsPerQueryEvalLog())) {
args.push('--old-eval-stats');
}
@@ -258,3 +258,15 @@ export class QueryServerClient extends DisposableObject {
export function findQueryLogFile(resultPath: string): string {
return path.join(resultPath, 'query.log');
}
export function findQueryEvalLogFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log.jsonl');
}
export function findQueryEvalLogSummaryFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log.summary');
}
export function findQueryEvalLogEndSummaryFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log-end.summary');
}

View File

@@ -21,6 +21,7 @@ import {
ProgressCallback,
UserCancellationException
} from './commandRunner';
import { getErrorMessage } from './pure/helpers-pure';
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
@@ -132,7 +133,7 @@ export async function displayQuickQuery(
await Window.showTextDocument(await workspace.openTextDocument(qlFile));
} catch (e) {
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
throw new UserCancellationException(e.message);
throw new UserCancellationException(getErrorMessage(e));
} else {
throw e;
}

View File

@@ -1,3 +1,4 @@
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { CancellationToken, ExtensionContext } from 'vscode';
@@ -6,10 +7,14 @@ import { Credentials } from '../authentication';
import { Logger } from '../logging';
import { downloadArtifactFromLink } from './gh-actions-api-client';
import { AnalysisSummary } from './shared/remote-query-result';
import { AnalysisResults, AnalysisAlert } from './shared/analysis-result';
import { AnalysisResults, AnalysisAlert, AnalysisRawResults } from './shared/analysis-result';
import { UserCancellationException } from '../commandRunner';
import { sarifParser } from '../sarif-parser';
import { extractAnalysisAlerts } from './sarif-processing';
import { CodeQLCliServer } from '../cli';
import { extractRawResults } from './bqrs-processing';
import { asyncFilter, getErrorMessage } from '../pure/helpers-pure';
import { createDownloadPath } from './download-link';
export class AnalysesResultsManager {
// Store for the results of various analyses for each remote query.
@@ -18,6 +23,7 @@ export class AnalysesResultsManager {
constructor(
private readonly ctx: ExtensionContext,
private readonly cliServer: CodeQLCliServer,
readonly storagePath: string,
private readonly logger: Logger,
) {
@@ -40,13 +46,22 @@ export class AnalysesResultsManager {
await this.downloadSingleAnalysisResults(analysisSummary, credentials, publishResults);
}
public async downloadAnalysesResults(
allAnalysesToDownload: AnalysisSummary[],
token: CancellationToken | undefined,
publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
/**
* Loads the array analysis results. For each analysis results, if it is not downloaded yet,
* it will be downloaded. If it is already downloaded, it will be loaded into memory.
* If it is already in memory, this will be a no-op.
*
* @param allAnalysesToLoad List of analyses to ensure are downloaded and in memory
* @param token Optional cancellation token
* @param publishResults Optional function to publish the results after loading
*/
public async loadAnalysesResults(
allAnalysesToLoad: AnalysisSummary[],
token?: CancellationToken,
publishResults: (analysesResults: AnalysisResults[]) => Promise<void> = () => Promise.resolve()
): Promise<void> {
// Filter out analyses that we have already in memory.
const analysesToDownload = allAnalysesToDownload.filter(x => !this.isAnalysisInMemory(x));
const analysesToDownload = allAnalysesToLoad.filter(x => !this.isAnalysisInMemory(x));
const credentials = await Credentials.initialize(this.ctx);
@@ -101,7 +116,7 @@ export class AnalysesResultsManager {
const analysisResults: AnalysisResults = {
nwo: analysis.nwo,
status: 'InProgress',
results: []
interpretedResults: []
};
const queryId = analysis.downloadLink.queryId;
const resultsForQuery = this.internalGetAnalysesResults(queryId);
@@ -115,19 +130,29 @@ export class AnalysesResultsManager {
artifactPath = await downloadArtifactFromLink(credentials, this.storagePath, analysis.downloadLink);
}
catch (e) {
throw new Error(`Could not download the analysis results for ${analysis.nwo}: ${e.message}`);
throw new Error(`Could not download the analysis results for ${analysis.nwo}: ${getErrorMessage(e)}`);
}
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(analysis.nwo, analysis.databaseSha);
let newAnaysisResults: AnalysisResults;
if (path.extname(artifactPath) === '.sarif') {
const queryResults = await this.readResults(artifactPath);
const fileExtension = path.extname(artifactPath);
if (fileExtension === '.sarif') {
const queryResults = await this.readSarifResults(artifactPath, fileLinkPrefix);
newAnaysisResults = {
...analysisResults,
results: queryResults,
interpretedResults: queryResults,
status: 'Completed'
};
} else if (fileExtension === '.bqrs') {
const queryResults = await this.readBqrsResults(artifactPath, fileLinkPrefix);
newAnaysisResults = {
...analysisResults,
rawResults: queryResults,
status: 'Completed'
};
} else {
void this.logger.log('Cannot download results. Only alert and path queries are fully supported.');
void this.logger.log(`Cannot download results. File type '${fileExtension}' not supported.`);
newAnaysisResults = {
...analysisResults,
status: 'Failed'
@@ -137,11 +162,30 @@ export class AnalysesResultsManager {
void publishResults([...resultsForQuery]);
}
private async readResults(filePath: string): Promise<AnalysisAlert[]> {
public async loadDownloadedAnalyses(
allAnalysesToCheck: AnalysisSummary[]
) {
// Find all analyses that are already downloaded.
const allDownloadedAnalyses = await asyncFilter(allAnalysesToCheck, x => this.isAnalysisDownloaded(x));
// Now, ensure that all of these analyses are in memory. Some may already be in memory. These are ignored.
await this.loadAnalysesResults(allDownloadedAnalyses);
}
private async isAnalysisDownloaded(analysis: AnalysisSummary): Promise<boolean> {
return await fs.pathExists(createDownloadPath(this.storagePath, analysis.downloadLink));
}
private async readBqrsResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisRawResults> {
return await extractRawResults(this.cliServer, this.logger, filePath, fileLinkPrefix);
}
private async readSarifResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisAlert[]> {
const sarifLog = await sarifParser(filePath);
const processedSarif = extractAnalysisAlerts(sarifLog);
if (processedSarif.errors) {
const processedSarif = extractAnalysisAlerts(sarifLog, fileLinkPrefix);
if (processedSarif.errors.length) {
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
}
@@ -151,4 +195,8 @@ export class AnalysesResultsManager {
private isAnalysisInMemory(analysis: AnalysisSummary): boolean {
return this.internalGetAnalysesResults(analysis.downloadLink.queryId).some(x => x.nwo === analysis.nwo);
}
private createGitHubDotcomFileLinkPrefix(nwo: string, sha: string): string {
return `https://github.com/${nwo}/blob/${sha}`;
}
}

View File

@@ -0,0 +1,35 @@
import { CodeQLCliServer } from '../cli';
import { Logger } from '../logging';
import { transformBqrsResultSet } from '../pure/bqrs-cli-types';
import { AnalysisRawResults } from './shared/analysis-result';
import { MAX_RAW_RESULTS } from './shared/result-limits';
export async function extractRawResults(
cliServer: CodeQLCliServer,
logger: Logger,
filePath: string,
fileLinkPrefix: string,
): Promise<AnalysisRawResults> {
const bqrsInfo = await cliServer.bqrsInfo(filePath);
const resultSets = bqrsInfo['result-sets'];
if (resultSets.length < 1) {
throw new Error('No result sets found in results file.');
}
if (resultSets.length > 1) {
void logger.log('Multiple result sets found in results file. Only the first one will be used.');
}
const schema = resultSets[0];
const chunk = await cliServer.bqrsDecode(
filePath,
schema.name,
{ pageSize: MAX_RAW_RESULTS });
const resultSet = transformBqrsResultSet(schema, chunk);
const capped = !!chunk.next;
return { schema, resultSet, fileLinkPrefix, capped };
}

View File

@@ -1,3 +1,5 @@
import * as path from 'path';
/**
* Represents a link to an artifact to be downloaded.
*/
@@ -23,3 +25,16 @@ export interface DownloadLink {
*/
queryId: string;
}
/**
* Converts a downloadLink to the path where the artifact should be stored.
*
* @param storagePath The base directory to store artifacts in.
* @param downloadLink The DownloadLink
* @param extension An optional file extension to append to the artifact (no `.`).
*
* @returns A full path to the download location of the artifact
*/
export function createDownloadPath(storagePath: string, downloadLink: DownloadLink, extension = '') {
return path.join(storagePath, downloadLink.queryId, downloadLink.id + (extension ? `.${extension}` : ''));
}

View File

@@ -5,13 +5,14 @@ import { showAndLogWarningMessage, tmpDir } from '../helpers';
import { Credentials } from '../authentication';
import { logger } from '../logging';
import { RemoteQueryWorkflowResult } from './remote-query-workflow-result';
import { DownloadLink } from './download-link';
import { DownloadLink, createDownloadPath } from './download-link';
import { RemoteQuery } from './remote-query';
import { RemoteQueryFailureIndexItem, RemoteQueryResultIndex, RemoteQuerySuccessIndexItem } from './remote-query-result-index';
interface ApiSuccessIndexItem {
nwo: string;
id: string;
sha?: string;
results_count: number;
bqrs_file_size: number;
sarif_file_size?: number;
@@ -41,7 +42,10 @@ export async function getRemoteQueryIndex(
const artifactsUrlPath = `/repos/${owner}/${repoName}/actions/artifacts`;
const artifactList = await listWorkflowRunArtifacts(credentials, owner, repoName, workflowRunId);
const resultIndexArtifactId = getArtifactIDfromName('result-index', workflowUri, artifactList);
const resultIndexArtifactId = tryGetArtifactIDfromName('result-index', artifactList);
if (!resultIndexArtifactId) {
return undefined;
}
const resultIndex = await getResultIndex(credentials, owner, repoName, resultIndexArtifactId);
const successes = resultIndex?.successes.map(item => {
@@ -51,6 +55,7 @@ export async function getRemoteQueryIndex(
id: item.id.toString(),
artifactId: artifactId,
nwo: item.nwo,
sha: item.sha,
resultCount: item.results_count,
bqrsFileSize: item.bqrs_file_size,
sarifFileSize: item.sarif_file_size
@@ -72,6 +77,18 @@ export async function getRemoteQueryIndex(
};
}
export async function cancelRemoteQuery(
credentials: Credentials,
remoteQuery: RemoteQuery
): Promise<void> {
const octokit = await credentials.getOctokit();
const { actionsWorkflowRunId, controllerRepository: { owner, name } } = remoteQuery;
const response = await octokit.request(`POST /repos/${owner}/${name}/actions/runs/${actionsWorkflowRunId}/cancel`);
if (response.status >= 300) {
throw new Error(`Error cancelling variant analysis: ${response.status} ${response?.data?.message || ''}`);
}
}
export async function downloadArtifactFromLink(
credentials: Credentials,
storagePath: string,
@@ -80,14 +97,14 @@ export async function downloadArtifactFromLink(
const octokit = await credentials.getOctokit();
const extractedPath = path.join(storagePath, downloadLink.queryId, downloadLink.id);
const extractedPath = createDownloadPath(storagePath, downloadLink);
// first check if we already have the artifact
if (!(await fs.pathExists(extractedPath))) {
// Download the zipped artifact.
const response = await octokit.request(`GET ${downloadLink.urlPath}/zip`, {});
const zipFilePath = path.join(storagePath, downloadLink.queryId, `${downloadLink.id}.zip`);
const zipFilePath = createDownloadPath(storagePath, downloadLink, 'zip');
await saveFile(`${zipFilePath}`, response.data as ArrayBuffer);
// Extract the zipped artifact.
@@ -209,15 +226,29 @@ function getArtifactIDfromName(
workflowUri: string,
artifacts: Array<{ id: number, name: string }>
): number {
const artifact = artifacts.find(a => a.name === artifactName);
const artifactId = tryGetArtifactIDfromName(artifactName, artifacts);
if (!artifact) {
if (!artifactId) {
const errorMessage =
`Could not find artifact with name ${artifactName} in workflow ${workflowUri}.
Please check whether the workflow run has successfully completed.`;
throw Error(errorMessage);
}
return artifactId;
}
/**
* @param artifactName The artifact name, as a string.
* @param artifacts An array of artifact details (from the "list workflow run artifacts" API response).
* @returns The artifact ID corresponding to the given artifact name, if it exists.
*/
function tryGetArtifactIDfromName(
artifactName: string,
artifacts: Array<{ id: number, name: string }>
): number | undefined {
const artifact = artifacts.find(a => a.name === artifactName);
return artifact?.id;
}
@@ -265,18 +296,18 @@ function getWorkflowError(conclusion: string | null): string {
}
if (conclusion === 'cancelled') {
return 'The remote query execution was cancelled.';
return 'Variant analysis execution was cancelled.';
}
if (conclusion === 'timed_out') {
return 'The remote query execution timed out.';
return 'Variant analysis execution timed out.';
}
if (conclusion === 'failure') {
// TODO: Get the actual error from the workflow or potentially
// from an artifact from the action itself.
return 'The remote query execution has failed.';
return 'Variant analysis execution has failed.';
}
return `Unexpected query execution conclusion: ${conclusion}`;
return `Unexpected variant analysis execution conclusion: ${conclusion}`;
}

View File

@@ -4,9 +4,7 @@ import {
window as Window,
ViewColumn,
Uri,
workspace,
extensions,
commands,
workspace
} from 'vscode';
import * as path from 'path';
@@ -14,8 +12,7 @@ import {
ToRemoteQueriesMessage,
FromRemoteQueriesMessage,
RemoteQueryDownloadAnalysisResultsMessage,
RemoteQueryDownloadAllAnalysesResultsMessage,
RemoteQueryViewAnalysisResultsMessage,
RemoteQueryDownloadAllAnalysesResultsMessage
} from '../pure/interface-types';
import { Logger } from '../logging';
import { getHtmlForWebview } from '../interface-utils';
@@ -33,6 +30,7 @@ import { AnalysisResults } from './shared/analysis-result';
export class RemoteQueriesInterfaceManager {
private panel: WebviewPanel | undefined;
private panelLoaded = false;
private currentQueryId: string | undefined;
private panelLoadedCallBacks: (() => void)[] = [];
constructor(
@@ -41,7 +39,7 @@ export class RemoteQueriesInterfaceManager {
private readonly analysesResultsManager: AnalysesResultsManager
) {
this.panelLoadedCallBacks.push(() => {
void logger.log('Remote queries view loaded');
void logger.log('Variant analysis results view loaded');
});
}
@@ -49,12 +47,18 @@ export class RemoteQueriesInterfaceManager {
this.getPanel().reveal(undefined, true);
await this.waitForPanelLoaded();
const model = this.buildViewModel(query, queryResult);
this.currentQueryId = queryResult.queryId;
await this.postMessage({
t: 'setRemoteQueryResult',
queryResult: this.buildViewModel(query, queryResult)
queryResult: model
});
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults(queryResult.queryId));
// Ensure all pre-downloaded artifacts are loaded into memory
await this.analysesResultsManager.loadDownloadedAnalyses(model.analysisSummaries);
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults(queryResult.queryId), queryResult.queryId);
}
/**
@@ -70,6 +74,7 @@ export class RemoteQueriesInterfaceManager {
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
const executionDuration = this.getDuration(queryResult.executionEndTime, query.executionStartTime);
const analysisSummaries = this.buildAnalysisSummaries(queryResult.analysisSummaries);
const totalRepositoryCount = queryResult.analysisSummaries.length;
const affectedRepositories = queryResult.analysisSummaries.filter(r => r.resultCount > 0);
return {
@@ -79,7 +84,7 @@ export class RemoteQueriesInterfaceManager {
queryText: query.queryText,
language: query.language,
workflowRunUrl: `https://github.com/${query.controllerRepository.owner}/${query.controllerRepository.name}/actions/runs/${query.actionsWorkflowRunId}`,
totalRepositoryCount: query.repositories.length,
totalRepositoryCount: totalRepositoryCount,
affectedRepositoryCount: affectedRepositories.length,
totalResultCount: totalResultCount,
executionTimestamp: this.formatDate(query.executionStartTime),
@@ -94,7 +99,7 @@ export class RemoteQueriesInterfaceManager {
const { ctx } = this;
const panel = (this.panel = Window.createWebviewPanel(
'remoteQueriesView',
'Remote Query Results',
'CodeQL Query Results',
{ viewColumn: ViewColumn.Active, preserveFocus: true },
{
enableScripts: true,
@@ -109,6 +114,7 @@ export class RemoteQueriesInterfaceManager {
this.panel.onDidDispose(
() => {
this.panel = undefined;
this.currentQueryId = undefined;
},
null,
ctx.subscriptions
@@ -189,7 +195,7 @@ export class RemoteQueriesInterfaceManager {
break;
case 'remoteQueryError':
void this.logger.log(
`Remote query error: ${msg.error}`
`Variant analysis error: ${msg.error}`
);
break;
case 'openFile':
@@ -204,57 +210,31 @@ export class RemoteQueriesInterfaceManager {
case 'remoteQueryDownloadAllAnalysesResults':
await this.downloadAllAnalysesResults(msg);
break;
case 'remoteQueryViewAnalysisResults':
await this.viewAnalysisResults(msg);
break;
default:
assertNever(msg);
}
}
private async downloadAnalysisResults(msg: RemoteQueryDownloadAnalysisResultsMessage): Promise<void> {
const queryId = this.currentQueryId;
await this.analysesResultsManager.downloadAnalysisResults(
msg.analysisSummary,
results => this.setAnalysisResults(results));
results => this.setAnalysisResults(results, queryId));
}
private async downloadAllAnalysesResults(msg: RemoteQueryDownloadAllAnalysesResultsMessage): Promise<void> {
await this.analysesResultsManager.downloadAnalysesResults(
const queryId = this.currentQueryId;
await this.analysesResultsManager.loadAnalysesResults(
msg.analysisSummaries,
undefined,
results => this.setAnalysisResults(results));
results => this.setAnalysisResults(results, queryId));
}
private async viewAnalysisResults(msg: RemoteQueryViewAnalysisResultsMessage): Promise<void> {
const downloadLink = msg.analysisSummary.downloadLink;
const filePath = path.join(this.analysesResultsManager.storagePath, downloadLink.queryId, downloadLink.id, downloadLink.innerFilePath || '');
const sarifViewerExtensionId = 'MS-SarifVSCode.sarif-viewer';
const sarifExt = extensions.getExtension(sarifViewerExtensionId);
if (!sarifExt) {
// Ask the user if they want to install the extension to view the results.
void commands.executeCommand('workbench.extensions.installExtension', sarifViewerExtensionId);
return;
}
if (!sarifExt.isActive) {
await sarifExt.activate();
}
// Clear any previous results before showing new results
await sarifExt.exports.closeAllLogs();
await sarifExt.exports.openLogs([
Uri.file(filePath),
]);
}
public async setAnalysisResults(analysesResults: AnalysisResults[]): Promise<void> {
if (this.panel?.active) {
public async setAnalysisResults(analysesResults: AnalysisResults[], queryId: string | undefined): Promise<void> {
if (this.panel?.active && this.currentQueryId === queryId) {
await this.postMessage({
t: 'setAnalysesResults',
analysesResults: analysesResults
analysesResults
});
}
}
@@ -319,6 +299,7 @@ export class RemoteQueriesInterfaceManager {
return sortedAnalysisSummaries.map((analysisResult) => ({
nwo: analysisResult.nwo,
databaseSha: analysisResult.databaseSha || 'HEAD',
resultCount: analysisResult.resultCount,
downloadLink: analysisResult.downloadLink,
fileSize: this.formatFileSize(analysisResult.fileSizeInBytes)

View File

@@ -6,7 +6,7 @@ import * as fs from 'fs-extra';
import { Credentials } from '../authentication';
import { CodeQLCliServer } from '../cli';
import { ProgressCallback } from '../commandRunner';
import { createTimestampFile, showAndLogErrorMessage, showInformationMessageWithAction } from '../helpers';
import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage, showInformationMessageWithAction } from '../helpers';
import { Logger } from '../logging';
import { runRemoteQuery } from './run-remote-query';
import { RemoteQueriesInterfaceManager } from './remote-queries-interface';
@@ -41,7 +41,7 @@ export class RemoteQueriesManager extends DisposableObject {
logger: Logger,
) {
super();
this.analysesResultsManager = new AnalysesResultsManager(ctx, storagePath, logger);
this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger);
this.interfaceManager = new RemoteQueriesInterfaceManager(ctx, logger, this.analysesResultsManager);
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
@@ -110,7 +110,6 @@ export class RemoteQueriesManager extends DisposableObject {
status: QueryStatus.InProgress,
completed: false,
queryId,
label: query.queryName,
remoteQuery: query,
};
await this.prepareStorageDirectory(queryHistoryItem);
@@ -132,36 +131,24 @@ export class RemoteQueriesManager extends DisposableObject {
const executionEndTime = Date.now();
if (queryWorkflowResult.status === 'CompletedSuccessfully') {
const resultIndex = await getRemoteQueryIndex(credentials, queryItem.remoteQuery);
queryItem.completed = true;
if (resultIndex) {
queryItem.status = QueryStatus.Completed;
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId);
await this.storeJsonFile(queryItem, 'query-result.json', queryResult);
// Kick off auto-download of results in the background.
void commands.executeCommand('codeQL.autoDownloadRemoteQueryResults', queryResult);
// Ask if the user wants to open the results in the background.
void this.askToOpenResults(queryItem.remoteQuery, queryResult).then(
noop,
err => {
void showAndLogErrorMessage(err);
}
);
} else {
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${queryItem.label}`);
queryItem.status = QueryStatus.Failed;
}
await this.downloadAvailableResults(queryItem, credentials, executionEndTime);
} else if (queryWorkflowResult.status === 'CompletedUnsuccessfully') {
queryItem.failureReason = queryWorkflowResult.error;
queryItem.status = QueryStatus.Failed;
void showAndLogErrorMessage(`Remote query execution failed. Error: ${queryWorkflowResult.error}`);
if (queryWorkflowResult.error?.includes('cancelled')) {
// workflow was cancelled on the server
queryItem.failureReason = 'Cancelled';
queryItem.status = QueryStatus.Failed;
await this.downloadAvailableResults(queryItem, credentials, executionEndTime);
void showAndLogInformationMessage('Variant analysis was cancelled');
} else {
queryItem.failureReason = queryWorkflowResult.error;
queryItem.status = QueryStatus.Failed;
void showAndLogErrorMessage(`Variant analysis execution failed. Error: ${queryWorkflowResult.error}`);
}
} else if (queryWorkflowResult.status === 'Cancelled') {
queryItem.failureReason = 'Cancelled';
queryItem.status = QueryStatus.Failed;
void showAndLogErrorMessage('Remote query monitoring was cancelled');
await this.downloadAvailableResults(queryItem, credentials, executionEndTime);
void showAndLogInformationMessage('Variant analysis was cancelled');
} else if (queryWorkflowResult.status === 'InProgress') {
// Should not get here. Only including this to ensure `assertNever` uses proper type checking.
void showAndLogErrorMessage(`Unexpected status: ${queryWorkflowResult.status}`);
@@ -181,21 +168,23 @@ export class RemoteQueriesManager extends DisposableObject {
.slice(0, autoDownloadMaxCount)
.map(a => ({
nwo: a.nwo,
databaseSha: a.databaseSha,
resultCount: a.resultCount,
downloadLink: a.downloadLink,
fileSize: String(a.fileSizeInBytes)
}));
await this.analysesResultsManager.downloadAnalysesResults(
await this.analysesResultsManager.loadAnalysesResults(
analysesToDownload,
token,
results => this.interfaceManager.setAnalysisResults(results));
results => this.interfaceManager.setAnalysisResults(results, queryResult.queryId));
}
private mapQueryResult(executionEndTime: number, resultIndex: RemoteQueryResultIndex, queryId: string): RemoteQueryResult {
const analysisSummaries = resultIndex.successes.map(item => ({
nwo: item.nwo,
databaseSha: item.sha || 'HEAD',
resultCount: item.resultCount,
fileSizeInBytes: item.sarifFileSize ? item.sarifFileSize : item.bqrsFileSize,
downloadLink: {
@@ -224,7 +213,8 @@ export class RemoteQueriesManager extends DisposableObject {
private async askToOpenResults(query: RemoteQuery, queryResult: RemoteQueryResult): Promise<void> {
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
const message = `Query "${query.queryName}" run on ${query.repositories.length} repositories and returned ${totalResultCount} results`;
const totalRepoCount = queryResult.analysisSummaries.length;
const message = `Query "${query.queryName}" run on ${totalRepoCount} repositories and returned ${totalResultCount} results`;
const shouldOpenView = await showInformationMessageWithAction(message, 'View');
if (shouldOpenView) {
@@ -273,4 +263,42 @@ export class RemoteQueriesManager extends DisposableObject {
const filePath = path.join(this.storagePath, queryItem.queryId);
return await fs.pathExists(filePath);
}
/**
* Checks whether there's a result index artifact available for the given query.
* If so, set the query status to `Completed` and auto-download the results.
*/
private async downloadAvailableResults(
queryItem: RemoteQueryHistoryItem,
credentials: Credentials,
executionEndTime: number
): Promise<void> {
const resultIndex = await getRemoteQueryIndex(credentials, queryItem.remoteQuery);
if (resultIndex) {
queryItem.completed = true;
queryItem.status = QueryStatus.Completed;
queryItem.failureReason = undefined;
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId);
await this.storeJsonFile(queryItem, 'query-result.json', queryResult);
// Kick off auto-download of results in the background.
void commands.executeCommand('codeQL.autoDownloadRemoteQueryResults', queryResult);
// Ask if the user wants to open the results in the background.
void this.askToOpenResults(queryItem.remoteQuery, queryResult).then(
noop,
err => {
void showAndLogErrorMessage(err);
}
);
} else {
const controllerRepo = `${queryItem.remoteQuery.controllerRepository.owner}/${queryItem.remoteQuery.controllerRepository.name}`;
const workflowRunUrl = `https://github.com/${controllerRepo}/actions/runs/${queryItem.remoteQuery.actionsWorkflowRunId}`;
void showAndLogErrorMessage(
`There was an issue retrieving the result for the query [${queryItem.remoteQuery.queryName}](${workflowRunUrl}).`
);
queryItem.status = QueryStatus.Failed;
}
}
}

View File

@@ -0,0 +1,209 @@
import { createRemoteFileRef } from '../pure/location-link-utils';
import { parseHighlightedLine, shouldHighlightLine } from '../pure/sarif-utils';
import { RemoteQuery } from './remote-query';
import { AnalysisAlert, AnalysisResults, CodeSnippet, FileLink, HighlightedRegion } from './shared/analysis-result';
// Each array item is a line of the markdown file.
export type MarkdownFile = string[];
/**
* Generates markdown files with variant analysis results.
*/
export function generateMarkdown(query: RemoteQuery, analysesResults: AnalysisResults[]): MarkdownFile[] {
const files: MarkdownFile[] = [];
// Generate summary file with links to individual files
const summaryLines: MarkdownFile = generateMarkdownSummary(query);
for (const analysisResult of analysesResults) {
if (analysisResult.interpretedResults.length === 0) {
// TODO: We'll add support for non-interpreted results later.
continue;
}
// Append nwo and results count to the summary table
const nwo = analysisResult.nwo;
const resultsCount = analysisResult.interpretedResults.length;
const link = createGistRelativeLink(nwo);
summaryLines.push(`| ${nwo} | [${resultsCount} result(s)](${link}) |`);
// Generate individual markdown file for each repository
const lines = [
`### ${analysisResult.nwo}`,
''
];
for (const interpretedResult of analysisResult.interpretedResults) {
const individualResult = generateMarkdownForInterpretedResult(interpretedResult, query.language);
lines.push(...individualResult);
}
files.push(lines);
}
return [summaryLines, ...files];
}
export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
const lines: MarkdownFile = [];
// Title
lines.push(
`### Results for "${query.queryName}"`,
''
);
// Expandable section containing query text
const queryCodeBlock = [
'```ql',
...query.queryText.split('\n'),
'```',
];
lines.push(
...buildExpandableMarkdownSection('Query', queryCodeBlock)
);
// Padding between sections
lines.push(
'<br />',
'',
);
// Summary table
lines.push(
'### Summary',
'',
'| Repository | Results |',
'| --- | --- |',
);
// nwo and result count will be appended to this table
return lines;
}
function generateMarkdownForInterpretedResult(interpretedResult: AnalysisAlert, language: string): MarkdownFile {
const lines: MarkdownFile = [];
lines.push(createMarkdownRemoteFileRef(
interpretedResult.fileLink,
interpretedResult.highlightedRegion?.startLine,
interpretedResult.highlightedRegion?.endLine
));
lines.push('');
const codeSnippet = interpretedResult.codeSnippet;
const highlightedRegion = interpretedResult.highlightedRegion;
if (codeSnippet) {
lines.push(
...generateMarkdownForCodeSnippet(codeSnippet, language, highlightedRegion),
);
}
const alertMessage = buildMarkdownAlertMessage(interpretedResult);
lines.push(alertMessage);
// Padding between results
lines.push(
'',
'----------------------------------------',
'',
);
return lines;
}
function generateMarkdownForCodeSnippet(
codeSnippet: CodeSnippet,
language: string,
highlightedRegion?: HighlightedRegion
): MarkdownFile {
const lines: MarkdownFile = [];
const snippetStartLine = codeSnippet.startLine || 0;
const codeLines = codeSnippet.text
.split('\n')
.map((line, index) =>
highlightCodeLines(line, index + snippetStartLine, highlightedRegion)
);
// Make sure there are no extra newlines before or after the <code> block:
const codeLinesWrapped = [...codeLines];
codeLinesWrapped[0] = `<pre><code class="${language}">${codeLinesWrapped[0]}`;
codeLinesWrapped[codeLinesWrapped.length - 1] = `${codeLinesWrapped[codeLinesWrapped.length - 1]}</code></pre>`;
lines.push(
...codeLinesWrapped,
'',
);
return lines;
}
function highlightCodeLines(
line: string,
lineNumber: number,
highlightedRegion?: HighlightedRegion
): string {
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
return line;
}
const partiallyHighlightedLine = parseHighlightedLine(
line,
lineNumber,
highlightedRegion
);
return `${partiallyHighlightedLine.plainSection1}<strong>${partiallyHighlightedLine.highlightedSection}</strong>${partiallyHighlightedLine.plainSection2}`;
}
function buildMarkdownAlertMessage(interpretedResult: AnalysisAlert): string {
let alertMessage = '';
for (const token of interpretedResult.message.tokens) {
if (token.t === 'text') {
alertMessage += token.text;
} else if (token.t === 'location') {
alertMessage += createMarkdownRemoteFileRef(
token.location.fileLink,
token.location.highlightedRegion?.startLine,
token.location.highlightedRegion?.endLine,
token.text,
);
}
}
// Italicize the alert message
return `*${alertMessage}*`;
}
/**
* Creates a markdown link to a remote file.
* If the "link text" is not provided, we use the file path.
*/
export function createMarkdownRemoteFileRef(
fileLink: FileLink,
startLine?: number,
endLine?: number,
linkText?: string,
): string {
const markdownLink = `[${linkText || fileLink.filePath}](${createRemoteFileRef(fileLink, startLine, endLine)})`;
return markdownLink;
}
/**
* Builds an expandable markdown section of the form:
* <details>
* <summary>title</summary>
*
* contents
*
* </details>
*/
function buildExpandableMarkdownSection(title: string, contents: MarkdownFile): MarkdownFile {
const expandableLines: MarkdownFile = [];
expandableLines.push(
'<details>',
`<summary>${title}</summary>`,
'',
...contents,
'',
'</details>',
''
);
return expandableLines;
}
/**
* Creates anchor link to a file in the gist. This is of the form:
* '#file-<name>-<file-extension>'
*
* TODO: Make sure these names align with the actual file names once we upload them to a gist.
*/
function createGistRelativeLink(nwo: string): string {
const [owner, repo] = nwo.split('/');
return `#file-${owner}-${repo}-md`;
}

View File

@@ -49,7 +49,7 @@ export class RemoteQueriesMonitor {
attemptCount++;
}
void this.logger.log('Remote query monitoring timed out after 2 days');
void this.logger.log('Variant analysis monitoring timed out after 2 days');
return { status: 'Cancelled' };
}

View File

@@ -10,6 +10,6 @@ export interface RemoteQueryHistoryItem {
status: QueryStatus;
completed: boolean;
readonly queryId: string,
label: string; // TODO, the query label should have interpolation like local queries
remoteQuery: RemoteQuery;
userSpecifiedLabel?: string;
}

View File

@@ -8,6 +8,7 @@ export interface RemoteQuerySuccessIndexItem {
id: string;
artifactId: number;
nwo: string;
sha?: string;
resultCount: number;
bqrsFileSize: number;
sarifFileSize?: number;

View File

@@ -10,6 +10,7 @@ export interface RemoteQueryResult {
export interface AnalysisSummary {
nwo: string,
databaseSha: string,
resultCount: number,
downloadLink: DownloadLink,
fileSizeInBytes: number

View File

@@ -6,7 +6,6 @@ export interface RemoteQuery {
queryText: string;
language: string;
controllerRepository: Repository;
repositories: Repository[];
executionStartTime: number; // Use number here since it needs to be serialized and desserialized.
actionsWorkflowRunId: number;
}

View File

@@ -0,0 +1,116 @@
import { QuickPickItem, window } from 'vscode';
import { logger } from '../logging';
import { getRemoteRepositoryLists } from '../config';
import { REPO_REGEX } from '../pure/helpers-pure';
import { UserCancellationException } from '../commandRunner';
export interface RepositorySelection {
repositories?: string[];
repositoryLists?: string[]
}
interface RepoListQuickPickItem extends QuickPickItem {
repositories?: string[];
repositoryList?: string;
useCustomRepository?: boolean;
}
/**
* Gets the repositories or repository lists to run the query against.
* @returns The user selection.
*/
export async function getRepositorySelection(): Promise<RepositorySelection> {
const quickPickItems = [
createCustomRepoQuickPickItem(),
...createSystemDefinedRepoListsQuickPickItems(),
...createUserDefinedRepoListsQuickPickItems(),
];
const options = {
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.variantAnalysis.repositoryLists` setting.',
ignoreFocusOut: true,
};
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
quickPickItems,
options);
if (quickpick?.repositories?.length) {
void logger.log(`Selected repositories: ${quickpick.repositories.join(', ')}`);
return { repositories: quickpick.repositories };
} else if (quickpick?.repositoryList) {
void logger.log(`Selected repository list: ${quickpick.repositoryList}`);
return { repositoryLists: [quickpick.repositoryList] };
} else if (quickpick?.useCustomRepository) {
const customRepo = await getCustomRepo();
if (!customRepo || !REPO_REGEX.test(customRepo)) {
throw new UserCancellationException('Invalid repository format. Please enter a valid repository in the format <owner>/<repo> (e.g. github/codeql)');
}
void logger.log(`Entered repository: ${customRepo}`);
return { repositories: [customRepo] };
} else {
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
// We set 'true' to make this a silent exception.
throw new UserCancellationException('No repositories selected', true);
}
}
/**
* Checks if the selection is valid or not.
* @param repoSelection The selection to check.
* @returns A boolean flag indicating if the selection is valid or not.
*/
export function isValidSelection(repoSelection: RepositorySelection): boolean {
if (repoSelection.repositories === undefined && repoSelection.repositoryLists === undefined) {
return false;
}
if (repoSelection.repositories !== undefined && repoSelection.repositories.length === 0) {
return false;
}
if (repoSelection.repositoryLists?.length === 0) {
return false;
}
return true;
}
function createSystemDefinedRepoListsQuickPickItems(): RepoListQuickPickItem[] {
const topNs = [10, 100, 1000];
return topNs.map(n => ({
label: '$(star) Top ' + n,
repositoryList: `top_${n}`,
alwaysShow: true
} as RepoListQuickPickItem));
}
function createUserDefinedRepoListsQuickPickItems(): RepoListQuickPickItem[] {
const repoLists = getRemoteRepositoryLists();
if (!repoLists) {
return [];
}
return Object.entries(repoLists).map<RepoListQuickPickItem>(([label, repositories]) => (
{
label, // the name of the repository list
repositories // the actual array of repositories
}
));
}
function createCustomRepoQuickPickItem(): RepoListQuickPickItem {
return {
label: '$(edit) Enter a GitHub repository',
useCustomRepository: true,
alwaysShow: true,
};
}
async function getCustomRepo(): Promise<string | undefined> {
return await window.showInputBox({
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
placeHolder: '<owner>/<repo>',
prompt: 'Tip: you can save frequently used repositories in the `codeQL.variantAnalysis.repositoryLists` setting',
ignoreFocusOut: true,
});
}

View File

@@ -1,4 +1,4 @@
import { CancellationToken, QuickPickItem, Uri, window } from 'vscode';
import { CancellationToken, Uri, window } from 'vscode';
import * as path from 'path';
import * as yaml from 'js-yaml';
import * as fs from 'fs-extra';
@@ -9,19 +9,20 @@ import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogInformationMessage,
showInformationMessageWithAction,
tryGetQueryMetadata,
tmpDir
} from '../helpers';
import { Credentials } from '../authentication';
import * as cli from '../cli';
import { logger } from '../logging';
import { getRemoteControllerRepo, getRemoteRepositoryLists, setRemoteControllerRepo } from '../config';
import { getActionBranch, getRemoteControllerRepo, setRemoteControllerRepo } from '../config';
import { ProgressCallback, UserCancellationException } from '../commandRunner';
import { OctokitResponse } from '@octokit/types/dist-types';
import { RemoteQuery } from './remote-query';
import { RemoteQuerySubmissionResult } from './remote-query-submission-result';
import { QueryMetadata } from '../pure/interface-types';
import { getErrorMessage, REPO_REGEX } from '../pure/helpers-pure';
import { getRepositorySelection, isValidSelection, RepositorySelection } from './repository-selection';
export interface QlPack {
name: string;
@@ -30,71 +31,21 @@ export interface QlPack {
defaultSuite?: Record<string, unknown>[];
defaultSuiteFile?: string;
}
interface RepoListQuickPickItem extends QuickPickItem {
repoList: string[];
}
interface QueriesResponse {
workflow_run_id: number
workflow_run_id: number,
errors?: {
invalid_repositories?: string[],
repositories_without_database?: string[],
},
repositories_queried?: string[],
}
/**
* This regex matches strings of the form `owner/repo` where:
* - `owner` is made up of alphanumeric characters or single hyphens, starting and ending in an alphanumeric character
* - `repo` is made up of alphanumeric characters, hyphens, or underscores
*/
const REPO_REGEX = /^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+\/[a-zA-Z0-9-_]+$/;
/**
* Well-known names for the query pack used by the server.
*/
const QUERY_PACK_NAME = 'codeql-remote/query';
/**
* Gets the repositories to run the query against.
*/
export async function getRepositories(): Promise<string[] | undefined> {
const repoLists = getRemoteRepositoryLists();
if (repoLists && Object.keys(repoLists).length) {
const quickPickItems = Object.entries(repoLists).map<RepoListQuickPickItem>(([key, value]) => (
{
label: key, // the name of the repository list
repoList: value, // the actual array of repositories
}
));
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
quickPickItems,
{
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.remoteQueries.repositoryLists` setting.',
ignoreFocusOut: true,
});
if (quickpick?.repoList.length) {
void logger.log(`Selected repositories: ${quickpick.repoList.join(', ')}`);
return quickpick.repoList;
} else {
void showAndLogErrorMessage('No repositories selected.');
return;
}
} else {
void logger.log('No repository lists defined. Displaying text input box.');
const remoteRepo = await window.showInputBox({
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
placeHolder: '<owner>/<repo>',
prompt: 'Tip: you can save frequently used repositories in the `codeQL.remoteQueries.repositoryLists` setting',
ignoreFocusOut: true,
});
if (!remoteRepo) {
void showAndLogErrorMessage('No repositories entered.');
return;
} else if (!REPO_REGEX.test(remoteRepo)) { // Check if user entered invalid input
void showAndLogErrorMessage('Invalid repository format. Must be in the format <owner>/<repo> (e.g. github/codeql)');
return;
}
void logger.log(`Entered repository: ${remoteRepo}`);
return [remoteRepo];
}
}
/**
* Two possibilities:
* 1. There is no qlpack.yml in this directory. Assume this is a lone query and generate a synthetic qlpack for it.
@@ -225,7 +176,7 @@ export async function runRemoteQuery(
token: CancellationToken
): Promise<void | RemoteQuerySubmissionResult> {
if (!(await cliServer.cliConstraints.supportsRemoteQueries())) {
throw new Error(`Remote queries are not supported by this version of CodeQL. Please upgrade to v${cli.CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES
throw new Error(`Variant analysis is not supported by this version of CodeQL. Please upgrade to v${cli.CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES
} or later.`);
}
@@ -243,8 +194,8 @@ export async function runRemoteQuery(
message: 'Determining query target language'
});
const repositories = await getRepositories();
if (!repositories || repositories.length === 0) {
const repoSelection = await getRepositorySelection();
if (!isValidSelection(repoSelection)) {
throw new UserCancellationException('No repositories to query.');
}
@@ -261,7 +212,7 @@ export async function runRemoteQuery(
if (!controllerRepo || !REPO_REGEX.test(controllerRepo)) {
void logger.log(controllerRepo ? 'Invalid controller repository name.' : 'No controller repository defined.');
controllerRepo = await window.showInputBox({
title: 'Controller repository in which to display progress and results of remote queries',
title: 'Controller repository in which to display progress and results of variant analysis',
placeHolder: '<owner>/<repo>',
prompt: 'Enter the name of a GitHub repository in the format <owner>/<repo>',
ignoreFocusOut: true,
@@ -302,7 +253,8 @@ export async function runRemoteQuery(
message: 'Sending request'
});
const workflowRunId = await runRemoteQueriesApiRequest(credentials, 'main', language, repositories, owner, repo, base64Pack, dryRun);
const actionBranch = getActionBranch();
const workflowRunId = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, owner, repo, base64Pack, dryRun);
const queryStartTime = Date.now();
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
@@ -314,7 +266,6 @@ export async function runRemoteQuery(
}
const remoteQuery = await buildRemoteQueryEntity(
repositories,
queryFile,
queryMetadata,
owner,
@@ -341,15 +292,30 @@ async function runRemoteQueriesApiRequest(
credentials: Credentials,
ref: string,
language: string,
repositories: string[],
repoSelection: RepositorySelection,
owner: string,
repo: string,
queryPackBase64: string,
dryRun = false
): Promise<void | number> {
const data = {
ref,
language,
repositories: repoSelection.repositories ?? undefined,
repository_lists: repoSelection.repositoryLists ?? undefined,
query_pack: queryPackBase64,
};
if (dryRun) {
void showAndLogInformationMessage('[DRY RUN] Would have sent request. See extension log for the payload.');
void logger.log(JSON.stringify({ ref, language, repositories, owner, repo, queryPackBase64: queryPackBase64.substring(0, 100) + '... ' + queryPackBase64.length + ' bytes' }));
void logger.log(JSON.stringify({
owner,
repo,
data: {
...data,
queryPackBase64: queryPackBase64.substring(0, 100) + '... ' + queryPackBase64.length + ' bytes'
}
}));
return;
}
@@ -360,57 +326,40 @@ async function runRemoteQueriesApiRequest(
{
owner,
repo,
data: {
ref,
language,
repositories,
query_pack: queryPackBase64,
}
data
}
);
const workflowRunId = response.data.workflow_run_id;
void showAndLogInformationMessage(`Successfully scheduled runs. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${workflowRunId}).`);
return workflowRunId;
const { popupMessage, logMessage } = parseResponse(owner, repo, response.data);
void showAndLogInformationMessage(popupMessage, { fullMessage: logMessage });
return response.data.workflow_run_id;
} catch (error) {
return await attemptRerun(error, credentials, ref, language, repositories, owner, repo, queryPackBase64, dryRun);
void showAndLogErrorMessage(getErrorMessage(error));
}
}
/** Attempts to rerun the query on only the valid repositories */
export async function attemptRerun(
error: any,
credentials: Credentials,
ref: string,
language: string,
repositories: string[],
owner: string,
repo: string,
queryPackBase64: string,
dryRun = false
) {
if (typeof error.message === 'string' && error.message.includes('Some repositories were invalid')) {
const invalidRepos = error?.response?.data?.invalid_repos || [];
void logger.log('Unable to run query on some of the specified repositories');
if (invalidRepos.length > 0) {
void logger.log(`Invalid repos: ${invalidRepos.join(', ')}`);
}
// exported for testng only
export function parseResponse(owner: string, repo: string, response: QueriesResponse) {
const popupMessage = `Successfully scheduled runs. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}).`
+ (response.errors ? '\n\nSome repositories could not be scheduled. See extension log for details.' : '');
if (invalidRepos.length === repositories.length) {
// Every repo is invalid in some way
void showAndLogErrorMessage('Unable to run query on any of the specified repositories.');
return;
}
const popupMessage = 'Unable to run query on some of the specified repositories. [See logs for more details](command:codeQL.showLogs).';
const rerunQuery = await showInformationMessageWithAction(popupMessage, 'Rerun on the valid repositories only');
if (rerunQuery) {
const validRepositories = repositories.filter(r => !invalidRepos.includes(r));
void logger.log(`Rerunning query on set of valid repositories: ${JSON.stringify(validRepositories)}`);
return await runRemoteQueriesApiRequest(credentials, ref, language, validRepositories, owner, repo, queryPackBase64, dryRun);
}
} else {
void showAndLogErrorMessage(error);
let logMessage = `Successfully scheduled runs. See https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}.`;
if (response.repositories_queried) {
logMessage += `\n\nRepositories queried:\n${response.repositories_queried.join(', ')}`;
}
if (response.errors) {
logMessage += '\n\nSome repositories could not be scheduled.';
if (response.errors.invalid_repositories?.length) {
logMessage += `\n\nInvalid repositories:\n${response.errors.invalid_repositories.join(', ')}`;
}
if (response.errors.repositories_without_database?.length) {
logMessage += `\n\nRepositories without databases:\n${response.errors.repositories_without_database.join(', ')}`;
}
}
return {
popupMessage,
logMessage
};
}
/**
@@ -430,7 +379,7 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string
qlpack.name = QUERY_PACK_NAME;
qlpack.defaultSuite = [{
description: 'Query suite for remote query'
description: 'Query suite for variant analysis'
}, {
query: packRelativePath.replace(/\\/g, '/')
}];
@@ -438,7 +387,6 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string
}
async function buildRemoteQueryEntity(
repositories: string[],
queryFilePath: string,
queryMetadata: QueryMetadata | undefined,
controllerRepoOwner: string,
@@ -450,11 +398,6 @@ async function buildRemoteQueryEntity(
// The query name is either the name as specified in the query metadata, or the file name.
const queryName = queryMetadata?.name ?? path.basename(queryFilePath);
const queryRepos = repositories.map(r => {
const [owner, repo] = r.split('/');
return { owner: owner, name: repo };
});
const queryText = await fs.readFile(queryFilePath, 'utf8');
return {
@@ -466,7 +409,6 @@ async function buildRemoteQueryEntity(
owner: controllerRepoOwner,
name: controllerRepoName,
},
repositories: queryRepos,
executionStartTime: queryStartTime,
actionsWorkflowRunId: workflowRunId
};

View File

@@ -1,207 +0,0 @@
import { RemoteQuery } from './remote-query';
import { RemoteQueryResult } from './remote-query-result';
import { AnalysisResults } from './shared/analysis-result';
export const sampleRemoteQuery: RemoteQuery = {
queryName: 'Inefficient regular expression',
queryFilePath: '/Users/foo/dev/vscode-codeql-starter/ql/javascript/ql/src/Performance/ReDoS.ql',
queryText: '/**\n * @name Inefficient regular expression\n * @description A regular expression that requires exponential time to match certain inputs\n * can be a performance bottleneck, and may be vulnerable to denial-of-service\n * attacks.\n * @kind problem\n * @problem.severity error\n * @security-severity 7.5\n * @precision high\n * @id js/redos\n * @tags security\n * external/cwe/cwe-1333\n * external/cwe/cwe-730\n * external/cwe/cwe-400\n */\n\nimport javascript\nimport semmle.javascript.security.performance.ReDoSUtil\nimport semmle.javascript.security.performance.ExponentialBackTracking\n\nfrom RegExpTerm t, string pump, State s, string prefixMsg\nwhere hasReDoSResult(t, pump, s, prefixMsg)\nselect t,\n "This part of the regular expression may cause exponential backtracking on strings " + prefixMsg +\n "containing many repetitions of \'" + pump + "\'."\n',
language: 'javascript',
controllerRepository: {
owner: 'big-corp',
name: 'controller-repo'
},
repositories: [
{
owner: 'big-corp',
name: 'repo1'
},
{
owner: 'big-corp',
name: 'repo2'
},
{
owner: 'big-corp',
name: 'repo3'
},
{
owner: 'big-corp',
name: 'repo4'
},
{
owner: 'big-corp',
name: 'repo5'
}
],
executionStartTime: new Date('2022-01-06T17:02:15.026Z').getTime(),
actionsWorkflowRunId: 1662757118
};
export const sampleRemoteQueryResult: RemoteQueryResult = {
queryId: 'query123',
executionEndTime: new Date('2022-01-06T17:04:37.026Z').getTime(),
analysisSummaries: [
{
nwo: 'big-corp/repo1',
resultCount: 85,
fileSizeInBytes: 14123,
downloadLink: {
id: '137697017',
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697017',
innerFilePath: 'results.sarif',
queryId: 'query.ql-123-xyz'
}
},
{
nwo: 'big-corp/repo2',
resultCount: 20,
fileSizeInBytes: 8698,
downloadLink: {
id: '137697018',
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697018',
innerFilePath: 'results.sarif',
queryId: 'query.ql-123-xyz'
}
},
{
nwo: 'big-corp/repo3',
resultCount: 8,
fileSizeInBytes: 4123,
downloadLink: {
id: '137697019',
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697019',
innerFilePath: 'results.sarif',
queryId: 'query.ql-123-xyz'
}
},
{
nwo: 'big-corp/repo4',
resultCount: 3,
fileSizeInBytes: 3313,
downloadLink: {
id: '137697020',
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697020',
innerFilePath: 'results.sarif',
queryId: 'query.ql-123-xyz'
}
}
],
analysisFailures: [
{
nwo: 'big-corp/repo5',
error: 'Error message'
},
{
nwo: 'big-corp/repo6',
error: 'Error message'
},
]
};
const createAnalysisResults = (n: number) => Array(n).fill(
{
message: 'This shell command depends on an uncontrolled [absolute path](1).',
severity: 'Error',
filePath: 'npm-packages/meteor-installer/config.js',
codeSnippet: {
startLine: 253,
endLine: 257,
text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n',
},
highlightedRegion: {
startLine: 255,
startColumn: 28,
endColumn: 62
}
}
);
export const sampleAnalysesResultsStage1: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo2',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo3',
status: 'InProgress',
results: []
},
// No entries for repo4
];
export const sampleAnalysesResultsStage2: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'InProgress',
results: []
},
{
nwo: 'big-corp/repo4',
status: 'InProgress',
results: []
},
];
export const sampleAnalysesResultsStage3: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'Completed',
results: createAnalysisResults(8)
},
{
nwo: 'big-corp/repo4',
status: 'Completed',
results: createAnalysisResults(3)
},
];
export const sampleAnalysesResultsWithFailure: AnalysisResults[] = [
{
nwo: 'big-corp/repo1',
status: 'Completed',
results: createAnalysisResults(85)
},
{
nwo: 'big-corp/repo2',
status: 'Completed',
results: createAnalysisResults(20)
},
{
nwo: 'big-corp/repo3',
status: 'Failed',
results: []
},
{
nwo: 'big-corp/repo4',
status: 'Completed',
results: createAnalysisResults(3)
},
];

View File

@@ -1,130 +1,100 @@
import * as sarif from 'sarif';
import { parseSarifPlainTextMessage, parseSarifRegion } from '../pure/sarif-utils';
import { AnalysisAlert, ResultSeverity } from './shared/analysis-result';
import {
AnalysisAlert,
CodeFlow,
AnalysisMessage,
AnalysisMessageToken,
ResultSeverity,
ThreadFlow,
CodeSnippet,
HighlightedRegion
} from './shared/analysis-result';
const defaultSeverity = 'Warning';
export function extractAnalysisAlerts(
sarifLog: sarif.Log
sarifLog: sarif.Log,
fileLinkPrefix: string
): {
alerts: AnalysisAlert[],
errors: string[]
} {
if (!sarifLog) {
return { alerts: [], errors: ['No SARIF log was found'] };
}
if (!sarifLog.runs) {
return { alerts: [], errors: ['No runs found in the SARIF file'] };
}
const errors: string[] = [];
const alerts: AnalysisAlert[] = [];
const errors: string[] = [];
for (const run of sarifLog.runs) {
if (!run.results) {
errors.push('No results found in the SARIF run');
continue;
}
for (const result of run.results) {
const message = result.message?.text;
if (!message) {
errors.push('No message found in the SARIF result');
for (const run of sarifLog.runs ?? []) {
for (const result of run.results ?? []) {
try {
alerts.push(...extractResultAlerts(run, result, fileLinkPrefix));
} catch (e) {
errors.push(`Error when processing SARIF result: ${e}`);
continue;
}
const severity = tryGetSeverity(run, result) || defaultSeverity;
if (!result.locations) {
errors.push('No locations found in the SARIF result');
continue;
}
for (const location of result.locations) {
const contextRegion = location.physicalLocation?.contextRegion;
if (!contextRegion) {
errors.push('No context region found in the SARIF result location');
continue;
}
if (contextRegion.startLine === undefined) {
errors.push('No start line set for a result context region');
continue;
}
if (contextRegion.endLine === undefined) {
errors.push('No end line set for a result context region');
continue;
}
if (!contextRegion.snippet?.text) {
errors.push('No text set for a result context region');
continue;
}
const region = location.physicalLocation?.region;
if (!region) {
errors.push('No region found in the SARIF result location');
continue;
}
if (region.startLine === undefined) {
errors.push('No start line set for a result region');
continue;
}
if (region.startColumn === undefined) {
errors.push('No start column set for a result region');
continue;
}
if (region.endColumn === undefined) {
errors.push('No end column set for a result region');
continue;
}
const filePath = location.physicalLocation?.artifactLocation?.uri;
if (!filePath) {
errors.push('No file path found in the SARIF result location');
continue;
}
const analysisAlert = {
message,
filePath,
severity,
codeSnippet: {
startLine: contextRegion.startLine,
endLine: contextRegion.endLine,
text: contextRegion.snippet.text
},
highlightedRegion: {
startLine: region.startLine,
startColumn: region.startColumn,
endLine: region.endLine,
endColumn: region.endColumn
}
};
const validationErrors = getAlertValidationErrors(analysisAlert);
if (validationErrors.length > 0) {
errors.push(...validationErrors);
continue;
}
alerts.push(analysisAlert);
}
}
}
return { alerts, errors };
}
export function tryGetSeverity(
sarifRun: sarif.Run,
result: sarif.Result
): ResultSeverity | undefined {
if (!sarifRun || !result) {
return undefined;
function extractResultAlerts(
run: sarif.Run,
result: sarif.Result,
fileLinkPrefix: string
): AnalysisAlert[] {
const alerts: AnalysisAlert[] = [];
const message = getMessage(result, fileLinkPrefix);
const rule = tryGetRule(run, result);
const severity = tryGetSeverity(run, result, rule) || defaultSeverity;
const codeFlows = getCodeFlows(result, fileLinkPrefix);
const shortDescription = getShortDescription(rule, message!);
for (const location of result.locations ?? []) {
const physicalLocation = location.physicalLocation!;
const filePath = physicalLocation.artifactLocation!.uri!;
const codeSnippet = getCodeSnippet(physicalLocation.contextRegion, physicalLocation.region);
const highlightedRegion = physicalLocation.region
? getHighlightedRegion(physicalLocation.region)
: undefined;
const analysisAlert: AnalysisAlert = {
message,
shortDescription,
fileLink: {
fileLinkPrefix,
filePath,
},
severity,
codeSnippet,
highlightedRegion,
codeFlows: codeFlows
};
alerts.push(analysisAlert);
}
const rule = tryGetRule(sarifRun, result);
if (!rule) {
return alerts;
}
function getShortDescription(
rule: sarif.ReportingDescriptor | undefined,
message: AnalysisMessage,
): string {
if (rule?.shortDescription?.text) {
return rule.shortDescription.text;
}
return message.tokens.map(token => token.text).join();
}
export function tryGetSeverity(
sarifRun: sarif.Run,
result: sarif.Result,
rule: sarif.ReportingDescriptor | undefined
): ResultSeverity | undefined {
if (!sarifRun || !result || !rule) {
return undefined;
}
@@ -186,18 +156,98 @@ export function tryGetRule(
return undefined;
}
function getAlertValidationErrors(alert: AnalysisAlert): string[] {
const errors = [];
function getCodeSnippet(region?: sarif.Region, alternateRegion?: sarif.Region): CodeSnippet | undefined {
region = region ?? alternateRegion;
if (alert.codeSnippet.startLine > alert.codeSnippet.endLine) {
errors.push('The code snippet start line is greater than the end line');
if (!region) {
return undefined;
}
const highlightedRegion = alert.highlightedRegion;
if (highlightedRegion.endLine === highlightedRegion.startLine &&
highlightedRegion.endColumn < highlightedRegion.startColumn) {
errors.push('The highlighted region end column is greater than the start column');
}
const text = region.snippet?.text || '';
const { startLine, endLine } = parseSarifRegion(region);
return errors;
return {
startLine,
endLine,
text
};
}
function getHighlightedRegion(region: sarif.Region): HighlightedRegion {
const { startLine, startColumn, endLine, endColumn } = parseSarifRegion(region);
return {
startLine,
startColumn,
endLine,
// parseSarifRegion currently shifts the end column by 1 to account
// for the way vscode counts columns so we need to shift it back.
endColumn: endColumn + 1
};
}
function getCodeFlows(
result: sarif.Result,
fileLinkPrefix: string
): CodeFlow[] {
const codeFlows = [];
if (result.codeFlows) {
for (const codeFlow of result.codeFlows) {
const threadFlows = [];
for (const threadFlow of codeFlow.threadFlows) {
for (const threadFlowLocation of threadFlow.locations) {
const physicalLocation = threadFlowLocation!.location!.physicalLocation!;
const filePath = physicalLocation!.artifactLocation!.uri!;
const codeSnippet = getCodeSnippet(physicalLocation.contextRegion, physicalLocation.region);
const highlightedRegion = physicalLocation.region
? getHighlightedRegion(physicalLocation.region)
: undefined;
threadFlows.push({
fileLink: {
fileLinkPrefix,
filePath,
},
codeSnippet,
highlightedRegion
} as ThreadFlow);
}
}
codeFlows.push({ threadFlows } as CodeFlow);
}
}
return codeFlows;
}
function getMessage(result: sarif.Result, fileLinkPrefix: string): AnalysisMessage {
const tokens: AnalysisMessageToken[] = [];
const messageText = result.message!.text!;
const messageParts = parseSarifPlainTextMessage(messageText);
for (const messagePart of messageParts) {
if (typeof messagePart === 'string') {
tokens.push({ t: 'text', text: messagePart });
} else {
const relatedLocation = result.relatedLocations!.find(rl => rl.id === messagePart.dest);
tokens.push({
t: 'location',
text: messagePart.text,
location: {
fileLink: {
fileLinkPrefix: fileLinkPrefix,
filePath: relatedLocation!.physicalLocation!.artifactLocation!.uri!,
},
highlightedRegion: getHighlightedRegion(relatedLocation!.physicalLocation!.region!),
}
});
}
}
return { tokens };
}

View File

@@ -1,17 +1,34 @@
import { RawResultSet, ResultSetSchema } from '../../pure/bqrs-cli-types';
export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
export interface AnalysisResults {
nwo: string;
status: AnalysisResultStatus;
results: AnalysisAlert[];
interpretedResults: AnalysisAlert[];
rawResults?: AnalysisRawResults;
}
export interface AnalysisRawResults {
schema: ResultSetSchema;
resultSet: RawResultSet;
fileLinkPrefix: string;
capped: boolean;
}
export interface AnalysisAlert {
message: string;
message: AnalysisMessage;
shortDescription: string;
severity: ResultSeverity;
fileLink: FileLink;
codeSnippet?: CodeSnippet;
highlightedRegion?: HighlightedRegion;
codeFlows: CodeFlow[];
}
export interface FileLink {
fileLinkPrefix: string;
filePath: string;
codeSnippet: CodeSnippet
highlightedRegion: HighlightedRegion
}
export interface CodeSnippet {
@@ -23,8 +40,41 @@ export interface CodeSnippet {
export interface HighlightedRegion {
startLine: number;
startColumn: number;
endLine: number | undefined;
endLine: number;
endColumn: number;
}
export interface CodeFlow {
threadFlows: ThreadFlow[];
}
export interface ThreadFlow {
fileLink: FileLink;
codeSnippet: CodeSnippet;
highlightedRegion?: HighlightedRegion;
message?: AnalysisMessage;
}
export interface AnalysisMessage {
tokens: AnalysisMessageToken[]
}
export type AnalysisMessageToken =
| AnalysisMessageTextToken
| AnalysisMessageLocationToken;
export interface AnalysisMessageTextToken {
t: 'text';
text: string;
}
export interface AnalysisMessageLocationToken {
t: 'location';
text: string;
location: {
fileLink: FileLink;
highlightedRegion?: HighlightedRegion;
};
}
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';

View File

@@ -19,6 +19,7 @@ export interface RemoteQueryResult {
export interface AnalysisSummary {
nwo: string,
databaseSha: string,
resultCount: number,
downloadLink: DownloadLink,
fileSize: string,

View File

@@ -0,0 +1,5 @@
// The maximum number of raw results to read from a BQRS file.
// This is used to avoid reading the entire result set into memory
// and trying to render it on screen. Users will be warned if the
// results are capped.
export const MAX_RAW_RESULTS = 500;

View File

@@ -1,15 +1,25 @@
import * as React from 'react';
import { AnalysisAlert } from '../shared/analysis-result';
import CodePaths from './CodePaths';
import FileCodeSnippet from './FileCodeSnippet';
const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => {
const showPathsLink = alert.codeFlows.length > 0;
return <FileCodeSnippet
filePath={alert.filePath}
fileLink={alert.fileLink}
codeSnippet={alert.codeSnippet}
highlightedRegion={alert.highlightedRegion}
severity={alert.severity}
message={alert.message}
messageChildren={
showPathsLink && <CodePaths
codeFlows={alert.codeFlows}
ruleDescription={alert.shortDescription}
severity={alert.severity}
message={alert.message}
/>
}
/>;
};

View File

@@ -0,0 +1,180 @@
import { TriangleDownIcon, XCircleIcon } from '@primer/octicons-react';
import { ActionList, ActionMenu, Box, Button, Label, Link, Overlay } from '@primer/react';
import * as React from 'react';
import { useRef, useState } from 'react';
import styled from 'styled-components';
import { CodeFlow, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
import FileCodeSnippet from './FileCodeSnippet';
import SectionTitle from './SectionTitle';
import VerticalSpace from './VerticalSpace';
const StyledCloseButton = styled.button`
position: absolute;
top: 1em;
right: 4em;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
border: none;
&:focus-visible {
outline: none
}
`;
const OverlayContainer = styled.div`
padding: 1em;
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} >
<XCircleIcon size={24} />
</StyledCloseButton>
);
const CodePath = ({
codeFlow,
message,
severity
}: {
codeFlow: CodeFlow;
message: AnalysisMessage;
severity: ResultSeverity;
}) => {
return <>
{codeFlow.threadFlows.map((threadFlow, index) =>
<div key={`thread-flow-${index}`} style={{ maxWidth: '55em' }}>
{index !== 0 && <VerticalSpace size={3} />}
<Box display="flex" justifyContent="center" alignItems="center">
<Box flexGrow={1} p={0} border="none">
<SectionTitle>Step {index + 1}</SectionTitle>
</Box>
{index === 0 &&
<Box p={0} border="none">
<Label>Source</Label>
</Box>
}
{index === codeFlow.threadFlows.length - 1 &&
<Box p={0} border="none">
<Label>Sink</Label>
</Box>
}
</Box>
<VerticalSpace size={2} />
<FileCodeSnippet
fileLink={threadFlow.fileLink}
codeSnippet={threadFlow.codeSnippet}
highlightedRegion={threadFlow.highlightedRegion}
severity={severity}
message={index === codeFlow.threadFlows.length - 1 ? message : threadFlow.message} />
</div>
)}
</>;
};
const getCodeFlowName = (codeFlow: CodeFlow) => {
const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].fileLink.filePath;
return filePath.substring(filePath.lastIndexOf('/') + 1);
};
const Menu = ({
codeFlows,
setSelectedCodeFlow
}: {
codeFlows: CodeFlow[],
setSelectedCodeFlow: (value: React.SetStateAction<CodeFlow>) => void
}) => {
return <ActionMenu>
<ActionMenu.Anchor>
<Button variant="invisible" sx={{ fontWeight: 'normal', color: 'var(--vscode-editor-foreground);', padding: 0 }} >
{getCodeFlowName(codeFlows[0])}
<TriangleDownIcon size={16} />
</Button>
</ActionMenu.Anchor>
<ActionMenu.Overlay sx={{ backgroundColor: 'var(--vscode-editor-background)' }}>
<ActionList>
{codeFlows.map((codeFlow, index) =>
<ActionList.Item
key={`codeflow-${index}'`}
onSelect={(e: React.MouseEvent) => { setSelectedCodeFlow(codeFlow); }}>
{getCodeFlowName(codeFlow)}
</ActionList.Item>
)}
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>;
};
const CodePaths = ({
codeFlows,
ruleDescription,
message,
severity
}: {
codeFlows: CodeFlow[],
ruleDescription: string,
message: AnalysisMessage,
severity: ResultSeverity
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
const anchorRef = useRef<HTMLDivElement>(null);
const linkRef = useRef<HTMLAnchorElement>(null);
const closeOverlay = () => setIsOpen(false);
return (
<Box ref={anchorRef}>
<Link
onClick={() => setIsOpen(true)}
ref={linkRef}
sx={{ cursor: 'pointer' }}>
Show paths
</Link>
{isOpen && (
<Overlay
returnFocusRef={linkRef}
onEscape={closeOverlay}
onClickOutside={closeOverlay}
anchorSide="outside-top">
<OverlayContainer>
<CloseButton onClick={closeOverlay} />
<SectionTitle>{ruleDescription}</SectionTitle>
<VerticalSpace size={2} />
<Box display="flex" justifyContent="center" alignItems="center">
<Box p={0} border="none">
{codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in
</Box>
<Box flexGrow={1} p={0} paddingLeft="0.2em" border="none">
<Menu codeFlows={codeFlows} setSelectedCodeFlow={setSelectedCodeFlow} />
</Box>
</Box>
<VerticalSpace size={2} />
<CodePath
codeFlow={selectedCodeFlow}
severity={severity}
message={message} />
<VerticalSpace size={3} />
</OverlayContainer>
</Overlay>
)}
</Box>
);
};
export default CodePaths;

View File

@@ -1,12 +1,14 @@
import * as React from 'react';
import styled from 'styled-components';
import { CodeSnippet, HighlightedRegion, ResultSeverity } from '../shared/analysis-result';
import { CodeSnippet, FileLink, HighlightedRegion, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
import { Box, Link } from '@primer/react';
import VerticalSpace from './VerticalSpace';
import { createRemoteFileRef } from '../../pure/location-link-utils';
import { parseHighlightedLine, shouldHighlightLine } from '../../pure/sarif-utils';
const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)';
const warningColor = '#966C23';
const highlightColor = '#534425';
const highlightColor = 'var(--vscode-editor-findMatchHighlightBackground)';
const getSeverityColor = (severity: ResultSeverity) => {
switch (severity) {
@@ -21,22 +23,9 @@ const getSeverityColor = (severity: ResultSeverity) => {
const replaceSpaceChar = (text: string) => text.replaceAll(' ', '\u00a0');
const shouldHighlightLine = (lineNumber: number, highlightedRegion: HighlightedRegion) => {
if (lineNumber < highlightedRegion.startLine) {
return false;
}
if (highlightedRegion.endLine == undefined) {
return lineNumber == highlightedRegion.startLine;
}
return lineNumber <= highlightedRegion.endLine;
};
const Container = styled.div`
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
font-size: x-small;
width: 55em;
`;
const TitleContainer = styled.div`
@@ -75,19 +64,19 @@ const HighlightedLine = ({ text }: { text: string }) => {
};
const Message = ({
messageText,
message,
currentLineNumber,
highlightedRegion,
borderColor,
children
}: {
messageText: string,
message: AnalysisMessage,
currentLineNumber: number,
highlightedRegion: HighlightedRegion,
highlightedRegion?: HighlightedRegion,
borderColor: string,
children: React.ReactNode
}) => {
if (highlightedRegion.startLine !== currentLineNumber) {
if (!highlightedRegion || highlightedRegion.endLine !== currentLineNumber) {
return <></>;
}
@@ -101,7 +90,23 @@ const Message = ({
paddingTop="1em"
paddingBottom="1em">
<MessageText>
{messageText}
{message.tokens.map((token, index) => {
switch (token.t) {
case 'text':
return <span key={`token-${index}`}>{token.text}</span>;
case 'location':
return <Link
key={`token-${index}`}
href={createRemoteFileRef(
token.location.fileLink,
token.location.highlightedRegion?.startLine,
token.location.highlightedRegion?.endLine)}>
{token.text}
</Link>;
default:
return <></>;
}
})}
{children && <>
<VerticalSpace size={2} />
{children}
@@ -120,76 +125,67 @@ const CodeLine = ({
}: {
line: string,
lineNumber: number,
highlightedRegion: HighlightedRegion
highlightedRegion?: HighlightedRegion
}) => {
if (!shouldHighlightLine(lineNumber, highlightedRegion)) {
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
return <PlainLine text={line} />;
}
const isSingleLineHighlight = highlightedRegion.endLine === undefined;
const isFirstHighlightedLine = lineNumber === highlightedRegion.startLine;
const isLastHighlightedLine = lineNumber === highlightedRegion.endLine;
const highlightStartColumn = isSingleLineHighlight
? highlightedRegion.startColumn
: isFirstHighlightedLine
? highlightedRegion.startColumn
: 0;
const highlightEndColumn = isSingleLineHighlight
? highlightedRegion.endColumn
: isLastHighlightedLine
? highlightedRegion.endColumn
: line.length;
const section1 = line.substring(0, highlightStartColumn - 1);
const section2 = line.substring(highlightStartColumn - 1, highlightEndColumn - 1);
const section3 = line.substring(highlightEndColumn - 1, line.length);
const partiallyHighlightedLine = parseHighlightedLine(line, lineNumber, highlightedRegion);
return (
<>
<PlainLine text={section1} />
<HighlightedLine text={section2} />
<PlainLine text={section3} />
<PlainLine text={partiallyHighlightedLine.plainSection1} />
<HighlightedLine text={partiallyHighlightedLine.highlightedSection} />
<PlainLine text={partiallyHighlightedLine.plainSection2} />
</>
);
};
const FileCodeSnippet = ({
filePath,
fileLink,
codeSnippet,
highlightedRegion,
severity,
message,
messageChildren,
}: {
filePath: string,
codeSnippet: CodeSnippet,
highlightedRegion: HighlightedRegion,
fileLink: FileLink,
codeSnippet?: CodeSnippet,
highlightedRegion?: HighlightedRegion,
severity?: ResultSeverity,
message?: string,
message?: AnalysisMessage,
messageChildren?: React.ReactNode,
}) => {
const code = codeSnippet.text.split('\n');
const startingLine = codeSnippet?.startLine || 0;
const endingLine = codeSnippet?.endLine || 0;
const startingLine = codeSnippet.startLine;
const titleFileUri = createRemoteFileRef(
fileLink,
startingLine,
endingLine);
if (!codeSnippet) {
return (
<Container>
<TitleContainer>
<Link href={titleFileUri}>{fileLink.filePath}</Link>
</TitleContainer>
</Container>
);
}
const code = codeSnippet.text.split('\n');
return (
<Container>
<TitleContainer>
<Link>{filePath}</Link>
<Link href={titleFileUri}>{fileLink.filePath}</Link>
</TitleContainer>
<CodeContainer>
{code.map((line, index) => (
<div key={index}>
{message && severity && <Message
messageText={message}
currentLineNumber={startingLine + index}
highlightedRegion={highlightedRegion}
borderColor={getSeverityColor(severity)}>
{messageChildren}
</Message>}
<Box display="flex">
<Box
p={2}
@@ -207,13 +203,21 @@ const FileCodeSnippet = ({
paddingTop="0.01em"
paddingLeft="1.5em"
paddingRight="0.5em"
paddingBottom="0.2em">
paddingBottom="0.2em"
sx={{ wordBreak: 'break-word' }}>
<CodeLine
line={line}
lineNumber={startingLine + index}
highlightedRegion={highlightedRegion} />
</Box>
</Box>
{message && severity && <Message
message={message}
currentLineNumber={startingLine + index}
highlightedRegion={highlightedRegion}
borderColor={getSeverityColor(severity)}>
{messageChildren}
</Message>}
</div>
))}
</CodeContainer>

View File

@@ -0,0 +1,91 @@
import * as React from 'react';
import { Box, Link } from '@primer/react';
import { CellValue, RawResultSet, ResultSetSchema } from '../../pure/bqrs-cli-types';
import { tryGetRemoteLocation } from '../../pure/bqrs-utils';
import { useState } from 'react';
import TextButton from './TextButton';
import { convertNonPrintableChars } from '../../text-utils';
const numOfResultsInContractedMode = 5;
const Row = ({
row,
fileLinkPrefix
}: {
row: CellValue[],
fileLinkPrefix: string
}) => (
<>
{row.map((cell, cellIndex) => (
<Box key={cellIndex}
borderColor="border.default"
borderStyle="solid"
justifyContent="center"
alignItems="center"
p={2}
sx={{ wordBreak: 'break-word' }}>
<Cell value={cell} fileLinkPrefix={fileLinkPrefix} />
</Box>
))}
</>
);
const Cell = ({
value,
fileLinkPrefix
}: {
value: CellValue,
fileLinkPrefix: string
}) => {
switch (typeof value) {
case 'string':
case 'number':
case 'boolean':
return <span>{convertNonPrintableChars(value.toString())}</span>;
case 'object': {
const url = tryGetRemoteLocation(value.url, fileLinkPrefix);
return <Link href={url}>{convertNonPrintableChars(value.label)}</Link>;
}
}
};
const RawResultsTable = ({
schema,
results,
fileLinkPrefix
}: {
schema: ResultSetSchema,
results: RawResultSet,
fileLinkPrefix: string
}) => {
const [tableExpanded, setTableExpanded] = useState(false);
const numOfResultsToShow = tableExpanded ? results.rows.length : numOfResultsInContractedMode;
const showButton = results.rows.length > numOfResultsInContractedMode;
// Create n equal size columns. We use minmax(0, 1fr) because the
// minimum width of 1fr is auto, not 0.
// https://css-tricks.com/equal-width-columns-in-css-grid-are-kinda-weird/
const gridTemplateColumns = `repeat(${schema.columns.length}, minmax(0, 1fr))`;
return (
<>
<Box
display="grid"
gridTemplateColumns={gridTemplateColumns}
maxWidth="45rem"
p={2}>
{results.rows.slice(0, numOfResultsToShow).map((row, rowIndex) => (
<Row key={rowIndex} row={row} fileLinkPrefix={fileLinkPrefix} />
))}
</Box>
{
showButton &&
<TextButton size='x-small' onClick={() => setTableExpanded(!tableExpanded)}>
{tableExpanded ? (<span>View less</span>) : (<span>View all</span>)}
</TextButton>
}
</>
);
};
export default RawResultsTable;

View File

@@ -4,7 +4,7 @@ import * as Rdom from 'react-dom';
import { Flash, ThemeProvider } from '@primer/react';
import { ToRemoteQueriesMessage } from '../../pure/interface-types';
import { AnalysisSummary, RemoteQueryResult } from '../shared/remote-query-result';
import { MAX_RAW_RESULTS } from '../shared/result-limits';
import { vscode } from '../../view/vscode-api';
import SectionTitle from './SectionTitle';
@@ -16,8 +16,10 @@ import DownloadButton from './DownloadButton';
import { AnalysisResults } from '../shared/analysis-result';
import DownloadSpinner from './DownloadSpinner';
import CollapsibleItem from './CollapsibleItem';
import { AlertIcon, CodeSquareIcon, FileCodeIcon, FileSymlinkFileIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
import { AlertIcon, CodeSquareIcon, FileCodeIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
import AnalysisAlertResult from './AnalysisAlertResult';
import RawResultsTable from './RawResultsTable';
import RepositoriesSearch from './RepositoriesSearch';
const numOfReposInContractedMode = 10;
@@ -51,13 +53,6 @@ const downloadAllAnalysesResults = (query: RemoteQueryResult) => {
});
};
const viewAnalysisResults = (analysisSummary: AnalysisSummary) => {
vscode.postMessage({
t: 'remoteQueryViewAnalysisResults',
analysisSummary
});
};
const openQueryFile = (queryResult: RemoteQueryResult) => {
vscode.postMessage({
t: 'openFile',
@@ -72,8 +67,13 @@ const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => {
});
};
const getAnalysisResultCount = (analysisResults: AnalysisResults): number => {
const rawResultCount = analysisResults.rawResults?.resultSet.rows.length || 0;
return analysisResults.interpretedResults.length + rawResultCount;
};
const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
analysesResults.reduce((acc, curr) => acc + curr.results.length, 0);
analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0);
const QueryInfo = (queryResult: RemoteQueryResult) => (
<>
@@ -154,7 +154,7 @@ const SummaryTitleNoResults = () => (
</div>
);
const SummaryItemDownloadAndView = ({
const SummaryItemDownload = ({
analysisSummary,
analysisResults
}: {
@@ -174,13 +174,7 @@ const SummaryItemDownloadAndView = ({
</>;
}
return <>
<HorizontalSpace size={2} />
<a className="vscode-codeql__analysis-result-file-link"
onClick={() => viewAnalysisResults(analysisSummary)} >
<FileSymlinkFileIcon size={16} />
</a>
</>;
return <></>;
};
const SummaryItem = ({
@@ -195,7 +189,7 @@ const SummaryItem = ({
<span className="vscode-codeql__analysis-item">{analysisSummary.nwo}</span>
<span className="vscode-codeql__analysis-item"><Badge text={analysisSummary.resultCount.toString()} /></span>
<span className="vscode-codeql__analysis-item">
<SummaryItemDownloadAndView
<SummaryItemDownload
analysisSummary={analysisSummary}
analysisResults={analysisResults} />
</span>
@@ -249,39 +243,73 @@ const AnalysesResultsTitle = ({ totalAnalysesResults, totalResults }: { totalAna
return <SectionTitle>{totalAnalysesResults}/{totalResults} results</SectionTitle>;
};
const AnalysesResultsDescription = ({ totalAnalysesResults, totalResults }: { totalAnalysesResults: number, totalResults: number }) => {
if (totalAnalysesResults < totalResults) {
return <>
<VerticalSpace size={1} />
Some results haven&apos;t been downloaded automatically because of their size or because enough were downloaded already.
Download them manually from the list above if you want to see them here.
</>;
}
const AnalysesResultsDescription = ({
queryResult,
analysesResults,
}: {
queryResult: RemoteQueryResult
analysesResults: AnalysisResults[],
}) => {
const showDownloadsMessage = queryResult.analysisSummaries.some(
s => !analysesResults.some(a => a.nwo === s.nwo && a.status === 'Completed'));
const downloadsMessage = <>
<VerticalSpace size={1} />
Some results haven&apos;t been downloaded automatically because of their size or because enough were downloaded already.
Download them manually from the list above if you want to see them here.
</>;
return <></>;
const showMaxResultsMessage = analysesResults.some(a => a.rawResults?.capped);
const maxRawResultsMessage = <>
<VerticalSpace size={1} />
Some repositories have more than {MAX_RAW_RESULTS} results. We will only show you up to&nbsp;
{MAX_RAW_RESULTS} results for each repository.
</>;
return (
<>
{showDownloadsMessage && downloadsMessage}
{showMaxResultsMessage && maxRawResultsMessage}
</>
);
};
const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
const numOfResults = getAnalysisResultCount(analysisResults);
const title = <>
{analysisResults.nwo}
<Badge text={analysisResults.results.length.toString()} />
<Badge text={numOfResults.toString()} />
</>;
return (
<CollapsibleItem title={title}>
<ul className="vscode-codeql__flat-list" >
{analysisResults.results.map((r, i) =>
{analysisResults.interpretedResults.map((r, i) =>
<li key={i}>
<AnalysisAlertResult alert={r} />
<VerticalSpace size={2} />
</li>)}
</ul>
{analysisResults.rawResults &&
<RawResultsTable
schema={analysisResults.rawResults.schema}
results={analysisResults.rawResults.resultSet}
fileLinkPrefix={analysisResults.rawResults.fileLinkPrefix} />
}
</CollapsibleItem>
);
};
const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: AnalysisResults[], totalResults: number }) => {
const AnalysesResults = ({
queryResult,
analysesResults,
totalResults
}: {
queryResult: RemoteQueryResult,
analysesResults: AnalysisResults[],
totalResults: number
}) => {
const totalAnalysesResults = sumAnalysesResults(analysesResults);
const [filterValue, setFilterValue] = React.useState('');
if (totalResults === 0) {
return <></>;
@@ -294,13 +322,22 @@ const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: A
totalAnalysesResults={totalAnalysesResults}
totalResults={totalResults} />
<AnalysesResultsDescription
totalAnalysesResults={totalAnalysesResults}
totalResults={totalResults} />
queryResult={queryResult}
analysesResults={analysesResults} />
<VerticalSpace size={2} />
<RepositoriesSearch
filterValue={filterValue}
setFilterValue={setFilterValue} />
<ul className="vscode-codeql__flat-list">
{analysesResults.filter(a => a.results.length > 0).map(r =>
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
<RepoAnalysisResults {...r} />
</li>)}
{analysesResults
.filter(a => a.interpretedResults.length > 0 || a.rawResults)
.filter(a => a.nwo.toLowerCase().includes(filterValue.toLowerCase()))
.map(r =>
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
<RepoAnalysisResults {...r} />
</li>)}
</ul>
</>
);
@@ -331,18 +368,21 @@ export function RemoteQueries(): JSX.Element {
return <div>Waiting for results to load.</div>;
}
const showAnalysesResults = false;
try {
return <div>
<ThemeProvider colorMode="auto">
<ViewTitle>{queryResult.queryTitle}</ViewTitle>
<QueryInfo {...queryResult} />
<Failures {...queryResult} />
<Summary queryResult={queryResult} analysesResults={analysesResults} />
{showAnalysesResults && <AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />}
</ThemeProvider>
</div>;
return (
<div className="vscode-codeql__remote-queries">
<ThemeProvider colorMode="auto">
<ViewTitle>{queryResult.queryTitle}</ViewTitle>
<QueryInfo {...queryResult} />
<Failures {...queryResult} />
<Summary queryResult={queryResult} analysesResults={analysesResults} />
<AnalysesResults
queryResult={queryResult}
analysesResults={analysesResults}
totalResults={queryResult.totalResultCount} />
</ThemeProvider>
</div>
);
} catch (err) {
console.error(err);
return <div>There was an error displaying the view.</div>;

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import { ChangeEvent } from 'react';
import { TextInput } from '@primer/react';
import { SearchIcon } from '@primer/octicons-react';
interface RepositoriesSearchProps {
filterValue: string;
setFilterValue: (value: string) => void;
}
const RepositoriesSearch = ({ filterValue, setFilterValue }: RepositoriesSearchProps) => {
return <>
<TextInput
block
sx={{
backgroundColor: 'var(--vscode-editor-background);',
color: 'var(--vscode-editor-foreground);',
width: 'calc(100% - 14px)',
}}
leadingVisual={SearchIcon}
aria-label="Repository search"
name="repository-search"
placeholder="Filter by repository owner/name"
value={filterValue}
onChange={(e: ChangeEvent) => setFilterValue((e.target as HTMLInputElement).value)}
/>
</>;
};
export default RepositoriesSearch;

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import styled from 'styled-components';
type Size = 'x-small' | 'small' | 'medium' | 'large' | 'x-large';
const StyledButton = styled.button<{ size: Size }>`
background: none;
color: var(--vscode-textLink-foreground);
border: none;
cursor: pointer;
font-size: ${props => props.size};
`;
const TextButton = ({
size,
onClick,
children
}: {
size: Size,
onClick: () => void,
children: React.ReactNode
}) => (
<StyledButton
size={size}
onClick={onClick}>
{children}
</StyledButton>
);
export default TextButton;

View File

@@ -1,3 +1,7 @@
.vscode-codeql__remote-queries {
max-width: 55em;
}
.vscode-codeql__query-info-link {
text-decoration: none;
padding-right: 1em;
@@ -33,10 +37,6 @@
font-size: x-small;
}
.vscode-codeql__analysis-result-file-link {
vertical-align: middle;
}
.vscode-codeql__analysis-failure {
margin: 0;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,

View File

@@ -29,13 +29,14 @@ import { ProgressCallback, UserCancellationException } from './commandRunner';
import { DatabaseInfo, QueryMetadata } from './pure/interface-types';
import { logger } from './logging';
import * as messages from './pure/messages';
import { InitialQueryInfo } from './query-results';
import { InitialQueryInfo, LocalQueryInfo } from './query-results';
import * as qsClient from './queryserver-client';
import { isQuickQueryPath } from './quick-query';
import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades';
import { ensureMetadataIsComplete } from './query-results';
import { SELECT_QUERY_NAME } from './contextual/locationFinder';
import { DecodedBqrsChunk } from './pure/bqrs-cli-types';
import { getErrorMessage } from './pure/helpers-pure';
/**
* run-queries.ts
@@ -94,6 +95,18 @@ export class QueryEvaluationInfo {
return qsClient.findQueryLogFile(this.querySaveDir);
}
get evalLogPath() {
return qsClient.findQueryEvalLogFile(this.querySaveDir);
}
get evalLogSummaryPath() {
return qsClient.findQueryEvalLogSummaryFile(this.querySaveDir);
}
get evalLogEndSummaryPath() {
return qsClient.findQueryEvalLogEndSummaryFile(this.querySaveDir);
}
get resultsPaths() {
return {
resultsPath: path.join(this.querySaveDir, 'results.bqrs'),
@@ -124,6 +137,7 @@ export class QueryEvaluationInfo {
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
queryInfo?: LocalQueryInfo,
): Promise<messages.EvaluationResult> {
if (!dbItem.contents || dbItem.error) {
throw new Error('Can\'t run query on invalid database.');
@@ -155,6 +169,13 @@ export class QueryEvaluationInfo {
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: 'default'
};
if (queryInfo && await qs.cliServer.cliConstraints.supportsPerQueryEvalLog()) {
await qs.sendRequest(messages.startLog, {
db: dataset,
logPath: this.evalLogPath,
});
}
const params: messages.EvaluateQueriesParams = {
db: dataset,
evaluateId: callbackId,
@@ -171,6 +192,26 @@ export class QueryEvaluationInfo {
}
} finally {
qs.unRegisterCallback(callbackId);
if (queryInfo && await qs.cliServer.cliConstraints.supportsPerQueryEvalLog()) {
await qs.sendRequest(messages.endLog, {
db: dataset,
logPath: this.evalLogPath,
});
if (await this.hasEvalLog()) {
queryInfo.evalLogLocation = this.evalLogPath;
await qs.cliServer.generateLogSummary(this.evalLogPath, this.evalLogSummaryPath, this.evalLogEndSummaryPath);
queryInfo.evalLogSummaryLocation = this.evalLogSummaryPath;
fs.readFile(this.evalLogEndSummaryPath, (err, buffer) => {
if (err) {
throw new Error(`Could not read structured evaluator log end of summary file at ${this.evalLogEndSummaryPath}.`);
}
void qs.logger.log(' --- Evaluator Log Summary --- ');
void qs.logger.log(buffer.toString());
});
} else {
void showAndLogWarningMessage(`Failed to write structured evaluator log to ${this.evalLogPath}.`);
}
}
}
return result || {
evaluationTime: 0,
@@ -284,6 +325,13 @@ export class QueryEvaluationInfo {
return this.dilPath;
}
/**
* Holds if this query already has a completed structured evaluator log
*/
async hasEvalLog(): Promise<boolean> {
return fs.pathExists(this.evalLogPath);
}
/**
* Creates the CSV file containing the results of this query. This will only be called if the query
* does not have interpreted results and the CSV file does not already exist.
@@ -657,6 +705,7 @@ export async function compileAndRunQueryAgainstDatabase(
progress: ProgressCallback,
token: CancellationToken,
templates?: messages.TemplateDefinitions,
queryInfo?: LocalQueryInfo, // May be omitted for queries not initiated by the user. If omitted we won't create a structured log for the query.
): Promise<QueryWithResults> {
if (!dbItem.contents || !dbItem.contents.dbSchemeUri) {
throw new Error(`Database ${dbItem.databaseUri} does not have a CodeQL database scheme.`);
@@ -742,7 +791,7 @@ export async function compileAndRunQueryAgainstDatabase(
}
if (errors.length === 0) {
const result = await query.run(qs, upgradeQlo, availableMlModels, dbItem, progress, token);
const result = await query.run(qs, upgradeQlo, availableMlModels, dbItem, progress, token, queryInfo);
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const message = result.message || 'Failed to run query';
void logger.log(message);
@@ -790,7 +839,7 @@ export async function compileAndRunQueryAgainstDatabase(
await upgradeDir?.cleanup();
} catch (e) {
void qs.logger.log(
`Could not clean up the upgrades dir. Reason: ${e.message || e}`,
`Could not clean up the upgrades dir. Reason: ${getErrorMessage(e)}`,
{ additionalLogLocation: query.logPath }
);
}

View File

@@ -4,17 +4,18 @@ import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import Assembler = require('stream-json/Assembler');
import { chain } from 'stream-chain';
import { getErrorMessage } from './pure/helpers-pure';
const DUMMY_TOOL : Sarif.Tool = {driver: {name: ''}};
const DUMMY_TOOL: Sarif.Tool = { driver: { name: '' } };
export async function sarifParser(interpretedResultsPath: string) : Promise<Sarif.Log> {
export async function sarifParser(interpretedResultsPath: string): Promise<Sarif.Log> {
try {
// Parse the SARIF file into token streams, filtering out only the results array.
const p = parser();
const pipeline = chain([
fs.createReadStream(interpretedResultsPath),
p,
pick({filter: 'runs.0.results'})
pick({ filter: 'runs.0.results' })
]);
// Creates JavaScript objects from the token stream
@@ -26,23 +27,23 @@ export async function sarifParser(interpretedResultsPath: string) : Promise<Sari
pipeline.on('error', (error) => {
reject(error);
});
asm.on('done', (asm) => {
const log : Sarif.Log = {
version: '2.1.0',
const log: Sarif.Log = {
version: '2.1.0',
runs: [
{
tool: DUMMY_TOOL,
{
tool: DUMMY_TOOL,
results: asm.current ?? []
}
]
};
resolve(log);
});
});
} catch (err) {
throw new Error(`Parsing output of interpretation failed: ${err.stderr || err}`);
} catch (e) {
throw new Error(`Parsing output of interpretation failed: ${(e as any).stderr || getErrorMessage(e)}`);
}
}
}

View File

@@ -76,7 +76,7 @@ export class QLTestAdapterFactory extends DisposableObject {
* @param ext The new extension, including the `.`.
*/
function changeExtension(p: string, ext: string): string {
return p.substr(0, p.length - path.extname(p).length) + ext;
return p.slice(0, -path.extname(p).length) + ext;
}
/**

View File

@@ -0,0 +1,31 @@
const CONTROL_CODE = '\u001F'.codePointAt(0)!;
const CONTROL_LABEL = '\u2400'.codePointAt(0)!;
/**
* Converts the given text so that any non-printable characters are replaced.
* @param label The text to convert.
* @returns The converted text.
*/
export function convertNonPrintableChars(label: string | undefined) {
// If the label was empty, use a placeholder instead, so the link is still clickable.
if (!label) {
return '[empty string]';
} else if (label.match(/^\s+$/)) {
return `[whitespace: "${label}"]`;
} else {
/**
* If the label contains certain non-printable characters, loop through each
* character and replace it with the cooresponding unicode control label.
*/
const convertedLabelArray: any[] = [];
for (let i = 0; i < label.length; i++) {
const labelCheck = label.codePointAt(i)!;
if (labelCheck <= CONTROL_CODE) {
convertedLabelArray[i] = String.fromCodePoint(labelCheck + CONTROL_LABEL);
} else {
convertedLabelArray[i] = label.charAt(i);
}
}
return convertedLabelArray.join('');
}
}

View File

@@ -1,10 +1,10 @@
import * as React from 'react';
import { renderLocation } from './result-table-utils';
import { ColumnValue } from '../pure/bqrs-cli-types';
import { CellValue } from '../pure/bqrs-cli-types';
interface Props {
value: ColumnValue;
value: CellValue;
databaseUri: string;
}

View File

@@ -5,6 +5,7 @@ import { RawResultsSortState, QueryMetadata, SortDirection } from '../pure/inter
import { assertNever } from '../pure/helpers-pure';
import { ResultSet } from '../pure/interface-types';
import { vscode } from './vscode-api';
import { convertNonPrintableChars } from '../text-utils';
export interface ResultTableProps {
resultSet: ResultSet;
@@ -37,9 +38,6 @@ export const oddRowClassName = 'vscode-codeql__result-table-row--odd';
export const pathRowClassName = 'vscode-codeql__result-table-row--path';
export const selectedRowClassName = 'vscode-codeql__result-table-row--selected';
const CONTROL_CODE = '\u001F'.codePointAt(0)!;
const CONTROL_LABEL = '\u2400'.codePointAt(0)!;
export function jumpToLocationHandler(
loc: ResolvableLocationValue,
databaseUri: string,
@@ -70,30 +68,6 @@ export function openFile(filePath: string): void {
});
}
function convertedNonprintableChars(label: string) {
// If the label was empty, use a placeholder instead, so the link is still clickable.
if (!label) {
return '[empty string]';
} else if (label.match(/^\s+$/)) {
return `[whitespace: "${label}"]`;
} else {
/**
* If the label contains certain non-printable characters, loop through each
* character and replace it with the cooresponding unicode control label.
*/
const convertedLabelArray: any[] = [];
for (let i = 0; i < label.length; i++) {
const labelCheck = label.codePointAt(i)!;
if (labelCheck <= CONTROL_CODE) {
convertedLabelArray[i] = String.fromCodePoint(labelCheck + CONTROL_LABEL);
} else {
convertedLabelArray[i] = label.charAt(i);
}
}
return convertedLabelArray.join('');
}
}
/**
* Render a location as a link which when clicked displays the original location.
*/
@@ -105,7 +79,7 @@ export function renderLocation(
callback?: () => void
): JSX.Element {
const displayLabel = convertedNonprintableChars(label!);
const displayLabel = convertNonPrintableChars(label!);
if (loc === undefined) {
return <span>{displayLabel}</span>;

View File

@@ -18,7 +18,7 @@ describe('Databases', function() {
this.timeout(60000);
const LGTM_URL = 'https://lgtm.com/projects/g/aeisenberg/angular-bind-notifier/';
let databaseManager: DatabaseManager;
let sandbox: sinon.SinonSandbox;
let inputBoxStub: sinon.SinonStub;
@@ -40,7 +40,7 @@ describe('Databases', function() {
progressCallback = sandbox.spy();
inputBoxStub = sandbox.stub(window, 'showInputBox');
} catch (e) {
fail(e);
fail(e as Error);
}
});
@@ -48,7 +48,7 @@ describe('Databases', function() {
try {
sandbox.restore();
} catch (e) {
fail(e);
fail(e as Error);
}
});

View File

@@ -1,6 +1,5 @@
import * as path from 'path';
import { extensions } from 'vscode';
import 'mocha';
import { CodeQLCliServer } from '../../cli';
import { CodeQLExtensionInterface } from '../../extension';

View File

@@ -1,7 +1,9 @@
import 'source-map-support/register';
import { runTestsInDirectory } from '../index-template';
import 'mocha';
import * as sinonChai from 'sinon-chai';
import * as chai from 'chai';
import 'chai/register-should';
import * as chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
chai.use(sinonChai);

View File

@@ -1,6 +1,5 @@
import * as sinon from 'sinon';
import { extensions, window } from 'vscode';
import 'mocha';
import * as path from 'path';
import * as pq from 'proxyquire';
@@ -8,6 +7,7 @@ import * as pq from 'proxyquire';
import { CliVersionConstraint, CodeQLCliServer } from '../../cli';
import { CodeQLExtensionInterface } from '../../extension';
import { expect } from 'chai';
import { getErrorMessage } from '../../pure/helpers-pure';
const proxyquire = pq.noPreserveCache();
@@ -121,8 +121,8 @@ describe('Packaging commands', function() {
await mod.handleInstallPackDependencies(cli, progress);
// This line should not be reached
expect(true).to.be.false;
} catch (error) {
expect(error.message).to.contain('Unable to install pack dependencies');
} catch (e) {
expect(getErrorMessage(e)).to.contain('Unable to install pack dependencies');
}
});
});

View File

@@ -3,7 +3,6 @@ import { CancellationToken, commands, ExtensionContext, extensions, Uri } from '
import * as sinon from 'sinon';
import * as path from 'path';
import * as fs from 'fs-extra';
import 'mocha';
import { expect } from 'chai';
import * as yaml from 'js-yaml';
@@ -78,7 +77,7 @@ describe('Queries', function() {
}
dbItem = maybeDbItem;
} catch (e) {
fail(e);
fail(e as Error);
}
});
@@ -86,7 +85,7 @@ describe('Queries', function() {
try {
sandbox.restore();
} catch (e) {
fail(e);
fail(e as Error);
}
});
@@ -107,7 +106,7 @@ describe('Queries', function() {
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
} catch (e) {
console.error('Test Failed');
fail(e);
fail(e as Error);
}
});
@@ -131,7 +130,7 @@ describe('Queries', function() {
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
} catch (e) {
console.error('Test Failed');
fail(e);
fail(e as Error);
}
});

View File

@@ -1,6 +1,5 @@
import { expect } from 'chai';
import * as fs from 'fs-extra';
import 'mocha';
import * as path from 'path';
import * as tmp from 'tmp';
import * as url from 'url';
@@ -8,7 +7,7 @@ import { CancellationTokenSource } from 'vscode-jsonrpc';
import * as messages from '../../pure/messages';
import * as qsClient from '../../queryserver-client';
import * as cli from '../../cli';
import { ColumnValue } from '../../pure/bqrs-cli-types';
import { CellValue } from '../../pure/bqrs-cli-types';
import { extensions } from 'vscode';
import { CodeQLExtensionInterface } from '../../extension';
import { fail } from 'assert';
@@ -53,7 +52,7 @@ class Checkpoint<T> {
}
type ResultSets = {
[name: string]: ColumnValue[][];
[name: string]: CellValue[][];
}
type QueryTestCase = {
@@ -113,7 +112,7 @@ describe('using the query server', function() {
throw new Error('Extension not initialized. Make sure cli is downloaded and installed properly.');
}
} catch (e) {
fail(e);
fail(e as Error);
}
});
@@ -163,7 +162,7 @@ describe('using the query server', function() {
await compilationSucceeded.resolve();
}
catch (e) {
await compilationSucceeded.reject(e);
await compilationSucceeded.reject(e as Error);
}
});
@@ -190,7 +189,7 @@ describe('using the query server', function() {
await qs.sendRequest(messages.runQueries, params, token, () => { /**/ });
}
catch (e) {
await evaluationSucceeded.reject(e);
await evaluationSucceeded.reject(e as Error);
}
});

View File

@@ -2,21 +2,20 @@ import { assert, expect } from 'chai';
import * as path from 'path';
import * as sinon from 'sinon';
import { CancellationToken, extensions, QuickPickItem, Uri, window } from 'vscode';
import 'mocha';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as yaml from 'js-yaml';
import { QlPack, runRemoteQuery } from '../../remote-queries/run-remote-query';
import { Credentials } from '../../authentication';
import { CliVersionConstraint, CodeQLCliServer } from '../../cli';
import { CodeQLExtensionInterface } from '../../extension';
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../config';
import { UserCancellationException } from '../../commandRunner';
import { QlPack, runRemoteQuery } from '../../../remote-queries/run-remote-query';
import { Credentials } from '../../../authentication';
import { CliVersionConstraint, CodeQLCliServer } from '../../../cli';
import { CodeQLExtensionInterface } from '../../../extension';
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../../config';
import { UserCancellationException } from '../../../commandRunner';
import { lte } from 'semver';
describe('Remote queries', function() {
const baseDir = path.join(__dirname, '../../../src/vscode-tests/cli-integration');
const baseDir = path.join(__dirname, '../../../../src/vscode-tests/cli-integration');
let sandbox: sinon.SinonSandbox;
@@ -53,7 +52,7 @@ describe('Remote queries', function() {
progress = sandbox.spy();
// Should not have asked for a language
showQuickPickSpy = sandbox.stub(window, 'showQuickPick')
.onFirstCall().resolves({ repoList: ['github/vscode-codeql'] } as unknown as QuickPickItem)
.onFirstCall().resolves({ repositories: ['github/vscode-codeql'] } as unknown as QuickPickItem)
.onSecondCall().resolves('javascript' as unknown as QuickPickItem);
// always run in the vscode-codeql repo
@@ -279,7 +278,7 @@ describe('Remote queries', function() {
},
library: false,
defaultSuite: [{
description: 'Query suite for remote query'
description: 'Query suite for variant analysis'
}, {
query: queryPath
}]

View File

@@ -44,7 +44,7 @@ const _10MB = _1MB * 10;
// CLI version to test. Hard code the latest as default. And be sure
// to update the env if it is not otherwise set.
const CLI_VERSION = process.env.CLI_VERSION || 'v2.8.2';
const CLI_VERSION = process.env.CLI_VERSION || 'v2.8.5';
process.env.CLI_VERSION = CLI_VERSION;
// Base dir where CLIs will be downloaded into

View File

@@ -1,13 +1,8 @@
import * as assert from 'assert';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import 'mocha';
import * as path from 'path';
import * as vscode from 'vscode';
import * as determiningSelectedQueryTest from './determining-selected-query-test';
chai.use(chaiAsPromised);
describe('launching with a minimal workspace', async () => {
const ext = vscode.extensions.getExtension('GitHub.vscode-codeql');

View File

@@ -1,9 +1,6 @@
import 'vscode-test';
import 'mocha';
import * as chaiAsPromised from 'chai-as-promised';
import 'sinon-chai';
import * as Sinon from 'sinon';
import * as chai from 'chai';
import { expect } from 'chai';
import { workspace } from 'vscode';
import {
@@ -12,9 +9,6 @@ import {
QueryServerConfigListener
} from '../../config';
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('config listeners', function() {
// Because we are adding some extra waiting, need to bump the test timeouts.
this.timeout(5000);

View File

@@ -1,5 +1,4 @@
import 'vscode-test';
import 'mocha';
import * as sinon from 'sinon';
import * as tmp from 'tmp';
import * as fs from 'fs-extra';

View File

@@ -1,12 +1,13 @@
import 'source-map-support/register';
import { runTestsInDirectory } from '../index-template';
import * as sinonChai from 'sinon-chai';
import * as chai from 'chai';
import 'chai/register-should';
import * as chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
chai.use(sinonChai);
export function run(): Promise<void> {
return runTestsInDirectory(__dirname);
}

View File

@@ -1,5 +1,4 @@
import 'vscode-test';
import 'mocha';
import { Uri, WorkspaceFolder } from 'vscode';
import { expect } from 'chai';
import * as fs from 'fs-extra';

View File

@@ -1,19 +1,13 @@
import * as fs from 'fs-extra';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { expect } from 'chai';
import * as sinon from 'sinon';
import * as yaml from 'js-yaml';
import { AstViewer, AstItem } from '../../astViewer';
import { commands, Range } from 'vscode';
import { commands, Range, Uri } from 'vscode';
import { DatabaseItem } from '../../databases';
import { testDisposeHandler } from '../test-dispose-handler';
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('AstViewer', () => {
let astRoots: AstItem[];
let viewer: AstViewer | undefined;
@@ -40,7 +34,7 @@ describe('AstViewer', () => {
it('should update the viewer roots', () => {
const item = {} as DatabaseItem;
viewer = new AstViewer();
viewer.updateRoots(astRoots, item, 'def/abc');
viewer.updateRoots(astRoots, item, Uri.file('def/abc'));
expect((viewer as any).treeDataProvider.roots).to.eq(astRoots);
expect((viewer as any).treeDataProvider.db).to.eq(item);
@@ -59,25 +53,31 @@ describe('AstViewer', () => {
doSelectionTest(expr, expr.fileLocation?.range);
});
it('should select nothing', () => {
it('should select nothing because of no overlap in range', () => {
doSelectionTest(undefined, new Range(2, 3, 4, 5));
});
it('should select nothing because of different file', () => {
doSelectionTest(undefined, astRoots[0].fileLocation?.range, Uri.file('def'));
});
const defaultUri = Uri.file('def/abc');
function doSelectionTest(
expectedSelection: any,
selectionRange: Range | undefined,
fsPath = 'def/abc',
fileUri = defaultUri
) {
const item = {} as DatabaseItem;
viewer = new AstViewer();
viewer.updateRoots(astRoots, item, fsPath);
viewer.updateRoots(astRoots, item, defaultUri);
const spy = sandbox.spy();
(viewer as any).treeView.reveal = spy;
Object.defineProperty((viewer as any).treeView, 'visible', {
value: true
});
const mockEvent = createMockEvent(selectionRange, fsPath);
const mockEvent = createMockEvent(selectionRange, fileUri);
(viewer as any).updateTreeSelection(mockEvent);
if (expectedSelection) {
expect(spy).to.have.been.calledWith(expectedSelection);
@@ -88,7 +88,7 @@ describe('AstViewer', () => {
function createMockEvent(
selectionRange: Range | undefined,
fsPath: string,
uri: Uri,
) {
return {
selections: [{
@@ -98,7 +98,7 @@ describe('AstViewer', () => {
textEditor: {
document: {
uri: {
fsPath
fsPath: uri.fsPath
}
}
}

View File

@@ -1,15 +1,12 @@
import * as fs from 'fs-extra';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { expect } from 'chai';
import * as sinon from 'sinon';
import AstBuilder from '../../../contextual/astBuilder';
import { QueryWithResults } from '../../../run-queries';
import { CodeQLCliServer } from '../../../cli';
import { DatabaseItem } from '../../../databases';
chai.use(chaiAsPromised);
const expect = chai.expect;
import { Uri } from 'vscode';
/**
*
@@ -145,7 +142,7 @@ describe('AstBuilder', () => {
resultsPath: '/a/b/c'
}
}
} as QueryWithResults, mockCli, {} as DatabaseItem, '');
} as QueryWithResults, mockCli, {} as DatabaseItem, Uri.file(''));
}
function mockDecode(resultSet: 'nodes' | 'edges' | 'graphProperties') {

View File

@@ -1,5 +1,4 @@
import 'vscode-test';
import 'mocha';
import { expect } from 'chai';
import { Uri, Range } from 'vscode';

View File

@@ -1,17 +1,12 @@
import 'vscode-test';
import 'mocha';
import * as yaml from 'js-yaml';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import * as chai from 'chai';
import * as sinonChai from 'sinon-chai';
import { expect } from 'chai';
import * as pq from 'proxyquire';
import { KeyType } from '../../../contextual/keyType';
import { getErrorMessage } from '../../../pure/helpers-pure';
const proxyquire = pq.noPreserveCache().noCallThru();
chai.use(chaiAsPromised);
chai.use(sinonChai);
const expect = chai.expect;
describe('queryResolver', () => {
let module: Record<string, Function>;
@@ -70,7 +65,7 @@ describe('queryResolver', () => {
// should reject
expect(true).to.be.false;
} catch (e) {
expect(e.message).to.eq(
expect(getErrorMessage(e)).to.eq(
'Couldn\'t find any queries tagged ide-contextual-queries/local-definitions in any of the following packs: my-qlpack.'
);
}

View File

@@ -22,6 +22,17 @@
"innerFilePath": "results.sarif",
"queryId": "MRVA Integration test 1-6sBi6oaky_fxqXW2NA4bx"
}
},
{
"nwo": "hucairz/i-dont-exist",
"resultCount": 5,
"fileSizeInBytes": 81237,
"downloadLink": {
"id": "999999",
"urlPath": "/these/results/will/never/be/downloaded/999999",
"innerFilePath": "results.sarif",
"queryId": "MRVA Integration test 2-UL-vbKAjP8ffObxjsp7hN"
}
}
],
"analysisFailures": [],

View File

@@ -1,27 +1,155 @@
import 'vscode-test';
import 'mocha';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import * as path from 'path';
import * as fs from 'fs-extra';
import * as tmp from 'tmp';
import * as chai from 'chai';
import { expect } from 'chai';
import { window } from 'vscode';
import {
convertToDatabaseUrl,
convertLgtmUrlToDatabaseUrl,
looksLikeLgtmUrl,
findDirWithFile,
looksLikeGithubRepo,
} from '../../databaseFetcher';
import { ProgressCallback } from '../../commandRunner';
chai.use(chaiAsPromised);
const expect = chai.expect;
import * as pq from 'proxyquire';
describe('databaseFetcher', function () {
const proxyquire = pq.noPreserveCache();
describe('databaseFetcher', function() {
// These tests make API calls and may need extra time to complete.
this.timeout(10000);
describe('convertToDatabaseUrl', () => {
describe('convertGithubNwoToDatabaseUrl', () => {
let sandbox: sinon.SinonSandbox;
let quickPickSpy: sinon.SinonStub;
let progressSpy: ProgressCallback;
let mockRequest: sinon.SinonStub;
let mod: any;
const credentials = getMockCredentials(0);
beforeEach(() => {
sandbox = sinon.createSandbox();
quickPickSpy = sandbox.stub(window, 'showQuickPick');
progressSpy = sandbox.spy();
mockRequest = sandbox.stub();
mod = proxyquire('../../databaseFetcher', {
'./authentication': {
Credentials: credentials,
},
});
});
afterEach(() => {
sandbox.restore();
});
it('should convert a GitHub nwo to a database url', async () => {
// We can't make the real octokit request (since we need credentials), so we mock the response.
const mockApiResponse = {
data: [
{
id: 1495869,
name: 'csharp-database',
language: 'csharp',
uploader: {},
content_type: 'application/zip',
state: 'uploaded',
size: 55599715,
created_at: '2022-03-24T10:46:24Z',
updated_at: '2022-03-24T10:46:27Z',
url: 'https://api.github.com/repositories/143040428/code-scanning/codeql/databases/csharp',
},
{
id: 1100671,
name: 'database.zip',
language: 'javascript',
uploader: {},
content_type: 'application/zip',
state: 'uploaded',
size: 29294434,
created_at: '2022-03-01T16:00:04Z',
updated_at: '2022-03-01T16:00:06Z',
url: 'https://api.github.com/repositories/143040428/code-scanning/codeql/databases/javascript',
},
{
id: 648738,
name: 'ql-database',
language: 'ql',
uploader: {},
content_type: 'application/json; charset=utf-8',
state: 'uploaded',
size: 39735500,
created_at: '2022-02-02T09:38:50Z',
updated_at: '2022-02-02T09:38:51Z',
url: 'https://api.github.com/repositories/143040428/code-scanning/codeql/databases/ql',
},
],
};
mockRequest.resolves(mockApiResponse);
quickPickSpy.resolves('javascript');
const githubRepo = 'github/codeql';
const { databaseUrl, name, owner } = await mod.convertGithubNwoToDatabaseUrl(
githubRepo,
credentials,
progressSpy
);
expect(databaseUrl).to.equal(
'https://api.github.com/repos/github/codeql/code-scanning/codeql/databases/javascript'
);
expect(name).to.equal('codeql');
expect(owner).to.equal('github');
expect(quickPickSpy.firstCall.args[0]).to.deep.equal([
'csharp',
'javascript',
'ql',
]);
});
// Repository doesn't exist, or the user has no access to the repository.
it('should fail on an invalid/inaccessible repository', async () => {
const mockApiResponse = {
data: {
message: 'Not Found',
},
status: 404,
};
mockRequest.resolves(mockApiResponse);
const githubRepo = 'foo/bar-not-real';
await expect(
mod.convertGithubNwoToDatabaseUrl(githubRepo, credentials, progressSpy)
).to.be.rejectedWith(/Unable to get database/);
expect(progressSpy).to.have.callCount(0);
});
// User has access to the repository, but there are no databases for any language.
it('should fail on a repository with no databases', async () => {
const mockApiResponse = {
data: [],
};
mockRequest.resolves(mockApiResponse);
const githubRepo = 'foo/bar-with-no-dbs';
await expect(
mod.convertGithubNwoToDatabaseUrl(githubRepo, credentials, progressSpy)
).to.be.rejectedWith(/Unable to get database/);
expect(progressSpy).to.have.been.calledOnce;
});
function getMockCredentials(response: any) {
mockRequest = sinon.stub().resolves(response);
return {
getOctokit: () => ({
request: mockRequest,
}),
};
}
});
describe('convertLgtmUrlToDatabaseUrl', () => {
let sandbox: sinon.SinonSandbox;
let quickPickSpy: sinon.SinonStub;
let progressSpy: ProgressCallback;
@@ -39,7 +167,7 @@ describe('databaseFetcher', function () {
it('should convert a project url to a database url', async () => {
quickPickSpy.resolves('javascript');
const lgtmUrl = 'https://lgtm.com/projects/g/github/codeql';
const dbUrl = await convertToDatabaseUrl(lgtmUrl, progressSpy);
const dbUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progressSpy);
expect(dbUrl).to.equal(
'https://lgtm.com/api/v1.0/snapshots/1506465042581/javascript'
@@ -52,7 +180,7 @@ describe('databaseFetcher', function () {
quickPickSpy.resolves('python');
const lgtmUrl =
'https://lgtm.com/projects/g/github/codeql/subpage/subpage2?query=xxx';
const dbUrl = await convertToDatabaseUrl(lgtmUrl, progressSpy);
const dbUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progressSpy);
expect(dbUrl).to.equal(
'https://lgtm.com/api/v1.0/snapshots/1506465042581/python'
@@ -64,7 +192,7 @@ describe('databaseFetcher', function () {
quickPickSpy.resolves('python');
const lgtmUrl =
'g/github/codeql';
const dbUrl = await convertToDatabaseUrl(lgtmUrl, progressSpy);
const dbUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progressSpy);
expect(dbUrl).to.equal(
'https://lgtm.com/api/v1.0/snapshots/1506465042581/python'
@@ -75,11 +203,37 @@ describe('databaseFetcher', function () {
it('should fail on a nonexistent project', async () => {
quickPickSpy.resolves('javascript');
const lgtmUrl = 'https://lgtm.com/projects/g/github/hucairz';
await expect(convertToDatabaseUrl(lgtmUrl, progressSpy)).to.rejectedWith(/Invalid LGTM URL/);
await expect(convertLgtmUrlToDatabaseUrl(lgtmUrl, progressSpy)).to.rejectedWith(/Invalid LGTM URL/);
expect(progressSpy).to.have.callCount(0);
});
});
describe('looksLikeGithubRepo', () => {
it('should handle invalid urls', () => {
expect(looksLikeGithubRepo(''))
.to.be.false;
expect(looksLikeGithubRepo('http://github.com/foo/bar'))
.to.be.false;
expect(looksLikeGithubRepo('https://ww.github.com/foo/bar'))
.to.be.false;
expect(looksLikeGithubRepo('https://ww.github.com/foo'))
.to.be.false;
expect(looksLikeGithubRepo('foo'))
.to.be.false;
});
it('should handle valid urls', () => {
expect(looksLikeGithubRepo('https://github.com/foo/bar'))
.to.be.true;
expect(looksLikeGithubRepo('https://www.github.com/foo/bar'))
.to.be.true;
expect(looksLikeGithubRepo('https://github.com/foo/bar/sub/pages'))
.to.be.true;
expect(looksLikeGithubRepo('foo/bar'))
.to.be.true;
});
});
describe('looksLikeLgtmUrl', () => {
it('should handle invalid urls', () => {
expect(looksLikeLgtmUrl('')).to.be.false;

View File

@@ -1,5 +1,4 @@
import 'vscode-test';
import 'mocha';
import * as tmp from 'tmp';
import * as path from 'path';
import * as fs from 'fs-extra';
@@ -8,6 +7,7 @@ import { Uri } from 'vscode';
import { DatabaseUI } from '../../databases-ui';
import { testDisposeHandler } from '../test-dispose-handler';
import { Credentials } from '../../authentication';
describe('databases-ui', () => {
describe('fixDbUri', () => {
@@ -78,7 +78,8 @@ describe('databases-ui', () => {
} as any,
{} as any,
storageDir,
storageDir
storageDir,
() => Promise.resolve({} as Credentials),
);
await databaseUI.handleRemoveOrphanedDatabases();

View File

@@ -1,18 +1,13 @@
import * as chai from 'chai';
import { expect } from 'chai';
import * as path from 'path';
import * as fetch from 'node-fetch';
import 'chai/register-should';
import * as semver from 'semver';
import * as sinonChai from 'sinon-chai';
import * as sinon from 'sinon';
import * as pq from 'proxyquire';
import 'mocha';
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer } from '../../distribution';
const proxyquire = pq.noPreserveCache();
chai.use(sinonChai);
const expect = chai.expect;
describe('Releases API consumer', () => {
const owner = 'someowner';
@@ -95,7 +90,7 @@ describe('Releases API consumer', () => {
it('fails if none of the releases are within the version range', async () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
await chai.expect(
await expect(
consumer.getLatestRelease(new semver.Range('5.*.*'))
).to.be.rejectedWith(Error);
});
@@ -114,7 +109,7 @@ describe('Releases API consumer', () => {
it('fails if none of the releases pass the additional compatibility test', async () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
await chai.expect(consumer.getLatestRelease(
await expect(consumer.getLatestRelease(
new semver.Range('2.*.*'),
true,
release => release.assets.some(asset => asset.name === 'otherExampleAsset.txt')

View File

@@ -0,0 +1,36 @@
import { expect } from 'chai';
import 'mocha';
import * as path from 'path';
import { DownloadLink, createDownloadPath } from '../../remote-queries/download-link';
describe('createDownloadPath', () => {
it('should return the correct path', () => {
const downloadLink: DownloadLink = {
id: 'abc',
urlPath: '',
innerFilePath: '',
queryId: 'def'
};
const expectedPath = path.join('storage', 'def', 'abc');
const actualPath = createDownloadPath('storage', downloadLink);
expect(actualPath).to.equal(expectedPath);
});
it('should return the correct path with extension', () => {
const downloadLink: DownloadLink = {
id: 'abc',
urlPath: '',
innerFilePath: '',
queryId: 'def'
};
const expectedPath = path.join('storage', 'def', 'abc.zip');
const actualPath = createDownloadPath('storage', downloadLink, 'zip');
expect(actualPath).to.equal(expectedPath);
});
});

View File

@@ -1,5 +1,4 @@
import { expect } from 'chai';
import 'mocha';
import {
EnvironmentVariableCollection,
EnvironmentVariableMutator,

Some files were not shown because too many files have changed in this diff Show More