Compare commits

..

169 Commits

Author SHA1 Message Date
Dave Bartolomeo
d4673d9ca0 Merge pull request #1493 from dbartol/v1.16.12
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.16.12
2022-09-01 16:38:14 -04:00
Dave Bartolomeo
87f45a7739 v1.16.12 2022-09-01 16:25:04 -04:00
Koen Vlaswinkel
0c89df9a80 Merge pull request #1482 from github/koesie10/bundle-codicons
Bundle Codicons using Webpack
2022-09-01 16:12:55 +02:00
Koen Vlaswinkel
ba8b32078d Simplify and clarify Webpack font config 2022-09-01 11:21:06 +02:00
Koen Vlaswinkel
fa4dd087e5 Remove Codicons references from webview 2022-09-01 09:39:19 +02:00
Dave Bartolomeo
ac74b967b3 Merge pull request #1490 from dbartol/dbartol/log-version/work 2022-08-31 21:01:36 -04:00
Dave Bartolomeo
c349c6a048 Fix race condition when generating evaluator log summaries
The original code that logged the human-readable log summary generated the log asynchronously, which was a reasonable choice. When I added support for viewing and scanning logs, I didn't notice that the summary was being generated asynchronously, and wrote my code assuming that the summary was already on disk when I opened it to find where each relation's log started. The effect was that, depending on timing, the evaluation sometimes failed with an error popup complaining about not being able to open the log summary file.

The fix is to _generate_ the log summary synchronously, but continue to _log_ it asynchronously.
2022-08-31 18:17:45 -04:00
Dave Bartolomeo
234b05994c Guard --sourcemap option based on CLI version 2022-08-31 18:08:21 -04:00
Koen Vlaswinkel
af8f0231c0 Merge pull request #1485 from github/koesie10/add-github-download-button
Remove canary requirement for GitHub database download
2022-08-31 16:57:12 +02:00
Edoardo Pirovano
84bd029749 Restart CLI server too when restarting query server 2022-08-31 14:39:44 +01:00
shati-patel
7d2e4b6de4 Bump CLI version to 2.10.4 for integration tests 2022-08-31 13:52:40 +01:00
Koen Vlaswinkel
23a0e03cef Completely remove using credentials in non-canary mode
This does not remove the previously added mechanism of not requesting
credentials, but using them when they are available. I expect this to be
used in the future.
2022-08-31 14:22:17 +02:00
Koen Vlaswinkel
21c5ed01ad Fix typo in getOctokit JSDoc
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-08-31 11:48:27 +02:00
Koen Vlaswinkel
d2af550bcc Merge remote-tracking branch 'origin/main' into koesie10/bundle-codicons 2022-08-31 09:51:46 +02:00
Koen Vlaswinkel
cf36a52762 Merge pull request #1478 from github/koesie10/abstract-interface-manager
Add abstract interface manager
2022-08-31 09:48:05 +02:00
Koen Vlaswinkel
ac1a97efa0 Refactor databaseFetcher tests to not use proxyquire 2022-08-30 15:32:08 +02:00
Koen Vlaswinkel
8d5067f622 Update CHANGELOG 2022-08-30 15:09:16 +02:00
Koen Vlaswinkel
fe5f1c417d Remove authentication requirement for download GitHub databases
This makes authentication for download GitHub CodeQL databases optional.
If you are already authenticated, your token will be used. If you are
not authenticated, an anonymous request will be made.

If the canary flag is enabled, you will be prompted for credentials when
downloading a database and you are not yet logged in.
2022-08-30 15:05:15 +02:00
Koen Vlaswinkel
95438bb7e3 Remove canary requirement for GitHub database download 2022-08-30 14:33:48 +02:00
Koen Vlaswinkel
6d7d0ca41a Merge pull request #1477 from github/koesie10/unified-webpack-bundle
Unify the Webpack bundle
2022-08-30 11:29:45 +02:00
Koen Vlaswinkel
3749e17769 Bundle Codicons using Webpack
This will include the Codicons inside the webview bundle, reducing the
number of files that need to be loaded and the resource roots that need
to be included.
2022-08-29 14:31:29 +02:00
Koen Vlaswinkel
ee49fb5070 Merge branch 'koesie10/unified-webpack-bundle' into koesie10/abstract-interface-manager 2022-08-29 14:12:20 +02:00
Koen Vlaswinkel
de6c523bad Merge remote-tracking branch 'origin/main' into koesie10/unified-webpack-bundle 2022-08-29 13:57:23 +02:00
Koen Vlaswinkel
6612c279ae Merge pull request #1479 from github/koesie10/improve-controller-repo-prompt
Improve prompot for controller repo
2022-08-29 09:53:26 +02:00
Koen Vlaswinkel
2dfa0e8b52 Simplify interface manager and types 2022-08-29 09:51:49 +02:00
Koen Vlaswinkel
0197306713 Remove unnecessary top-level package-lock.json 2022-08-29 09:47:24 +02:00
Dave Bartolomeo
269165eaa3 Merge pull request #1476 from github/version/bump-to-v1.6.12
Bump version to v1.6.12
2022-08-26 10:38:05 -04:00
Koen Vlaswinkel
14c736d72e Improve prompot for controller repo
This will improve the prompt for the controller repo by making clear
that the GitHub Actions workflow will be run in the specified repo.
2022-08-26 13:58:11 +02:00
Koen Vlaswinkel
b8898b939c Add abstract interface manager
This will add a new abstract class that implements the creation of the
panel and webview to reduce duplication across the different interface
managers.
2022-08-26 12:34:28 +02:00
Koen Vlaswinkel
45da1e0f1f Unify the Webpack bundle
This will move all webviews into a single Webpack bundle. This will make
it easier to add new webviews since we don't need to add a new bundle,
but just need to add a new directory with an `index.tsx` file.

It also moves the CSS processing to Webpack so that we don't need to
specify the CSS files to use separately, but can simply do so in the
TypeScript files.
2022-08-26 11:15:24 +02:00
dbartol
88c990c6ae Bump version to v1.6.12 2022-08-25 20:46:21 +00:00
Dave Bartolomeo
ac7211c117 Merge pull request #1475 from dbartol/dbartol/extension-release/work
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
Prepare for release 1.6.11
2022-08-25 16:36:44 -04:00
Dave Bartolomeo
d1d13fbd2e Update changelog for release 2022-08-25 13:11:50 -04:00
Dave Bartolomeo
f99166d26c Update Node version to match vscode 2022-08-25 13:01:35 -04:00
dependabot[bot]
9cd6f9a768 Bump d3 and @types/d3 in /extensions/ql-vscode (#1461)
Bumps [d3](https://github.com/d3/d3) and [@types/d3](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/d3). These dependencies needed to be updated together.

Updates `d3` from 6.7.0 to 7.6.1
- [Release notes](https://github.com/d3/d3/releases)
- [Changelog](https://github.com/d3/d3/blob/main/CHANGES.md)
- [Commits](https://github.com/d3/d3/compare/v6.7.0...v7.6.1)

Updates `@types/d3` from 6.7.5 to 7.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/d3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-24 08:13:04 -07:00
Koen Vlaswinkel
4dd16f4611 Merge pull request #1472 from github/koesie10/fix-data-not-loaded-in-mrva-results
Fix data not being loaded in MRVA results panel
2022-08-24 15:14:26 +02:00
Koen Vlaswinkel
2113d08545 Fix data not being loaded in MRVA results panel
When the MRVA results panel is closed (so the panel gets disposed) and
opened again, it would not load the MRVA data (such as whether a query
has already been downloaded). This fixes it by also resetting the
internal state of whether the panel is loaded when the panel is
disposed.
2022-08-24 13:20:01 +02:00
Koen Vlaswinkel
5b5ef26864 Merge pull request #1471 from github/revert-1466-koesie10/add-github-download-button
Revert "Remove canary requirement for GitHub database download"
2022-08-24 12:12:44 +02:00
Koen Vlaswinkel
c5a6e64df8 Revert "Remove canary requirement for GitHub database download" 2022-08-24 11:51:44 +02:00
Charis Kyriakou
178d626062 Upgrade webview-ui-toolkit version with long link fix (#1469) 2022-08-24 10:17:41 +01:00
Dave Bartolomeo
d1d48b3506 Merge pull request #1468 from dbartol/dbartol/pmod-highlight/work
Add `implements` and `signature` to syntax highlighting
2022-08-23 17:02:03 -04:00
Dave Bartolomeo
9180d1d9fc Fix comment 2022-08-23 16:42:57 -04:00
Dave Bartolomeo
674c5ecbff Add implements and signature to syntax highlighting 2022-08-23 14:56:47 -04:00
Koen Vlaswinkel
edcac6925c Merge pull request #1466 from github/koesie10/add-github-download-button
Remove canary requirement for GitHub database download
2022-08-23 16:24:12 +02:00
Koen Vlaswinkel
c10500c5ea Update CHANGELOG 2022-08-23 14:58:36 +02:00
Koen Vlaswinkel
0832850009 Remove canary requirement for GitHub database download 2022-08-23 14:45:48 +02:00
Alexander Eyers-Taylor
b352830674 Improve startup time (#1465)
* ArchiveFileSystem: Only parse zips once

* CLIServer: Only get version once
2022-08-23 11:05:10 +01:00
Andrew Eisenberg
e913165249 Merge pull request #1463 from github/aeisenberg/bump-timeout 2022-08-17 17:21:31 -07:00
Andrew Eisenberg
ef94bb3d38 Bump telemetry test timeout
This test is failing occasionally on our CI system. Let's see if this
change prevents the failures.
2022-08-17 15:47:30 -07:00
Shati Patel
4d6076c4ea Escape HTML characters when rendering MRVA results as markdown (#1462) 2022-08-17 10:52:36 +01:00
Dave Bartolomeo
43650fde00 Merge pull request #1454 from github/dbartol/join-order
Report suspicious join orders
2022-08-15 14:13:35 -04:00
Angela P Wen
f2c72a67f6 Bump CLI version to 2.10.3 for integration tests (#1460) 2022-08-15 16:41:26 +00:00
Dave Bartolomeo
2b1f3227ce Fix computation of result sizes in IN_LAYER events 2022-08-12 17:00:26 -04:00
Dave Bartolomeo
841f1d3310 Replace console logging to route through problem reporter 2022-08-12 16:43:21 -04:00
Dave Bartolomeo
99756ae63b Fix PR feedback 2022-08-12 16:25:52 -04:00
Dave Bartolomeo
9a2bea39e6 Better handling of missing log data 2022-08-12 16:14:24 -04:00
Dave Bartolomeo
1aab49c719 Specify return type 2022-08-12 16:01:58 -04:00
Dave Bartolomeo
cf925c256f Update extensions/ql-vscode/src/log-insights/log-scanner-service.ts
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-08-12 15:50:28 -04:00
Dave Bartolomeo
8383a76e43 Merge branch 'dbartol/join-order' of https://github.com/github/vscode-codeql into dbartol/join-order 2022-08-12 15:41:52 -04:00
Dave Bartolomeo
c6d792f41e Fix PR feedback
Better handling of malformed RA
2022-08-12 15:39:32 -04:00
Dave Bartolomeo
277192e7d3 Update extensions/ql-vscode/src/log-insights/join-order.ts
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-08-12 14:59:20 -04:00
Dave Bartolomeo
85988ecf34 Update extensions/ql-vscode/src/log-insights/join-order.ts
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-08-12 14:50:10 -04:00
Dave Bartolomeo
49d12674b7 Cache regexprs 2022-08-12 14:47:50 -04:00
Dave Bartolomeo
beeb19dc05 Fix typo 2022-08-12 12:58:46 -04:00
Dave Bartolomeo
de88d27057 Update extensions/ql-vscode/src/log-insights/join-order.ts
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-08-12 12:49:29 -04:00
Dave Bartolomeo
eb2d00e999 Update extensions/ql-vscode/src/log-insights/join-order.ts
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-08-12 12:48:28 -04:00
Dave Bartolomeo
d58fb54928 Better formatting of metrics 2022-08-11 13:51:11 -04:00
Dave Bartolomeo
fdc209ca08 Test for log scanning 2022-08-10 18:07:59 -04:00
Dave Bartolomeo
28092f2b86 Move more of log scanning into pure code 2022-08-10 17:33:55 -04:00
Dave Bartolomeo
8970ad78ae Remove code added via bad merge 2022-08-10 13:51:08 -04:00
Dave Bartolomeo
e7a0c58940 Fix CodeQL alert 2022-08-10 13:18:00 -04:00
Dave Bartolomeo
02270aaeee Fix lint 2022-08-10 13:13:59 -04:00
Dave Bartolomeo
51fb03b4b1 Fix tests to match code changes 2022-08-10 13:11:34 -04:00
Dave Bartolomeo
838a2b71ac Scan logs on change in current query 2022-08-09 18:02:27 -04:00
Charis Kyriakou
f01c421d42 Merge pull request #1458 from github/version/bump-to-v1.6.11
Bump version to v1.6.11
2022-08-09 16:59:14 +01:00
charisk
561bc6f53c Bump version to v1.6.11 2022-08-09 15:21:26 +00:00
Charis Kyriakou
24b421e82d v1.6.10 (#1456)
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-08-09 16:17:57 +01:00
Dave Bartolomeo
3c57597a19 Share code for splitting records from pseudo-JSONL 2022-08-05 17:36:45 -04:00
Dave Bartolomeo
e8d5029912 Merge remote-tracking branch 'origin/main' into dbartol/join-order-temp 2022-08-05 17:34:52 -04:00
Dave Bartolomeo
cb514f5c78 Pre-cleanup to avoid merge conflicts 2022-08-05 14:59:40 -04:00
Dave Bartolomeo
57bb8cee41 Update regexes to match new summary text 2022-08-04 16:17:27 -04:00
Dave Bartolomeo
1219ef4a8c Remove unnecessary command 2022-08-04 16:17:09 -04:00
Dave Bartolomeo
677a0f7940 Fix lint 2022-08-04 14:42:47 -04:00
Charis Kyriakou
b8cca29eb3 Ensure query history state is persisted after new query is added (#1451) 2022-08-04 15:06:47 +01:00
Shati Patel
4cbf104bdf (Minor) Remove outdated comment (#1453) 2022-08-04 13:24:48 +00:00
Angela P Wen
26ccde9e7d Bump CLI version to 2.10.2 for integration tests 2022-08-03 10:30:55 +01:00
Angela P Wen
beb5b78b89 Add 50ms wait for flaky telemetry popup test (#1449) 2022-08-02 08:24:07 -07:00
Dave Bartolomeo
c3a21b93c0 Merge pull request #1430 from github/dbartol/goto-ql
Initial implementation of sourcemap-based jump-to-QL command
2022-08-01 13:52:06 -04:00
Dave Bartolomeo
6b9f73e156 Add comment to test data file 2022-08-01 13:19:15 -04:00
Dave Bartolomeo
6409e09063 Code cleanup 2022-08-01 12:28:35 -04:00
Dave Bartolomeo
8f5611b074 Move sourcemap tests to cli-integration 2022-08-01 12:14:00 -04:00
Dave Bartolomeo
7f3fcce1ac Temporarily increase delay for extension activation in test 2022-07-29 13:11:46 -04:00
Dave Bartolomeo
4bc1d1ed8a Force activation of extension 2022-07-29 12:44:06 -04:00
Dave Bartolomeo
02e5b4e830 Fix installation of dependent extensions 2022-07-29 12:03:43 -04:00
Dave Bartolomeo
538792e8bb Try installing extension dependencies for minimal-workspace tests 2022-07-29 11:35:52 -04:00
Dave Bartolomeo
56ec970121 Merge branch 'dbartol/goto-ql' of https://github.com/github/vscode-codeql into dbartol/goto-ql 2022-07-29 11:01:07 -04:00
Dave Bartolomeo
57a04297bd Only disable specific extensions for minimal-workspace tests 2022-07-29 11:01:02 -04:00
Dave Bartolomeo
59f1e4e90a Update extensions/ql-vscode/src/pure/log-summary-parser.ts 2022-07-28 22:31:18 -04:00
Dave Bartolomeo
7c1fce3319 Merge remote-tracking branch 'origin/main' into dbartol/goto-ql 2022-07-28 22:29:45 -04:00
Dave Bartolomeo
476ea7aef0 Integration test 2022-07-28 22:20:22 -04:00
Elena Tanasoiu
0c654c4320 Merge pull request #1444 from github/elenatanasoiu/fix-bugs
Don't show parentheses when results are not yet fetched in Query History
2022-07-26 10:33:32 +01:00
Elena Tanasoiu
895ac6ae26 Squash extra whitespace for Query History labels
We'd like to remove duplicate whitespace in these labels in order
to make it less likely that we introduce extra space.

We initially also tried trimming whitespaces at the start and end
of these labels but that had no effect.
2022-07-26 09:49:27 +01:00
Elena Tanasoiu
52484f1211 Don't show parentheses when results are not yet fetched
We missed a place where we needed to check if results are present
before attempting to show them.

Let's also add tests for this.
2022-07-26 09:47:55 +01:00
Elena Tanasoiu
cba188b4db Use named arguments for mock function
We'd like to be able to add tests for when the result count exists and
when it's missing.

Let's change the createMockRemoteQueryInfo method so that we can pass
in parameters by name, e.g.

```
createMockRemoteQueryInfo(undefined, 2)
```

becomes

```
createMockRemoteQueryInfo({ repositoryCount: 2 }
```
2022-07-26 09:46:10 +01:00
Elena Tanasoiu
123b1fc085 Clarify title description
To make it clear it's referring to a `user-specified` label, not that the user is not specified.
2022-07-25 17:16:36 +01:00
Angela P Wen
833f8e06ca Add a tree viewer UI for the evaluator logs (#1433)
Co-authored-by: Aditya Sharad <6874315+adityasharad@users.noreply.github.com>
2022-07-22 12:01:39 +02:00
Andrew Eisenberg
747049ed1b Merge pull request #1435 from github/dependabot/npm_and_yarn/extensions/ql-vscode/yargs-parser-20.2.4
Bump yargs-parser from 5.0.0-security.0 to 20.2.4 in /extensions/ql-vscode
2022-07-20 08:47:15 -07:00
Andrew Eisenberg
d62e9181f2 Merge pull request #1436 from github/dependabot/npm_and_yarn/extensions/ql-vscode/semver-regex-and-husky-3.1.4
Bump semver-regex and husky in /extensions/ql-vscode
2022-07-20 08:45:04 -07:00
Dave Bartolomeo
e4d1f4e73e Fix newline handling for cross-platform logs
We were splitting JSONL records based on the current OS newline sequence. In order to handle reading of logs from the opposite OS, I've switched our split to handle both flavors of line ending. This originally showed up as log parser unit tests failing on Windows (the checked-in log used Unix line endings), but could affect real world usage as well.
2022-07-20 11:21:53 -04:00
dependabot[bot]
c1922126d3 Bump terser from 5.14.1 to 5.14.2 in /extensions/ql-vscode
Bumps [terser](https://github.com/terser/terser) from 5.14.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-20 12:03:15 +01:00
elenatanasoiu
d2ebb3d20a Bump version to v1.6.10 2022-07-20 11:57:41 +01:00
Angela P Wen
72858e341a Bump CLI version to 2.10.1 for integration tests (#1442) 2022-07-20 11:55:43 +02:00
Elena Tanasoiu
4499773f6f Merge pull request #1440 from github/v1.6.9
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.9
2022-07-20 10:16:21 +01:00
Elena Tanasoiu
1d3b0e0ca9 v1.6.9 2022-07-20 10:01:12 +01:00
Elena Tanasoiu
98e503c768 Merge pull request #1438 from github/shati-patel/gist-description
MRVA: Fix Gist description when repository count is undefined
2022-07-20 09:46:22 +01:00
Elena Tanasoiu
62c3974d35 Check for undefined, null or zero repositories
`undefined`, `null` and 0 will evaluate to `false` so if we only want to
display the repository count when these values are not present we can
check for a truthy value:

```
query.repositoryCount ? `(${pluralize(...)})` : '';
```

instead of checking explicitly:

```
query.repositoryCount !== undefined && query.repositoryCount !== null && query.repositoryCount != 0 ? `(${pluralize(...)})` : '';
```
2022-07-20 09:30:54 +01:00
Dave Bartolomeo
40e0027074 Fix newline handling for cross-platform logs
We were splitting JSONL records based on the current OS newline sequence. In order to handle reading of logs from the opposite OS, I've switched our split to handle both flavors of line ending. This originally showed up as log parser unit tests failing on Windows (the checked-in log used Unix line endings), but could affect real world usage as well.
2022-07-19 17:29:33 -04:00
shati-patel
ab1c2e0a0d Explicitly check for undefined 2022-07-19 20:00:10 +01:00
shati-patel
d918c41197 Fix Gist description when repository count is undefined 2022-07-19 18:25:25 +01:00
Dave Bartolomeo
84048ccac1 Merge remote-tracking branch 'origin/main' into dbartol/goto-ql 2022-07-19 09:39:51 -04:00
dependabot[bot]
cbb09da0d0 Bump semver-regex and husky in /extensions/ql-vscode
Bumps [semver-regex](https://github.com/sindresorhus/semver-regex) and [husky](https://github.com/typicode/husky). These dependencies needed to be updated together.

Updates `semver-regex` from 2.0.0 to 3.1.4
- [Release notes](https://github.com/sindresorhus/semver-regex/releases)
- [Commits](https://github.com/sindresorhus/semver-regex/compare/v2.0.0...v3.1.4)

Updates `husky` from 4.2.5 to 4.3.8
- [Release notes](https://github.com/typicode/husky/releases)
- [Commits](https://github.com/typicode/husky/compare/v4.2.5...v4.3.8)

---
updated-dependencies:
- dependency-name: semver-regex
  dependency-type: indirect
- dependency-name: husky
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-19 12:51:24 +00:00
dependabot[bot]
c8d3428f21 Bump yargs-parser in /extensions/ql-vscode
Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 5.0.0-security.0 to 20.2.4.
- [Release notes](https://github.com/yargs/yargs-parser/releases)
- [Changelog](https://github.com/yargs/yargs-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs-parser/commits/v20.2.4)

---
updated-dependencies:
- dependency-name: yargs-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-19 12:51:22 +00:00
Elena Tanasoiu
2cf5b39cfe Merge pull request #1432 from github/charisk-elena/result-count-on-history-labels
Add result count to remote queries in Query History
2022-07-19 13:50:22 +01:00
Elena Tanasoiu
13921bf8a2 Extract sum method for adding up repo results
When a queryResult is created, it comes with an array for AnalysisSummaries.
There is one summary per repository.

We've had to calculate the total number of results for all summaries in multiple
places, so let's extract a method for this as well.
2022-07-19 13:26:56 +01:00
Elena Tanasoiu
12a97ecba2 Shorten param forwarding for repositoryCount 2022-07-19 13:26:54 +01:00
Elena Tanasoiu
26529232f4 Rename numRepositoriesQueries to repositoryCount
To make it consistent with `resultCount`.
2022-07-19 13:25:48 +01:00
Elena Tanasoiu
1b425fc261 DRY up labels using the new pluralize method 2022-07-19 13:25:40 +01:00
Elena Tanasoiu
9c598c2f06 Extract pluralize method
There are at least 4 different files where this method could DRY things up,
so let's extract it.

I've chosen to move it to src/helpers.ts but happy to be told there's a better
place for shared utility methods like this one.
2022-07-19 12:32:24 +01:00
Elena Tanasoiu
99a784f072 Be able to sort remote queries by number of results
Previously we would set all remote query results to -1 when someone
attempted to sort queries.

We would then only sort local queries as those had access to the number
of results.

Let's include number of results for remote queries in the sorting.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-19 12:32:24 +01:00
Elena Tanasoiu
030488a459 Make local and remote query results match
In the previous commit we're now displaying number of results for remote
queries.

Previously we could only do this for local queries.

Let's make the format match for both types of queries by displaying
number of results in parentheses: `(x results)`.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-19 12:32:24 +01:00
Elena Tanasoiu
377f7965b1 Add result count to remote queries in Query History
When you run a remote query, we'd like to display more information about
it in the Query History panel.

At the moment we've improved this [1] by adding the language and number of repositories.

In this commit we're also adding the number of results for a remote query.

So the final format of the query history item will change from:

`<query_name> - <query_status>`

to

`<query_name> (<language>) on x repositories (y results) - <query_status>`

[1]: https://github.com/github/vscode-codeql/pull/1427

Co-authored-by: Charis Kyriakou <charisk@github.com>
Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-19 12:32:22 +01:00
Charis Kyriakou
651a6fbda8 Ensure completed flag is set on remote query history items (#1434) 2022-07-19 10:40:02 +01:00
Elena Tanasoiu
55ffdf7963 Merge pull request #1431 from github/shati-elena/rename-gist
Add useful information to MRVA gist titles
2022-07-19 09:11:47 +01:00
Elena Tanasoiu
cc907d2f31 Add test for exportResultsToGist method
While we're here we're also adding a test for the `exportResultsToGist`
method, as there were no tests for the `export-results.ts` file.

We initially attempted to add the test to the pure-tests folder, but the
`export-results.ts` file imports some components from `vscode`, which
meant we needed to set up the test in an environment where VSCode
dependencies are available.

We chose to add the test to `vscode-tests/no-workspace` for convenience,
as there are already other unit tests there.

We've also had to import our own query and analysis result to be able
to work with data closer to reality for exported results.

Since we've introduced functionality to build a gist title, let's check
that the `exportResultsToGist` method will forward the correct title to
the GitHub Actions API.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:52:51 +01:00
Dave Bartolomeo
49a1576d14 Merge branch 'dbartol/goto-ql' of https://github.com/github/vscode-codeql into dbartol/goto-ql 2022-07-18 14:33:37 -04:00
Dave Bartolomeo
0cc4561ee9 Discard cached sourcemap when summary document is closed
Also some minor lint feedback
2022-07-18 14:33:33 -04:00
Elena Tanasoiu
c4df9dbec8 Extract method for creating Extension context
We'd like to re-use this to test the `exportResultsToGist` method in
`export-results.ts`.

So let's move it to a shared folder in the `vscode-tests/no-workspace` folder.

Since there's no `helper.ts` file in this folder and to avoid any confusion with
the `helpers.test.ts` file, I've opted to put this shared method into `index.ts`.

Happy to be told there's a better pattern for this as it doesn't feel very nice!
2022-07-18 19:22:44 +01:00
Elena Tanasoiu
c384a631dc Handle missing repo count gracefully
Let's handle this case gracefully and skip displaying the number of repositories
when they're not available.

Similarly let's add a check to see if we should pluralize the `repository` noun
or not.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:22:44 +01:00
Elena Tanasoiu
b079690f0e Add useful information to MRVA gist titles
All exported MRVA gists are given the name `CodeQL variant analysis
results', which makes it hard to work out what it contains at a glance.

We're adding more information in the gist title to make it more useful.

Example of new title:

`Empty Block (Go) x results (y repositories)`

This translates to:

`<query name> (<query language>) <number of results> results (<number of repositories> repositories)`

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:22:41 +01:00
Elena Tanasoiu
4e863e995b Introduce method to add analysis results
We'd like to improve MRVA query gists by giving them more descriptive
titles that contain useful information about the query.

Let's add the number of query results to the title of the gist.

To do this we'll first need to count all the results provided to us in
the `analysisResults` array. There is an item in this array for each of
the repositories we've queried, so we're introducing a method to sum up
results for all the items in the array.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:20:58 +01:00
Dave Bartolomeo
576737cac8 Update extensions/ql-vscode/src/log-insights/summary-language-support.ts
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-07-15 14:50:48 -04:00
Dave Bartolomeo
742aa4ca19 Use error message helper 2022-07-15 11:43:05 -04:00
Shati Patel
f992679e94 MRVA: Include more info in query history label (#1427)
Co-authored-by: Elena Tanasoiu <elenatanasoiu@github.com>
2022-07-15 13:58:45 +01:00
Shati Patel
ffe1704ac0 Replace code paths dropdown with VS Code UI Toolkit (#1429) 2022-07-15 13:04:36 +01:00
Dave Bartolomeo
b5e6700cba Log message on failure to open sourcemap 2022-07-14 18:10:58 -04:00
Dave Bartolomeo
7f5302dc37 fs-extra 2022-07-14 17:39:16 -04:00
Dave Bartolomeo
3ea5524048 Hide "Go to QL Code" behind canary flag 2022-07-14 17:21:52 -04:00
Dave Bartolomeo
1823ae8397 Fix test expectation 2022-07-14 17:03:39 -04:00
Dave Bartolomeo
6dca9ccbeb Fix linter issues 2022-07-14 14:12:10 -04:00
Dave Bartolomeo
f3c2862937 Fix lint error 2022-07-14 14:06:19 -04:00
Dave Bartolomeo
855cb485d5 Initial implementation of sourcemap-based jump-to-QL command 2022-07-14 13:55:46 -04:00
Edoardo Pirovano
bd2dd04ac6 Regularly scrub query history view 2022-07-14 16:59:08 +01:00
Edoardo Pirovano
bbf4a03b03 Fix typo in config parameter name 2022-07-13 16:34:18 +01:00
Shati Patel
f38eb4895d Replace "repository search" filter box with VS Code UI Toolkit (#1424) 2022-07-13 15:13:31 +01:00
Andrew Eisenberg
f559b59ee5 Merge pull request #1420 from github/robertbrignull/api-retry
Add API retries for octokit requests
2022-07-12 08:12:21 -07:00
Angela P Wen
c9d895ea42 Parse summary of evaluator logs into data model (#1405)
Co-authored-by: Aditya Sharad <6874315+adityasharad@users.noreply.github.com>
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2022-07-12 14:04:55 +02:00
Shati Patel
e57bbcb711 Use VSCodeTags instead of Primer Labels in webview (#1421) 2022-07-01 16:21:44 +01:00
Shati Patel
b311991644 MRVA: Fix grammar in pop-up message (#1416) 2022-07-01 12:43:46 +01:00
Robert
825054a271 Use octokit retry module 2022-07-01 11:19:49 +00:00
Robert
f7aa0a5ae5 Install @octokot/plugin-retry 2022-07-01 11:06:22 +00:00
Andrew Eisenberg
f486ccfac6 Merge pull request #1418 from github/aeisenberg/resolve-ml-libs
Resolve ml-queries from directory
2022-06-30 08:56:15 -07:00
Andrew Eisenberg
70f74d3baf Resolve ml-queries from directory
Previously, there was a bug where quick eval queries would crash when
the eval snippet is in a library file.

The problem was that the `codeql resolve queries` command fails when
passed a library file. The fix is to avoid passing the library file at
all. Instead, pass the directory. This is safe because the resolve
queries command only needs to know which query pack the file is
contained in. Passing in the parent directory is the same as passing in
a file in this particular case.
2022-06-30 08:36:55 -07:00
Charis Kyriakou
ebad1844df MRVA: Don't show notification if user aborts firing off a query (#1417) 2022-06-30 14:35:33 +01:00
Charis Kyriakou
a40a2edaf2 Merge pull request #1414 from github/version/bump-to-v1.6.9
Bump version to v1.6.9
2022-06-29 13:17:30 +01:00
charisk
5f3d525ff8 Bump version to v1.6.9 2022-06-29 11:56:36 +00:00
Dave Bartolomeo
cff235c420 Auto-format 2022-05-03 18:14:03 -04:00
Dave Bartolomeo
1089a052ec Initial implementation of join order metric scanning 2022-05-03 13:20:30 -04:00
Dave Bartolomeo
1d195cb347 Merge remote-tracking branch 'origin/main' into dbartol/join-order 2022-04-27 17:50:50 -04:00
Dave Bartolomeo
8d8ed28aea Add necessary dependencies 2022-04-27 17:50:46 -04:00
128 changed files with 22525 additions and 2687 deletions

View File

@@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '16.13.0'
node-version: '16.14.0'
- name: Install dependencies
working-directory: extensions/ql-vscode
@@ -82,7 +82,7 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '16.13.0'
node-version: '16.14.0'
- name: Install dependencies
working-directory: extensions/ql-vscode
@@ -139,7 +139,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
version: ['v2.6.3', 'v2.7.6', 'v2.8.5', 'v2.9.4', 'v2.10.0', 'nightly']
version: ['v2.6.3', 'v2.7.6', 'v2.8.5', 'v2.9.4', 'v2.10.4', 'nightly']
env:
CLI_VERSION: ${{ matrix.version }}
NIGHTLY_URL: ${{ needs.find-nightly.outputs.url }}
@@ -151,7 +151,7 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '16.13.0'
node-version: '16.14.0'
- name: Install dependencies
working-directory: extensions/ql-vscode

View File

@@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '16.13.0'
node-version: '16.14.0'
- name: Install dependencies
run: |

View File

@@ -1 +1 @@
v16.13.0
v16.14.0

View File

@@ -1,5 +1,23 @@
# CodeQL for Visual Studio Code: Changelog
## 1.6.12 - 1 September 2022
- Add ability for users to download databases directly from GitHub. [#1485](https://github.com/github/vscode-codeql/pull/1485)
- Fix a race condition that could cause a failure to open the evaluator log when running a query. [#1490](https://github.com/github/vscode-codeql/pull/1490)
- Fix an error when running a query with an older version of the CodeQL CLI. [#1490](https://github.com/github/vscode-codeql/pull/1490)
## 1.6.11 - 25 August 2022
No user facing changes.
## 1.6.10 - 9 August 2022
No user facing changes.
## 1.6.9 - 20 July 2022
No user facing changes.
## 1.6.8 - 29 June 2022
- Fix a bug where quick queries cannot be compiled if the core libraries are not in the workspace. [#1411](https://github.com/github/vscode-codeql/pull/1411)

View File

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

View File

@@ -39,13 +39,3 @@ export function compileTypeScript() {
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')
.pipe(gulp.dest('out'));
}

View File

@@ -1,12 +1,11 @@
import * as path from 'path';
import * as webpack from 'webpack';
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
export const config: webpack.Configuration = {
mode: 'development',
entry: {
resultsView: './src/view/results.tsx',
compareView: './src/compare/view/Compare.tsx',
remoteQueriesView: './src/remote-queries/view/RemoteQueries.tsx',
webview: './src/view/webview.tsx'
},
output: {
path: path.resolve(__dirname, '..', 'out'),
@@ -31,9 +30,7 @@ export const config: webpack.Configuration = {
{
test: /\.less$/,
use: [
{
loader: 'style-loader'
},
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
@@ -53,17 +50,31 @@ export const config: webpack.Configuration = {
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
MiniCssExtractPlugin.loader,
{
loader: 'css-loader'
}
]
},
{
test: /\.(woff(2)?|ttf|eot)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
// We need this to make Webpack use the correct path for the fonts.
// Without this, the CSS file will use `url([object Module])`
esModule: false
}
},
],
}
]
},
performance: {
hints: false
}
},
plugins: [new MiniCssExtractPlugin()],
};

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.8",
"version": "1.6.12",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -35,9 +35,11 @@
},
"activationEvents": [
"onLanguage:ql",
"onLanguage:ql-summary",
"onView:codeQLDatabases",
"onView:codeQLQueryHistory",
"onView:codeQLAstViewer",
"onView:codeQLEvalLogViewer",
"onView:test-explorer",
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.authenticateToGitHub",
@@ -110,6 +112,12 @@
"extensions": [
".qhelp"
]
},
{
"id": "ql-summary",
"filenames": [
"evaluator-log.summary"
]
}
],
"grammars": [
@@ -224,7 +232,7 @@
},
"codeQL.queryHistory.format": {
"type": "string",
"default": "%q on %d - %s, %r [%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": {
@@ -280,7 +288,7 @@
"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 Variant Analysis\" command. The repository should be of the form `<owner>/<repo>`)."
"markdownDescription": "[For internal use only] The name of the GitHub repository in which the GitHub Actions workflow is run when using the \"Run Variant Analysis\" command. The repository should be of the form `<owner>/<repo>`)."
}
}
},
@@ -527,11 +535,15 @@
},
{
"command": "codeQLQueryHistory.showEvalLog",
"title": "Show Evaluator Log (Raw)"
"title": "Show Evaluator Log (Raw JSON)"
},
{
"command": "codeQLQueryHistory.showEvalLogSummary",
"title": "Show Evaluator Log (Summary)"
"title": "Show Evaluator Log (Summary Text)"
},
{
"command": "codeQLQueryHistory.showEvalLogViewer",
"title": "Show Evaluator Log (UI)"
},
{
"command": "codeQLQueryHistory.cancel",
@@ -608,6 +620,19 @@
"light": "media/light/clear-all.svg",
"dark": "media/dark/clear-all.svg"
}
},
{
"command": "codeQLEvalLogViewer.clear",
"title": "Clear Viewer",
"icon": {
"light": "media/light/clear-all.svg",
"dark": "media/dark/clear-all.svg"
}
},
{
"command": "codeQL.gotoQL",
"title": "CodeQL: Go to QL Code",
"enablement": "codeql.hasQLSource"
}
],
"menus": {
@@ -639,7 +664,7 @@
},
{
"command": "codeQLDatabases.chooseDatabaseGithub",
"when": "config.codeQL.canary && view == codeQLDatabases",
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
@@ -681,6 +706,11 @@
"command": "codeQLAstViewer.clear",
"when": "view == codeQLAstViewer",
"group": "navigation"
},
{
"command": "codeQLEvalLogViewer.clear",
"when": "view == codeQLEvalLogViewer",
"group": "navigation"
}
],
"view/item/context": [
@@ -754,6 +784,11 @@
"group": "9_qlCommands",
"when": "codeql.supportsEvalLog && viewItem == rawResultsItem || codeql.supportsEvalLog && viewItem == interpretedResultsItem || codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showEvalLogViewer",
"group": "9_qlCommands",
"when": "config.codeQL.canary && codeql.supportsEvalLog && viewItem == rawResultsItem || config.codeQL.canary && codeql.supportsEvalLog && viewItem == interpretedResultsItem || config.codeQL.canary && codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showQueryText",
"group": "9_qlCommands",
@@ -891,10 +926,6 @@
"command": "codeQL.viewCfg",
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
},
{
"command": "codeQL.chooseDatabaseGithub",
"when": "config.codeQL.canary"
},
{
"command": "codeQLDatabases.setCurrentDatabase",
"when": "false"
@@ -975,6 +1006,10 @@
"command": "codeQLQueryHistory.showEvalLogSummary",
"when": "false"
},
{
"command": "codeQLQueryHistory.showEvalLogViewer",
"when": "false"
},
{
"command": "codeQLQueryHistory.openQueryDirectory",
"when": "false"
@@ -1043,6 +1078,10 @@
"command": "codeQLAstViewer.clear",
"when": "false"
},
{
"command": "codeQLEvalLogViewer.clear",
"when": "false"
},
{
"command": "codeQLTests.acceptOutput",
"when": "false"
@@ -1084,6 +1123,10 @@
{
"command": "codeQL.previewQueryHelp",
"when": "resourceExtname == .qhelp && isWorkspaceTrusted"
},
{
"command": "codeQL.gotoQL",
"when": "editorLangId == ql-summary && config.codeQL.canary"
}
]
},
@@ -1109,6 +1152,11 @@
{
"id": "codeQLAstViewer",
"name": "AST Viewer"
},
{
"id": "codeQLEvalLogViewer",
"name": "Evaluator Log Viewer",
"when": "config.codeQL.canary"
}
]
},
@@ -1123,7 +1171,11 @@
},
{
"view": "codeQLDatabases",
"contents": "Add a CodeQL database:\n[From a folder](command:codeQLDatabases.chooseDatabaseFolder)\n[From an archive](command:codeQLDatabases.chooseDatabaseArchive)\n[From a URL (as a zip file)](command:codeQLDatabases.chooseDatabaseInternet)\n[From LGTM](command:codeQLDatabases.chooseDatabaseLgtm)"
"contents": "Add a CodeQL database:\n[From a folder](command:codeQLDatabases.chooseDatabaseFolder)\n[From an archive](command:codeQLDatabases.chooseDatabaseArchive)\n[From a URL (as a zip file)](command:codeQLDatabases.chooseDatabaseInternet)\n[From GitHub](command:codeQLDatabases.chooseDatabaseGithub)\n[From LGTM](command:codeQLDatabases.chooseDatabaseLgtm)"
},
{
"view": "codeQLEvalLogViewer",
"contents": "Run the 'Show Evaluator Log (UI)' command on a CodeQL query run in the Query History view."
}
]
},
@@ -1132,7 +1184,6 @@
"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",
@@ -1143,16 +1194,19 @@
"format-staged": "lint-staged"
},
"dependencies": {
"@octokit/plugin-retry": "^3.0.9",
"@octokit/rest": "^18.5.6",
"@primer/octicons-react": "^16.3.0",
"@primer/react": "^35.0.0",
"@vscode/webview-ui-toolkit": "^1.0.0",
"@vscode/codicons": "^0.0.31",
"@vscode/webview-ui-toolkit": "^1.0.1",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"d3": "^6.3.1",
"d3": "^7.6.1",
"d3-graphviz": "^2.6.1",
"fs-extra": "^10.0.1",
"glob-promise": "^4.2.2",
"immutable": "^4.0.0",
"js-yaml": "^4.1.0",
"minimist": "~1.2.6",
"nanoid": "^3.2.0",
@@ -1161,8 +1215,8 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"semver": "~7.3.2",
"source-map-support": "^0.5.21",
"source-map": "^0.7.4",
"source-map-support": "^0.5.21",
"stream": "^0.0.2",
"stream-chain": "~2.2.4",
"stream-json": "~1.7.3",
@@ -1183,7 +1237,7 @@
"@types/chai-as-promised": "~7.1.2",
"@types/child-process-promise": "^2.2.1",
"@types/classnames": "~2.2.9",
"@types/d3": "^6.2.0",
"@types/d3": "^7.4.0",
"@types/d3-graphviz": "^2.6.6",
"@types/del": "^4.0.0",
"@types/fs-extra": "^9.0.6",
@@ -1212,6 +1266,7 @@
"@types/unzipper": "~0.10.1",
"@types/vscode": "^1.59.0",
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.18.0",
"@types/xml2js": "~0.4.4",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
@@ -1223,13 +1278,15 @@
"del": "^6.0.0",
"eslint": "~6.8.0",
"eslint-plugin-react": "~7.19.0",
"file-loader": "^6.2.0",
"glob": "^7.1.4",
"gulp": "^4.0.2",
"gulp-replace": "^1.1.3",
"gulp-sourcemaps": "^3.0.0",
"gulp-typescript": "^5.0.1",
"husky": "~4.2.5",
"husky": "~4.3.8",
"lint-staged": "~10.2.2",
"mini-css-extract-plugin": "^2.6.1",
"mocha": "^10.0.0",
"mocha-sinon": "~2.1.2",
"npm-run-all": "^4.1.5",
@@ -1237,7 +1294,6 @@
"proxyquire": "~2.1.3",
"sinon": "~13.0.1",
"sinon-chai": "~3.5.0",
"style-loader": "~3.3.1",
"through2": "^4.0.2",
"ts-loader": "^8.1.0",
"ts-node": "^10.7.0",

View File

@@ -0,0 +1,118 @@
import {
WebviewPanel,
ExtensionContext,
window as Window,
ViewColumn,
Uri,
WebviewPanelOptions,
WebviewOptions
} from 'vscode';
import * as path from 'path';
import { DisposableObject } from './pure/disposable-object';
import { tmpDir } from './helpers';
import { getHtmlForWebview, WebviewMessage, WebviewView } from './interface-utils';
export type InterfacePanelConfig = {
viewId: string;
title: string;
viewColumn: ViewColumn;
view: WebviewView;
preserveFocus?: boolean;
additionalOptions?: WebviewPanelOptions & WebviewOptions;
}
export abstract class AbstractInterfaceManager<ToMessage extends WebviewMessage, FromMessage extends WebviewMessage> extends DisposableObject {
protected panel: WebviewPanel | undefined;
protected panelLoaded = false;
protected panelLoadedCallBacks: (() => void)[] = [];
constructor(
protected readonly ctx: ExtensionContext
) {
super();
}
protected get isShowingPanel() {
return !!this.panel;
}
protected getPanel(): WebviewPanel {
if (this.panel == undefined) {
const { ctx } = this;
const config = this.getPanelConfig();
this.panel = Window.createWebviewPanel(
config.viewId,
config.title,
{ viewColumn: ViewColumn.Active, preserveFocus: true },
{
enableScripts: true,
enableFindWidget: true,
retainContextWhenHidden: true,
...config.additionalOptions,
localResourceRoots: [
...(config.additionalOptions?.localResourceRoots ?? []),
Uri.file(tmpDir.name),
Uri.file(path.join(ctx.extensionPath, 'out'))
],
}
);
this.push(
this.panel.onDidDispose(
() => {
this.panel = undefined;
this.panelLoaded = false;
this.onPanelDispose();
},
null,
ctx.subscriptions
)
);
this.panel.webview.html = getHtmlForWebview(
ctx,
this.panel.webview,
config.view,
{
allowInlineStyles: true,
}
);
this.push(
this.panel.webview.onDidReceiveMessage(
async (e) => this.onMessage(e),
undefined,
ctx.subscriptions
)
);
}
return this.panel;
}
protected abstract getPanelConfig(): InterfacePanelConfig;
protected abstract onPanelDispose(): void;
protected abstract onMessage(msg: FromMessage): Promise<void>;
protected waitForPanelLoaded(): Promise<void> {
return new Promise((resolve) => {
if (this.panelLoaded) {
resolve();
} else {
this.panelLoadedCallBacks.push(resolve);
}
});
}
protected onWebViewLoaded(): void {
this.panelLoaded = true;
this.panelLoadedCallBacks.forEach((cb) => cb());
this.panelLoadedCallBacks = [];
}
protected postMessage(msg: ToMessage): Thenable<boolean> {
return this.getPanel().webview.postMessage(msg);
}
}

View File

@@ -167,21 +167,26 @@ type Archive = {
dirMap: DirectoryHierarchyMap;
};
async function parse_zip(zipPath: string): Promise<Archive> {
if (!await fs.pathExists(zipPath))
throw vscode.FileSystemError.FileNotFound(zipPath);
const archive: Archive = { unzipped: await unzipper.Open.file(zipPath), dirMap: new Map };
archive.unzipped.files.forEach(f => { ensureFile(archive.dirMap, path.resolve('/', f.path)); });
return archive;
}
export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
private readOnlyError = vscode.FileSystemError.NoPermissions('write operation attempted, but source archive filesystem is readonly');
private archives: Map<string, Archive> = new Map;
private archives: Map<string, Promise<Archive>> = new Map;
private async getArchive(zipPath: string): Promise<Archive> {
if (!this.archives.has(zipPath)) {
if (!await fs.pathExists(zipPath))
throw vscode.FileSystemError.FileNotFound(zipPath);
const archive: Archive = { unzipped: await unzipper.Open.file(zipPath), dirMap: new Map };
archive.unzipped.files.forEach(f => { ensureFile(archive.dirMap, path.resolve('/', f.path)); });
this.archives.set(zipPath, archive);
this.archives.set(zipPath, parse_zip(zipPath));
}
return this.archives.get(zipPath)!;
return await this.archives.get(zipPath)!;
}
root = new Directory('');
// metadata

View File

@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import * as Octokit from '@octokit/rest';
import { retry } from '@octokit/plugin-retry';
const GITHUB_AUTH_PROVIDER_ID = 'github';
@@ -51,14 +52,15 @@ export class Credentials {
private async createOctokit(createIfNone: boolean, overrideToken?: string): Promise<Octokit.Octokit | undefined> {
if (overrideToken) {
return new Octokit.Octokit({ auth: overrideToken });
return new Octokit.Octokit({ auth: overrideToken, retry });
}
const session = await vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { createIfNone });
if (session) {
return new Octokit.Octokit({
auth: session.accessToken
auth: session.accessToken,
retry
});
} else {
return undefined;
@@ -74,16 +76,27 @@ export class Credentials {
}));
}
async getOctokit(): Promise<Octokit.Octokit> {
/**
* Creates or returns an instance of Octokit.
*
* @param requireAuthentication Whether the Octokit instance needs to be authenticated as user.
* @returns An instance of Octokit.
*/
async getOctokit(requireAuthentication = true): Promise<Octokit.Octokit> {
if (this.octokit) {
return this.octokit;
}
this.octokit = await this.createOctokit(true);
// octokit shouldn't be undefined, since we've set "createIfNone: true".
// The following block is mainly here to prevent a compiler error.
this.octokit = await this.createOctokit(requireAuthentication);
if (!this.octokit) {
throw new Error('Did not initialize Octokit.');
if (requireAuthentication) {
throw new Error('Did not initialize Octokit.');
}
// We don't want to set this in this.octokit because that would prevent
// authenticating when requireCredentials is true.
return new Octokit.Octokit({ retry });
}
return this.octokit;
}

View File

@@ -168,7 +168,7 @@ export class CodeQLCliServer implements Disposable {
nullBuffer: Buffer;
/** Version of current cli, lazily computed by the `getVersion()` method */
private _version: SemVer | undefined;
private _version: Promise<SemVer> | undefined;
/**
* The languages supported by the current version of the CLI, computed by `getSupportedLanguages()`.
@@ -240,7 +240,7 @@ export class CodeQLCliServer implements Disposable {
/**
* Restart the server when the current command terminates
*/
private restartCliServer(): void {
restartCliServer(): void {
const callback = (): void => {
try {
this.killProcessIfRunning();
@@ -606,7 +606,8 @@ export class CodeQLCliServer implements Disposable {
/** Resolves the ML models that should be available when evaluating a query. */
async resolveMlModels(additionalPacks: string[], queryPath: string): Promise<MlModelsInfo> {
const args = await this.cliConstraints.supportsPreciseResolveMlModels()
? [...this.getAdditionalPacksArg(additionalPacks), queryPath]
// use the dirname of the path so that we can handle query libraries
? [...this.getAdditionalPacksArg(additionalPacks), path.dirname(queryPath)]
: this.getAdditionalPacksArg(additionalPacks);
return await this.runJsonCodeQlCliCommand<MlModelsInfo>(
['resolve', 'ml-models'],
@@ -682,12 +683,30 @@ export class CodeQLCliServer implements Disposable {
const subcommandArgs = [
'--format=text',
`--end-summary=${endSummaryPath}`,
...(await this.cliConstraints.supportsSourceMap() ? ['--sourcemap'] : []),
inputPath,
outputPath
];
return await this.runCodeQlCliCommand(['generate', 'log-summary'], subcommandArgs, 'Generating log summary');
}
/**
* Generate a JSON summary of an evaluation log.
* @param inputPath The path of an evaluation event log.
* @param outputPath The path to write a JSON summary of it to.
*/
async generateJsonLogSummary(
inputPath: string,
outputPath: string,
): Promise<string> {
const subcommandArgs = [
'--format=predicates',
inputPath,
outputPath
];
return await this.runCodeQlCliCommand(['generate', 'log-summary'], subcommandArgs, 'Generating JSON log summary');
}
/**
* Gets the results from a bqrs.
* @param bqrsPath The path to the bqrs.
@@ -966,13 +985,13 @@ export class CodeQLCliServer implements Disposable {
public async getVersion() {
if (!this._version) {
this._version = await this.refreshVersion();
this._version = 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;
return await this._version;
}
private async refreshVersion() {
@@ -1303,6 +1322,11 @@ export class CliVersionConstraint {
*/
public static CLI_VERSION_WITH_PER_QUERY_EVAL_LOG = new SemVer('2.9.0');
/**
* CLI version that supports the `--sourcemap` option for log generation.
*/
public static CLI_VERSION_WITH_SOURCEMAP = new SemVer('2.10.3');
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1370,4 +1394,8 @@ export class CliVersionConstraint {
async supportsPerQueryEvalLog() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG);
}
async supportsSourceMap() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_SOURCEMAP);
}
}

View File

@@ -1,14 +1,8 @@
import { DisposableObject } from '../pure/disposable-object';
import {
WebviewPanel,
ExtensionContext,
window as Window,
ViewColumn,
Uri,
} from 'vscode';
import * as path from 'path';
import { tmpDir } from '../helpers';
import {
FromCompareViewMessage,
ToCompareViewMessage,
@@ -17,26 +11,24 @@ import {
import { Logger } from '../logging';
import { CodeQLCliServer } from '../cli';
import { DatabaseManager } from '../databases';
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
import { 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';
import { AbstractInterfaceManager, InterfacePanelConfig } from '../abstract-interface-manager';
interface ComparePair {
from: CompletedLocalQueryInfo;
to: CompletedLocalQueryInfo;
}
export class CompareInterfaceManager extends DisposableObject {
export class CompareInterfaceManager extends AbstractInterfaceManager<ToCompareViewMessage, FromCompareViewMessage> {
private comparePair: ComparePair | undefined;
private panel: WebviewPanel | undefined;
private panelLoaded = false;
private panelLoadedCallBacks: (() => void)[] = [];
constructor(
private ctx: ExtensionContext,
ctx: ExtensionContext,
private databaseManager: DatabaseManager,
private cliServer: CodeQLCliServer,
private logger: Logger,
@@ -45,7 +37,7 @@ export class CompareInterfaceManager extends DisposableObject {
item: CompletedLocalQueryInfo
) => Promise<void>
) {
super();
super(ctx);
}
async showResults(
@@ -103,73 +95,24 @@ export class CompareInterfaceManager extends DisposableObject {
}
}
getPanel(): WebviewPanel {
if (this.panel == undefined) {
const { ctx } = this;
const panel = (this.panel = Window.createWebviewPanel(
'compareView',
'Compare CodeQL Query Results',
{ viewColumn: ViewColumn.Active, preserveFocus: true },
{
enableScripts: true,
enableFindWidget: true,
retainContextWhenHidden: true,
localResourceRoots: [
Uri.file(tmpDir.name),
Uri.file(path.join(this.ctx.extensionPath, 'out')),
],
}
));
this.push(this.panel.onDidDispose(
() => {
this.panel = undefined;
this.comparePair = undefined;
},
null,
ctx.subscriptions
));
const scriptPathOnDisk = Uri.file(
ctx.asAbsolutePath('out/compareView.js')
);
const stylesheetPathOnDisk = Uri.file(
ctx.asAbsolutePath('out/view/resultsView.css')
);
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
[stylesheetPathOnDisk],
false
);
this.push(panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
));
}
return this.panel;
protected getPanelConfig(): InterfacePanelConfig {
return {
viewId: 'compareView',
title: 'Compare CodeQL Query Results',
viewColumn: ViewColumn.Active,
preserveFocus: true,
view: 'compare',
};
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve) => {
if (this.panelLoaded) {
resolve();
} else {
this.panelLoadedCallBacks.push(resolve);
}
});
protected onPanelDispose(): void {
this.comparePair = undefined;
}
private async handleMsgFromView(
msg: FromCompareViewMessage
): Promise<void> {
protected async onMessage(msg: FromCompareViewMessage): Promise<void> {
switch (msg.t) {
case 'compareViewLoaded':
this.panelLoaded = true;
this.panelLoadedCallBacks.forEach((cb) => cb());
this.panelLoadedCallBacks = [];
this.onWebViewLoaded();
break;
case 'changeCompare':
@@ -186,10 +129,6 @@ export class CompareInterfaceManager extends DisposableObject {
}
}
private postMessage(msg: ToCompareViewMessage): Thenable<boolean> {
return this.getPanel().webview.postMessage(msg);
}
private async findCommonResultSetNames(
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo,

View File

@@ -1,13 +0,0 @@
module.exports = {
env: {
browser: true
},
extends: [
"plugin:react/recommended"
],
settings: {
react: {
version: 'detect'
}
}
}

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"target": "es6",
"outDir": "out",
"lib": ["ES2021", "dom"],
"jsx": "react",
"sourceMap": true,
"rootDir": "..",
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true
},
"exclude": ["node_modules"]
}

View File

@@ -59,7 +59,7 @@ const PERSONAL_ACCESS_TOKEN_SETTING = new Setting('personalAccessToken', DISTRIB
// Query History configuration
const QUERY_HISTORY_SETTING = new Setting('queryHistory', ROOT_SETTING);
const QUERY_HISTORY_FORMAT_SETTING = new Setting('format', QUERY_HISTORY_SETTING);
const QUERY_HISTORY_TTL = new Setting('format', QUERY_HISTORY_SETTING);
const QUERY_HISTORY_TTL = new Setting('ttl', QUERY_HISTORY_SETTING);
/** When these settings change, the distribution should be updated. */
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING];

View File

@@ -10,6 +10,8 @@ import {
import { CodeQLCliServer } from './cli';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as Octokit from '@octokit/rest';
import { retry } from '@octokit/plugin-retry';
import { DatabaseManager, DatabaseItem } from './databases';
import {
@@ -76,7 +78,7 @@ export async function promptImportInternetDatabase(
export async function promptImportGithubDatabase(
databaseManager: DatabaseManager,
storagePath: string,
credentials: Credentials,
credentials: Credentials | undefined,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer
@@ -99,14 +101,15 @@ export async function promptImportGithubDatabase(
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
}
const result = await convertGithubNwoToDatabaseUrl(githubRepo, credentials, progress);
const octokit = credentials ? await credentials.getOctokit(true) : new Octokit.Octokit({ retry });
const result = await convertGithubNwoToDatabaseUrl(githubRepo, octokit, 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:
@@ -118,14 +121,9 @@ export async function promptImportGithubDatabase(
* 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}` },
{ 'Accept': 'application/zip', 'Authorization': octokitToken ? `Bearer ${octokitToken}` : '' },
databaseManager,
storagePath,
`${owner}/${name}`,
@@ -523,7 +521,7 @@ function convertGitHubUrlToNwo(githubUrl: string): string | undefined {
export async function convertGithubNwoToDatabaseUrl(
githubRepo: string,
credentials: Credentials,
octokit: Octokit.Octokit,
progress: ProgressCallback): Promise<{
databaseUrl: string,
owner: string,
@@ -533,7 +531,6 @@ export async function convertGithubNwoToDatabaseUrl(
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);

View File

@@ -40,6 +40,7 @@ import {
import { CancellationToken } from 'vscode';
import { asyncFilter, getErrorMessage } from './pure/helpers-pure';
import { Credentials } from './authentication';
import { isCanary } from './config';
type ThemableIconPath = { light: string; dark: string } | string;
@@ -301,7 +302,7 @@ export class DatabaseUI extends DisposableObject {
progress: ProgressCallback,
token: CancellationToken
) => {
const credentials = await this.getCredentials();
const credentials = isCanary() ? await this.getCredentials() : undefined;
await this.handleChooseDatabaseGithub(credentials, progress, token);
},
{
@@ -480,7 +481,7 @@ export class DatabaseUI extends DisposableObject {
};
handleChooseDatabaseGithub = async (
credentials: Credentials,
credentials: Credentials | undefined,
progress: ProgressCallback,
token: CancellationToken
): Promise<DatabaseItem | undefined> => {

View File

@@ -0,0 +1,67 @@
import { ChildEvalLogTreeItem, EvalLogTreeItem } from './eval-log-viewer';
import { EvalLogData as EvalLogData } from './pure/log-summary-parser';
/** Builds the tree data for the evaluator log viewer for a single query run. */
export default class EvalLogTreeBuilder {
private queryName: string;
private evalLogDataItems: EvalLogData[];
constructor(queryName: string, evaluatorLogDataItems: EvalLogData[]) {
this.queryName = queryName;
this.evalLogDataItems = evaluatorLogDataItems;
}
async getRoots(): Promise<EvalLogTreeItem[]> {
return await this.parseRoots();
}
private async parseRoots(): Promise<EvalLogTreeItem[]> {
const roots: EvalLogTreeItem[] = [];
// Once the viewer can show logs for multiple queries, there will be more than 1 item at the root
// level. For now, there will always be one root (the one query being shown).
const queryItem: EvalLogTreeItem = {
label: this.queryName,
children: [] // Will assign predicate items as children shortly.
};
// Display descriptive message when no data exists
if (this.evalLogDataItems.length === 0) {
const noResultsItem: ChildEvalLogTreeItem = {
label: 'No predicates evaluated in this query run.',
parent: queryItem,
children: [],
};
queryItem.children.push(noResultsItem);
}
// For each predicate, create a TreeItem object with appropriate parents/children
this.evalLogDataItems.forEach(logDataItem => {
const predicateLabel = `${logDataItem.predicateName} (${logDataItem.resultSize} tuples, ${logDataItem.millis} ms)`;
const predicateItem: ChildEvalLogTreeItem = {
label: predicateLabel,
parent: queryItem,
children: [] // Will assign pipeline items as children shortly.
};
for (const [pipelineName, steps] of Object.entries(logDataItem.ra)) {
const pipelineLabel = `Pipeline: ${pipelineName}`;
const pipelineItem: ChildEvalLogTreeItem = {
label: pipelineLabel,
parent: predicateItem,
children: [] // Will assign step items as children shortly.
};
predicateItem.children.push(pipelineItem);
pipelineItem.children = steps.map((step: string) => ({
label: step,
parent: pipelineItem,
children: []
}));
}
queryItem.children.push(predicateItem);
});
roots.push(queryItem);
return roots;
}
}

View File

@@ -0,0 +1,92 @@
import { window, TreeDataProvider, TreeView, TreeItem, ProviderResult, Event, EventEmitter, TreeItemCollapsibleState } from 'vscode';
import { commandRunner } from './commandRunner';
import { DisposableObject } from './pure/disposable-object';
import { showAndLogErrorMessage } from './helpers';
export interface EvalLogTreeItem {
label?: string;
children: ChildEvalLogTreeItem[];
}
export interface ChildEvalLogTreeItem extends EvalLogTreeItem {
parent: ChildEvalLogTreeItem | EvalLogTreeItem;
}
/** Provides data from parsed CodeQL evaluator logs to be rendered in a tree view. */
class EvalLogDataProvider extends DisposableObject implements TreeDataProvider<EvalLogTreeItem> {
public roots: EvalLogTreeItem[] = [];
private _onDidChangeTreeData: EventEmitter<EvalLogTreeItem | undefined | null | void> = new EventEmitter<EvalLogTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: Event<EvalLogTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: EvalLogTreeItem): TreeItem | Thenable<TreeItem> {
const state = element.children.length
? TreeItemCollapsibleState.Collapsed
: TreeItemCollapsibleState.None;
const treeItem = new TreeItem(element.label || '', state);
treeItem.tooltip = `${treeItem.label} || ''}`;
return treeItem;
}
getChildren(element?: EvalLogTreeItem): ProviderResult<EvalLogTreeItem[]> {
// If no item is passed, return the root.
if (!element) {
return this.roots || [];
}
// Otherwise it is called with an existing item, to load its children.
return element.children;
}
getParent(element: ChildEvalLogTreeItem): ProviderResult<EvalLogTreeItem> {
return element.parent;
}
}
/** Manages a tree viewer of structured evaluator logs. */
export class EvalLogViewer extends DisposableObject {
private treeView: TreeView<EvalLogTreeItem>;
private treeDataProvider: EvalLogDataProvider;
constructor() {
super();
this.treeDataProvider = new EvalLogDataProvider();
this.treeView = window.createTreeView('codeQLEvalLogViewer', {
treeDataProvider: this.treeDataProvider,
showCollapseAll: true
});
this.push(this.treeView);
this.push(this.treeDataProvider);
this.push(
commandRunner('codeQLEvalLogViewer.clear', async () => {
this.clear();
})
);
}
private clear(): void {
this.treeDataProvider.roots = [];
this.treeDataProvider.refresh();
this.treeView.message = undefined;
}
// Called when the Show Evaluator Log (UI) command is run on a new query.
updateRoots(roots: EvalLogTreeItem[]): void {
this.treeDataProvider.roots = roots;
this.treeDataProvider.refresh();
this.treeView.message = 'Viewer for query run:'; // Currently only one query supported at a time.
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(roots[0], { focus: false })?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
);
}
}

View File

@@ -98,6 +98,10 @@ import { handleDownloadPacks, handleInstallPackDependencies } from './packaging'
import { HistoryItemLabelProvider } from './history-item-label-provider';
import { exportRemoteQueryResults } from './remote-queries/export-results';
import { RemoteQuery } from './remote-queries/remote-query';
import { EvalLogViewer } from './eval-log-viewer';
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
import { JoinOrderScannerProvider } from './log-insights/join-order';
import { LogScannerService } from './log-insights/log-scanner-service';
/**
* extension.ts
@@ -442,6 +446,10 @@ async function activateWithInstalledDistribution(
databaseUI.init();
ctx.subscriptions.push(databaseUI);
void logger.log('Initializing evaluator log viewer.');
const evalLogViewer = new EvalLogViewer();
ctx.subscriptions.push(evalLogViewer);
void logger.log('Initializing query history manager.');
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
ctx.subscriptions.push(queryHistoryConfigurationListener);
@@ -465,6 +473,7 @@ async function activateWithInstalledDistribution(
dbm,
intm,
rqm,
evalLogViewer,
queryStorageDir,
ctx,
queryHistoryConfigurationListener,
@@ -476,6 +485,11 @@ async function activateWithInstalledDistribution(
ctx.subscriptions.push(qhm);
void logger.log('Initializing evaluation log scanners.');
const logScannerService = new LogScannerService(qhm);
ctx.subscriptions.push(logScannerService);
ctx.subscriptions.push(logScannerService.scanners.registerLogScannerProvider(new JoinOrderScannerProvider()));
void logger.log('Reading query history');
await qhm.readQueryHistory();
@@ -549,7 +563,7 @@ async function activateWithInstalledDistribution(
undefined,
item,
);
item.completeThisQuery(completedQueryInfo);
qhm.completeQuery(item, 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
@@ -924,6 +938,8 @@ async function activateWithInstalledDistribution(
progress: ProgressCallback,
token: CancellationToken
) => {
// We restart the CLI server too, to ensure they are the same version
cliServer.restartCliServer();
await qs.restartQueryServer(progress, token);
void showAndLogInformationMessage('CodeQL Query Server restarted.', {
outputLogger: queryServerLogger,
@@ -956,7 +972,7 @@ async function activateWithInstalledDistribution(
progress: ProgressCallback,
token: CancellationToken
) => {
const credentials = await Credentials.initialize(ctx);
const credentials = isCanary() ? await Credentials.initialize(ctx) : undefined;
await databaseUI.handleChooseDatabaseGithub(credentials, progress, token);
},
{
@@ -1004,19 +1020,16 @@ async function activateWithInstalledDistribution(
}
};
// The "authenticateToGitHub" command is internal-only.
ctx.subscriptions.push(
commandRunner('codeQL.authenticateToGitHub', async () => {
if (isCanary()) {
/**
* Credentials for authenticating to GitHub.
* These are used when making API calls.
*/
const credentials = await Credentials.initialize(ctx);
const octokit = await credentials.getOctokit();
const userInfo = await octokit.users.getAuthenticated();
void showAndLogInformationMessage(`Authenticated to GitHub as user: ${userInfo.data.login}`);
}
/**
* Credentials for authenticating to GitHub.
* These are used when making API calls.
*/
const credentials = await Credentials.initialize(ctx);
const octokit = await credentials.getOctokit();
const userInfo = await octokit.users.getAuthenticated();
void showAndLogInformationMessage(`Authenticated to GitHub as user: ${userInfo.data.login}`);
}));
ctx.subscriptions.push(
@@ -1045,6 +1058,8 @@ async function activateWithInstalledDistribution(
})
);
ctx.subscriptions.push(new SummaryLanguageSupport());
void logger.log('Starting language server.');
ctx.subscriptions.push(client.start());

View File

@@ -581,3 +581,11 @@ export async function* walkDirectory(dir: string): AsyncIterableIterator<string>
}
}
}
/**
* Pluralizes a word.
* Example: Returns "N repository" if N is one, "N repositories" otherwise.
*/
export function pluralize(numItems: number | undefined, singular: string, plural: string): string {
return numItems ? `${numItems} ${numItems === 1 ? singular : plural}` : '';
}

View File

@@ -3,6 +3,7 @@ import * as path from 'path';
import { QueryHistoryConfig } from './config';
import { LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
import { pluralize } from './helpers';
interface InterpolateReplacements {
t: string; // Start time
@@ -45,10 +46,12 @@ export class HistoryItemLabelProvider {
private interpolate(rawLabel: string, replacements: InterpolateReplacements): string {
return rawLabel.replace(/%(.)/g, (match, key: keyof InterpolateReplacements) => {
const label = rawLabel.replace(/%(.)/g, (match, key: keyof InterpolateReplacements) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
return label.replace(/\s+/g, ' ');
}
private getLocalInterpolateReplacements(item: LocalQueryInfo): InterpolateReplacements {
@@ -57,23 +60,31 @@ export class HistoryItemLabelProvider {
t: item.startTime,
q: item.getQueryName(),
d: item.initialInfo.databaseInfo.name,
r: `${resultCount} results`,
r: `(${resultCount} results)`,
s: statusString,
f: item.getQueryFileName(),
'%': '%',
};
}
// Return the number of repositories queried if available. Otherwise, use the controller repository name.
private buildRepoLabel(item: RemoteQueryHistoryItem): string {
const repositoryCount = item.remoteQuery.repositoryCount;
if (repositoryCount) {
return pluralize(repositoryCount, 'repository', 'repositories');
}
return `${item.remoteQuery.controllerRepository.owner}/${item.remoteQuery.controllerRepository.name}`;
}
private getRemoteInterpolateReplacements(item: RemoteQueryHistoryItem): InterpolateReplacements {
const resultCount = item.resultCount ? `(${pluralize(item.resultCount, 'result', 'results')})` : '';
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: '',
q: `${item.remoteQuery.queryName} (${item.remoteQuery.language})`,
d: this.buildRepoLabel(item),
r: resultCount,
s: item.status,
f: path.basename(item.remoteQuery.queryFilePath),
'%': '%'

View File

@@ -4,6 +4,7 @@ import {
Uri,
Location,
Range,
ExtensionContext,
WebviewPanel,
Webview,
workspace,
@@ -111,16 +112,36 @@ export function tryResolveLocation(
}
}
export type WebviewView = 'results' | 'compare' | 'remote-queries';
export interface WebviewMessage {
t: string;
}
/**
* Returns HTML to populate the given webview.
* Uses a content security policy that only loads the given script.
*/
export function getHtmlForWebview(
ctx: ExtensionContext,
webview: Webview,
scriptUriOnDisk: Uri,
stylesheetUrisOnDisk: Uri[],
allowInlineStyles: boolean
view: WebviewView,
{
allowInlineStyles,
}: {
allowInlineStyles?: boolean;
} = {
allowInlineStyles: false,
}
): string {
const scriptUriOnDisk = Uri.file(
ctx.asAbsolutePath('out/webview.js')
);
const stylesheetUrisOnDisk = [
Uri.file(ctx.asAbsolutePath('out/webview.css'))
];
// Convert the on-disk URIs into webview URIs.
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
const stylesheetWebviewUris = stylesheetUrisOnDisk.map(stylesheetUriOnDisk =>
@@ -137,6 +158,8 @@ export function getHtmlForWebview(
? `${webview.cspSource} vscode-file: 'unsafe-inline'`
: `'nonce-${nonce}'`;
const fontSrc = webview.cspSource;
/*
* Content security policy:
* default-src: allow nothing by default.
@@ -149,11 +172,11 @@ export function getHtmlForWebview(
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${styleSrc}; connect-src ${webview.cspSource};">
content="default-src 'none'; script-src 'nonce-${nonce}'; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${webview.cspSource};">
${stylesheetsHtmlLines.join(` ${os.EOL}`)}
</head>
<body>
<div id=root>
<div id=root data-view="${view}">
</div>
<script nonce="${nonce}" src="${scriptWebviewUri}">
</script>

View File

@@ -1,6 +1,4 @@
import * as path from 'path';
import * as Sarif from 'sarif';
import { DisposableObject } from './pure/disposable-object';
import * as vscode from 'vscode';
import {
Diagnostic,
@@ -14,7 +12,7 @@ import {
import * as cli from './cli';
import { CodeQLCliServer } from './cli';
import { DatabaseEventKind, DatabaseItem, DatabaseManager } from './databases';
import { showAndLogErrorMessage, tmpDir } from './helpers';
import { showAndLogErrorMessage } from './helpers';
import { assertNever, getErrorMessage, getErrorStack } from './pure/helpers-pure';
import {
FromResultsViewMsg,
@@ -40,13 +38,13 @@ import {
WebviewReveal,
fileUriToWebviewUri,
tryResolveLocation,
getHtmlForWebview,
shownLocationDecoration,
shownLocationLineDecoration,
jumpToLocation,
} from './interface-utils';
import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-types';
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
import { AbstractInterfaceManager, InterfacePanelConfig } from './abstract-interface-manager';
import { PAGE_SIZE } from './config';
import { CompletedLocalQueryInfo } from './query-results';
import { HistoryItemLabelProvider } from './history-item-label-provider';
@@ -122,12 +120,9 @@ function numInterpretedPages(interpretation: Interpretation | undefined): number
return Math.ceil(n / pageSize);
}
export class InterfaceManager extends DisposableObject {
export class InterfaceManager extends AbstractInterfaceManager<IntoResultsViewMsg, FromResultsViewMsg> {
private _displayedQuery?: CompletedLocalQueryInfo;
private _interpretation?: Interpretation;
private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false;
private _panelLoadedCallBacks: (() => void)[] = [];
private readonly _diagnosticCollection = languages.createDiagnosticCollection(
'codeql-query-results'
@@ -140,7 +135,7 @@ export class InterfaceManager extends DisposableObject {
public logger: Logger,
private labelProvider: HistoryItemLabelProvider
) {
super();
super(ctx);
this.push(this._diagnosticCollection);
this.push(
vscode.window.onDidChangeTextEditorSelection(
@@ -165,7 +160,7 @@ export class InterfaceManager extends DisposableObject {
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
if (kind === DatabaseEventKind.Remove) {
this._diagnosticCollection.clear();
if (this.isShowingPanel()) {
if (this.isShowingPanel) {
void this.postMessage({
t: 'untoggleShowProblems'
});
@@ -179,59 +174,81 @@ export class InterfaceManager extends DisposableObject {
await this.postMessage({ t: 'navigatePath', direction });
}
private isShowingPanel() {
return !!this._panel;
protected getPanelConfig(): InterfacePanelConfig {
return {
viewId: 'resultsView',
title: 'CodeQL Query Results',
viewColumn: this.chooseColumnForWebview(),
preserveFocus: true,
view: 'results',
};
}
// Returns the webview panel, creating it if it doesn't already
// exist.
getPanel(): vscode.WebviewPanel {
if (this._panel == undefined) {
const { ctx } = this;
const webViewColumn = this.chooseColumnForWebview();
const panel = (this._panel = Window.createWebviewPanel(
'resultsView', // internal name
'CodeQL Query Results', // user-visible name
{ viewColumn: webViewColumn, preserveFocus: true },
{
enableScripts: true,
enableFindWidget: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.file(tmpDir.name),
vscode.Uri.file(path.join(this.ctx.extensionPath, 'out'))
]
}
));
protected onPanelDispose(): void {
this._displayedQuery = undefined;
}
this.push(this._panel.onDidDispose(
() => {
this._panel = undefined;
this._displayedQuery = undefined;
this._panelLoaded = false;
},
null,
ctx.subscriptions
));
const scriptPathOnDisk = vscode.Uri.file(
ctx.asAbsolutePath('out/resultsView.js')
);
const stylesheetPathOnDisk = vscode.Uri.file(
ctx.asAbsolutePath('out/view/resultsView.css')
);
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
[stylesheetPathOnDisk],
false
);
this.push(panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
));
protected async onMessage(msg: FromResultsViewMsg): Promise<void> {
try {
switch (msg.t) {
case 'resultViewLoaded':
this.onWebViewLoaded();
break;
case 'viewSourceFile': {
await jumpToLocation(msg, this.databaseManager, this.logger);
break;
}
case 'toggleDiagnostics': {
if (msg.visible) {
const databaseItem = this.databaseManager.findDatabaseItem(
Uri.parse(msg.databaseUri)
);
if (databaseItem !== undefined) {
await this.showResultsAsDiagnostics(
msg.origResultsPaths,
msg.metadata,
databaseItem
);
}
} else {
// TODO: Only clear diagnostics on the same database.
this._diagnosticCollection.clear();
}
break;
}
case 'changeSort':
await this.changeRawSortState(msg.resultSetName, msg.sortState);
break;
case 'changeInterpretedSort':
await this.changeInterpretedSortState(msg.sortState);
break;
case 'changePage':
if (msg.selectedTable === ALERTS_TABLE_NAME || msg.selectedTable === GRAPH_TABLE_NAME) {
await this.showPageOfInterpretedResults(msg.pageNumber);
}
else {
await this.showPageOfRawResults(
msg.selectedTable,
msg.pageNumber,
// When we are in an unsorted state, we guarantee that
// sortedResultsInfo doesn't have an entry for the current
// result set. Use this to determine whether or not we use
// the sorted bqrs file.
!!this._displayedQuery?.completedQuery.sortedResultsInfo[msg.selectedTable]
);
}
break;
case 'openFile':
await this.openFile(msg.filePath);
break;
default:
assertNever(msg);
}
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e), {
fullMessage: getErrorStack(e)
});
}
return this._panel;
}
/**
@@ -296,85 +313,6 @@ export class InterfaceManager extends DisposableObject {
await this.showPageOfRawResults(resultSetName, 0, true);
}
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
try {
switch (msg.t) {
case 'viewSourceFile': {
await jumpToLocation(msg, this.databaseManager, this.logger);
break;
}
case 'toggleDiagnostics': {
if (msg.visible) {
const databaseItem = this.databaseManager.findDatabaseItem(
Uri.parse(msg.databaseUri)
);
if (databaseItem !== undefined) {
await this.showResultsAsDiagnostics(
msg.origResultsPaths,
msg.metadata,
databaseItem
);
}
} else {
// TODO: Only clear diagnostics on the same database.
this._diagnosticCollection.clear();
}
break;
}
case 'resultViewLoaded':
this._panelLoaded = true;
this._panelLoadedCallBacks.forEach((cb) => cb());
this._panelLoadedCallBacks = [];
break;
case 'changeSort':
await this.changeRawSortState(msg.resultSetName, msg.sortState);
break;
case 'changeInterpretedSort':
await this.changeInterpretedSortState(msg.sortState);
break;
case 'changePage':
if (msg.selectedTable === ALERTS_TABLE_NAME || msg.selectedTable === GRAPH_TABLE_NAME) {
await this.showPageOfInterpretedResults(msg.pageNumber);
}
else {
await this.showPageOfRawResults(
msg.selectedTable,
msg.pageNumber,
// When we are in an unsorted state, we guarantee that
// sortedResultsInfo doesn't have an entry for the current
// result set. Use this to determine whether or not we use
// the sorted bqrs file.
!!this._displayedQuery?.completedQuery.sortedResultsInfo[msg.selectedTable]
);
}
break;
case 'openFile':
await this.openFile(msg.filePath);
break;
default:
assertNever(msg);
}
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e), {
fullMessage: getErrorStack(e)
});
}
}
postMessage(msg: IntoResultsViewMsg): Thenable<boolean> {
return this.getPanel().webview.postMessage(msg);
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve) => {
if (this._panelLoaded) {
resolve();
} else {
this._panelLoadedCallBacks.push(resolve);
}
});
}
/**
* Show query results in webview panel.
* @param fullQuery Evaluation info for the executed query.

View File

@@ -0,0 +1,460 @@
import * as I from 'immutable';
import { EvaluationLogProblemReporter, EvaluationLogScanner, EvaluationLogScannerProvider } from './log-scanner';
import { InLayer, ComputeRecursive, SummaryEvent, PipelineRun, ComputeSimple } from './log-summary';
const DEFAULT_WARNING_THRESHOLD = 50;
/**
* Like `max`, but returns 0 if no meaningful maximum can be computed.
*/
function safeMax(it: Iterable<number>) {
const m = Math.max(...it);
return Number.isFinite(m) ? m : 0;
}
/**
* Compute a key for the maps that that is sent to report generation.
* Should only be used on events that are known to define queryCausingWork.
*/
function makeKey(
queryCausingWork: string | undefined,
predicate: string,
suffix = ''
): string {
if (queryCausingWork === undefined) {
throw new Error(
'queryCausingWork was not defined on an event we expected it to be defined for!'
);
}
return `${queryCausingWork}:${predicate}${suffix ? ' ' + suffix : ''}`;
}
const DEPENDENT_PREDICATES_REGEXP = (() => {
const regexps = [
// SCAN id
String.raw`SCAN\s+([0-9a-zA-Z:#_]+)\s`,
// JOIN id WITH id
String.raw`JOIN\s+([0-9a-zA-Z:#_]+)\s+WITH\s+([0-9a-zA-Z:#_]+)\s`,
// AGGREGATE id, id
String.raw`AGGREGATE\s+([0-9a-zA-Z:#_]+)\s*,\s+([0-9a-zA-Z:#_]+)`,
// id AND NOT id
String.raw`([0-9a-zA-Z:#_]+)\s+AND\s+NOT\s+([0-9a-zA-Z:#_]+)`,
// INVOKE HIGHER-ORDER RELATION rel ON <id, ..., id>
String.raw`INVOKE\s+HIGHER-ORDER\s+RELATION\s[^\s]+\sON\s+<([0-9a-zA-Z:#_<>]+)((?:,[0-9a-zA-Z:#_<>]+)*)>`,
// SELECT id
String.raw`SELECT\s+([0-9a-zA-Z:#_]+)`
];
return new RegExp(
`${String.raw`\{[0-9]+\}\s+[0-9a-zA-Z]+\s=\s(?:` + regexps.join('|')})`
);
})();
function getDependentPredicates(operations: string[]): I.List<string> {
return I.List(operations).flatMap(operation => {
const matches = DEPENDENT_PREDICATES_REGEXP.exec(operation.trim());
if (matches !== null) {
return I.List(matches)
.rest() // Skip the first group as it's just the entire string
.filter(x => !!x && !x.match('r[0-9]+|PRIMITIVE')) // Only keep the references to predicates.
.flatMap(x => x.split(',')) // Group 2 in the INVOKE HIGHER_ORDER RELATION case is a comma-separated list of identifiers.
.filter(x => !!x); // Remove empty strings
} else {
return I.List();
}
});
}
function getMainHash(event: InLayer | ComputeRecursive): string {
switch (event.evaluationStrategy) {
case 'IN_LAYER':
return event.mainHash;
case 'COMPUTE_RECURSIVE':
return event.raHash;
}
}
/**
* Sum arrays a and b element-wise. The shorter array is padded with 0s if the arrays are not the same length.
*/
function pointwiseSum(a: Int32Array, b: Int32Array, problemReporter: EvaluationLogProblemReporter): Int32Array {
function reportIfInconsistent(ai: number, bi: number) {
if (ai === -1 && bi !== -1) {
problemReporter.log(
`Operation was not evaluated in the first pipeline, but it was evaluated in the accumulated pipeline (with tuple count ${bi}).`
);
}
if (ai !== -1 && bi === -1) {
problemReporter.log(
`Operation was evaluated in the first pipeline (with tuple count ${ai}), but it was not evaluated in the accumulated pipeline.`
);
}
}
const length = Math.max(a.length, b.length);
const result = new Int32Array(length);
for (let i = 0; i < length; i++) {
const ai = a[i] || 0;
const bi = b[i] || 0;
// -1 is used to represent the absence of a tuple count for a line in the pretty-printed RA (e.g. an empty line), so we ignore those.
if (i < a.length && i < b.length && (ai === -1 || bi === -1)) {
result[i] = -1;
reportIfInconsistent(ai, bi);
} else {
result[i] = ai + bi;
}
}
return result;
}
function pushValue<K, V>(m: Map<K, V[]>, k: K, v: V) {
if (!m.has(k)) {
m.set(k, []);
}
m.get(k)!.push(v);
return m;
}
function computeJoinOrderBadness(
maxTupleCount: number,
maxDependentPredicateSize: number,
resultSize: number
): number {
return maxTupleCount / Math.max(maxDependentPredicateSize, resultSize);
}
/**
* A bucket contains the pointwise sum of the tuple counts, result sizes and dependent predicate sizes
* For each (predicate, order) in an SCC, we will compute a bucket.
*/
interface Bucket {
tupleCounts: Int32Array;
resultSize: number;
dependentPredicateSizes: I.Map<string, number>;
}
class JoinOrderScanner implements EvaluationLogScanner {
// Map a predicate hash to its result size
private readonly predicateSizes = new Map<string, number>();
private readonly layerEvents = new Map<string, (ComputeRecursive | InLayer)[]>();
// Map a key of the form 'query-with-demand : predicate name' to its badness input.
private readonly maxTupleCountMap = new Map<string, number[]>();
private readonly resultSizeMap = new Map<string, number[]>();
private readonly maxDependentPredicateSizeMap = new Map<string, number[]>();
private readonly joinOrderMetricMap = new Map<string, number>();
constructor(
private readonly problemReporter: EvaluationLogProblemReporter,
private readonly warningThreshold: number) {
}
public onEvent(event: SummaryEvent): void {
if (
event.completionType !== undefined &&
event.completionType !== 'SUCCESS'
) {
return; // Skip any evaluation that wasn't successful
}
this.recordPredicateSizes(event);
this.computeBadnessMetric(event);
}
public onDone(): void {
void this;
}
private recordPredicateSizes(event: SummaryEvent): void {
switch (event.evaluationStrategy) {
case 'EXTENSIONAL':
case 'COMPUTED_EXTENSIONAL':
case 'COMPUTE_SIMPLE':
case 'CACHACA':
case 'CACHE_HIT': {
this.predicateSizes.set(event.raHash, event.resultSize);
break;
}
case 'SENTINEL_EMPTY': {
this.predicateSizes.set(event.raHash, 0);
break;
}
case 'COMPUTE_RECURSIVE':
case 'IN_LAYER': {
this.predicateSizes.set(event.raHash, event.resultSize);
// layerEvents are indexed by the mainHash.
const hash = getMainHash(event);
if (!this.layerEvents.has(hash)) {
this.layerEvents.set(hash, []);
}
this.layerEvents.get(hash)!.push(event);
break;
}
}
}
private reportProblemIfNecessary(event: SummaryEvent, iteration: number, metric: number): void {
if (metric >= this.warningThreshold) {
this.problemReporter.reportProblem(event.predicateName, event.raHash, iteration,
`Relation '${event.predicateName}' has an inefficient join order. Its join order metric is ${metric.toFixed(2)}, which is larger than the threshold of ${this.warningThreshold.toFixed(2)}.`);
}
}
private computeBadnessMetric(event: SummaryEvent): void {
if (
event.completionType !== undefined &&
event.completionType !== 'SUCCESS'
) {
return; // Skip any evaluation that wasn't successful
}
switch (event.evaluationStrategy) {
case 'COMPUTE_SIMPLE': {
if (!event.pipelineRuns) {
// skip if the optional pipelineRuns field is not present.
break;
}
// Compute the badness metric for a non-recursive predicate. The metric in this case is defined as:
// badness = (max tuple count in the pipeline) / (largest predicate this pipeline depends on)
const key = makeKey(event.queryCausingWork, event.predicateName);
const resultSize = event.resultSize;
// There is only one entry in `pipelineRuns` if it's a non-recursive predicate.
const { maxTupleCount, maxDependentPredicateSize } =
this.badnessInputsForNonRecursiveDelta(event.pipelineRuns[0], event);
if (maxDependentPredicateSize > 0) {
pushValue(this.maxTupleCountMap, key, maxTupleCount);
pushValue(this.resultSizeMap, key, resultSize);
pushValue(
this.maxDependentPredicateSizeMap,
key,
maxDependentPredicateSize
);
const metric = computeJoinOrderBadness(maxTupleCount, maxDependentPredicateSize, resultSize!);
this.joinOrderMetricMap.set(key, metric);
this.reportProblemIfNecessary(event, 0, metric);
}
break;
}
case 'COMPUTE_RECURSIVE': {
// Compute the badness metric for a recursive predicate for each ordering.
const sccMetricInput = this.badnessInputsForRecursiveDelta(event);
// Loop through each predicate in the SCC
sccMetricInput.forEach((buckets, predicate) => {
// Loop through each ordering of the predicate
buckets.forEach((bucket, raReference) => {
// Format the key as demanding-query:name (ordering)
const key = makeKey(
event.queryCausingWork,
predicate,
`(${raReference})`
);
const maxTupleCount = Math.max(...bucket.tupleCounts);
const resultSize = bucket.resultSize;
const maxDependentPredicateSize = Math.max(
...bucket.dependentPredicateSizes.values()
);
if (maxDependentPredicateSize > 0) {
pushValue(this.maxTupleCountMap, key, maxTupleCount);
pushValue(this.resultSizeMap, key, resultSize);
pushValue(
this.maxDependentPredicateSizeMap,
key,
maxDependentPredicateSize
);
const metric = computeJoinOrderBadness(maxTupleCount, maxDependentPredicateSize, resultSize);
const oldMetric = this.joinOrderMetricMap.get(key);
if ((oldMetric === undefined) || (metric > oldMetric)) {
this.joinOrderMetricMap.set(key, metric);
}
}
});
});
break;
}
}
}
/**
* Iterate through an SCC with main node `event`.
*/
private iterateSCC(
event: ComputeRecursive,
func: (
inLayerEvent: ComputeRecursive | InLayer,
run: PipelineRun,
iteration: number
) => void
): void {
const sccEvents = this.layerEvents.get(event.raHash)!;
const nextPipeline: number[] = new Array(sccEvents.length).fill(0);
const maxIteration = Math.max(
...sccEvents.map(e => e.predicateIterationMillis.length)
);
for (let iteration = 0; iteration < maxIteration; ++iteration) {
// Loop through each predicate in this iteration
for (let predicate = 0; predicate < sccEvents.length; ++predicate) {
const inLayerEvent = sccEvents[predicate];
const iterationTime =
inLayerEvent.predicateIterationMillis.length <= iteration
? -1
: inLayerEvent.predicateIterationMillis[iteration];
if (iterationTime != -1) {
const run: PipelineRun =
inLayerEvent.pipelineRuns[nextPipeline[predicate]++];
func(inLayerEvent, run, iteration);
}
}
}
}
/**
* Compute the maximum tuple count and maximum dependent predicate size for a non-recursive pipeline
*/
private badnessInputsForNonRecursiveDelta(
pipelineRun: PipelineRun,
event: ComputeSimple
): { maxTupleCount: number; maxDependentPredicateSize: number } {
const dependentPredicateSizes = Object.values(event.dependencies).map(hash =>
this.predicateSizes.get(hash) ?? 0 // Should always be present, but zero is a safe default.
);
const maxDependentPredicateSize = safeMax(dependentPredicateSizes);
return {
maxTupleCount: safeMax(pipelineRun.counts),
maxDependentPredicateSize: maxDependentPredicateSize
};
}
private prevDeltaSizes(event: ComputeRecursive, predicate: string, i: number) {
// If an iteration isn't present in the map it means it was skipped because the optimizer
// inferred that it was empty. So its size is 0.
return this.curDeltaSizes(event, predicate, i - 1);
}
private curDeltaSizes(event: ComputeRecursive, predicate: string, i: number) {
// If an iteration isn't present in the map it means it was skipped because the optimizer
// inferred that it was empty. So its size is 0.
return (
this.layerEvents.get(event.raHash)?.find(x => x.predicateName === predicate)?.deltaSizes[i] ?? 0
);
}
/**
* Compute the metric dependent predicate sizes and the result size for a predicate in an SCC.
*/
private badnessInputsForLayer(
event: ComputeRecursive,
inLayerEvent: InLayer | ComputeRecursive,
raReference: string,
iteration: number
) {
const dependentPredicates = getDependentPredicates(
inLayerEvent.ra[raReference]
);
let dependentPredicateSizes: I.Map<string, number>;
// We treat the base case as a non-recursive pipeline. In that case, the dependent predicates are
// the dependencies of the base case and the cur_deltas.
if (raReference === 'base') {
dependentPredicateSizes = I.Map(
dependentPredicates.map((pred): [string, number] => {
// A base case cannot contain a `prev_delta`, but it can contain a `cur_delta`.
let size = 0;
if (pred.endsWith('#cur_delta')) {
size = this.curDeltaSizes(
event,
pred.slice(0, -'#cur_delta'.length),
iteration
);
} else {
const hash = event.dependencies[pred];
size = this.predicateSizes.get(hash)!;
}
return [pred, size];
})
);
} else {
// It's a non-base case in a recursive pipeline. In that case, the dependent predicates are
// only the prev_deltas.
dependentPredicateSizes = I.Map(
dependentPredicates
.flatMap(pred => {
// If it's actually a prev_delta
if (pred.endsWith('#prev_delta')) {
// Return the predicate without the #prev_delta suffix.
return [pred.slice(0, -'#prev_delta'.length)];
} else {
// Not a recursive delta. Skip it.
return [];
}
})
.map((prev): [string, number] => {
const size = this.prevDeltaSizes(event, prev, iteration);
return [prev, size];
})
);
}
const deltaSize = inLayerEvent.deltaSizes[iteration];
return { dependentPredicateSizes, deltaSize };
}
/**
* Compute the metric input for all the events in a SCC that starts with main node `event`
*/
private badnessInputsForRecursiveDelta(event: ComputeRecursive): Map<string, Map<string, Bucket>> {
// nameToOrderToBucket : predicate name -> ordering (i.e., standard, order_500000, etc.) -> bucket
const nameToOrderToBucket = new Map<string, Map<string, Bucket>>();
// Iterate through the SCC and compute the metric inputs
this.iterateSCC(event, (inLayerEvent, run, iteration) => {
const raReference = run.raReference;
const predicateName = inLayerEvent.predicateName;
if (!nameToOrderToBucket.has(predicateName)) {
nameToOrderToBucket.set(predicateName, new Map());
}
const orderTobucket = nameToOrderToBucket.get(predicateName)!;
if (!orderTobucket.has(raReference)) {
orderTobucket.set(raReference, {
tupleCounts: new Int32Array(0),
resultSize: 0,
dependentPredicateSizes: I.Map()
});
}
const { dependentPredicateSizes, deltaSize } = this.badnessInputsForLayer(
event,
inLayerEvent,
raReference,
iteration
);
const bucket = orderTobucket.get(raReference)!;
// Pointwise sum the tuple counts
const newTupleCounts = pointwiseSum(
bucket.tupleCounts,
new Int32Array(run.counts),
this.problemReporter
);
const resultSize = bucket.resultSize + deltaSize;
// Pointwise sum the deltas.
const newDependentPredicateSizes = bucket.dependentPredicateSizes.mergeWith(
(oldSize, newSize) => oldSize + newSize,
dependentPredicateSizes
);
orderTobucket.set(raReference, {
tupleCounts: newTupleCounts,
resultSize: resultSize,
dependentPredicateSizes: newDependentPredicateSizes
});
});
return nameToOrderToBucket;
}
}
export class JoinOrderScannerProvider implements EvaluationLogScannerProvider {
public createScanner(problemReporter: EvaluationLogProblemReporter): EvaluationLogScanner {
return new JoinOrderScanner(problemReporter, DEFAULT_WARNING_THRESHOLD);
}
}

View File

@@ -0,0 +1,23 @@
import * as fs from 'fs-extra';
/**
* Read a file consisting of multiple JSON objects. Each object is separated from the previous one
* by a double newline sequence. This is basically a more human-readable form of JSONL.
*
* The current implementation reads the entire text of the document into memory, but in the future
* it will stream the document to improve the performance with large documents.
*
* @param path The path to the file.
* @param handler Callback to be invoked for each top-level JSON object in order.
*/
export async function readJsonlFile(path: string, handler: (value: any) => Promise<void>): Promise<void> {
const logSummary = await fs.readFile(path, 'utf-8');
// Remove newline delimiters because summary is in .jsonl format.
const jsonSummaryObjects: string[] = logSummary.split(/\r?\n\r?\n/g);
for (const obj of jsonSummaryObjects) {
const jsonObj = JSON.parse(obj);
await handler(jsonObj);
}
}

View File

@@ -0,0 +1,109 @@
import { Diagnostic, DiagnosticSeverity, languages, Range, Uri } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
import { QueryHistoryManager } from '../query-history';
import { QueryHistoryInfo } from '../query-results';
import { EvaluationLogProblemReporter, EvaluationLogScannerSet } from './log-scanner';
import { PipelineInfo, SummarySymbols } from './summary-parser';
import * as fs from 'fs-extra';
import { logger } from '../logging';
/**
* Compute the key used to find a predicate in the summary symbols.
* @param name The name of the predicate.
* @param raHash The RA hash of the predicate.
* @returns The key of the predicate, consisting of `name@shortHash`, where `shortHash` is the first
* eight characters of `raHash`.
*/
function predicateSymbolKey(name: string, raHash: string): string {
return `${name}@${raHash.substring(0, 8)}`;
}
/**
* Implementation of `EvaluationLogProblemReporter` that generates `Diagnostic` objects to display
* in the VS Code "Problems" view.
*/
class ProblemReporter implements EvaluationLogProblemReporter {
public readonly diagnostics: Diagnostic[] = [];
constructor(private readonly symbols: SummarySymbols | undefined) {
}
public reportProblem(predicateName: string, raHash: string, iteration: number, message: string): void {
const nameWithHash = predicateSymbolKey(predicateName, raHash);
const predicateSymbol = this.symbols?.predicates[nameWithHash];
let predicateInfo: PipelineInfo | undefined = undefined;
if (predicateSymbol !== undefined) {
predicateInfo = predicateSymbol.iterations[iteration];
}
if (predicateInfo !== undefined) {
const range = new Range(predicateInfo.raStartLine, 0, predicateInfo.raEndLine + 1, 0);
this.diagnostics.push(new Diagnostic(range, message, DiagnosticSeverity.Error));
}
}
public log(message: string): void {
void logger.log(message);
}
}
export class LogScannerService extends DisposableObject {
public readonly scanners = new EvaluationLogScannerSet();
private readonly diagnosticCollection = this.push(languages.createDiagnosticCollection('ql-eval-log'));
private currentItem: QueryHistoryInfo | undefined = undefined;
constructor(qhm: QueryHistoryManager) {
super();
this.push(qhm.onDidChangeCurrentQueryItem(async (item) => {
if (item !== this.currentItem) {
this.currentItem = item;
await this.scanEvalLog(item);
}
}));
this.push(qhm.onDidCompleteQuery(async (item) => {
if (item === this.currentItem) {
await this.scanEvalLog(item);
}
}));
}
/**
* Scan the evaluation log for a query, and report any diagnostics.
*
* @param query The query whose log is to be scanned.
*/
public async scanEvalLog(
query: QueryHistoryInfo | undefined
): Promise<void> {
this.diagnosticCollection.clear();
if ((query?.t !== 'local')
|| (query.evalLogSummaryLocation === undefined)
|| (query.jsonEvalLogSummaryLocation === undefined)) {
return;
}
const diagnostics = await this.scanLog(query.jsonEvalLogSummaryLocation, query.evalLogSummarySymbolsLocation);
const uri = Uri.file(query.evalLogSummaryLocation);
this.diagnosticCollection.set(uri, diagnostics);
}
/**
* Scan the evaluator summary log for problems, using the scanners for all registered providers.
* @param jsonSummaryLocation The file path of the JSON summary log.
* @param symbolsLocation The file path of the symbols file for the human-readable log summary.
* @returns An array of `Diagnostic`s representing the problems found by scanners.
*/
private async scanLog(jsonSummaryLocation: string, symbolsLocation: string | undefined): Promise<Diagnostic[]> {
let symbols: SummarySymbols | undefined = undefined;
if (symbolsLocation !== undefined) {
symbols = JSON.parse(await fs.readFile(symbolsLocation, { encoding: 'utf-8' }));
}
const problemReporter = new ProblemReporter(symbols);
await this.scanners.scanLog(jsonSummaryLocation, problemReporter);
return problemReporter.diagnostics;
}
}

View File

@@ -0,0 +1,103 @@
import { SummaryEvent } from './log-summary';
import { readJsonlFile } from './jsonl-reader';
/**
* Callback interface used to report diagnostics from a log scanner.
*/
export interface EvaluationLogProblemReporter {
/**
* Report a potential problem detected in the evaluation log.
*
* @param predicateName The mangled name of the predicate with the problem.
* @param raHash The RA hash of the predicate with the problem.
* @param iteration The iteration number with the problem. For a non-recursive predicate, this
* must be zero.
* @param message The problem message.
*/
reportProblem(predicateName: string, raHash: string, iteration: number, message: string): void;
/**
* Log a message about a problem in the implementation of the scanner. These will typically be
* displayed separate from any problems reported via `reportProblem()`.
*/
log(message: string): void;
}
/**
* Interface implemented by a log scanner. Instances are created via
* `EvaluationLogScannerProvider.createScanner()`.
*/
export interface EvaluationLogScanner {
/**
* Called for each event in the log summary, in order. The implementation can report problems via
* the `EvaluationLogProblemReporter` interface that was supplied to `createScanner()`.
* @param event The log summary event.
*/
onEvent(event: SummaryEvent): void;
/**
* Called after all events in the log summary have been processed. The implementation can report
* problems via the `EvaluationLogProblemReporter` interface that was supplied to
* `createScanner()`.
*/
onDone(): void;
}
/**
* A factory for log scanners. When a log is to be scanned, all registered
* `EvaluationLogScannerProviders` will be asked to create a new instance of `EvaluationLogScanner`
* to do the scanning.
*/
export interface EvaluationLogScannerProvider {
/**
* Create a new instance of `EvaluationLogScanner` to scan a single summary log.
* @param problemReporter Callback interface for reporting any problems discovered.
*/
createScanner(problemReporter: EvaluationLogProblemReporter): EvaluationLogScanner;
}
/**
* Same as VSCode's `Disposable`, but avoids a dependency on VS Code.
*/
export interface Disposable {
dispose(): void;
}
export class EvaluationLogScannerSet {
private readonly scannerProviders = new Map<number, EvaluationLogScannerProvider>();
private nextScannerProviderId = 0;
/**
* Register a provider that can create instances of `EvaluationLogScanner` to scan evaluation logs
* for problems.
* @param provider The provider.
* @returns A `Disposable` that, when disposed, will unregister the provider.
*/
public registerLogScannerProvider(provider: EvaluationLogScannerProvider): Disposable {
const id = this.nextScannerProviderId;
this.nextScannerProviderId++;
this.scannerProviders.set(id, provider);
return {
dispose: () => {
this.scannerProviders.delete(id);
}
};
}
/**
* Scan the evaluator summary log for problems, using the scanners for all registered providers.
* @param jsonSummaryLocation The file path of the JSON summary log.
* @param problemReporter Callback interface for reporting any problems discovered.
*/
public async scanLog(jsonSummaryLocation: string, problemReporter: EvaluationLogProblemReporter): Promise<void> {
const scanners = [...this.scannerProviders.values()].map(p => p.createScanner(problemReporter));
await readJsonlFile(jsonSummaryLocation, async obj => {
scanners.forEach(scanner => {
scanner.onEvent(obj);
});
});
scanners.forEach(scanner => scanner.onDone());
}
}

View File

@@ -0,0 +1,93 @@
export interface PipelineRun {
raReference: string;
counts: number[];
duplicationPercentages: number[];
}
export interface Ra {
[key: string]: string[];
}
export type EvaluationStrategy =
'COMPUTE_SIMPLE' |
'COMPUTE_RECURSIVE' |
'IN_LAYER' |
'COMPUTED_EXTENSIONAL' |
'EXTENSIONAL' |
'SENTINEL_EMPTY' |
'CACHACA' |
'CACHE_HIT';
interface SummaryEventBase {
evaluationStrategy: EvaluationStrategy;
predicateName: string;
raHash: string;
appearsAs: { [key: string]: { [key: string]: number[] } };
completionType?: string;
}
interface ResultEventBase extends SummaryEventBase {
resultSize: number;
}
export interface ComputeSimple extends ResultEventBase {
evaluationStrategy: 'COMPUTE_SIMPLE';
ra: Ra;
pipelineRuns?: [PipelineRun];
queryCausingWork?: string;
dependencies: { [key: string]: string };
}
export interface ComputeRecursive extends ResultEventBase {
evaluationStrategy: 'COMPUTE_RECURSIVE';
deltaSizes: number[];
ra: Ra;
pipelineRuns: PipelineRun[];
queryCausingWork?: string;
dependencies: { [key: string]: string };
predicateIterationMillis: number[];
}
export interface InLayer extends ResultEventBase {
evaluationStrategy: 'IN_LAYER';
deltaSizes: number[];
ra: Ra;
pipelineRuns: PipelineRun[];
queryCausingWork?: string;
mainHash: string;
predicateIterationMillis: number[];
}
export interface ComputedExtensional extends ResultEventBase {
evaluationStrategy: 'COMPUTED_EXTENSIONAL';
queryCausingWork?: string;
}
export interface NonComputedExtensional extends ResultEventBase {
evaluationStrategy: 'EXTENSIONAL';
queryCausingWork?: string;
}
export interface SentinelEmpty extends SummaryEventBase {
evaluationStrategy: 'SENTINEL_EMPTY';
sentinelRaHash: string;
}
export interface Cachaca extends ResultEventBase {
evaluationStrategy: 'CACHACA';
}
export interface CacheHit extends ResultEventBase {
evaluationStrategy: 'CACHE_HIT';
}
export type Extensional = ComputedExtensional | NonComputedExtensional;
export type SummaryEvent =
| ComputeSimple
| ComputeRecursive
| InLayer
| Extensional
| SentinelEmpty
| Cachaca
| CacheHit;

View File

@@ -0,0 +1,154 @@
import * as fs from 'fs-extra';
import { RawSourceMap, SourceMapConsumer } from 'source-map';
import { commands, Position, Selection, TextDocument, TextEditor, TextEditorRevealType, TextEditorSelectionChangeEvent, ViewColumn, window, workspace } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
import { commandRunner } from '../commandRunner';
import { logger } from '../logging';
import { getErrorMessage } from '../pure/helpers-pure';
/** A `Position` within a specified file on disk. */
interface PositionInFile {
filePath: string;
position: Position;
}
/**
* Opens the specified source location in a text editor.
* @param position The position (including file path) to show.
*/
async function showSourceLocation(position: PositionInFile): Promise<void> {
const document = await workspace.openTextDocument(position.filePath);
const editor = await window.showTextDocument(document, ViewColumn.Active);
editor.selection = new Selection(position.position, position.position);
editor.revealRange(editor.selection, TextEditorRevealType.InCenterIfOutsideViewport);
}
/**
* Simple language support for human-readable evaluator log summaries.
*
* This class implements the `codeQL.gotoQL` command, which jumps from RA code to the corresponding
* QL code that generated it. It also tracks the current selection and active editor to enable and
* disable that command based on whether there is a QL mapping for the current selection.
*/
export class SummaryLanguageSupport extends DisposableObject {
/**
* The last `TextDocument` (with language `ql-summary`) for which we tried to find a sourcemap, or
* `undefined` if we have not seen such a document yet.
*/
private lastDocument: TextDocument | undefined = undefined;
/**
* The sourcemap for `lastDocument`, or `undefined` if there was no such sourcemap or document.
*/
private sourceMap: SourceMapConsumer | undefined = undefined;
constructor() {
super();
this.push(window.onDidChangeActiveTextEditor(this.handleDidChangeActiveTextEditor));
this.push(window.onDidChangeTextEditorSelection(this.handleDidChangeTextEditorSelection));
this.push(workspace.onDidCloseTextDocument(this.handleDidCloseTextDocument));
this.push(commandRunner('codeQL.gotoQL', this.handleGotoQL));
}
/**
* Gets the location of the QL code that generated the RA at the current selection in the active
* editor, or `undefined` if there is no mapping.
*/
private async getQLSourceLocation(): Promise<PositionInFile | undefined> {
const editor = window.activeTextEditor;
if (editor === undefined) {
return undefined;
}
const document = editor.document;
if (document.languageId !== 'ql-summary') {
return undefined;
}
if (document.uri.scheme !== 'file') {
return undefined;
}
if (this.lastDocument !== document) {
this.clearCache();
const mapPath = document.uri.fsPath + '.map';
try {
const sourceMapText = await fs.readFile(mapPath, 'utf-8');
const rawMap: RawSourceMap = JSON.parse(sourceMapText);
this.sourceMap = await new SourceMapConsumer(rawMap);
} catch (e: unknown) {
// Error reading sourcemap. Pretend there was no sourcemap.
void logger.log(`Error reading sourcemap file '${mapPath}': ${getErrorMessage(e)}`);
this.sourceMap = undefined;
}
this.lastDocument = document;
}
if (this.sourceMap === undefined) {
return undefined;
}
const qlPosition = this.sourceMap.originalPositionFor({
line: editor.selection.start.line + 1,
column: editor.selection.start.character,
bias: SourceMapConsumer.GREATEST_LOWER_BOUND
});
if ((qlPosition.source === null) || (qlPosition.line === null)) {
// No position found.
return undefined;
}
const line = qlPosition.line - 1; // In `source-map`, lines are 1-based...
const column = qlPosition.column ?? 0; // ...but columns are 0-based :(
return {
filePath: qlPosition.source,
position: new Position(line, column)
};
}
/**
* Clears the cached sourcemap and its corresponding `TextDocument`.
*/
private clearCache(): void {
if (this.sourceMap !== undefined) {
this.sourceMap.destroy();
this.sourceMap = undefined;
this.lastDocument = undefined;
}
}
/**
* Updates the `codeql.hasQLSource` context variable based on the current selection. This variable
* controls whether or not the `codeQL.gotoQL` command is enabled.
*/
private async updateContext(): Promise<void> {
const position = await this.getQLSourceLocation();
await commands.executeCommand('setContext', 'codeql.hasQLSource', position !== undefined);
}
handleDidChangeActiveTextEditor = async (_editor: TextEditor | undefined): Promise<void> => {
await this.updateContext();
}
handleDidChangeTextEditorSelection = async (_e: TextEditorSelectionChangeEvent): Promise<void> => {
await this.updateContext();
}
handleDidCloseTextDocument = (document: TextDocument): void => {
if (this.lastDocument === document) {
this.clearCache();
}
}
handleGotoQL = async (): Promise<void> => {
const position = await this.getQLSourceLocation();
if (position !== undefined) {
await showSourceLocation(position);
}
};
}

View File

@@ -0,0 +1,113 @@
import * as fs from 'fs-extra';
/**
* Location information for a single pipeline invocation in the RA.
*/
export interface PipelineInfo {
startLine: number;
raStartLine: number;
raEndLine: number;
}
/**
* Location information for a single predicate in the RA.
*/
export interface PredicateSymbol {
/**
* `PipelineInfo` for each iteration. A non-recursive predicate will have a single iteration `0`.
*/
iterations: Record<number, PipelineInfo>;
}
/**
* Location information for the RA from an evaluation log. Line numbers point into the
* human-readable log summary.
*/
export interface SummarySymbols {
predicates: Record<string, PredicateSymbol>;
}
// Tuple counts for Expr::Expr::getParent#dispred#f0820431#ff@76d6745o:
const NON_RECURSIVE_TUPLE_COUNT_REGEXP = /^Evaluated relational algebra for predicate (?<predicateName>\S+) with tuple counts:$/;
// Tuple counts for Expr::Expr::getEnclosingStmt#f0820431#bf@923ddwj9 on iteration 0 running pipeline base:
const RECURSIVE_TUPLE_COUNT_REGEXP = /^Evaluated relational algebra for predicate (?<predicateName>\S+) on iteration (?<iteration>\d+) running pipeline (?<pipeline>\S+) with tuple counts:$/;
const RETURN_REGEXP = /^\s*return /;
/**
* Parse a human-readable evaluation log summary to find the location of the RA for each pipeline
* run.
*
* TODO: Once we're more certain about the symbol format, we should have the CLI generate this as it
* generates the human-readabe summary to avoid having to rely on regular expression matching of the
* human-readable text.
*
* @param summaryPath The path to the summary file.
* @param symbolsPath The path to the symbols file to generate.
*/
export async function generateSummarySymbolsFile(summaryPath: string, symbolsPath: string): Promise<void> {
const symbols = await generateSummarySymbols(summaryPath);
await fs.writeFile(symbolsPath, JSON.stringify(symbols));
}
/**
* Parse a human-readable evaluation log summary to find the location of the RA for each pipeline
* run.
*
* @param fileLocation The path to the summary file.
* @returns Symbol information for the summary file.
*/
async function generateSummarySymbols(summaryPath: string): Promise<SummarySymbols> {
const summary = await fs.promises.readFile(summaryPath, { encoding: 'utf-8' });
const symbols: SummarySymbols = {
predicates: {}
};
const lines = summary.split(/\r?\n/);
let lineNumber = 0;
while (lineNumber < lines.length) {
const startLineNumber = lineNumber;
lineNumber++;
const startLine = lines[startLineNumber];
const nonRecursiveMatch = startLine.match(NON_RECURSIVE_TUPLE_COUNT_REGEXP);
let predicateName: string | undefined = undefined;
let iteration = 0;
if (nonRecursiveMatch) {
predicateName = nonRecursiveMatch.groups!.predicateName;
} else {
const recursiveMatch = startLine.match(RECURSIVE_TUPLE_COUNT_REGEXP);
if (recursiveMatch?.groups) {
predicateName = recursiveMatch.groups.predicateName;
iteration = parseInt(recursiveMatch.groups.iteration);
}
}
if (predicateName !== undefined) {
const raStartLine = lineNumber;
let raEndLine: number | undefined = undefined;
while ((lineNumber < lines.length) && (raEndLine === undefined)) {
const raLine = lines[lineNumber];
const returnMatch = raLine.match(RETURN_REGEXP);
if (returnMatch) {
raEndLine = lineNumber;
}
lineNumber++;
}
if (raEndLine !== undefined) {
let symbol = symbols.predicates[predicateName];
if (symbol === undefined) {
symbol = {
iterations: {}
};
symbols.predicates[predicateName] = symbol;
}
symbol.iterations[iteration] = {
startLine: lineNumber,
raStartLine: raStartLine,
raEndLine: raEndLine
};
}
}
}
return symbols;
}

View File

@@ -0,0 +1,34 @@
import { readJsonlFile } from '../log-insights/jsonl-reader';
// TODO(angelapwen): Only load in necessary information and
// location in bytes for this log to save memory.
export interface EvalLogData {
predicateName: string;
millis: number;
resultSize: number;
// Key: pipeline identifier; Value: array of pipeline steps
ra: Record<string, string[]>;
}
/**
* A pure method that parses a string of evaluator log summaries into
* an array of EvalLogData objects.
*/
export async function parseViewerData(jsonSummaryPath: string): Promise<EvalLogData[]> {
const viewerData: EvalLogData[] = [];
await readJsonlFile(jsonSummaryPath, async jsonObj => {
// Only convert log items that have an RA and millis field
if (jsonObj.ra !== undefined && jsonObj.millis !== undefined) {
const newLogData: EvalLogData = {
predicateName: jsonObj.predicateName,
millis: jsonObj.millis,
resultSize: jsonObj.resultSize,
ra: jsonObj.ra
};
viewerData.push(newLogData);
}
});
return viewerData;
}

View File

@@ -155,6 +155,10 @@ export interface CompilationOptions {
* get reported anyway. Useful for universal compilation options.
*/
computeDefaultStrings: boolean;
/**
* Emit debug information in compiled query.
*/
emitDebugInfo: boolean;
}
/**
@@ -650,7 +654,7 @@ export interface ClearCacheParams {
/**
* Parameters to start a new structured log
*/
export interface StartLogParams {
export interface StartLogParams {
/**
* The dataset for which we want to start a new structured log
*/
@@ -664,7 +668,7 @@ export interface ClearCacheParams {
/**
* Parameters to terminate a structured log
*/
export interface EndLogParams {
export interface EndLogParams {
/**
* The dataset for which we want to terminated the log
*/
@@ -1070,12 +1074,12 @@ export const compileUpgradeSequence = new rpc.RequestType<WithProgressId<Compile
/**
* 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');
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');
export const endLog = new rpc.RequestType<WithProgressId<EndLogParams>, EndLogResult, void, void>('evaluation/endLog');
/**
* Clear the cache of a dataset

View File

@@ -3,6 +3,7 @@ import * as os from 'os';
import * as path from 'path';
import { Disposable, ExtensionContext } from 'vscode';
import { logger } from './logging';
import { QueryHistoryManager } from './query-history';
const LAST_SCRUB_TIME_KEY = 'lastScrubTime';
@@ -30,12 +31,13 @@ export function registerQueryHistoryScubber(
throttleTime: number,
maxQueryTime: number,
queryDirectory: string,
qhm: QueryHistoryManager,
ctx: ExtensionContext,
// optional counter to keep track of how many times the scrubber has run
counter?: Counter
): Disposable {
const deregister = setInterval(scrubQueries, wakeInterval, throttleTime, maxQueryTime, queryDirectory, ctx, counter);
const deregister = setInterval(scrubQueries, wakeInterval, throttleTime, maxQueryTime, queryDirectory, qhm, ctx, counter);
return {
dispose: () => {
@@ -48,6 +50,7 @@ async function scrubQueries(
throttleTime: number,
maxQueryTime: number,
queryDirectory: string,
qhm: QueryHistoryManager,
ctx: ExtensionContext,
counter?: Counter
) {
@@ -89,6 +92,7 @@ async function scrubQueries(
} finally {
void logger.log(`Scrubbed ${scrubCount} old queries.`);
}
await qhm.removeDeletedQueries();
}
}

View File

@@ -9,6 +9,7 @@ import {
ProviderResult,
Range,
ThemeIcon,
TreeDataProvider,
TreeItem,
TreeView,
Uri,
@@ -44,6 +45,10 @@ import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
import { InterfaceManager } from './interface';
import { WebviewReveal } from './interface-utils';
import { EvalLogViewer } from './eval-log-viewer';
import EvalLogTreeBuilder from './eval-log-tree-builder';
import { EvalLogData, parseViewerData } from './pure/log-summary-parser';
import { QueryWithResults } from './run-queries';
/**
* query-history.ts
@@ -111,7 +116,7 @@ const WORKSPACE_QUERY_HISTORY_FILE = 'workspace-query-history.json';
/**
* Tree data provider for the query history view.
*/
export class HistoryTreeDataProvider extends DisposableObject {
export class HistoryTreeDataProvider extends DisposableObject implements TreeDataProvider<QueryHistoryInfo> {
private _sortOrder = SortOrder.DateAsc;
private _onDidChangeTreeData = super.push(new EventEmitter<QueryHistoryInfo | undefined>());
@@ -119,6 +124,10 @@ export class HistoryTreeDataProvider extends DisposableObject {
readonly onDidChangeTreeData: Event<QueryHistoryInfo | undefined> = this
._onDidChangeTreeData.event;
private _onDidChangeCurrentQueryItem = super.push(new EventEmitter<QueryHistoryInfo | undefined>());
public readonly onDidChangeCurrentQueryItem = this._onDidChangeCurrentQueryItem.event;
private history: QueryHistoryInfo[] = [];
private failedIconPath: string;
@@ -205,13 +214,12 @@ export class HistoryTreeDataProvider extends DisposableObject {
? 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;
: h1.resultCount ?? -1;
const resultCount2 = h2.t === 'local'
? h2.completedQuery?.resultCount ?? -1
: -1;
: h2.resultCount ?? -1;
switch (this.sortOrder) {
case SortOrder.NameAsc:
@@ -258,7 +266,10 @@ export class HistoryTreeDataProvider extends DisposableObject {
}
setCurrentItem(item?: QueryHistoryInfo) {
this.current = item;
if (item !== this.current) {
this.current = item;
this._onDidChangeCurrentQueryItem.fire(item);
}
}
remove(item: QueryHistoryInfo) {
@@ -284,7 +295,7 @@ export class HistoryTreeDataProvider extends DisposableObject {
set allHistory(history: QueryHistoryInfo[]) {
this.history = history;
this.current = history[0];
this.setCurrentItem(history[0]);
this.refresh();
}
@@ -311,11 +322,18 @@ export class QueryHistoryManager extends DisposableObject {
queryHistoryScrubber: Disposable | undefined;
private queryMetadataStorageLocation;
private readonly _onDidChangeCurrentQueryItem = super.push(new EventEmitter<QueryHistoryInfo | undefined>());
readonly onDidChangeCurrentQueryItem = this._onDidChangeCurrentQueryItem.event;
private readonly _onDidCompleteQuery = super.push(new EventEmitter<LocalQueryInfo>());
readonly onDidCompleteQuery = this._onDidCompleteQuery.event;
constructor(
private readonly qs: QueryServerClient,
private readonly dbm: DatabaseManager,
private readonly localQueriesInterfaceManager: InterfaceManager,
private readonly remoteQueriesManager: RemoteQueriesManager,
private readonly evalLogViewer: EvalLogViewer,
private readonly queryStorageDir: string,
private readonly ctx: ExtensionContext,
private readonly queryHistoryConfigListener: QueryHistoryConfig,
@@ -342,6 +360,11 @@ export class QueryHistoryManager extends DisposableObject {
canSelectMany: true,
}));
// Forward any change of current history item from the tree data.
this.push(this.treeDataProvider.onDidChangeCurrentQueryItem((item) => {
this._onDidChangeCurrentQueryItem.fire(item);
}));
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
this.push(
@@ -433,6 +456,12 @@ export class QueryHistoryManager extends DisposableObject {
this.handleShowEvalLogSummary.bind(this)
)
);
this.push(
commandRunner(
'codeQLQueryHistory.showEvalLogViewer',
this.handleShowEvalLogViewer.bind(this)
)
);
this.push(
commandRunner(
'codeQLQueryHistory.cancel',
@@ -505,7 +534,7 @@ export class QueryHistoryManager extends DisposableObject {
this.push(
queryHistoryConfigListener.onDidChangeConfiguration(() => {
this.treeDataProvider.refresh();
this.registerQueryHistoryScrubber(queryHistoryConfigListener, ctx);
this.registerQueryHistoryScrubber(queryHistoryConfigListener, this, ctx);
})
);
@@ -524,10 +553,15 @@ export class QueryHistoryManager extends DisposableObject {
},
}));
this.registerQueryHistoryScrubber(queryHistoryConfigListener, ctx);
this.registerQueryHistoryScrubber(queryHistoryConfigListener, this, ctx);
this.registerToRemoteQueriesEvents();
}
public completeQuery(info: LocalQueryInfo, results: QueryWithResults): void {
info.completeThisQuery(results);
this._onDidCompleteQuery.fire(info);
}
private getCredentials() {
return Credentials.initialize(this.ctx);
}
@@ -535,7 +569,7 @@ export class QueryHistoryManager extends DisposableObject {
/**
* Register and create the history scrubber.
*/
private registerQueryHistoryScrubber(queryHistoryConfigListener: QueryHistoryConfig, ctx: ExtensionContext) {
private registerQueryHistoryScrubber(queryHistoryConfigListener: QueryHistoryConfig, qhm: QueryHistoryManager, ctx: ExtensionContext) {
this.queryHistoryScrubber?.dispose();
// Every hour check if we need to re-run the query history scrubber.
this.queryHistoryScrubber = this.push(
@@ -544,13 +578,14 @@ export class QueryHistoryManager extends DisposableObject {
TWO_HOURS_IN_MS,
queryHistoryConfigListener.ttlInMillis,
this.queryStorageDir,
qhm,
ctx
)
);
}
private registerToRemoteQueriesEvents() {
const queryAddedSubscription = this.remoteQueriesManager.onRemoteQueryAdded(event => {
const queryAddedSubscription = this.remoteQueriesManager.onRemoteQueryAdded(async (event) => {
this.addQuery({
t: 'remote',
status: QueryStatus.InProgress,
@@ -558,6 +593,8 @@ export class QueryHistoryManager extends DisposableObject {
queryId: event.queryId,
remoteQuery: event.query,
});
await this.refreshTreeView();
});
const queryRemovedSubscription = this.remoteQueriesManager.onRemoteQueryRemoved(async (event) => {
@@ -573,6 +610,10 @@ export class QueryHistoryManager extends DisposableObject {
const remoteQueryHistoryItem = item as RemoteQueryHistoryItem;
remoteQueryHistoryItem.status = event.status;
remoteQueryHistoryItem.failureReason = event.failureReason;
remoteQueryHistoryItem.resultCount = event.resultCount;
if (event.status === QueryStatus.Completed) {
remoteQueryHistoryItem.completed = true;
}
await this.refreshTreeView();
} else {
void logger.log('Variant analysis status update event received for unknown variant analysis');
@@ -639,6 +680,15 @@ export class QueryHistoryManager extends DisposableObject {
return this.treeDataProvider.getCurrent();
}
async removeDeletedQueries() {
await Promise.all(this.treeDataProvider.allHistory.map(async (item) => {
if (item.t == 'local' && item.completedQuery && !(await fs.pathExists(item.completedQuery?.query.querySaveDir))) {
this.treeDataProvider.remove(item);
item.completedQuery?.dispose();
}
}));
}
async handleRemoveHistoryItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] = []
@@ -854,16 +904,16 @@ export class QueryHistoryManager extends DisposableObject {
}
}
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(`Evaluator log summary and evaluator log are not available for this run. Perhaps they failed before evaluation, or you are running with a version of CodeQL before ${CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG}?`);
private warnNoEvalLogs() {
void showAndLogWarningMessage(`Evaluator log, summary, and viewer are not 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 warnInProgressEvalLogSummary() {
void showAndLogWarningMessage('The evaluator log summary is still being generated. Please try again later. The summary generation process is tracked in the "CodeQL Extension Log" view.');
void showAndLogWarningMessage('The evaluator log summary is still being generated for this run. Please try again later. The summary generation process is tracked in the "CodeQL Extension Log" view.');
}
private warnInProgressEvalLogViewer() {
void showAndLogWarningMessage('The viewer\'s data is still being generated for this run. Please try again or re-run the query.');
}
async handleShowEvalLog(
@@ -880,7 +930,7 @@ export class QueryHistoryManager extends DisposableObject {
if (finalSingleItem.evalLogLocation) {
await this.tryOpenExternalFile(finalSingleItem.evalLogLocation);
} else {
this.warnNoEvalLog();
this.warnNoEvalLogs();
}
}
@@ -897,15 +947,41 @@ export class QueryHistoryManager extends DisposableObject {
if (finalSingleItem.evalLogSummaryLocation) {
await this.tryOpenExternalFile(finalSingleItem.evalLogSummaryLocation);
return;
}
// Summary log file doesn't exist.
else {
if (finalSingleItem.evalLogLocation && fs.pathExists(finalSingleItem.evalLogLocation)) {
// If raw log does exist, then the summary log is still being generated.
this.warnInProgressEvalLogSummary();
} else {
this.warnNoEvalLogSummary();
}
if (finalSingleItem.evalLogLocation && await fs.pathExists(finalSingleItem.evalLogLocation)) {
// If raw log does exist, then the summary log is still being generated.
this.warnInProgressEvalLogSummary();
} else {
this.warnNoEvalLogs();
}
}
async handleShowEvalLogViewer(
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 the JSON summary file location wasn't saved, display error
if (finalSingleItem.jsonEvalLogSummaryLocation == undefined) {
this.warnInProgressEvalLogViewer();
return;
}
// TODO(angelapwen): Stream the file in.
try {
const evalLogData: EvalLogData[] = await parseViewerData(finalSingleItem.jsonEvalLogSummaryLocation);
const evalLogTreeBuilder = new EvalLogTreeBuilder(finalSingleItem.getQueryName(), evalLogData);
this.evalLogViewer.updateRoots(await evalLogTreeBuilder.getRoots());
} catch (e) {
throw new Error(`Could not read evaluator log summary JSON file to generate viewer data at ${finalSingleItem.jsonEvalLogSummaryLocation}.`);
}
}
@@ -913,8 +989,6 @@ export class QueryHistoryManager extends DisposableObject {
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
// Local queries only
// In the future, we may support cancelling remote queries, but this is not a short term plan.
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
const selected = finalMultiSelect || [finalSingleItem];

View File

@@ -217,6 +217,8 @@ export class LocalQueryInfo {
public completedQuery: CompletedQueryInfo | undefined;
public evalLogLocation: string | undefined;
public evalLogSummaryLocation: string | undefined;
public jsonEvalLogSummaryLocation: string | undefined;
public evalLogSummarySymbolsLocation: string | undefined;
/**
* Note that in the {@link slurpQueryHistory} method, we create a FullQueryInfo instance
@@ -281,7 +283,7 @@ export class LocalQueryInfo {
return !!this.completedQuery;
}
completeThisQuery(info: QueryWithResults) {
completeThisQuery(info: QueryWithResults): void {
this.completedQuery = new CompletedQueryInfo(info);
// dispose of the cancellation token source and also ensure the source is not serialized as JSON

View File

@@ -4,6 +4,7 @@ import * as path from 'path';
import { showAndLogErrorMessage } from './helpers';
import { asyncFilter, getErrorMessage, getErrorStack } from './pure/helpers-pure';
import { CompletedQueryInfo, LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { QueryStatus } from './query-status';
import { QueryEvaluationInfo } from './run-queries';
export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInfo[]> {
@@ -39,7 +40,12 @@ export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInf
q.completedQuery.dispose = () => { /**/ };
}
} else if (q.t === 'remote') {
// noop
// A bug was introduced that didn't set the completed flag in query history
// items. The following code makes sure that the flag is set in order to
// "patch" older query history items.
if (q.status === QueryStatus.Completed) {
q.completed = true;
}
}
return q;
});

View File

@@ -267,6 +267,14 @@ export function findQueryEvalLogSummaryFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log.summary');
}
export function findJsonQueryEvalLogSummaryFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log.summary.jsonl');
}
export function findQueryEvalLogSummarySymbolsFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log.summary.symbols.json');
}
export function findQueryEvalLogEndSummaryFile(resultPath: string): string {
return path.join(resultPath, 'evaluator-log-end.summary');
}
}

View File

@@ -4,14 +4,17 @@ import * as fs from 'fs-extra';
import { window, commands, Uri, ExtensionContext, QuickPickItem, workspace, ViewColumn } from 'vscode';
import { Credentials } from '../authentication';
import { UserCancellationException } from '../commandRunner';
import { showInformationMessageWithAction } from '../helpers';
import {
showInformationMessageWithAction,
pluralize
} from '../helpers';
import { logger } from '../logging';
import { QueryHistoryManager } from '../query-history';
import { createGist } from './gh-actions-api-client';
import { RemoteQueriesManager } from './remote-queries-manager';
import { generateMarkdown } from './remote-queries-markdown-generation';
import { RemoteQuery } from './remote-query';
import { AnalysisResults } from './shared/analysis-result';
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
/**
* Exports the results of the currently-selected remote query.
@@ -74,13 +77,13 @@ async function determineExportFormat(
/**
* Converts the results of a remote query to markdown and uploads the files as a secret gist.
*/
async function exportResultsToGist(
export async function exportResultsToGist(
ctx: ExtensionContext,
query: RemoteQuery,
analysesResults: AnalysisResults[]
): Promise<void> {
const credentials = await Credentials.initialize(ctx);
const description = 'CodeQL Variant Analysis Results';
const description = buildGistDescription(query, analysesResults);
const markdownFiles = generateMarkdown(query, analysesResults, 'gist');
// Convert markdownFiles to the appropriate format for uploading to gist
const gistFiles = markdownFiles.reduce((acc, cur) => {
@@ -100,6 +103,17 @@ async function exportResultsToGist(
}
}
/**
* Builds Gist description
* Ex: Empty Block (Go) x results (y repositories)
*/
const buildGistDescription = (query: RemoteQuery, analysesResults: AnalysisResults[]) => {
const resultCount = sumAnalysesResults(analysesResults);
const resultLabel = pluralize(resultCount, 'result', 'results');
const repositoryLabel = query.repositoryCount ? `(${pluralize(query.repositoryCount, 'repository', 'repositories')})` : '';
return `${query.queryName} (${query.language}) ${resultLabel} ${repositoryLabel}`;
};
/**
* Converts the results of a remote query to markdown and saves the files locally
* in the query directory (where query results and metadata are also saved).

View File

@@ -1,11 +1,10 @@
import {
WebviewPanel,
ExtensionContext,
window as Window,
ViewColumn,
Uri,
workspace,
commands
commands,
} from 'vscode';
import * as path from 'path';
@@ -16,30 +15,34 @@ import {
RemoteQueryDownloadAllAnalysesResultsMessage
} from '../pure/interface-types';
import { Logger } from '../logging';
import { getHtmlForWebview } from '../interface-utils';
import { assertNever } from '../pure/helpers-pure';
import { AnalysisSummary, RemoteQueryResult } from './remote-query-result';
import {
AnalysisSummary,
RemoteQueryResult,
sumAnalysisSummariesResults
} from './remote-query-result';
import { RemoteQuery } from './remote-query';
import { RemoteQueryResult as RemoteQueryResultViewModel } from './shared/remote-query-result';
import { AnalysisSummary as AnalysisResultViewModel } from './shared/remote-query-result';
import {
AnalysisSummary as AnalysisResultViewModel,
RemoteQueryResult as RemoteQueryResultViewModel
} from './shared/remote-query-result';
import { showAndLogWarningMessage } from '../helpers';
import { URLSearchParams } from 'url';
import { SHOW_QUERY_TEXT_MSG } from '../query-history';
import { AnalysesResultsManager } from './analyses-results-manager';
import { AnalysisResults } from './shared/analysis-result';
import { humanizeUnit } from '../pure/time';
import { AbstractInterfaceManager, InterfacePanelConfig } from '../abstract-interface-manager';
export class RemoteQueriesInterfaceManager {
private panel: WebviewPanel | undefined;
private panelLoaded = false;
export class RemoteQueriesInterfaceManager extends AbstractInterfaceManager<ToRemoteQueriesMessage, FromRemoteQueriesMessage> {
private currentQueryId: string | undefined;
private panelLoadedCallBacks: (() => void)[] = [];
constructor(
private readonly ctx: ExtensionContext,
ctx: ExtensionContext,
private readonly logger: Logger,
private readonly analysesResultsManager: AnalysesResultsManager
) {
super(ctx);
this.panelLoadedCallBacks.push(() => {
void logger.log('Variant analysis results view loaded');
});
@@ -73,7 +76,7 @@ export class RemoteQueriesInterfaceManager {
*/
private buildViewModel(query: RemoteQuery, queryResult: RemoteQueryResult): RemoteQueryResultViewModel {
const queryFileName = path.basename(query.queryFilePath);
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
const totalResultCount = sumAnalysisSummariesResults(queryResult.analysisSummaries);
const executionDuration = this.getDuration(queryResult.executionEndTime, query.executionStartTime);
const analysisSummaries = this.buildAnalysisSummaries(queryResult.analysisSummaries);
const totalRepositoryCount = queryResult.analysisSummaries.length;
@@ -97,104 +100,29 @@ export class RemoteQueriesInterfaceManager {
};
}
getPanel(): WebviewPanel {
if (this.panel == undefined) {
const { ctx } = this;
const panel = (this.panel = Window.createWebviewPanel(
'remoteQueriesView',
'CodeQL Query Results',
{ viewColumn: ViewColumn.Active, preserveFocus: true },
{
enableScripts: true,
enableFindWidget: true,
retainContextWhenHidden: true,
localResourceRoots: [
Uri.file(this.analysesResultsManager.storagePath),
Uri.file(path.join(this.ctx.extensionPath, 'out')),
],
}
));
this.panel.onDidDispose(
() => {
this.panel = undefined;
this.currentQueryId = undefined;
},
null,
ctx.subscriptions
);
const scriptPathOnDisk = Uri.file(
ctx.asAbsolutePath('out/remoteQueriesView.js')
);
const baseStylesheetUriOnDisk = Uri.file(
ctx.asAbsolutePath('out/remote-queries/view/baseStyles.css')
);
const stylesheetPathOnDisk = Uri.file(
ctx.asAbsolutePath('out/remote-queries/view/remoteQueries.css')
);
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
[baseStylesheetUriOnDisk, stylesheetPathOnDisk],
true
);
ctx.subscriptions.push(
panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
)
);
}
return this.panel;
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve) => {
if (this.panelLoaded) {
resolve();
} else {
this.panelLoadedCallBacks.push(resolve);
protected getPanelConfig(): InterfacePanelConfig {
return {
viewId: 'remoteQueriesView',
title: 'CodeQL Query Results',
viewColumn: ViewColumn.Active,
preserveFocus: true,
view: 'remote-queries',
additionalOptions: {
localResourceRoots: [
Uri.file(this.analysesResultsManager.storagePath)
]
}
});
};
}
private async openFile(filePath: string) {
try {
const textDocument = await workspace.openTextDocument(filePath);
await Window.showTextDocument(textDocument, ViewColumn.One);
} catch (error) {
void showAndLogWarningMessage(`Could not open file: ${filePath}`);
}
protected onPanelDispose(): void {
this.currentQueryId = undefined;
}
private async openVirtualFile(text: string) {
try {
const params = new URLSearchParams({
queryText: encodeURIComponent(SHOW_QUERY_TEXT_MSG + text)
});
const uri = Uri.parse(
`remote-query:query-text.ql?${params.toString()}`,
true
);
const doc = await workspace.openTextDocument(uri);
await Window.showTextDocument(doc, { preview: false });
} catch (error) {
void showAndLogWarningMessage('Could not open query text');
}
}
private async handleMsgFromView(
msg: FromRemoteQueriesMessage
): Promise<void> {
protected async onMessage(msg: FromRemoteQueriesMessage): Promise<void> {
switch (msg.t) {
case 'remoteQueryLoaded':
this.panelLoaded = true;
this.panelLoadedCallBacks.forEach((cb) => cb());
this.panelLoadedCallBacks = [];
this.onWebViewLoaded();
break;
case 'remoteQueryError':
void this.logger.log(
@@ -224,6 +152,31 @@ export class RemoteQueriesInterfaceManager {
}
}
private async openFile(filePath: string) {
try {
const textDocument = await workspace.openTextDocument(filePath);
await Window.showTextDocument(textDocument, ViewColumn.One);
} catch (error) {
void showAndLogWarningMessage(`Could not open file: ${filePath}`);
}
}
private async openVirtualFile(text: string) {
try {
const params = new URLSearchParams({
queryText: encodeURIComponent(SHOW_QUERY_TEXT_MSG + text)
});
const uri = Uri.parse(
`remote-query:query-text.ql?${params.toString()}`,
true
);
const doc = await workspace.openTextDocument(uri);
await Window.showTextDocument(doc, { preview: false });
} catch (error) {
void showAndLogWarningMessage('Could not open query text');
}
}
private async downloadAnalysisResults(msg: RemoteQueryDownloadAnalysisResultsMessage): Promise<void> {
const queryId = this.currentQueryId;
await this.analysesResultsManager.downloadAnalysisResults(
@@ -248,10 +201,6 @@ export class RemoteQueriesInterfaceManager {
}
}
private postMessage(msg: ToRemoteQueriesMessage): Thenable<boolean> {
return this.getPanel().webview.postMessage(msg);
}
private getDuration(startTime: number, endTime: number): string {
const diffInMs = startTime - endTime;
return humanizeUnit(diffInMs);

View File

@@ -15,7 +15,7 @@ import { RemoteQuery } from './remote-query';
import { RemoteQueriesMonitor } from './remote-queries-monitor';
import { getRemoteQueryIndex, getRepositoriesMetadata, RepositoriesMetadata } from './gh-actions-api-client';
import { RemoteQueryResultIndex } from './remote-query-result-index';
import { RemoteQueryResult } from './remote-query-result';
import { RemoteQueryResult, sumAnalysisSummariesResults } from './remote-query-result';
import { DownloadLink } from './download-link';
import { AnalysesResultsManager } from './analyses-results-manager';
import { assertNever } from '../pure/helpers-pure';
@@ -41,6 +41,8 @@ export interface UpdatedQueryStatusEvent {
queryId: string;
status: QueryStatus;
failureReason?: string;
repositoryCount?: number;
resultCount?: number;
}
export class RemoteQueriesManager extends DisposableObject {
@@ -73,6 +75,8 @@ export class RemoteQueriesManager extends DisposableObject {
this.onRemoteQueryAdded = this.remoteQueryAddedEventEmitter.event;
this.onRemoteQueryRemoved = this.remoteQueryRemovedEventEmitter.event;
this.onRemoteQueryStatusUpdate = this.remoteQueryStatusUpdateEventEmitter.event;
this.push(this.interfaceManager);
}
public async rehydrateRemoteQuery(queryId: string, query: RemoteQuery, status: QueryStatus) {
@@ -248,7 +252,7 @@ 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 totalResultCount = sumAnalysisSummariesResults(queryResult.analysisSummaries);
const totalRepoCount = queryResult.analysisSummaries.length;
const message = `Query "${query.queryName}" run on ${totalRepoCount} repositories and returned ${totalResultCount} results`;
@@ -314,9 +318,15 @@ export class RemoteQueriesManager extends DisposableObject {
): Promise<void> {
const resultIndex = await getRemoteQueryIndex(credentials, remoteQuery);
if (resultIndex) {
this.remoteQueryStatusUpdateEventEmitter.fire({ queryId, status: QueryStatus.Completed });
const metadata = await this.getRepositoriesMetadata(resultIndex, credentials);
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryId, metadata);
const resultCount = sumAnalysisSummariesResults(queryResult.analysisSummaries);
this.remoteQueryStatusUpdateEventEmitter.fire({
queryId,
status: QueryStatus.Completed,
repositoryCount: queryResult.analysisSummaries.length,
resultCount
});
await this.storeJsonFile(queryId, 'query-result.json', queryResult);

View File

@@ -138,7 +138,7 @@ function generateMarkdownForCodeSnippet(
const codeLines = codeSnippet.text
.split('\n')
.map((line, index) =>
highlightCodeLines(line, index + snippetStartLine, highlightedRegion)
highlightAndEscapeCodeLines(line, index + snippetStartLine, highlightedRegion)
);
// Make sure there are no extra newlines before or after the <code> block:
@@ -153,20 +153,25 @@ function generateMarkdownForCodeSnippet(
return lines;
}
function highlightCodeLines(
function highlightAndEscapeCodeLines(
line: string,
lineNumber: number,
highlightedRegion?: HighlightedRegion
): string {
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
return line;
return escapeHtmlCharacters(line);
}
const partiallyHighlightedLine = parseHighlightedLine(
line,
lineNumber,
highlightedRegion
);
return `${partiallyHighlightedLine.plainSection1}<strong>${partiallyHighlightedLine.highlightedSection}</strong>${partiallyHighlightedLine.plainSection2}`;
const plainSection1 = escapeHtmlCharacters(partiallyHighlightedLine.plainSection1);
const highlightedSection = escapeHtmlCharacters(partiallyHighlightedLine.highlightedSection);
const plainSection2 = escapeHtmlCharacters(partiallyHighlightedLine.plainSection2);
return `${plainSection1}<strong>${highlightedSection}</strong>${plainSection2}`;
}
function generateMarkdownForAlertMessage(
@@ -330,3 +335,10 @@ function createFileName(nwo: string) {
const [owner, repo] = nwo.split('/');
return `${owner}-${repo}`;
}
/**
* Escape characters that could be interpreted as HTML instead of raw code.
*/
function escapeHtmlCharacters(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

View File

@@ -7,6 +7,7 @@ import { RemoteQuery } from './remote-query';
export interface RemoteQueryHistoryItem {
readonly t: 'remote';
failureReason?: string;
resultCount?: number;
status: QueryStatus;
completed: boolean;
readonly queryId: string,

View File

@@ -18,3 +18,10 @@ export interface AnalysisSummary {
starCount?: number,
lastUpdated?: number,
}
/**
* Sums up the number of results for all repos queried via a remote query.
*/
export const sumAnalysisSummariesResults = (analysisSummaries: AnalysisSummary[]): number => {
return analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
};

View File

@@ -8,4 +8,5 @@ export interface RemoteQuery {
controllerRepository: Repository;
executionStartTime: number; // Use number here since it needs to be serialized and desserialized.
actionsWorkflowRunId: number;
repositoryCount: number;
}

View File

@@ -52,6 +52,10 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
return { repositoryLists: [quickpick.repositoryList] };
} else if (quickpick?.useCustomRepo) {
const customRepo = await getCustomRepo();
if (customRepo === undefined) {
// The user cancelled, do nothing.
throw new UserCancellationException('No repositories selected', true);
}
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)');
}
@@ -59,6 +63,10 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
return { repositories: [customRepo] };
} else if (quickpick?.useAllReposOfOwner) {
const owner = await getOwner();
if (owner === undefined) {
// The user cancelled, do nothing.
throw new UserCancellationException('No repositories selected', true);
}
if (!owner || !OWNER_REGEX.test(owner)) {
throw new Error(`Invalid user or organization: ${owner}`);
}
@@ -197,6 +205,6 @@ async function getCustomRepo(): Promise<string | undefined> {
async function getOwner(): Promise<string | undefined> {
return await window.showInputBox({
title: 'Enter a GitHub user or organization',
ignoreFocusOut: true,
ignoreFocusOut: true
});
}

View File

@@ -11,6 +11,7 @@ import {
showAndLogErrorMessage,
showAndLogInformationMessage,
tryGetQueryMetadata,
pluralize,
tmpDir
} from '../helpers';
import { Credentials } from '../authentication';
@@ -142,7 +143,7 @@ async function findPackRoot(queryFile: string): Promise<string> {
while (!(await fs.pathExists(path.join(dir, 'qlpack.yml')))) {
dir = path.dirname(dir);
if (isFileSystemRoot(dir)) {
// there is no qlpack.yml in this direcory or any parent directory.
// there is no qlpack.yml in this directory or any parent directory.
// just use the query file's directory as the pack root.
return path.dirname(queryFile);
}
@@ -216,7 +217,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 variant analysis',
title: 'Controller repository in which to run the GitHub Actions workflow for this variant analysis',
placeHolder: '<owner>/<repo>',
prompt: 'Enter the name of a GitHub repository in the format <owner>/<repo>',
ignoreFocusOut: true,
@@ -258,17 +259,19 @@ export async function runRemoteQuery(
});
const actionBranch = getActionBranch();
const workflowRunId = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, owner, repo, base64Pack, dryRun);
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, owner, repo, base64Pack, dryRun);
const queryStartTime = Date.now();
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
if (dryRun) {
return { queryDirPath: remoteQueryDir.path };
} else {
if (!workflowRunId) {
if (!apiResponse) {
return;
}
const workflowRunId = apiResponse.workflow_run_id;
const repositoryCount = apiResponse.repositories_queried.length;
const remoteQuery = await buildRemoteQueryEntity(
queryFile,
queryMetadata,
@@ -276,7 +279,8 @@ export async function runRemoteQuery(
repo,
queryStartTime,
workflowRunId,
language);
language,
repositoryCount);
// don't return the path because it has been deleted
return { query: remoteQuery };
@@ -301,7 +305,7 @@ async function runRemoteQueriesApiRequest(
repo: string,
queryPackBase64: string,
dryRun = false
): Promise<void | number> {
): Promise<void | QueriesResponse> {
const data = {
ref,
language,
@@ -336,7 +340,7 @@ async function runRemoteQueriesApiRequest(
);
const { popupMessage, logMessage } = parseResponse(owner, repo, response.data);
void showAndLogInformationMessage(popupMessage, { fullMessage: logMessage });
return response.data.workflow_run_id;
return response.data;
} catch (error: any) {
if (error.status === 404) {
void showAndLogErrorMessage(`Controller repository was not found. Please make sure it's a valid repo name.${eol}`);
@@ -352,33 +356,34 @@ const eol2 = os.EOL + os.EOL;
// exported for testing only
export function parseResponse(owner: string, repo: string, response: QueriesResponse) {
const repositoriesQueried = response.repositories_queried;
const numRepositoriesQueried = repositoriesQueried.length;
const repositoryCount = repositoriesQueried.length;
const popupMessage = `Successfully scheduled runs on ${numRepositoriesQueried} repositories. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}).`
const popupMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}).`
+ (response.errors ? `${eol2}Some repositories could not be scheduled. See extension log for details.` : '');
let logMessage = `Successfully scheduled runs on ${numRepositoriesQueried} repositories. See https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}.`;
let logMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. See https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}.`;
logMessage += `${eol2}Repositories queried:${eol}${repositoriesQueried.join(', ')}`;
if (response.errors) {
const { invalid_repositories, repositories_without_database, private_repositories, cutoff_repositories, cutoff_repositories_count } = response.errors;
logMessage += `${eol2}Some repositories could not be scheduled.`;
if (invalid_repositories?.length) {
logMessage += `${eol2}${invalid_repositories.length} repositories were invalid and could not be found:${eol}${invalid_repositories.join(', ')}`;
logMessage += `${eol2}${pluralize(invalid_repositories.length, 'repository', 'repositories')} invalid and could not be found:${eol}${invalid_repositories.join(', ')}`;
}
if (repositories_without_database?.length) {
logMessage += `${eol2}${repositories_without_database.length} repositories did not have a CodeQL database available:${eol}${repositories_without_database.join(', ')}`;
logMessage += `${eol2}${pluralize(repositories_without_database.length, 'repository', 'repositories')} did not have a CodeQL database available:${eol}${repositories_without_database.join(', ')}`;
logMessage += `${eol}For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.`;
}
if (private_repositories?.length) {
logMessage += `${eol2}${private_repositories.length} repositories are not public:${eol}${private_repositories.join(', ')}`;
logMessage += `${eol2}${pluralize(private_repositories.length, 'repository', 'repositories')} not public:${eol}${private_repositories.join(', ')}`;
logMessage += `${eol}When using a public controller repository, only public repositories can be queried.`;
}
if (cutoff_repositories_count) {
logMessage += `${eol2}${cutoff_repositories_count} repositories over the limit for a single request`;
logMessage += `${eol2}${pluralize(cutoff_repositories_count, 'repository', 'repositories')} over the limit for a single request`;
if (cutoff_repositories) {
logMessage += `:${eol}${cutoff_repositories.join(', ')}`;
if (cutoff_repositories_count !== cutoff_repositories.length) {
logMessage += `${eol}...${eol}And ${cutoff_repositories_count - cutoff_repositories.length} more repositrories.`;
const moreRepositories = cutoff_repositories_count - cutoff_repositories.length;
logMessage += `${eol}...${eol}And another ${pluralize(moreRepositories, 'repository', 'repositories')}.`;
}
} else {
logMessage += '.';
@@ -424,7 +429,8 @@ async function buildRemoteQueryEntity(
controllerRepoName: string,
queryStartTime: number,
workflowRunId: number,
language: string
language: string,
repositoryCount: number
): Promise<RemoteQuery> {
// The query name is either the name as specified in the query metadata, or the file name.
const queryName = queryMetadata?.name ?? path.basename(queryFilePath);
@@ -441,6 +447,7 @@ async function buildRemoteQueryEntity(
name: controllerRepoName,
},
executionStartTime: queryStartTime,
actionsWorkflowRunId: workflowRunId
actionsWorkflowRunId: workflowRunId,
repositoryCount,
};
}

View File

@@ -90,3 +90,9 @@ export const getAnalysisResultCount = (analysisResults: AnalysisResults): number
const rawResultCount = analysisResults.rawResults?.resultSet.rows.length || 0;
return analysisResults.interpretedResults.length + rawResultCount;
};
/**
* Returns the total number of results for an analysis by adding all individual repo results.
*/
export const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0);

View File

@@ -1,13 +0,0 @@
module.exports = {
env: {
browser: true
},
extends: [
"plugin:react/recommended"
],
settings: {
react: {
version: 'detect'
}
}
}

View File

@@ -1,30 +0,0 @@
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

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"target": "es6",
"outDir": "out",
"lib": ["ES2021", "dom"],
"jsx": "react",
"sourceMap": true,
"rootDir": "..",
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true
},
"exclude": ["node_modules"]
}

View File

@@ -36,7 +36,8 @@ import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, u
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';
import { asError, getErrorMessage } from './pure/helpers-pure';
import { generateSummarySymbolsFile } from './log-insights/summary-parser';
/**
* run-queries.ts
@@ -103,6 +104,14 @@ export class QueryEvaluationInfo {
return qsClient.findQueryEvalLogSummaryFile(this.querySaveDir);
}
get jsonEvalLogSummaryPath() {
return qsClient.findJsonQueryEvalLogSummaryFile(this.querySaveDir);
}
get evalLogSummarySymbolsPath() {
return qsClient.findQueryEvalLogSummarySymbolsFile(this.querySaveDir);
}
get evalLogEndSummaryPath() {
return qsClient.findQueryEvalLogEndSummaryFile(this.querySaveDir);
}
@@ -199,21 +208,15 @@ export class QueryEvaluationInfo {
});
if (await this.hasEvalLog()) {
queryInfo.evalLogLocation = this.evalLogPath;
void qs.cliServer.generateLogSummary(this.evalLogPath, this.evalLogSummaryPath, this.evalLogEndSummaryPath)
.then(() => {
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 --- ', { additionalLogLocation: this.logPath });
void qs.logger.log(buffer.toString(), { additionalLogLocation: this.logPath });
});
})
queryInfo.evalLogSummaryLocation = await this.generateHumanReadableLogSummary(qs);
void this.logEndSummary(queryInfo.evalLogSummaryLocation, qs); // Logged asynchrnously
.catch(err => {
void showAndLogWarningMessage(`Failed to generate structured evaluator log summary. Reason: ${err.message}`);
});
if (config.isCanary()) { // Generate JSON summary for viewer.
await qs.cliServer.generateJsonLogSummary(this.evalLogPath, this.jsonEvalLogSummaryPath);
queryInfo.jsonEvalLogSummaryLocation = this.jsonEvalLogSummaryPath;
await generateSummarySymbolsFile(this.evalLogSummaryPath, this.evalLogSummarySymbolsPath);
queryInfo.evalLogSummarySymbolsLocation = this.evalLogSummarySymbolsPath;
}
} else {
void showAndLogWarningMessage(`Failed to write structured evaluator log to ${this.evalLogPath}.`);
}
@@ -248,7 +251,8 @@ export class QueryEvaluationInfo {
localChecking: false,
noComputeGetUrl: false,
noComputeToString: false,
computeDefaultStrings: true
computeDefaultStrings: true,
emitDebugInfo: true
},
extraOptions: {
timeoutSecs: qs.config.timeoutSecs
@@ -338,6 +342,43 @@ export class QueryEvaluationInfo {
return fs.pathExists(this.evalLogPath);
}
/**
* Calls the appropriate CLI command to generate a human-readable log summary.
* @param qs The query server client.
* @returns The path to the log summary, or `undefined` if the summary could not be generated.
*/
private async generateHumanReadableLogSummary(qs: qsClient.QueryServerClient): Promise<string | undefined> {
try {
await qs.cliServer.generateLogSummary(this.evalLogPath, this.evalLogSummaryPath, this.evalLogEndSummaryPath);
return this.evalLogSummaryPath;
} catch (e) {
const err = asError(e);
void showAndLogWarningMessage(`Failed to generate human-readable structured evaluator log summary. Reason: ${err.message}`);
return undefined;
}
}
/**
* Logs the end summary to the Output window and log file.
* @param logSummaryPath Path to the human-readable log summary
* @param qs The query server client.
*/
private async logEndSummary(logSummaryPath: string | undefined, qs: qsClient.QueryServerClient): Promise<void> {
if (logSummaryPath === undefined) {
// Failed to generate the log, so we don't expect an end summary either.
return;
}
try {
const endSummaryContent = await fs.readFile(this.evalLogEndSummaryPath, 'utf-8');
void qs.logger.log(' --- Evaluator Log Summary --- ', { additionalLogLocation: this.logPath });
void qs.logger.log(endSummaryContent, { additionalLogLocation: this.logPath });
} catch (e) {
void showAndLogWarningMessage(`Could not read structured evaluator log end of summary file at ${this.evalLogEndSummaryPath}.`);
}
}
/**
* 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.
@@ -789,9 +830,7 @@ export async function compileAndRunQueryAgainstDatabase(
const metadata = await tryGetQueryMetadata(cliServer, qlProgram.queryPath);
let availableMlModels: cli.MlModelInfo[] = [];
if (!initialInfo.queryPath.endsWith('.ql')) {
void logger.log('Quick evaluation within a query library does not currently support using ML models. Continuing without any ML models.');
} else if (!await cliServer.cliConstraints.supportsResolveMlModels()) {
if (!await cliServer.cliConstraints.supportsResolveMlModels()) {
void logger.log('Resolving ML models is unsupported by this version of the CLI. Running the query without any ML models.');
} else {
try {

View File

@@ -1,15 +1,16 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import * as Rdom from 'react-dom';
import {
ToCompareViewMessage,
SetComparisonsMessage,
} from '../../pure/interface-types';
import CompareSelector from './CompareSelector';
import { vscode } from '../../view/vscode-api';
import { vscode } from '../vscode-api';
import CompareTable from './CompareTable';
import '../results/resultsView.css';
const emptyComparison: SetComparisonsMessage = {
t: 'setComparisons',
stats: {},
@@ -75,10 +76,3 @@ export function Compare(_: Record<string, never>): JSX.Element {
return <div>Error!</div>;
}
}
Rdom.render(
<Compare />,
document.getElementById('root'),
// Post a message to the extension when fully loaded.
() => vscode.postMessage({ t: 'compareViewLoaded' })
);

View File

@@ -1,11 +1,11 @@
import * as React from 'react';
import { SetComparisonsMessage } from '../../pure/interface-types';
import RawTableHeader from '../../view/RawTableHeader';
import { className } from '../../view/result-table-utils';
import RawTableHeader from '../results/RawTableHeader';
import { className } from '../results/result-table-utils';
import { ResultRow } from '../../pure/bqrs-cli-types';
import RawTableRow from '../../view/RawTableRow';
import { vscode } from '../../view/vscode-api';
import RawTableRow from '../results/RawTableRow';
import { vscode } from '../vscode-api';
interface Props {
comparison: SetComparisonsMessage;

View File

@@ -0,0 +1,10 @@
import * as React from 'react';
import { WebviewDefinition } from '../webview-interface';
import { Compare } from './Compare';
const definition: WebviewDefinition = {
component: <Compare />,
loadedMessage: 'compareViewLoaded'
};
export default definition;

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { AnalysisAlert } from '../shared/analysis-result';
import { AnalysisAlert } from '../../remote-queries/shared/analysis-result';
import CodePaths from './CodePaths';
import FileCodeSnippet from './FileCodeSnippet';

View File

@@ -1,10 +1,10 @@
import { TriangleDownIcon, XCircleIcon } from '@primer/octicons-react';
import { ActionList, ActionMenu, Button, Label, Overlay } from '@primer/react';
import { VSCodeLink } from '@vscode/webview-ui-toolkit/react';
import { XCircleIcon } from '@primer/octicons-react';
import { Overlay } from '@primer/react';
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTag } from '@vscode/webview-ui-toolkit/react';
import * as React from 'react';
import { useRef, useState } from 'react';
import { ChangeEvent, useRef, useState } from 'react';
import styled from 'styled-components';
import { CodeFlow, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
import { CodeFlow, AnalysisMessage, ResultSeverity } from '../../remote-queries/shared/analysis-result';
import FileCodeSnippet from './FileCodeSnippet';
import SectionTitle from './SectionTitle';
import VerticalSpace from './VerticalSpace';
@@ -60,12 +60,12 @@ const CodePath = ({
</div>
{index === 0 &&
<div style={{ padding: 0, border: 'none' }}>
<Label>Source</Label>
<VSCodeTag>Source</VSCodeTag>
</div>
}
{index === codeFlow.threadFlows.length - 1 &&
<div style={{ padding: 0, border: 'none' }}>
<Label>Sink</Label>
<VSCodeTag>Sink</VSCodeTag>
</div>
}
</div>
@@ -94,25 +94,22 @@ const Menu = ({
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>;
return <VSCodeDropdown
onChange={(event: ChangeEvent<HTMLSelectElement>) => {
const selectedOption = event.target;
const selectedIndex = selectedOption.value as unknown as number;
setSelectedCodeFlow(codeFlows[selectedIndex]);
}}
>
{codeFlows.map((codeFlow, index) =>
<VSCodeOption
key={`codeflow-${index}'`}
value={index}
>
{getCodeFlowName(codeFlow)}
</VSCodeOption>
)}
</VSCodeDropdown>;
};
const CodePaths = ({

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import styled from 'styled-components';
import { CodeSnippet, FileLink, HighlightedRegion, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
import { CodeSnippet, FileLink, HighlightedRegion, AnalysisMessage, ResultSeverity } from '../../remote-queries/shared/analysis-result';
import VerticalSpace from './VerticalSpace';
import { createRemoteFileRef } from '../../pure/location-link-utils';
import { parseHighlightedLine, shouldHighlightLine } from '../../pure/sarif-utils';

View File

@@ -1,18 +1,17 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
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 { AnalysisSummary, RemoteQueryResult } from '../../remote-queries/shared/remote-query-result';
import { MAX_RAW_RESULTS } from '../../remote-queries/shared/result-limits';
import { vscode } from '../vscode-api';
import { VSCodeBadge, VSCodeButton } from '@vscode/webview-ui-toolkit/react';
import SectionTitle from './SectionTitle';
import VerticalSpace from './VerticalSpace';
import HorizontalSpace from './HorizontalSpace';
import ViewTitle from './ViewTitle';
import DownloadButton from './DownloadButton';
import { AnalysisResults, getAnalysisResultCount } from '../shared/analysis-result';
import { AnalysisResults, getAnalysisResultCount } from '../../remote-queries/shared/analysis-result';
import DownloadSpinner from './DownloadSpinner';
import CollapsibleItem from './CollapsibleItem';
import { AlertIcon, CodeSquareIcon, FileCodeIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
@@ -24,6 +23,9 @@ import SortRepoFilter, { Sort, sorter } from './SortRepoFilter';
import LastUpdated from './LastUpdated';
import RepoListCopyButton from './RepoListCopyButton';
import './baseStyles.css';
import './remoteQueries.css';
const numOfReposInContractedMode = 10;
const emptyQueryResult: RemoteQueryResult = {
@@ -440,10 +442,3 @@ export function RemoteQueries(): JSX.Element {
return <div>There was an error displaying the view.</div>;
}
}
Rdom.render(
<RemoteQueries />,
document.getElementById('root'),
// Post a message to the extension when fully loaded.
() => vscode.postMessage({ t: 'remoteQueryLoaded' })
);

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { vscode } from '../../view/vscode-api';
import { RemoteQueryResult } from '../shared/remote-query-result';
import { vscode } from '../vscode-api';
import { RemoteQueryResult } from '../../remote-queries/shared/remote-query-result';
import { CopyIcon } from '@primer/octicons-react';
import { IconButton } from '@primer/react';

View File

@@ -0,0 +1,24 @@
import * as React from 'react';
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react';
interface RepositoriesSearchProps {
filterValue: string;
setFilterValue: (value: string) => void;
}
const RepositoriesSearch = ({ filterValue, setFilterValue }: RepositoriesSearchProps) => {
return <>
<VSCodeTextField
style={{ width: '100%' }}
placeholder='Filter by repository owner/name'
ariaLabel="Repository search"
name="repository-search"
value={filterValue}
onInput={(e: InputEvent) => setFilterValue((e.target as HTMLInputElement).value)}
>
<span slot="start" className="codicon codicon-search"></span>
</VSCodeTextField>
</>;
};
export default RepositoriesSearch;

View File

@@ -0,0 +1,10 @@
import * as React from 'react';
import { WebviewDefinition } from '../webview-interface';
import { RemoteQueries } from './RemoteQueries';
const definition: WebviewDefinition = {
component: <RemoteQueries />,
loadedMessage: 'remoteQueryLoaded'
};
export default definition;

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import { vscode } from './vscode-api';
import { RawResultsSortState, SortDirection } from '../pure/interface-types';
import { vscode } from '../vscode-api';
import { RawResultsSortState, SortDirection } from '../../pure/interface-types';
import { nextSortDirection } from './result-table-utils';
import { Column } from '../pure/bqrs-cli-types';
import { Column } from '../../pure/bqrs-cli-types';
interface Props {
readonly columns: readonly Column[];

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { ResultRow } from '../pure/bqrs-cli-types';
import { ResultRow } from '../../pure/bqrs-cli-types';
import { zebraStripe } from './result-table-utils';
import RawTableValue from './RawTableValue';

View File

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

View File

@@ -1,19 +1,19 @@
import * as path from 'path';
import * as React from 'react';
import * as Sarif from 'sarif';
import * as Keys from '../pure/result-keys';
import * as Keys from '../../pure/result-keys';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection, emptyQueryResultsMessage } from './result-table-utils';
import { onNavigation, NavigationEvent } from './results';
import { InterpretedResultSet, SarifInterpretationData } from '../pure/interface-types';
import { InterpretedResultSet, SarifInterpretationData } from '../../pure/interface-types';
import {
parseSarifPlainTextMessage,
parseSarifLocation,
isNoLocation
} from '../pure/sarif-utils';
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../pure/interface-types';
import { vscode } from './vscode-api';
import { isWholeFileLoc, isLineColumnLoc } from '../pure/bqrs-utils';
} from '../../pure/sarif-utils';
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../../pure/interface-types';
import { vscode } from '../vscode-api';
import { isWholeFileLoc, isLineColumnLoc } from '../../pure/bqrs-utils';
export type PathTableProps = ResultTableProps & { resultSet: InterpretedResultSet<SarifInterpretationData> };
export interface PathTableState {

View File

@@ -1,10 +1,10 @@
import * as React from 'react';
import * as d3 from 'd3';
import { ResultTableProps } from './result-table-utils';
import { InterpretedResultSet, GraphInterpretationData } from '../pure/interface-types';
import { InterpretedResultSet, GraphInterpretationData } from '../../pure/interface-types';
import { graphviz } from 'd3-graphviz';
import { jumpToLocation } from './result-table-utils';
import { tryGetLocationFromString } from '../pure/bqrs-utils';
import { tryGetLocationFromString } from '../../pure/bqrs-utils';
export type GraphProps = ResultTableProps & { resultSet: InterpretedResultSet<GraphInterpretationData> };
const graphClassName = 'vscode-codeql__result-tables-graph';

View File

@@ -0,0 +1,10 @@
import * as React from 'react';
import { WebviewDefinition } from '../webview-interface';
import { ResultsApp } from './results';
const definition: WebviewDefinition = {
component: <ResultsApp />,
loadedMessage: 'resultViewLoaded'
};
export default definition;

View File

@@ -1,10 +1,10 @@
import * as React from 'react';
import { ResultTableProps, className, emptyQueryResultsMessage } from './result-table-utils';
import { RAW_RESULTS_LIMIT, RawResultsSortState } from '../pure/interface-types';
import { RawTableResultSet } from '../pure/interface-types';
import { RAW_RESULTS_LIMIT, RawResultsSortState } from '../../pure/interface-types';
import { RawTableResultSet } from '../../pure/interface-types';
import RawTableHeader from './RawTableHeader';
import RawTableRow from './RawTableRow';
import { ResultRow } from '../pure/bqrs-cli-types';
import { ResultRow } from '../../pure/bqrs-cli-types';
export type RawTableProps = ResultTableProps & {
resultSet: RawTableResultSet;

View File

@@ -1,11 +1,11 @@
import * as React from 'react';
import { UrlValue, ResolvableLocationValue } from '../pure/bqrs-cli-types';
import { isStringLoc, tryGetResolvableLocation } from '../pure/bqrs-utils';
import { RawResultsSortState, QueryMetadata, SortDirection } from '../pure/interface-types';
import { assertNever } from '../pure/helpers-pure';
import { ResultSet } from '../pure/interface-types';
import { vscode } from './vscode-api';
import { convertNonPrintableChars } from '../text-utils';
import { UrlValue, ResolvableLocationValue } from '../../pure/bqrs-cli-types';
import { isStringLoc, tryGetResolvableLocation } from '../../pure/bqrs-utils';
import { RawResultsSortState, QueryMetadata, SortDirection } from '../../pure/interface-types';
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;

View File

@@ -13,7 +13,7 @@ import {
getDefaultResultSetName,
ParsedResultSets,
IntoResultsViewMsg,
} from '../pure/interface-types';
} from '../../pure/interface-types';
import { PathTable } from './alert-table';
import { Graph } from './graph';
import { RawTable } from './raw-results-table';
@@ -25,7 +25,7 @@ import {
alertExtrasClassName,
openFile
} from './result-table-utils';
import { vscode } from './vscode-api';
import { vscode } from '../vscode-api';
const FILE_PATH_REGEX = /^(?:.+[\\/])*(.+)$/;

View File

@@ -1,6 +1,5 @@
import * as React from 'react';
import * as Rdom from 'react-dom';
import { assertNever } from '../pure/helpers-pure';
import { assertNever } from '../../pure/helpers-pure';
import {
DatabaseInfo,
Interpretation,
@@ -13,11 +12,12 @@ import {
ALERTS_TABLE_NAME,
GRAPH_TABLE_NAME,
ParsedResultSets,
} from '../pure/interface-types';
} from '../../pure/interface-types';
import { EventHandlers as EventHandlerList } from './event-handler-list';
import { ResultTables } from './result-tables';
import { ResultSet } from '../pure/interface-types';
import { vscode } from './vscode-api';
import { ResultSet } from '../../pure/interface-types';
import './resultsView.css';
/**
* results.tsx
@@ -72,7 +72,7 @@ export const onNavigation = new EventHandlerList<NavigationEvent>();
/**
* A minimal state container for displaying results.
*/
class App extends React.Component<Record<string, never>, ResultsViewState> {
export class ResultsApp extends React.Component<Record<string, never>, ResultsViewState> {
constructor(props: any) {
super(props);
this.state = {
@@ -302,7 +302,6 @@ class App extends React.Component<Record<string, never>, ResultsViewState> {
componentDidMount(): void {
this.vscodeMessageHandler = this.vscodeMessageHandler.bind(this);
window.addEventListener('message', this.vscodeMessageHandler);
vscode.postMessage({ t: 'resultViewLoaded' });
}
componentWillUnmount(): void {
@@ -319,5 +318,3 @@ class App extends React.Component<Record<string, never>, ResultsViewState> {
: console.error(`Invalid event origin ${origin}`);
}
}
Rdom.render(<App />, document.getElementById('root'));

View File

@@ -0,0 +1,4 @@
export type WebviewDefinition = {
component: JSX.Element,
loadedMessage: 'compareViewLoaded' | 'remoteQueryLoaded' | 'resultViewLoaded';
}

View File

@@ -0,0 +1,36 @@
import * as ReactDOM from 'react-dom';
import { vscode } from './vscode-api';
import { WebviewDefinition } from './webview-interface';
// Allow all views to use Codicons
import '@vscode/codicons/dist/codicon.css';
const render = () => {
const element = document.getElementById('root');
if (!element) {
console.error('Could not find element with id "root"');
return;
}
const viewName = element.dataset.view;
if (!viewName) {
console.error('Could not find view name in data-view attribute');
return;
}
// It's a lot harder to use dynamic imports since those don't import the CSS
// and require a less strict CSP policy
// eslint-disable-next-line @typescript-eslint/no-var-requires
const view: WebviewDefinition = require(`./${viewName}/index.tsx`).default;
ReactDOM.render(
view.component,
document.getElementById('root'),
// Post a message to the extension when fully loaded.
() => vscode.postMessage({ t: view.loadedMessage })
);
};
render();

View File

@@ -151,7 +151,8 @@ describe('using the query server', function() {
localChecking: false,
noComputeGetUrl: false,
noComputeToString: false,
computeDefaultStrings: true
computeDefaultStrings: true,
emitDebugInfo: true
},
queryToCheck: qlProgram,
resultPath: COMPILED_QUERY_PATH,

View File

@@ -0,0 +1,66 @@
import { fail } from 'assert';
import { commands, Selection, window, workspace } from 'vscode';
import * as path from 'path';
import * as assert from 'assert';
import { expect } from 'chai';
import { tmpDir } from '../../helpers';
import * as fs from 'fs-extra';
/**
* Integration tests for queries
*/
describe('SourceMap', function() {
this.timeout(20000);
it('should jump to QL code', async () => {
try {
const root = workspace.workspaceFolders![0].uri.fsPath;
const srcFiles = {
summary: path.join(root, 'log-summary', 'evaluator-log.summary'),
summaryMap: path.join(root, 'log-summary', 'evaluator-log.summary.map')
};
// We need to modify the source map so that its paths point to the actual location of the
// workspace root on this machine. We'll copy the summary and its source map to a temp
// directory, modify the source map their, and open that summary.
const tempFiles = await copyFilesToTempDirectory(srcFiles);
// The checked-in sourcemap has placeholders of the form `${root}`, which we need to replace
// with the actual root directory.
const mapText = await fs.readFile(tempFiles.summaryMap, 'utf-8');
// Always use forward slashes, since they work everywhere.
const slashRoot = root.replaceAll('\\', '/');
const newMapText = mapText.replaceAll('${root}', slashRoot);
await fs.writeFile(tempFiles.summaryMap, newMapText);
const summaryDocument = await workspace.openTextDocument(tempFiles.summary);
assert(summaryDocument.languageId === 'ql-summary');
const summaryEditor = await window.showTextDocument(summaryDocument);
summaryEditor.selection = new Selection(356, 10, 356, 10);
await commands.executeCommand('codeQL.gotoQL');
const newEditor = window.activeTextEditor;
expect(newEditor).to.be.not.undefined;
const newDocument = newEditor!.document;
expect(path.basename(newDocument.fileName)).to.equal('Namespace.qll');
const newSelection = newEditor!.selection;
expect(newSelection.start.line).to.equal(60);
expect(newSelection.start.character).to.equal(2);
} catch (e) {
console.error('Test Failed');
fail(e as Error);
}
});
async function copyFilesToTempDirectory<T extends Record<string, string>>(files: T): Promise<T> {
const tempDir = path.join(tmpDir.name, 'log-summary');
await fs.ensureDir(tempDir);
const result: Record<string, string> = {};
for (const [key, srcPath] of Object.entries(files)) {
const destPath = path.join(tempDir, path.basename(srcPath));
await fs.copy(srcPath, destPath);
result[key] = destPath;
}
return result as T;
}
});

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.10.0';
const CLI_VERSION = process.env.CLI_VERSION || 'v2.10.4';
process.env.CLI_VERSION = CLI_VERSION;
// Base dir where CLIs will be downloaded into

View File

@@ -0,0 +1,462 @@
[
{
"nwo": "github/codeql",
"status": "Completed",
"interpretedResults": [
{
"message": {
"tokens": [
{
"t": "text",
"text": "This shell command depends on an uncontrolled "
},
{
"t": "location",
"text": "absolute path",
"location": {
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 35,
"endLine": 4,
"endColumn": 44
}
}
},
{ "t": "text", "text": "." }
]
},
"shortDescription": "This shell command depends on an uncontrolled ,absolute path,.",
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"severity": "Warning",
"codeSnippet": {
"startLine": 3,
"endLine": 6,
"text": "function cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 5,
"startColumn": 15,
"endLine": 5,
"endColumn": 18
},
"codeFlows": [
{
"threadFlows": [
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 2,
"endLine": 6,
"text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 35,
"endLine": 4,
"endColumn": 44
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 2,
"endLine": 6,
"text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 25,
"endLine": 4,
"endColumn": 53
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 2,
"endLine": 6,
"text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 13,
"endLine": 4,
"endColumn": 53
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 2,
"endLine": 6,
"text": " path = require(\"path\");\nfunction cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 4,
"startColumn": 7,
"endLine": 4,
"endColumn": 53
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 3,
"endLine": 6,
"text": "function cleanupTemp() {\n let cmd = \"rm -rf \" + path.join(__dirname, \"temp\");\n cp.execSync(cmd); // BAD\n}\n"
},
"highlightedRegion": {
"startLine": 5,
"startColumn": 15,
"endLine": 5,
"endColumn": 18
}
}
]
}
]
},
{
"message": {
"tokens": [
{
"t": "text",
"text": "This shell command depends on an uncontrolled "
},
{
"t": "location",
"text": "absolute path",
"location": {
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 36,
"endLine": 6,
"endColumn": 45
}
}
},
{ "t": "text", "text": "." }
]
},
"shortDescription": "This shell command depends on an uncontrolled ,absolute path,.",
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"severity": "Warning",
"codeSnippet": {
"startLine": 4,
"endLine": 8,
"text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 14,
"endLine": 6,
"endColumn": 54
},
"codeFlows": [
{
"threadFlows": [
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 4,
"endLine": 8,
"text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 36,
"endLine": 6,
"endColumn": 45
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 4,
"endLine": 8,
"text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 26,
"endLine": 6,
"endColumn": 54
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b",
"filePath": "javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js"
},
"codeSnippet": {
"startLine": 4,
"endLine": 8,
"text": "(function() {\n\tcp.execFileSync('rm', ['-rf', path.join(__dirname, \"temp\")]); // GOOD\n\tcp.execSync('rm -rf ' + path.join(__dirname, \"temp\")); // BAD\n\n\texeca.shell('rm -rf ' + path.join(__dirname, \"temp\")); // NOT OK\n"
},
"highlightedRegion": {
"startLine": 6,
"startColumn": 14,
"endLine": 6,
"endColumn": 54
}
}
]
}
]
}
]
},
{
"nwo": "test/no-results",
"status": "Completed",
"interpretedResults": []
},
{
"nwo": "meteor/meteor",
"status": "Completed",
"interpretedResults": [
{
"message": {
"tokens": [
{
"t": "text",
"text": "This shell command depends on an uncontrolled "
},
{
"t": "location",
"text": "absolute path",
"location": {
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"highlightedRegion": {
"startLine": 39,
"startColumn": 20,
"endLine": 39,
"endColumn": 61
}
}
},
{ "t": "text", "text": "." }
]
},
"shortDescription": "This shell command depends on an uncontrolled ,absolute path,.",
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"severity": "Warning",
"codeSnippet": {
"startLine": 257,
"endLine": 261,
"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": 259,
"startColumn": 28,
"endLine": 259,
"endColumn": 62
},
"codeFlows": [
{
"threadFlows": [
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"codeSnippet": {
"startLine": 37,
"endLine": 41,
"text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n"
},
"highlightedRegion": {
"startLine": 39,
"startColumn": 20,
"endLine": 39,
"endColumn": 61
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"codeSnippet": {
"startLine": 37,
"endLine": 41,
"text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n"
},
"highlightedRegion": {
"startLine": 39,
"startColumn": 7,
"endLine": 39,
"endColumn": 61
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"codeSnippet": {
"startLine": 42,
"endLine": 46,
"text": " METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n"
},
"highlightedRegion": {
"startLine": 44,
"startColumn": 3,
"endLine": 44,
"endColumn": 13
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 10,
"endLine": 14,
"text": "const os = require('os');\nconst {\n meteorPath,\n release,\n startedPath,\n"
},
"highlightedRegion": {
"startLine": 12,
"startColumn": 3,
"endLine": 12,
"endColumn": 13
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 9,
"endLine": 25,
"text": "const tmp = require('tmp');\nconst os = require('os');\nconst {\n meteorPath,\n release,\n startedPath,\n extractPath,\n isWindows,\n rootPath,\n sudoUser,\n isSudo,\n isMac,\n METEOR_LATEST_VERSION,\n shouldSetupExecPath,\n} = require('./config.js');\nconst { uninstall } = require('./uninstall');\nconst {\n"
},
"highlightedRegion": {
"startLine": 11,
"startColumn": 7,
"endLine": 23,
"endColumn": 27
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 257,
"endLine": 261,
"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": 259,
"startColumn": 42,
"endLine": 259,
"endColumn": 52
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 257,
"endLine": 261,
"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": 259,
"startColumn": 28,
"endLine": 259,
"endColumn": 62
}
}
]
},
{
"threadFlows": [
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/config.js"
},
"codeSnippet": {
"startLine": 37,
"endLine": 41,
"text": "\nconst meteorLocalFolder = '.meteor';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n"
},
"highlightedRegion": {
"startLine": 39,
"startColumn": 20,
"endLine": 39,
"endColumn": 61
}
},
{
"fileLink": {
"fileLinkPrefix": "https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec",
"filePath": "npm-packages/meteor-installer/install.js"
},
"codeSnippet": {
"startLine": 257,
"endLine": 261,
"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": 259,
"startColumn": 28,
"endLine": 259,
"endColumn": 62
}
}
]
}
]
}
]
}
]

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