Compare commits

...

142 Commits

Author SHA1 Message Date
Andrew Eisenberg
005372abba v1.5.5
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
2021-09-08 15:49:18 -07:00
Andrew Eisenberg
3f22587a7c Update changelog 2021-09-08 17:02:49 -04:00
Andrew Eisenberg
b95533e8c0 Remove support for 2.2.6 CLI
This is old enough that we don't need to support it.
2021-09-08 17:02:49 -04:00
Andrew Eisenberg
210d8a3c64 Expand qlpack resolution integration test to all languages
Go is not yet supported since we do not include the go submodule in the
integration tests.
2021-09-08 17:02:49 -04:00
Andrew Eisenberg
c2d3829a72 Fix AST Viewer
The previous synthetic query suite was not finding the ast query because
the `qlpack` directive in a query suite only matches queries from the
default suite, which `printAST.ql` is not part of.

This changes to using `from` and `queries` directives.

Also, adds an integration test to ensure we find the queries using
different CLIs. However, this only tests using the latest `main` from
the codeql repository. I wonder if we should start testing using
different versions of the repo.
2021-09-08 17:02:49 -04:00
Robert
cd427ee119 fix strings again 2021-09-08 18:21:41 +01:00
Robert
ad4c30ecf8 Include clickable link to show logs in message 2021-09-08 18:21:41 +01:00
Robert
db7f5f5114 Add spaces to printed array 2021-09-08 18:21:41 +01:00
Robert
7c9fa03da8 update messages 2021-09-08 18:21:41 +01:00
Robert
615dd691bf offer option to rerun on subset of valid repositories 2021-09-08 18:21:41 +01:00
shati-patel
64ba2cabad Attempt to fix quick query test 2021-09-08 13:02:57 +01:00
shati-patel
a9dcb2d705 Attempt to fix qlpack test 2021-09-08 13:02:57 +01:00
Shati Patel
4c81cdec98 Update CLI version for integration tests 2021-09-08 13:02:57 +01:00
Harry Maclean
db529d5247 Update changelog 2021-09-07 15:58:33 +01:00
Harry Maclean
4f568ea331 Wait for document to be saved before running query
This prevents a race condition where the query runs before the editor has saved the file.
2021-09-07 15:58:33 +01:00
Shati Patel
6d41362251 Configure correct TypeScript version to use in VS Code 2021-09-07 09:38:16 +01:00
Andrew Eisenberg
7f65a54060 Fix dependabot warning 2021-09-03 09:53:59 +01:00
aeisenberg
0c6ca81437 Bump version to v1.5.5 2021-09-02 12:40:04 -07:00
Andrew Eisenberg
b2422216b5 Update changelog for v2.5.4 release
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
2021-09-02 11:51:35 -07:00
Andrew Eisenberg
71f374d797 Fix unit test and add new test
Test that old CLIs properly ignore the library packs.
2021-09-02 11:14:32 -07:00
Dave Bartolomeo
7e78a6bc5c Fix PR feedback 2021-09-02 11:14:32 -07:00
Dave Bartolomeo
a4532fdc61 Add changenote for AST viewer failure fix 2021-09-02 11:14:32 -07:00
Dave Bartolomeo
7c5135d7d0 Fix AST viewer for refactored language packs
Most of the languages have recently been refactored into separate library and query packs, with the contextual queries defined in the query pack. In the near future, these contextual queries will move to the library pack.

Current CLI releases throw an error in `codeql resolve queries` when the extension tries to search the library pack for contextual queries. This change makes two related fixes:

1. If the queries are not found in the library pack, it then scans the corresponding standard query pack as a fallback.
2. It detects the problematic combination of CLI and packs, and avoids scanning the library pack at all in those cases. If no queries are found in the problematic scenario, the error message instructs the user to upgrade to the latest CLI version, instead of claiming that the language simply doesn't support the contextual queries yet.

This change depends on CLI 2.6.1, which is being released soon, adding the `--allow-library-packs` option to `codeql resolve queries`. That PR is already open against the CLI.
2021-09-02 11:14:32 -07:00
shati-patel
cdd6738748 Try using a separate test query 2021-09-02 16:44:22 +01:00
shati-patel
6f16192865 Add test for resolveQueryByLanguage 2021-09-02 16:44:22 +01:00
shati-patel
8151739f87 Update syntax 2021-09-01 11:02:21 +01:00
shati-patel
72fc53ba9c Add "resolveLanguages" test 2021-09-01 11:02:21 +01:00
shati-patel
3e6ee01c4e Move findLanguage function into helpers.ts 2021-09-01 11:02:21 +01:00
Benjamin Muskalla
f6485dac95 Add changelog for sticky pagination controls 2021-08-26 08:14:25 -07:00
Benjamin Muskalla
48f15b5fc7 Stick result pagination to top 2021-08-26 08:14:25 -07:00
shati-patel
f856e3ac2c Address review comments 2021-08-25 09:27:37 +01:00
shati-patel
38a64017f2 New setting to specify number of paths per alert 2021-08-25 09:27:37 +01:00
Andrew Eisenberg
20b15b6e1d Add v2.6.0 to list of versions we use for integration testing 2021-08-24 21:03:27 -07:00
Edoardo Pirovano
e119218828 Update extensions/ql-vscode/CHANGELOG.md
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2021-08-23 21:30:30 +01:00
Edoardo Pirovano
f494988ba6 Address PR comments from @aeisenberg and @shati-patel 2021-08-23 21:30:30 +01:00
Edoardo Pirovano
2561db1721 Allow exporting of results for non-alert queries 2021-08-23 21:30:30 +01:00
Benjamin Muskalla
089b23f0aa Remove old changelog entry 2021-08-19 13:04:16 +01:00
Benjamin Muskalla
fbed7dd1ca Mention filename pattern in changelog 2021-08-19 13:04:16 +01:00
Benjamin Muskalla
06ef67f22d Add support for filename pattern in history view 2021-08-19 13:04:16 +01:00
shati-patel
3d647f68e1 Bump version to v1.5.4 2021-08-18 16:43:37 +01:00
shati-patel
6a36dc34cc v1.5.3
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2021-08-18 16:29:41 +01:00
Shati Patel
b48aaeac7b Fix type for "remote repo list" setting 2021-08-18 15:33:26 +01:00
Edoardo Pirovano
2da1065027 PR Checks: Use version of codeql repo suitable for CLI version 2021-08-18 15:25:21 +01:00
shati-patel
3536124fbc Tweak the regex 2021-08-17 16:54:48 +01:00
shati-patel
10b4e08bf8 Validate user input for "owner/repo" 2021-08-17 16:54:48 +01:00
shati-patel
b1f426672c Add option to enter a single repo 2021-08-17 16:54:48 +01:00
shati-patel
087cae287f Add a new "remote repository lists" setting 2021-08-17 16:54:48 +01:00
Eric Kim
3d8032c9b7 Update Changelog 2021-08-17 08:28:25 -07:00
Eric Kim
6470238311 Adjust test-adapter to display diffs only for failing tests 2021-08-17 08:28:25 -07:00
Andrew Eisenberg
0093af8994 Update the CLI versions to run integration tests against 2021-08-09 15:00:01 -07:00
shati-patel
2bfcd119db Don't show empty list of DBs 2021-08-04 20:00:49 +01:00
shati-patel
5932bdba96 Address review comments
- Tweak return types + logging
- Update changelog
2021-08-04 20:00:49 +01:00
shati-patel
1afe6b56fa Autodetect language using "resolve queries"
Also use autodection in relevant places
- When running on multiple databases
- When running a remote query
2021-08-04 20:00:49 +01:00
Andrew Eisenberg
72776e8254 Update the CLI versions to run integration tests against 2021-07-26 19:24:10 +01:00
shati-patel
d2d1a09723 Update changelog 2021-07-16 09:34:45 +01:00
shati-patel
793b82333f Rename variable and tweak error display 2021-07-16 09:34:45 +01:00
shati-patel
b3abff3e88 Add some error handling 2021-07-16 09:34:45 +01:00
shati-patel
890549f9e7 Fix database selection 2021-07-16 09:34:45 +01:00
shati-patel
66825d6a37 Add command for running queries on multiple databases 2021-07-16 09:34:45 +01:00
Andrew Eisenberg
d42982ee4c Fix dependabot errors
Updates the package dependencies to avoid dependabot errors.

I updated the `@types/gulp` to avoid some typings errors that were
introduced by incompatible versions of `@types/undertaker`.

Also, I forced resolution on `"glob-parent": "~6.0.0"` that avoids
a vulnerability on earlier versions.

I did a smoke test of features that use glob, as well as running a few
queries. All looks good.
2021-07-15 20:03:48 -07:00
shati-patel
7df634f050 Bump version to v1.5.3 2021-07-13 18:50:52 +01:00
shati-patel
46606aa7b5 v1.5.2
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2021-07-13 18:27:28 +01:00
shati-patel
de5704974d Use new endpoint for running remote queries 2021-07-13 18:09:21 +01:00
shati-patel
977b061048 Fix error from "Open Query Results" button 2021-07-13 09:04:00 +01:00
Chuan-kai Lin
560f694f73 Calculate databasesUnderTest with a loop
Currently QLTestAdapter.run() calculates the databases affected by a set of
tests (those databases will be deleted and then reopened after test completion)
using a nested filter-find expression. Which does not work because the
predicate is an async function, so the expression is testing the truthiness of
a Promise instead of the async result.

This commit fixes the problem by implementing the same check with a loop so
that we can invoke the async predicate using await.
2021-07-12 16:00:46 -07:00
shati-patel
7a58d360fd Update changelog 2021-07-12 09:36:10 +01:00
shati-patel
9601d6c140 Render command description as markdown 2021-07-12 09:36:10 +01:00
Edoardo Pirovano
db66184c35 Run tests with nightly CLI 2021-07-02 17:21:03 +01:00
Shati Patel
93e7daea49 Update CLI integration tests with latest version of CLI
CodeQL CLI v2.5.7 is now released 🎉
2021-07-02 15:34:54 +01:00
shati-patel
1a18c6d056 Update changelog 2021-06-25 16:14:12 -07:00
shati-patel
7eb12e0004 Loop through DBs individually, instead of adding multiple DBs in parallel 2021-06-25 16:14:12 -07:00
shati-patel
d3192b7e3b New command to add database source folder to workspace 2021-06-25 16:14:12 -07:00
Shati Patel
e7ab2969d7 Update CLI integration tests with latest version of CLI (#889)
CodeQL CLI v2.5.6 was released yesterday 🎉
2021-06-23 12:06:31 -07:00
Shati Patel
49a35343f6 Run PR checks on "ready_for_review" 2021-06-23 19:53:21 +01:00
shati-patel
c361671e36 Bump version to v1.5.2 2021-06-23 19:28:31 +01:00
shati-patel
b71452b87c v1.5.1
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
2021-06-23 16:11:05 +01:00
Shati Patel
06170f9713 Changes from dev branch (#882)
Two new "canary" commands:
* GitHub authentication (from #874)
* Workflow dispatch (run remote query)
2021-06-23 09:14:42 +01:00
Andrew Eisenberg
920515c071 Add CODEOWNERS 2021-06-17 10:01:31 -07:00
Shati Patel
6a124685bd Don't run on pull requests
I don't think we ever need to run on PRs 🤔
2021-06-15 18:19:32 +01:00
shati-patel
75f76ecd23 Create version bump PRs in draft mode
Currently, the token we use to create these PRs doesn't have sufficient permissions to set off PR checks. Maybe if we create the PR as a draft and have a real person mark the PR as "ready-for-review", this will be enough to start PR checks.
2021-06-15 18:19:32 +01:00
shati-patel
5a0b1b290f Bump version to v1.5.1 2021-06-14 20:23:08 +01:00
shati-patel
472008888c v1.5.0
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
2021-06-14 20:00:34 +01:00
shati-patel
aa0d844dc1 Add more context in changelog 2021-06-14 18:42:42 +01:00
shati-patel
2523f81640 Update changelog 2021-06-14 18:42:42 +01:00
shati-patel
9e8b1ffd50 Update to VS Code 1.57.0
This version of VS Code has workspace trust enabled by default
2021-06-14 18:42:42 +01:00
shati-patel
06b22511a7 Update to VS Code 1.48.0
partial cherry-pick from `qc-development` branch
2021-06-14 18:42:42 +01:00
shati-patel
61373209ff Use the workspace trust feature 2021-06-14 18:42:42 +01:00
Andrew Eisenberg
b1e28f6b7d Fix running integration tests
The main fix is in `telemetry.ts:213`.
2021-06-11 14:08:25 -07:00
Andrew Eisenberg
1d414bac55 Update linting rules
Add the `@typescript-eslint/no-floating-promises` rule with an allowance
for floating promises if `void` is used.

This increases safety and ensures that we are explicit when we avoid
awaiting a promise. I already caught a few bugish locations.

In general, we don't need to await the results of logging calls.

databases-ui, we were using a deprecated method for removing a
directory. `fs.rmdir` instead of `fs.remove`.
2021-06-11 14:08:25 -07:00
shati-patel
2f3be92a71 Make functions async + other review comments 2021-05-21 21:41:40 +01:00
shati-patel
a8fd6cc0ee Add changelog note 2021-05-21 21:41:40 +01:00
shati-patel
e591236c4e Update tests 2021-05-21 21:41:40 +01:00
shati-patel
41f4e04379 Create custom log directory, if possible
(I haven't got the error handling to work asynchronously, so I stuck with `mkdirSync` for now)
2021-05-21 21:41:40 +01:00
shati-patel
7e27f20e0e Specify custom directory for storing query server logs 2021-05-21 21:41:40 +01:00
Eric Kim
f550cbe98f Increase font size and add margins to empty query message 2021-05-21 12:35:29 -07:00
Eric Kim
5315c16338 Adjust empty query message 2021-05-21 12:35:29 -07:00
Chuan-kai Lin
540cb99de4 Reregister testproj databases around test runs
To deal with the problem of CodeQL tests modifying open testproj databases,
this commit removes open databases from the extension prior to running tests,
and tries to open those databases again after tests finish running.
2021-05-20 16:00:45 -07:00
Eric Kim
3abc8df8fc Update ChangeLog 2021-05-17 19:01:03 -07:00
Eric Kim
ca93f0e84b Add link to language guides for empty query results 2021-05-17 19:01:03 -07:00
Andrew Eisenberg
d9ff5bdca4 Update cli integration tests with new cli version 2021-05-17 12:39:25 -07:00
Andrew Eisenberg
c4b12250ba Update ChangeLog 2021-05-14 08:00:25 -07:00
Andrew Eisenberg
d73f00196b Add version info while downloading 2021-05-14 08:00:25 -07:00
Andrew Eisenberg
6bf616ff4d Fix code scanning errors and dependabot issues
* Log injection errors
* Also, ran `npm audit fix`
2021-05-10 09:39:55 -07:00
Andrew Eisenberg
ff02d1da05 Add extra emphasis in contributing docs 2021-05-06 14:54:48 -07:00
shati-patel
72d57eec6e Bump version to v1.4.9 2021-05-05 10:04:39 -07:00
shati-patel
692e1235e8 v1.4.8
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
2021-05-05 17:41:02 +01:00
Andrew Eisenberg
b69bbf5c5d Update integration test cli versions 2021-04-30 10:11:03 -07:00
Shati Patel
b64284c43e Apply suggestions from code review
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2021-04-29 10:31:51 -07:00
Shati Patel
67eaaadfce Update changelog 2021-04-29 10:31:51 -07:00
Shati Patel
a9545458b9 minor unrelated typo fixes 2021-04-29 10:31:51 -07:00
Shati Patel
3e1b121471 Prompt users to choose a DB language 2021-04-29 10:31:51 -07:00
Shati Patel
28d7a26b5f Fix syntax in CodeQL code scanning workflow 2021-04-28 16:19:24 +01:00
Andrew Eisenberg
1d49ae5b99 Actions: Add permissions block to code scanning workflow (#850) 2021-04-26 17:57:13 +00:00
Andrew Eisenberg
b00826d76a Use the main branch of the codeql action
This commit switches to the bleeding edge, main branch of the
codeql action. This helps us test the action before merging all
of the new changes into main, which occurs roughly once a week.

If there are commits that introduce bugs in codeql-action, then
we will be more likely to catch it before releasing to the world
if we are using it in this extension.
2021-04-26 08:50:42 -07:00
Shati Patel
eab5865a5c Fix conflict in changelog 2021-04-26 07:53:03 -07:00
Shati Patel
0e8cd0d2b1 Update changelog 2021-04-26 07:53:03 -07:00
Shati Patel
8281f408dc Add command to copy version information 2021-04-26 07:53:03 -07:00
Andrew Eisenberg
fce9bbce20 Update changelog 2021-04-23 14:57:28 -07:00
Andrew Eisenberg
dc5efcedba Watch for changes in directory structure
This ensures that directories renamed, added or deleted are
properly checked to see if they contain tests. The test tree
will be correctly updated when any directory changes.s
2021-04-23 14:57:28 -07:00
aeisenberg
f6c67bf696 Bump version to v1.4.8 2021-04-23 17:29:40 +01:00
Andrew Eisenberg
3fce04a24b v1.4.7
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
2021-04-23 08:11:50 -07:00
Henry Mercer
fba8f51d1b Add polyfill for path to fix a bug that prevented the results view from being loaded (#842)
* Add a polyfill for the Node.js path module

Webpack >v5 doesn't include polyfills for core modules from Node.js by
default. Since we use `path` in the results table UI, we need to include
our own polyfill. This commit adds `path-browserify` to the
distributed extension.

As future work, we could move SARIF location rendering into the core
extension so we don't need to use `path.basename` in the UI. This would
allow us to remove the polyfill.

* Add changelog note
2021-04-23 12:53:48 +01:00
aeisenberg
31ee3cb978 Bump version to v1.4.7 2021-04-23 03:57:48 -07:00
Andrew Eisenberg
4d99126994 v1.4.6
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
2021-04-21 11:33:47 -07:00
Henry Mercer
ced34ad704 Add changelog note 2021-04-21 15:43:57 +01:00
Henry Mercer
f5e0011aa1 Forward all query metadata to the queryserver 2021-04-21 15:43:57 +01:00
Andrew Eisenberg
a0b759ecd8 Avoid printing a stack trace when there is no resultsPath
I don't know exactly when this can happen, but a customer has just
shown me a stack trace like this:

```
TypeError: Cannot destructure property 'resultsPath' of 'resultsPaths' as it is undefined.
    at Object.interpretResults (/xxx/.vscode/extensions/github.vscode-codeql-1.4.5/out/query-results.js:120:13)
    at InterfaceManager._getInterpretedResults (/xxx/.vscode/extensions/github.vscode-codeql-1.4.5/out/interface.js:377:45)
    at InterfaceManager.showResultsAsDiagnostics (/xxx/.vscode/extensions/github.vscode-codeql-1.4.5/out/interface.js:447:43)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async InterfaceManager.handleMsgFromView (/xxx/.vscode/extensions/github.vscode-codeql-1.4.5/out/interface.js:151:29)
```

This commit will avoid printing this stack trace and instead print
a more descriptive message to the logs.
2021-04-20 12:55:13 -07:00
Andrew Eisenberg
58cf4db9ee Add v2.5.1 to cli versions in integration test 2021-04-19 13:53:21 -07:00
Henry Mercer
e0c5ae815c Remove commented out code 2021-04-19 08:44:57 -07:00
Andrew Eisenberg
bf5ed193be Avoid opening the results panel on db deletion
Fixes https://github.com/github/vscode-codeql/issues/823
2021-04-19 08:05:27 -07:00
Aditya Sharad
aa60fbc213 Actions: Simplify code scanning workflow
Run only on pushes and PRs against `main`.
2021-04-14 11:58:46 -07:00
Andrew Eisenberg
bdb2feb559 Refactor version constraints
A simple refactoring that simplifies and unifies how we check if a
feature is supported by a specific cli version.
2021-04-13 10:36:54 -07:00
Andrew Eisenberg
5b08fd0df1 Fix CHANGELOG 2021-04-10 11:19:32 -07:00
Andrew Eisenberg
c83dbde20f Add cli version for message 2021-04-09 15:19:47 -07:00
Edoardo Pirovano
e033578cd2 Add feature to jump to the .ql file referenced by a .qlref 2021-04-09 15:19:47 -07:00
Andrew Eisenberg
c082a38b6b Add a canary setting to avoid caching AST viewer queries (#818)
When codeql library developers are working on PrintAST queries, it is
not easy to use the AST Viewer. The AST Viewer caches results so that
multiple calls to view the AST of the same file are nearly
instantaneous.

However, this breaks down if you are changing the actual queries that
perform AST viewing. In this case, you do not want the cache to be
active.

This commit adds an undocumented setting that prevents caching. To
enable, set:

```
"codeQL.isCanary": true,
"codeQL.astViewer.disableCache": true
```

Note that *both* settings must be true for this to work.

This behaviour and all canary behaviour should be documented somewhere.
I will add that later.
2021-04-01 14:12:13 -07:00
Andrew Eisenberg
bdda27703a Ensure snippets.json is copied when packaging the extension 2021-03-31 10:47:48 -07:00
Andrew Eisenberg
36bfb3987e Fix dependabot warnings (#816)
This commit updates to webpack 5 in order to fix some dependabot errors.
Because webpack 5 introduces some breaking changes, this commit also
makes some minor changes to the build code.
2021-03-29 19:46:20 +00:00
Andrew Eisenberg
6d26491243 Avoid displaying error message for @kind table queries
Also, add a unit test for this area.
2021-03-29 08:16:51 -07:00
Edoardo Pirovano
98a2bbbb47 Limit error messages shown in popups to 2 lines 2021-03-28 16:14:55 -07:00
Aditya Sharad
fb6bed6042 Actions: Test against CodeQL CLI 2.5.0 (#812) 2021-03-26 11:31:31 -07:00
github-actions[bot]
df0cc921fd Bump version to v1.4.6 (#805)
* Bump version to v1.4.6

* Update CHANGELOG.md

Co-authored-by: adityasharad <adityasharad@users.noreply.github.com>
Co-authored-by: Aditya Sharad <6874315+adityasharad@users.noreply.github.com>
2021-03-23 00:40:39 +00:00
76 changed files with 5266 additions and 4990 deletions

View File

@@ -10,7 +10,12 @@ assignees: ''
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
**Version**
The CodeQL and VS Code version in which the bug occurs.
<!-- To copy version information for the CodeQL extension, click "CodeQL CLI vX.X.X" in the status bar at the bottom of the screen.
To copy detailed version information for VS Code itself, see https://code.visualstudio.com/docs/supporting/FAQ#_how-do-i-find-the-version. -->
**To reproduce**
Steps to reproduce the behavior.
**Expected behavior**

View File

@@ -2,24 +2,30 @@ name: "Code Scanning - CodeQL"
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 0'
- cron: '21 17 * * 0'
jobs:
codeql:
strategy:
fail-fast: false
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@main
with:
languages: javascript
config-file: ./.github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@main

View File

@@ -2,6 +2,7 @@ name: Build Extension
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches:
- main
@@ -50,12 +51,30 @@ jobs:
name: vscode-codeql-extension
path: artifacts
find-nightly:
name: Find Nightly Release
runs-on: ubuntu-latest
outputs:
url: ${{ steps.get-url.outputs.nightly-url }}
steps:
- name: Get Nightly Release URL
id: get-url
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
shell: bash
# This workflow step gets an unstable testing version of the CodeQL CLI. It should not be used outside of these tests.
run: |
LATEST=`gh api repos/dsp-testing/codeql-cli-nightlies/releases --jq '.[].tag_name' --method GET --raw-field 'per_page=1'`
echo "::set-output name=nightly-url::https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/$LATEST"
test:
name: Test
runs-on: ${{ matrix.os }}
needs: [find-nightly]
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
version: [stable, nightly]
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -88,7 +107,12 @@ jobs:
- name: Install CodeQL
run: |
mkdir codeql-home
curl -L --silent https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip -o codeql-home/codeql.zip
if [ ${{ matrix.version }} = "stable" ]
then
curl -L --silent https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip -o codeql-home/codeql.zip
else
curl -L --silent ${{ needs.find-nightly.outputs.url }}/codeql.zip -o codeql-home/codeql.zip
fi
unzip -q -o codeql-home/codeql.zip -d codeql-home
unzip -q -o codeql-home/codeql.zip codeql/codeql.exe -d codeql-home
rm codeql-home/codeql.zip
@@ -123,12 +147,14 @@ jobs:
cli-test:
name: CLI Test
runs-on: ${{ matrix.os }}
needs: [find-nightly]
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
version: ['v2.2.6', 'v2.3.3', 'v2.4.5', 'v2.4.6']
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.1', 'nightly']
env:
CLI_VERSION: ${{ matrix.version }}
NIGHTLY_URL: ${{ needs.find-nightly.outputs.url }}
TEST_CODEQL_PATH: '${{ github.workspace }}/codeql'
steps:
@@ -151,10 +177,26 @@ jobs:
npm run build
shell: bash
- name: Decide on ref of CodeQL repo
id: choose-ref
shell: bash
run: |
if [[ "${{ matrix.version }}" == "nightly" ]]
then
REF="codeql-cli/latest"
elif [[ "${{ matrix.version }}" == "v2.2.6" || "${{ matrix.version }}" == "v2.3.3" ]]
then
REF="codeql-cli/v2.4.5"
else
REF="codeql-cli/${{ matrix.version }}"
fi
echo "::set-output name=ref::$REF"
- name: Checkout QL
uses: actions/checkout@v2
with:
repository: github/codeql
ref: ${{ steps.choose-ref.outputs.ref }}
path: codeql
- name: Run CLI tests (Linux)

View File

@@ -6,10 +6,6 @@
name: Release
on:
pull_request:
paths:
- '**/workflows/release.yml'
workflow_dispatch:
push:
@@ -129,6 +125,7 @@ jobs:
body: This PR was automatically generated by the GitHub Actions release workflow in this repository.
branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
base: main
draft: true
vscode-publish:
name: Publish to VS Code Marketplace

View File

@@ -22,7 +22,8 @@
"common/temp": true,
"**/.vscode-test": true
},
"typescript.tsdk": "./common/temp/node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version
"typescript.tsdk": "./extensions/ql-vscode/node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version
"typescript.enablePromptUseWorkspaceTsdk": true,
"eslint.validate": [
"javascript",
"javascriptreact",

1
CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
**/* @github/codeql-vscode-reviewers

View File

@@ -56,7 +56,8 @@ We recommend that you keep `npm run watch` running in the backgound and you only
1. on first checkout
2. whenever any of the non-TypeScript resources have changed
3. on any change to files included in the webview
3. on any change to files included in one of the webviews
- **Important**: This is easy to forget. You must explicitly run `npm run build` whenever one of the files in the webview is changed. These are the files in the `src/view` and `src/compare/view` folders.
### Installing the extension

View File

@@ -22,8 +22,10 @@ module.exports = {
},
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": [ "error", { ignoreVoid: true } ],
"prefer-const": ["warn", { destructuring: "all" }],
indent: "off",
"@typescript-eslint/indent": "off",

View File

@@ -1,6 +1,59 @@
# CodeQL for Visual Studio Code: Changelog
## [UNRELEASED]
## 1.5.5 - 08 September 2021
- Fix bug where a query is sometimes run before the file is saved. [#947](https://github.com/github/vscode-codeql/pull/947)
- Fix broken contextual queries, including _View AST_. [#949](https://github.com/github/vscode-codeql/pull/949)
## 1.5.4 - 02 September 2021
- Add support for filename pattern in history view. [#930](https://github.com/github/vscode-codeql/pull/930)
- Add an option _View Results (CSV)_ to view the results of a non-alert query. The existing options for alert queries have been renamed to _View Alerts_ to avoid confusion. [#929](https://github.com/github/vscode-codeql/pull/929)
- Allow users to specify the number of paths to display for each alert. [#931](https://github.com/github/vscode-codeql/pull/931)
- Adjust pagination controls in _CodeQL Query Results_ to always be visible [#936](https://github.com/github/vscode-codeql/pull/936)
- Fix bug where _View AST_ fails due to recent refactoring in the standard library and query packs. [#939](https://github.com/github/vscode-codeql/pull/939)
## 1.5.3 - 18 August 2021
- Add a command _CodeQL: Run Query on Multiple Databases_, which lets users select multiple databases to run a query on. [#898](https://github.com/github/vscode-codeql/pull/898)
- Autodetect what language a query targets. This refines the _CodeQL: Run Query on Multiple Databases_ command to only show relevant databases. [#915](https://github.com/github/vscode-codeql/pull/915)
- Adjust test log output to display diffs only when comparing failed test results with expected test results. [#920](https://github.com/github/vscode-codeql/pull/920)
## 1.5.2 - 13 July 2021
- Add the _Add Database Source to Workspace_ command to the right-click context menu in the databases view. This lets users re-add a database's source folder to the workspace and browse the source code. [#891](https://github.com/github/vscode-codeql/pull/891)
- Fix markdown rendering in the description of the `codeQL.cli.executablePath` setting. [#908](https://github.com/github/vscode-codeql/pull/908)
- Fix the _Open Query Results_ command in the query history view. [#909](https://github.com/github/vscode-codeql/pull/909)
## 1.5.1 - 23 June 2021
No user facing changes.
## 1.5.0 - 14 June 2021
- Display CodeQL CLI version being downloaded during an upgrade. [#862](https://github.com/github/vscode-codeql/pull/862)
- Display a helpful message and link to documentation when a query produces no results. [#866](https://github.com/github/vscode-codeql/pull/866)
- Refresh test databases automatically after a test run. [#868](https://github.com/github/vscode-codeql/pull/868)
- Allow users to specify a custom directory for storing query server logs (`codeQL.runningQueries.customLogDirectory`). The extension will not delete these logs automatically. [#863](https://github.com/github/vscode-codeql/pull/863)
- Support the VS Code [Workspace Trust feature](https://code.visualstudio.com/docs/editor/workspace-trust). This extension is now enabled in untrusted workspaces, but it restricts commands that contain arbitrary paths. [#861](https://github.com/github/vscode-codeql/pull/861)
- Allow the `codeQL.cli.executablePath` configuration setting to be set in workspace-scoped configuration files. This means that each workspace can now specify its own CodeQL CLI compiler, a feature that is unblocked due to implementing Workspace Trust. [#861](https://github.com/github/vscode-codeql/pull/861)
## 1.4.8 - 05 May 2021
- Copy version information to the clipboard when a user clicks the CodeQL section of the status bar. [#845](https://github.com/github/vscode-codeql/pull/845)
- Ensure changes in directories that contain tests will be properly updated in the test explorer. [#846](https://github.com/github/vscode-codeql/pull/846)
- Remind users to choose a language when downloading a database from LGTM. [#852](https://github.com/github/vscode-codeql/pull/852)
## 1.4.7 - 23 April 2021
- Fix a bug that prevented the results view from being loaded. [#842](https://github.com/github/vscode-codeql/pull/842)
## 1.4.6 - 21 April 2021
- Avoid showing an error popup when running a query with `@kind table` metadata. [#814](https://github.com/github/vscode-codeql/pull/814)
- Add an option to jump from a .qlref file to the .ql file it references. [#815](https://github.com/github/vscode-codeql/pull/815)
- Avoid opening the results panel when a database is deleted. [#831](https://github.com/github/vscode-codeql/pull/831)
- Forward all query metadata to the CLI when interpreting results. [#838](https://github.com/github/vscode-codeql/pull/838)
## 1.4.5 - 22 March 2021

View File

@@ -13,6 +13,7 @@ const packageFiles = [
'CHANGELOG.md',
'README.md',
'language-configuration.json',
'snippets.json',
'media',
'node_modules',
'out'

View File

@@ -13,7 +13,10 @@ export const config: webpack.Configuration = {
},
devtool: 'inline-source-map',
resolve: {
extensions: ['.js', '.ts', '.tsx', '.json']
extensions: ['.js', '.ts', '.tsx', '.json'],
fallback: {
path: require.resolve('path-browserify')
}
},
module: {
rules: [

View File

@@ -6,21 +6,23 @@ export function compileView(cb: (err?: Error) => void) {
if (error) {
cb(error);
}
console.log(stats.toString({
errorDetails: true,
colors: true,
assets: false,
builtAt: false,
version: false,
hash: false,
entrypoints: false,
timings: false,
modules: false,
errors: true
}));
if (stats.hasErrors()) {
cb(new Error('Compilation errors detected.'));
return;
if (stats) {
console.log(stats.toString({
errorDetails: true,
colors: true,
assets: false,
builtAt: false,
version: false,
hash: false,
entrypoints: false,
timings: false,
modules: false,
errors: true
}));
if (stats.hasErrors()) {
cb(new Error('Compilation errors detected.'));
return;
}
}
cb();

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.4.5",
"version": "1.5.5",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -13,7 +13,7 @@
"url": "https://github.com/github/vscode-codeql"
},
"engines": {
"vscode": "^1.43.0"
"vscode": "^1.57.0"
},
"categories": [
"Programming Languages"
@@ -21,6 +21,16 @@
"extensionDependencies": [
"hbenl.vscode-test-explorer"
],
"capabilities": {
"untrustedWorkspaces": {
"supported": "limited",
"description": "Workspace trust is required to execute commands that can contain arbitrary paths.",
"restrictedConfigurations": [
"codeQL.cli.executablePath",
"codeQL.runningTests.additionalTestArguments"
]
}
},
"activationEvents": [
"onLanguage:ql",
"onView:codeQLDatabases",
@@ -28,12 +38,14 @@
"onView:codeQLAstViewer",
"onView:test-explorer",
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.authenticateToGitHub",
"onCommand:codeQLDatabases.chooseDatabaseFolder",
"onCommand:codeQLDatabases.chooseDatabaseArchive",
"onCommand:codeQLDatabases.chooseDatabaseInternet",
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
"onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQL.viewAst",
"onCommand:codeQL.openReferencedFile",
"onCommand:codeQL.chooseDatabaseFolder",
"onCommand:codeQL.chooseDatabaseArchive",
"onCommand:codeQL.chooseDatabaseInternet",
@@ -117,10 +129,10 @@
"title": "CodeQL",
"properties": {
"codeQL.cli.executablePath": {
"scope": "machine",
"scope": "window",
"type": "string",
"default": "",
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. If empty, the extension will look for a CodeQL executable on your shell PATH, or if CodeQL is not on your PATH, download and manage its own CodeQL executable."
"markdownDescription": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. If empty, the extension will look for a CodeQL executable on your shell PATH, or if CodeQL is not on your PATH, download and manage its own CodeQL executable."
},
"codeQL.runningQueries.numberOfThreads": {
"type": "integer",
@@ -168,6 +180,13 @@
"default": false,
"description": "Enable debug logging and tuple counting when running CodeQL queries. This information is useful for debugging query performance."
},
"codeQL.runningQueries.maxPaths": {
"type": "integer",
"default": 4,
"minimum": 1,
"maximum": 256,
"markdownDescription": "Max number of paths to display for each alert found by a path query (`@kind path-problem`)."
},
"codeQL.runningQueries.autoSave": {
"type": "boolean",
"default": false,
@@ -178,6 +197,14 @@
"default": 20,
"description": "Max number of simultaneous queries to run using the 'CodeQL: Run Queries' command."
},
"codeQL.runningQueries.customLogDirectory": {
"type": [
"string",
null
],
"default": null,
"description": "Path to a directory where the CodeQL extension should store query server logs. If empty, the extension stores logs in a temporary workspace folder and deletes the contents after each run."
},
"codeQL.resultsDisplay.pageSize": {
"type": "integer",
"default": 200,
@@ -186,10 +213,10 @@
"codeQL.queryHistory.format": {
"type": "string",
"default": "%q on %d - %s, %r result count [%t]",
"description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, %r is the number of results, and %s is a status string."
"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.runningTests.additionalTestArguments": {
"scope": "machine",
"scope": "window",
"type": "array",
"default": [],
"markdownDescription": "Additional command line arguments to pass to the CLI when [running tests](https://codeql.github.com/docs/codeql-cli/manual/test-run/). This setting should be an array of strings, each containing an argument to be passed."
@@ -213,14 +240,42 @@
"default": false,
"scope": "application",
"description": "Specifies whether or not to write telemetry events to the extension log."
},
"codeQL.remoteRepositoryLists": {
"type": [
"object",
null
],
"patternProperties": {
".*": {
"type": "array",
"items": {
"type": "string"
}
}
},
"default": null,
"markdownDescription": "[For internal use only] Lists of GitHub repositories that you want to query remotely. This should be a JSON object where each key is a user-specified name for this repository list, and the value is an array of GitHub repositories (of the form `<owner>/<repo>`)."
}
}
},
"commands": [
{
"command": "codeQL.authenticateToGitHub",
"title": "CodeQL: Authenticate to GitHub"
},
{
"command": "codeQL.runQuery",
"title": "CodeQL: Run Query"
},
{
"command": "codeQL.runQueryOnMultipleDatabases",
"title": "CodeQL: Run Query on Multiple Databases"
},
{
"command": "codeQL.runRemoteQuery",
"title": "CodeQL: Run Remote Query"
},
{
"command": "codeQL.runQueries",
"title": "CodeQL: Run Queries in Selected Files"
@@ -229,6 +284,10 @@
"command": "codeQL.quickEval",
"title": "CodeQL: Quick Evaluation"
},
{
"command": "codeQL.openReferencedFile",
"title": "CodeQL: Open Referenced File"
},
{
"command": "codeQL.quickQuery",
"title": "CodeQL: Quick Query"
@@ -237,6 +296,10 @@
"command": "codeQL.openDocumentation",
"title": "CodeQL: Open Documentation"
},
{
"command": "codeQL.copyVersion",
"title": "CodeQL: Copy Version Information"
},
{
"command": "codeQLDatabases.chooseDatabaseFolder",
"title": "Choose Database from Folder",
@@ -309,6 +372,10 @@
"command": "codeQLDatabases.openDatabaseFolder",
"title": "Show Database Directory"
},
{
"command": "codeQLDatabases.addDatabaseSource",
"title": "Add Database Source to Workspace"
},
{
"command": "codeQL.chooseDatabaseFolder",
"title": "CodeQL: Choose Database from Folder"
@@ -406,8 +473,12 @@
"title": "View Results (CSV)"
},
{
"command": "codeQLQueryHistory.viewSarifResults",
"title": "View Results (SARIF)"
"command": "codeQLQueryHistory.viewCsvAlerts",
"title": "View Alerts (CSV)"
},
{
"command": "codeQLQueryHistory.viewSarifAlerts",
"title": "View Alerts (SARIF)"
},
{
"command": "codeQLQueryHistory.viewDil",
@@ -548,6 +619,11 @@
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLDatabases.addDatabaseSource",
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLQueryHistory.openQuery",
"group": "9_qlCommands",
@@ -581,10 +657,15 @@
{
"command": "codeQLQueryHistory.viewCsvResults",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory && viewItem != interpretedResultsItem"
},
{
"command": "codeQLQueryHistory.viewCsvAlerts",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
},
{
"command": "codeQLQueryHistory.viewSarifResults",
"command": "codeQLQueryHistory.viewSarifAlerts",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
},
@@ -619,13 +700,30 @@
"command": "codeQL.runQueries",
"group": "9_qlCommands",
"when": "resourceScheme != codeql-zip-archive"
},
{
"command": "codeQL.openReferencedFile",
"group": "9_qlCommands",
"when": "resourceExtname == .qlref"
}
],
"commandPalette": [
{
"command": "codeQL.authenticateToGitHub",
"when": "config.codeQL.canary"
},
{
"command": "codeQL.runQuery",
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runQueryOnMultipleDatabases",
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runRemoteQuery",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runQueries",
"when": "false"
@@ -634,6 +732,10 @@
"command": "codeQL.quickEval",
"when": "editorLangId == ql"
},
{
"command": "codeQL.openReferencedFile",
"when": "resourceExtname == .qlref"
},
{
"command": "codeQL.setCurrentDatabase",
"when": "false"
@@ -654,6 +756,10 @@
"command": "codeQLDatabases.openDatabaseFolder",
"when": "false"
},
{
"command": "codeQLDatabases.addDatabaseSource",
"when": "false"
},
{
"command": "codeQLDatabases.sortByName",
"when": "false"
@@ -715,7 +821,11 @@
"when": "false"
},
{
"command": "codeQLQueryHistory.viewSarifResults",
"command": "codeQLQueryHistory.viewCsvAlerts",
"when": "false"
},
{
"command": "codeQLQueryHistory.viewSarifAlerts",
"when": "false"
},
{
@@ -764,6 +874,14 @@
"command": "codeQL.runQuery",
"when": "editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runQueryOnMultipleDatabases",
"when": "editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runRemoteQuery",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.viewAst",
"when": "resourceScheme == codeql-zip-archive"
@@ -771,6 +889,10 @@
{
"command": "codeQL.quickEval",
"when": "editorLangId == ql"
},
{
"command": "codeQL.openReferencedFile",
"when": "resourceExtname == .qlref"
}
]
},
@@ -828,6 +950,7 @@
"format-staged": "lint-staged"
},
"dependencies": {
"@octokit/rest": "^18.5.6",
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"fs-extra": "^9.0.1",
@@ -835,6 +958,7 @@
"js-yaml": "^3.14.0",
"minimist": "~1.2.5",
"node-fetch": "~2.6.0",
"path-browserify": "^1.0.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"semver": "~7.3.2",
@@ -857,7 +981,7 @@
"@types/fs-extra": "^9.0.6",
"@types/glob": "^7.1.1",
"@types/google-protobuf": "^3.2.7",
"@types/gulp": "^4.0.6",
"@types/gulp": "^4.0.9",
"@types/gulp-replace": "0.0.31",
"@types/gulp-sourcemaps": "0.0.32",
"@types/js-yaml": "^3.12.5",
@@ -875,11 +999,11 @@
"@types/through2": "^2.0.36",
"@types/tmp": "^0.1.0",
"@types/unzipper": "~0.10.1",
"@types/vscode": "^1.43.0",
"@types/vscode": "^1.57.0",
"@types/webpack": "^4.32.1",
"@types/xml2js": "~0.4.4",
"@typescript-eslint/eslint-plugin": "~2.23.0",
"@typescript-eslint/parser": "~2.23.0",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"ansi-colors": "^4.1.1",
"applicationinsights": "^1.8.7",
"chai": "^4.2.0",
@@ -904,15 +1028,15 @@
"sinon-chai": "~3.5.0",
"style-loader": "~0.23.1",
"through2": "^3.0.1",
"ts-loader": "^5.4.5",
"ts-loader": "^8.1.0",
"ts-node": "^8.3.0",
"ts-protoc-gen": "^0.9.0",
"typescript": "~3.8.3",
"typescript": "^4.3.2",
"typescript-formatter": "^7.2.2",
"vsce": "^1.65.0",
"vscode-test": "^1.4.0",
"webpack": "^4.38.0",
"webpack-cli": "^3.3.2"
"webpack": "^5.28.0",
"webpack-cli": "^4.6.0"
},
"husky": {
"hooks": {
@@ -928,5 +1052,8 @@
"tsfmt -r",
"eslint --fix"
]
},
"resolutions": {
"glob-parent": "~6.0.0"
}
}

View File

@@ -115,7 +115,7 @@ class InvalidSourceArchiveUriError extends Error {
export function decodeSourceArchiveUri(uri: vscode.Uri): ZipFileReference {
if (!uri.authority) {
// Uri is malformed, but this is recoverable
logger.log(`Warning: ${new InvalidSourceArchiveUriError(uri).message}`);
void logger.log(`Warning: ${new InvalidSourceArchiveUriError(uri).message}`);
return {
pathWithinSourceArchive: '/',
sourceArchiveZipPath: uri.path
@@ -141,7 +141,7 @@ function ensureFile(map: DirectoryHierarchyMap, file: string) {
const dirname = path.dirname(file);
if (dirname === '.') {
const error = `Ill-formed path ${file} in zip archive (expected absolute path)`;
logger.log(error);
void logger.log(error);
throw new Error(error);
}
ensureDir(map, dirname);

View File

@@ -56,7 +56,7 @@ class AstViewerDataProvider extends DisposableObject implements TreeDataProvider
}
refresh(): void {
this._onDidChangeTreeData.fire();
this._onDidChangeTreeData.fire(undefined);
}
getChildren(item?: AstItem): ProviderResult<AstItem[]> {
const children = item ? item.children : this.roots;

View File

@@ -0,0 +1,81 @@
import * as vscode from 'vscode';
import * as Octokit from '@octokit/rest';
const GITHUB_AUTH_PROVIDER_ID = 'github';
// 'repo' scope should be enough for triggering workflows. For a comprehensive list, see:
// https://docs.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps
const SCOPES = ['repo'];
interface OctokitAndToken {
octokit: Octokit.Octokit;
token: string;
}
/**
* Handles authentication to GitHub, using the VS Code [authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication).
*/
export class Credentials {
private octokitAndToken: OctokitAndToken | undefined;
// Explicitly make the constructor private, so that we can't accidentally call the constructor from outside the class
// without also initializing the class.
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() { }
static async initialize(context: vscode.ExtensionContext): Promise<Credentials> {
const c = new Credentials();
c.registerListeners(context);
c.octokitAndToken = await c.createOctokit(false);
return c;
}
private async createOctokit(createIfNone: boolean): Promise<OctokitAndToken | undefined> {
const session = await vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { createIfNone });
if (session) {
return {
octokit: new Octokit.Octokit({
auth: session.accessToken
}),
token: session.accessToken
};
} else {
return undefined;
}
}
registerListeners(context: vscode.ExtensionContext): void {
// Sessions are changed when a user logs in or logs out.
context.subscriptions.push(vscode.authentication.onDidChangeSessions(async e => {
if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) {
this.octokitAndToken = await this.createOctokit(false);
}
}));
}
async getOctokit(): Promise<Octokit.Octokit> {
if (this.octokitAndToken) {
return this.octokitAndToken.octokit;
}
this.octokitAndToken = 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.
if (!this.octokitAndToken) {
throw new Error('Did not initialize Octokit.');
}
return this.octokitAndToken.octokit;
}
async getToken(): Promise<string> {
if (this.octokitAndToken) {
return this.octokitAndToken.token;
}
this.octokitAndToken = await this.createOctokit(true);
if (!this.octokitAndToken) {
throw new Error('Did not initialize Octokit.');
}
return this.octokitAndToken.token;
}
}

View File

@@ -18,7 +18,7 @@ export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): P
} catch (e) {
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted.
// Either way, we can't determine compatibility.
logger.log(`Failed to run 'codeql version'. Reason: ${e.message}`);
void logger.log(`Failed to run 'codeql version'. Reason: ${e.message}`);
return undefined;
}
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
import * as cpp from 'child-process-promise';
import * as child_process from 'child_process';
import * as fs from 'fs-extra';
@@ -9,10 +8,9 @@ import { Readable } from 'stream';
import { StringDecoder } from 'string_decoder';
import * as tk from 'tree-kill';
import { promisify } from 'util';
import { CancellationToken, Disposable } from 'vscode';
import { CancellationToken, Disposable, Uri } from 'vscode';
import { BQRSInfo, DecodedBqrsChunk } from './pure/bqrs-cli-types';
import * as config from './config';
import { CliConfig } from './config';
import { DistributionProvider, FindDistributionResultKind } from './distribution';
import { assertNever } from './pure/helpers-pure';
@@ -45,6 +43,16 @@ export interface QuerySetup {
compilationCache?: string;
}
/**
* The expected output of `codeql resolve queries --format bylanguage`.
*/
export interface QueryInfoByLanguage {
// Using `unknown` as a placeholder. For now, the value is only ever an empty object.
byLanguage: Record<string, Record<string, unknown>>;
noDeclaredLanguage: Record<string, unknown>;
multipleDeclaredLanguages: Record<string, unknown>;
}
/**
* The expected output of `codeql resolve database`.
*/
@@ -73,6 +81,16 @@ export interface UpgradesInfo {
*/
export type QlpacksInfo = { [name: string]: string[] };
/**
* The expected output of `codeql resolve languages`.
*/
export type LanguagesInfo = { [name: string]: string[] };
/**
* The expected output of `codeql resolve qlref`.
*/
export type QlrefInfo = { resolvedPath: string };
// `codeql bqrs interpret` requires both of these to be present or
// both absent.
export interface SourceInfo {
@@ -105,6 +123,7 @@ export interface TestCompleted {
expected: string;
diff: string[] | undefined;
failureDescription?: string;
failureStage?: string;
}
/**
@@ -127,21 +146,6 @@ interface BqrsDecodeOptions {
*/
export class CodeQLCliServer implements Disposable {
/**
* CLI version where --kind=DIL was introduced
*/
private static CLI_VERSION_WITH_DECOMPILE_KIND_DIL = new SemVer('2.3.0');
/**
* CLI version where languages are exposed during a `codeql resolve database` command.
*/
private static CLI_VERSION_WITH_LANGUAGE = new SemVer('2.4.1');
/**
* CLI version where `codeql resolve upgrades` supports
* the `--allow-downgrades` flag
*/
private static CLI_VERSION_WITH_DOWNGRADES = new SemVer('2.4.4');
/** The process for the cli server, or undefined if one doesn't exist yet */
process?: child_process.ChildProcessWithoutNullStreams;
@@ -158,6 +162,8 @@ export class CodeQLCliServer implements Disposable {
/** Path to current codeQL executable, or undefined if not running yet. */
codeQlPath: string | undefined;
cliConstraints = new CliVersionConstraint(this);
/**
* When set to true, ignore some modal popups and assume user has clicked "yes".
*/
@@ -192,15 +198,15 @@ export class CodeQLCliServer implements Disposable {
killProcessIfRunning(): void {
if (this.process) {
// Tell the Java CLI server process to shut down.
this.logger.log('Sending shutdown request');
void this.logger.log('Sending shutdown request');
try {
this.process.stdin.write(JSON.stringify(['shutdown']), 'utf8');
this.process.stdin.write(this.nullBuffer);
this.logger.log('Sent shutdown request');
void this.logger.log('Sent shutdown request');
} catch (e) {
// We are probably fine here, the process has already closed stdin.
this.logger.log(`Shutdown request failed: process stdin may have already closed. The error was ${e}`);
this.logger.log('Stopping the process anyway.');
void this.logger.log(`Shutdown request failed: process stdin may have already closed. The error was ${e}`);
void this.logger.log('Stopping the process anyway.');
}
// Close the stdin and stdout streams.
// This is important on Windows where the child process may not die cleanly.
@@ -280,7 +286,7 @@ export class CodeQLCliServer implements Disposable {
// Compute the full args array
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(' ');
this.logger.log(`${description} using CodeQL CLI: ${argsString}...`);
void this.logger.log(`${description} using CodeQL CLI: ${argsString}...`);
try {
await new Promise<void>((resolve, reject) => {
// Start listening to stdout
@@ -307,7 +313,7 @@ export class CodeQLCliServer implements Disposable {
const fullBuffer = Buffer.concat(stdoutBuffers);
// Make sure we remove the terminator;
const data = fullBuffer.toString('utf8', 0, fullBuffer.length - 1);
this.logger.log('CLI command succeeded.');
void this.logger.log('CLI command succeeded.');
return data;
} catch (err) {
// Kill the process if it isn't already dead.
@@ -320,7 +326,7 @@ export class CodeQLCliServer implements Disposable {
newError.stack += (err.stack || '');
throw newError;
} finally {
this.logger.log(Buffer.concat(stderrBuffers).toString('utf8'));
void this.logger.log(Buffer.concat(stderrBuffers).toString('utf8'));
// Remove the listeners we set up.
process.stdout.removeAllListeners('data');
process.stderr.removeAllListeners('data');
@@ -380,7 +386,7 @@ export class CodeQLCliServer implements Disposable {
}
if (logger !== undefined) {
// The human-readable output goes to stderr.
logStream(child.stderr!, logger);
void logStream(child.stderr!, logger);
}
for await (const event of await splitStreamAtSeparators(child.stdout!, ['\0'])) {
@@ -461,12 +467,15 @@ export class CodeQLCliServer implements Disposable {
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param description Description of the action being run, to be shown in log and error messages.
* @param addFormat Whether or not to add commandline arguments to specify the format as JSON.
* @param progressReporter Used to output progress messages, e.g. to the status bar.
* @returns The contents of the command's stdout, if the command succeeded.
*/
async runJsonCodeQlCliCommand<OutputType>(command: string[], commandArgs: string[], description: string, progressReporter?: ProgressReporter): Promise<OutputType> {
// Add format argument first, in case commandArgs contains positional parameters.
const args = ['--format', 'json'].concat(commandArgs);
async runJsonCodeQlCliCommand<OutputType>(command: string[], commandArgs: string[], description: string, addFormat = true, progressReporter?: ProgressReporter): Promise<OutputType> {
let args: string[] = [];
if (addFormat) // Add format argument first, in case commandArgs contains positional parameters.
args = args.concat(['--format', 'json']);
args = args.concat(commandArgs);
const result = await this.runCodeQlCliCommand(command, args, description, progressReporter);
try {
return JSON.parse(result) as OutputType;
@@ -489,6 +498,20 @@ export class CodeQLCliServer implements Disposable {
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, 'Resolving library paths');
}
/**
* Resolves the language for a query.
* @param queryUri The URI of the query
*/
async resolveQueryByLanguage(workspaces: string[], queryUri: Uri): Promise<QueryInfoByLanguage> {
const subcommandArgs = [
'--format', 'bylanguage',
queryUri.fsPath,
'--additional-packs',
workspaces.join(path.delimiter)
];
return JSON.parse(await this.runCodeQlCliCommand(['resolve', 'queries'], subcommandArgs, 'Resolving query by language'));
}
/**
* Finds all available QL tests in a given directory.
* @param testPath Root of directory tree to search for tests.
@@ -505,6 +528,18 @@ export class CodeQLCliServer implements Disposable {
);
}
public async resolveQlref(qlref: string): Promise<QlrefInfo> {
const subcommandArgs = [
qlref
];
return await this.runJsonCodeQlCliCommand<QlrefInfo>(
['resolve', 'qlref'],
subcommandArgs,
'Resolving qlref',
false
);
}
/**
* Runs QL tests.
* @param testPaths Full paths of the tests to run.
@@ -549,7 +584,7 @@ export class CodeQLCliServer implements Disposable {
if (queryMemoryMb !== undefined) {
args.push('--ram', queryMemoryMb.toString());
}
return await this.runJsonCodeQlCliCommand<string[]>(['resolve', 'ram'], args, 'Resolving RAM settings', progressReporter);
return await this.runJsonCodeQlCliCommand<string[]>(['resolve', 'ram'], args, 'Resolving RAM settings', true, progressReporter);
}
/**
* Gets the headers (and optionally pagination info) of a bqrs.
@@ -590,10 +625,10 @@ export class CodeQLCliServer implements Disposable {
async runInterpretCommand(format: string, metadata: QueryMetadata, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo) {
const args = [
`-t=kind=${metadata.kind}`,
`-t=id=${metadata.id}`,
'--output', interpretedResultsPath,
'--format', format,
// Forward all of the query metadata.
...Object.entries(metadata).map(([key, value]) => `-t=${key}=${value}`)
];
if (format == SARIF_FORMAT) {
// TODO: This flag means that we don't group interpreted results
@@ -602,9 +637,6 @@ export class CodeQLCliServer implements Disposable {
// grouping client-side.
args.push('--no-group-results');
}
if (config.isCanary() && metadata.scored !== undefined) {
args.push(`-t=scored=${metadata.scored}`);
}
if (sourceInfo !== undefined) {
args.push(
'--source-archive', sourceInfo.sourceArchive,
@@ -617,6 +649,11 @@ export class CodeQLCliServer implements Disposable {
this.cliConfig.numberThreads.toString(),
);
args.push(
'--max-paths',
this.cliConfig.maxPaths.toString(),
);
args.push(resultsPath);
await this.runCodeQlCliCommand(['bqrs', 'interpret'], args, 'Interpreting query results');
}
@@ -691,7 +728,7 @@ export class CodeQLCliServer implements Disposable {
const args = ['--additional-packs', searchPath.join(path.delimiter), '--dbscheme', dbScheme];
if (targetDbScheme) {
args.push('--target-dbscheme', targetDbScheme);
if (allowDowngradesIfPossible && await this.supportsDowngrades()) {
if (allowDowngradesIfPossible && await this.cliConstraints.supportsDowngrades()) {
args.push('--allow-downgrades');
}
}
@@ -722,6 +759,14 @@ export class CodeQLCliServer implements Disposable {
);
}
/**
* Gets information about the available languages.
* @returns A dictionary mapping language name to the directory it comes from
*/
async resolveLanguages(): Promise<LanguagesInfo> {
return await this.runJsonCodeQlCliCommand<LanguagesInfo>(['resolve', 'languages'], [], 'Resolving languages');
}
/**
* Gets information about queries in a query suite.
* @param suite The suite to resolve.
@@ -730,11 +775,15 @@ export class CodeQLCliServer implements Disposable {
* the default CLI search path is used.
* @returns A list of query files found.
*/
resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
async resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
if (searchPath !== undefined) {
args.push('--search-path', path.join(...searchPath));
}
if (await this.cliConstraints.supportsAllowLibraryPacksInResolveQueries()) {
// All of our usage of `codeql resolve queries` needs to handle library packs.
args.push('--allow-library-packs');
}
args.push(suite);
return this.runJsonCodeQlCliCommand<string[]>(
['resolve', 'queries'],
@@ -744,7 +793,7 @@ export class CodeQLCliServer implements Disposable {
}
async generateDil(qloFile: string, outFile: string): Promise<void> {
const extraArgs = await this.supportsDecompileDil()
const extraArgs = await this.cliConstraints.supportsDecompileDil()
? ['--kind', 'dil', '-o', outFile, qloFile]
: ['-o', outFile, qloFile];
await this.runCodeQlCliCommand(
@@ -761,18 +810,6 @@ export class CodeQLCliServer implements Disposable {
return this._version;
}
private async supportsDecompileDil() {
return (await this.getVersion()).compare(CodeQLCliServer.CLI_VERSION_WITH_DECOMPILE_KIND_DIL) >= 0;
}
public async supportsLanguageName() {
return (await this.getVersion()).compare(CodeQLCliServer.CLI_VERSION_WITH_LANGUAGE) >= 0;
}
public async supportsDowngrades() {
return (await this.getVersion()).compare(CodeQLCliServer.CLI_VERSION_WITH_DOWNGRADES) >= 0;
}
private async refreshVersion() {
const distribution = await this.distributionProvider.getDistribution();
switch (distribution.kind) {
@@ -822,7 +859,7 @@ export function spawnServer(
if (progressReporter !== undefined) {
progressReporter.report({ message: `Starting ${name}` });
}
logger.log(`Starting ${name} using CodeQL CLI: ${base} ${argsString}`);
void logger.log(`Starting ${name} using CodeQL CLI: ${base} ${argsString}`);
const child = child_process.spawn(base, args);
if (!child || !child.pid) {
throw new Error(`Failed to start ${name} using command ${base} ${argsString}.`);
@@ -838,7 +875,7 @@ export function spawnServer(
if (progressReporter !== undefined) {
progressReporter.report({ message: `Started ${name}` });
}
logger.log(`${name} started on PID: ${child.pid}`);
void logger.log(`${name} started on PID: ${child.pid}`);
return child;
}
@@ -867,10 +904,10 @@ export async function runCodeQlCliCommand(
if (progressReporter !== undefined) {
progressReporter.report({ message: description });
}
logger.log(`${description} using CodeQL CLI: ${codeQlPath} ${argsString}...`);
void logger.log(`${description} using CodeQL CLI: ${codeQlPath} ${argsString}...`);
const result = await promisify(child_process.execFile)(codeQlPath, args);
logger.log(result.stderr);
logger.log('CLI command succeeded.');
void logger.log(result.stderr);
void logger.log('CLI command succeeded.');
return result.stdout;
} catch (err) {
throw new Error(`${description} failed: ${err.stderr || err}`);
@@ -908,11 +945,11 @@ class SplitBuffer {
/**
* A version of startsWith that isn't overriden by a broken version of ms-python.
*
*
* The definition comes from
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
* which is CC0/public domain
*
*
* See https://github.com/github/vscode-codeql/issues/802 for more context as to why we need it.
*/
private static startsWith(s: string, searchString: string, position: number): boolean {
@@ -985,7 +1022,8 @@ const lineEndings = ['\r\n', '\r', '\n'];
*/
async function logStream(stream: Readable, logger: Logger): Promise<void> {
for await (const line of await splitStreamAtSeparators(stream, lineEndings)) {
logger.log(line);
// Await the result of log here in order to ensure the logs are written in the correct order.
await logger.log(line);
}
}
@@ -1001,3 +1039,70 @@ export function shouldDebugQueryServer() {
&& process.env.QUERY_SERVER_JAVA_DEBUG !== '0'
&& process.env.QUERY_SERVER_JAVA_DEBUG?.toLocaleLowerCase() !== 'false';
}
export class CliVersionConstraint {
/**
* CLI version where --kind=DIL was introduced
*/
public static CLI_VERSION_WITH_DECOMPILE_KIND_DIL = new SemVer('2.3.0');
/**
* CLI version where languages are exposed during a `codeql resolve database` command.
*/
public static CLI_VERSION_WITH_LANGUAGE = new SemVer('2.4.1');
/**
* CLI version where `codeql resolve upgrades` supports
* the `--allow-downgrades` flag
*/
public static CLI_VERSION_WITH_DOWNGRADES = new SemVer('2.4.4');
/**
* CLI version where the `codeql resolve qlref` command is available.
*/
public static CLI_VERSION_WITH_RESOLVE_QLREF = new SemVer('2.5.1');
/**
* CLI version where database registration was introduced
*/
public static CLI_VERSION_WITH_DB_REGISTRATION = new SemVer('2.4.1');
/**
* CLI version where the `--allow-library-packs` option to `codeql resolve queries` was
* introduced.
*/
public static CLI_VERSION_WITH_ALLOW_LIBRARY_PACKS_IN_RESOLVE_QUERIES = new SemVer('2.6.1');
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
private async isVersionAtLeast(v: SemVer) {
return (await this.cli.getVersion()).compare(v) >= 0;
}
public async supportsDecompileDil() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_DECOMPILE_KIND_DIL);
}
public async supportsLanguageName() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_LANGUAGE);
}
public async supportsDowngrades() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_DOWNGRADES);
}
public async supportsResolveQlref() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_QLREF);
}
public async supportsAllowLibraryPacksInResolveQueries() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_ALLOW_LIBRARY_PACKS_IN_RESOLVE_QUERIES);
}
async supportsDatabaseRegistration() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_DB_REGISTRATION);
}
}

View File

@@ -126,16 +126,16 @@ export function commandRunner(
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
logger.log(errorMessage);
void logger.log(errorMessage);
} else {
showAndLogWarningMessage(errorMessage);
void showAndLogWarningMessage(errorMessage);
}
} else {
// Include the full stack in the error log only.
const fullMessage = e.stack
? `${errorMessage}\n${e.stack}`
: errorMessage;
showAndLogErrorMessage(errorMessage, {
void showAndLogErrorMessage(errorMessage, {
fullMessage
});
}
@@ -177,16 +177,16 @@ export function commandRunnerWithProgress<R>(
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
logger.log(errorMessage);
void logger.log(errorMessage);
} else {
showAndLogWarningMessage(errorMessage);
void showAndLogWarningMessage(errorMessage);
}
} else {
// Include the full stack in the error log only.
const fullMessage = e.stack
? `${errorMessage}\n${e.stack}`
: errorMessage;
showAndLogErrorMessage(errorMessage, {
void showAndLogErrorMessage(errorMessage, {
fullMessage
});
}

View File

@@ -173,7 +173,7 @@ export class CompareInterfaceManager extends DisposableObject {
break;
case 'changeCompare':
this.changeTable(msg.newResultSetName);
await this.changeTable(msg.newResultSetName);
break;
case 'viewSourceFile':
@@ -267,11 +267,11 @@ export class CompareInterfaceManager extends DisposableObject {
return resultsDiff(fromResults, toResults);
}
private openQuery(kind: 'from' | 'to') {
private async openQuery(kind: 'from' | 'to') {
const toOpen =
kind === 'from' ? this.comparePair?.from : this.comparePair?.to;
if (toOpen) {
this.showQueryResultsCallback(toOpen);
await this.showQueryResultsCallback(toOpen);
}
}
}

View File

@@ -21,7 +21,7 @@ const emptyComparison: SetComparisonsMessage = {
message: 'Empty comparison'
};
export function Compare(_: {}): JSX.Element {
export function Compare(_: Record<string, never>): JSX.Element {
const [comparison, setComparison] = useState<SetComparisonsMessage>(
emptyComparison
);
@@ -38,7 +38,9 @@ export function Compare(_: {}): JSX.Element {
setComparison(msg);
}
} else {
console.error(`Invalid event origin ${evt.origin}`);
// sanitize origin
const origin = evt.origin.replace(/\n|\r/g, '');
console.error(`Invalid event origin ${origin}`);
}
});
});
@@ -64,8 +66,8 @@ export function Compare(_: {}): JSX.Element {
{hasRows ? (
<CompareTable comparison={comparison}></CompareTable>
) : (
<div className="vscode-codeql__compare-message">{message}</div>
)}
<div className="vscode-codeql__compare-message">{message}</div>
)}
</>
);
} catch (err) {

View File

@@ -41,6 +41,7 @@ const ROOT_SETTING = new Setting('codeQL');
// Global configuration
const TELEMETRY_SETTING = new Setting('telemetry', ROOT_SETTING);
const AST_VIEWER_SETTING = new Setting('astViewer', ROOT_SETTING);
const GLOBAL_TELEMETRY_SETTING = new Setting('telemetry');
export const LOG_TELEMETRY = new Setting('logTelemetry', TELEMETRY_SETTING);
@@ -78,6 +79,7 @@ const CACHE_SIZE_SETTING = new Setting('cacheSize', RUNNING_QUERIES_SETTING);
const TIMEOUT_SETTING = new Setting('timeout', RUNNING_QUERIES_SETTING);
const MEMORY_SETTING = new Setting('memory', RUNNING_QUERIES_SETTING);
const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
const MAX_PATHS = new Setting('maxPaths', RUNNING_QUERIES_SETTING);
const RUNNING_TESTS_SETTING = new Setting('runningTests', ROOT_SETTING);
const RESULTS_DISPLAY_SETTING = new Setting('resultsDisplay', ROOT_SETTING);
@@ -86,9 +88,10 @@ export const NUMBER_OF_TEST_THREADS_SETTING = new Setting('numberOfThreads', RUN
export const MAX_QUERIES = new Setting('maxQueries', RUNNING_QUERIES_SETTING);
export const AUTOSAVE_SETTING = new Setting('autoSave', RUNNING_QUERIES_SETTING);
export const PAGE_SIZE = new Setting('pageSize', RESULTS_DISPLAY_SETTING);
const CUSTOM_LOG_DIRECTORY_SETTING = new Setting('customLogDirectory', RUNNING_QUERIES_SETTING);
/** When these settings change, the running query server should be restarted. */
const QUERY_SERVER_RESTARTING_SETTINGS = [NUMBER_OF_THREADS_SETTING, SAVE_CACHE_SETTING, CACHE_SIZE_SETTING, MEMORY_SETTING, DEBUG_SETTING];
const QUERY_SERVER_RESTARTING_SETTINGS = [NUMBER_OF_THREADS_SETTING, SAVE_CACHE_SETTING, CACHE_SIZE_SETTING, MEMORY_SETTING, DEBUG_SETTING, CUSTOM_LOG_DIRECTORY_SETTING];
export interface QueryServerConfig {
codeQlPath: string;
@@ -98,6 +101,7 @@ export interface QueryServerConfig {
cacheSize: number;
queryMemoryMb?: number;
timeoutSecs: number;
customLogDirectory?: string;
onDidChangeConfiguration?: Event<void>;
}
@@ -109,12 +113,13 @@ export interface QueryHistoryConfig {
onDidChangeConfiguration: Event<void>;
}
const CLI_SETTINGS = [ADDITIONAL_TEST_ARGUMENTS_SETTING, NUMBER_OF_TEST_THREADS_SETTING, NUMBER_OF_THREADS_SETTING];
const CLI_SETTINGS = [ADDITIONAL_TEST_ARGUMENTS_SETTING, NUMBER_OF_TEST_THREADS_SETTING, NUMBER_OF_THREADS_SETTING, MAX_PATHS];
export interface CliConfig {
additionalTestArguments: string[];
numberTestThreads: number;
numberThreads: number;
maxPaths: number;
onDidChangeConfiguration?: Event<void>;
}
@@ -144,7 +149,7 @@ export abstract class ConfigListener extends DisposableObject {
protected abstract handleDidChangeConfiguration(e: ConfigurationChangeEvent): void;
private updateConfiguration(): void {
this._onDidChangeConfiguration.fire();
this._onDidChangeConfiguration.fire(undefined);
}
public get onDidChangeConfiguration(): Event<void> {
@@ -186,7 +191,7 @@ export class QueryServerConfigListener extends ConfigListener implements QuerySe
config.push(distributionManager.onDidChangeDistribution(async () => {
const codeQlPath = await distributionManager.getCodeQlPathWithoutVersionCheck();
config._codeQlPath = codeQlPath!;
config._onDidChangeConfiguration.fire();
config._onDidChangeConfiguration.fire(undefined);
}));
}
return config;
@@ -196,6 +201,10 @@ export class QueryServerConfigListener extends ConfigListener implements QuerySe
return this._codeQlPath;
}
public get customLogDirectory(): string | undefined {
return CUSTOM_LOG_DIRECTORY_SETTING.getValue<string>() || undefined;
}
public get numThreads(): number {
return NUMBER_OF_THREADS_SETTING.getValue<number>();
}
@@ -219,7 +228,7 @@ export class QueryServerConfigListener extends ConfigListener implements QuerySe
return undefined;
}
if (memory == 0 || typeof (memory) !== 'number') {
logger.log(`Ignoring value '${memory}' for setting ${MEMORY_SETTING.qualifiedName}`);
void logger.log(`Ignoring value '${memory}' for setting ${MEMORY_SETTING.qualifiedName}`);
return undefined;
}
return memory;
@@ -257,6 +266,10 @@ export class CliConfigListener extends ConfigListener implements CliConfig {
return NUMBER_OF_THREADS_SETTING.getValue<number>();
}
public get maxPaths(): number {
return MAX_PATHS.getValue<number>();
}
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(CLI_SETTINGS, e);
}
@@ -279,3 +292,21 @@ export const CANARY_FEATURES = new Setting('canary', ROOT_SETTING);
export function isCanary() {
return !!CANARY_FEATURES.getValue<boolean>();
}
/**
* Avoids caching in the AST viewer if the user is also a canary user.
*/
export const NO_CACHE_AST_VIEWER = new Setting('disableCache', AST_VIEWER_SETTING);
/**
* Lists of GitHub repositories that you want to query remotely via the "Run Remote query" command.
* Note: This command is only available for internal users.
*
* This setting should be a JSON object where each key is a user-specified name (string),
* and the value is an array of GitHub repositories (of the form `<owner>/<repo>`).
*/
const REMOTE_REPO_LISTS = new Setting('remoteRepositoryLists', ROOT_SETTING);
export function getRemoteRepositoryLists(): Record<string, string[]> | undefined {
return REMOTE_REPO_LISTS.getValue<Record<string, string[]>>() || undefined;
}

View File

@@ -12,7 +12,7 @@ import { ProgressCallback } from '../commandRunner';
import { KeyType } from './keyType';
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
const SELECT_QUERY_NAME = '#select';
export const SELECT_QUERY_NAME = '#select';
export const TEMPLATE_NAME = 'selectedSourceFile';
export interface FullLocationLink extends vscode.LocationLink {

View File

@@ -11,8 +11,9 @@ import {
} from './keyType';
import { CodeQLCliServer } from '../cli';
import { DatabaseItem } from '../databases';
import { QlPacksForLanguage } from '../helpers';
export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<string> {
export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<QlPacksForLanguage> {
if (db.contents === undefined) {
throw new Error('Database is invalid and cannot infer QLPack.');
}
@@ -21,28 +22,85 @@ export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem):
return await helpers.getQlPackForDbscheme(cli, dbscheme);
}
export async function resolveQueries(cli: CodeQLCliServer, qlpack: string, keyType: KeyType): Promise<string[]> {
/**
* Finds the contextual queries with the specified key in a list of CodeQL packs.
*
* @param cli The CLI instance to use.
* @param qlpacks The list of packs to search.
* @param keyType The contextual query key of the query to search for.
* @returns The found queries from the first pack in which any matching queries were found.
*/
async function resolveQueriesFromPacks(cli: CodeQLCliServer, qlpacks: string[], keyType: KeyType): Promise<string[]> {
const suiteFile = (await tmp.file({
postfix: '.qls'
})).path;
const suiteYaml = {
qlpack,
include: {
kind: kindOfKeyType(keyType),
'tags contain': tagOfKeyType(keyType)
}
};
const suiteYaml = [];
for (const qlpack of qlpacks) {
suiteYaml.push({
from: qlpack,
queries: '.',
include: {
kind: kindOfKeyType(keyType),
'tags contain': tagOfKeyType(keyType)
}
});
}
await fs.writeFile(suiteFile, yaml.safeDump(suiteYaml), 'utf8');
const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
if (queries.length === 0) {
helpers.showAndLogErrorMessage(
`No ${nameOfKeyType(keyType)} queries (tagged "${tagOfKeyType(keyType)}") could be found in the current library path. \
Try upgrading the CodeQL libraries. If that doesn't work, then ${nameOfKeyType(keyType)} queries are not yet available \
for this language.`
);
throw new Error(`Couldn't find any queries tagged ${tagOfKeyType(keyType)} for qlpack ${qlpack}`);
}
return queries;
}
export async function resolveQueries(cli: CodeQLCliServer, qlpacks: QlPacksForLanguage, keyType: KeyType): Promise<string[]> {
const cliCanHandleLibraryPack = await cli.cliConstraints.supportsAllowLibraryPacksInResolveQueries();
const packsToSearch: string[] = [];
let blameCli: boolean;
if (cliCanHandleLibraryPack) {
// The CLI can handle both library packs and query packs, so search both packs in order.
packsToSearch.push(qlpacks.dbschemePack);
if (qlpacks.queryPack !== undefined) {
packsToSearch.push(qlpacks.queryPack);
}
// If we don't find the query, it's because it's not there, not because the CLI was unable to
// search the pack.
blameCli = false;
} else {
// Older CLIs can't handle `codeql resolve queries` with a suite that references a library pack.
if (qlpacks.dbschemePackIsLibraryPack) {
if (qlpacks.queryPack !== undefined) {
// Just search the query pack, because some older library/query releases still had the
// contextual queries in the query pack.
packsToSearch.push(qlpacks.queryPack);
}
// If we don't find it, it's because the CLI was unable to search the library pack that
// actually contains the query. Blame any failure on the CLI, not the packs.
blameCli = true;
} else {
// We have an old CLI, but the dbscheme pack is old enough that it's still a unified pack with
// both libraries and queries. Just search that pack.
packsToSearch.push(qlpacks.dbschemePack);
// Any CLI should be able to search the single query pack, so if we don't find it, it's
// because the language doesn't support it.
blameCli = false;
}
}
const queries = await resolveQueriesFromPacks(cli, packsToSearch, keyType);
if (queries.length > 0) {
return queries;
}
// No queries found. Determine the correct error message for the various scenarios.
const errorMessage = blameCli ?
`Your current version of the CodeQL CLI, '${(await cli.getVersion()).version}', \
is unable to use contextual queries from recent versions of the standard CodeQL libraries. \
Please upgrade to the latest version of the CodeQL CLI.`
:
`No ${nameOfKeyType(keyType)} queries (tagged "${tagOfKeyType(keyType)}") could be found in the current library path. \
Try upgrading the CodeQL libraries. If that doesn't work, then ${nameOfKeyType(keyType)} queries are not yet available \
for this language.`;
void helpers.showAndLogErrorMessage(errorMessage);
throw new Error(`Couldn't find any queries tagged ${tagOfKeyType(keyType)} in any of the following packs: ${packsToSearch.join(', ')}.`);
}

View File

@@ -25,6 +25,7 @@ import {
} from './keyType';
import { FullLocationLink, getLocationsForUriString, TEMPLATE_NAME } from './locationFinder';
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
import { isCanary, NO_CACHE_AST_VIEWER } from '../config';
/**
* Run templated CodeQL queries to find definitions and references in
@@ -141,7 +142,9 @@ export class TemplatePrintAstProvider {
if (!document) {
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
}
const queryResults = await this.cache.get(document.uri.toString(), progress, token);
const queryResults = this.shouldCache()
? await this.cache.get(document.uri.toString(), progress, token)
: await this.getAst(document.uri.toString(), progress, token);
return new AstBuilder(
queryResults, this.cli,
@@ -150,6 +153,10 @@ export class TemplatePrintAstProvider {
);
}
private shouldCache() {
return !(isCanary() && NO_CACHE_AST_VIEWER.getValue<boolean>());
}
private async getAst(
uriString: string,
progress: ProgressCallback,
@@ -168,8 +175,8 @@ export class TemplatePrintAstProvider {
throw new Error('Can\'t infer database from the provided source.');
}
const qlpack = await qlpackOfDatabase(this.cli, db);
const queries = await resolveQueries(this.cli, qlpack, KeyType.PrintAstQuery);
const qlpacks = await qlpackOfDatabase(this.cli, db);
const queries = await resolveQueries(this.cli, qlpacks, KeyType.PrintAstQuery);
if (queries.length > 1) {
throw new Error('Found multiple Print AST queries. Can\'t continue');
}

View File

@@ -51,8 +51,8 @@ export async function promptImportInternetDatabase(
);
if (item) {
commands.executeCommand('codeQLDatabases.focus');
showAndLogInformationMessage('Database downloaded and imported successfully.');
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database downloaded and imported successfully.');
}
return item;
@@ -91,8 +91,8 @@ export async function promptImportLgtmDatabase(
token
);
if (item) {
commands.executeCommand('codeQLDatabases.focus');
showAndLogInformationMessage('Database downloaded and imported successfully.');
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database downloaded and imported successfully.');
}
return item;
}
@@ -125,8 +125,8 @@ export async function importArchiveDatabase(
token
);
if (item) {
commands.executeCommand('codeQLDatabases.focus');
showAndLogInformationMessage('Database unzipped and imported successfully.');
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database unzipped and imported successfully.');
}
return item;
} catch (e) {
@@ -433,7 +433,7 @@ export async function convertToDatabaseUrl(lgtmUrl: string) {
language,
].join('/')}`;
} catch (e) {
logger.log(`Error: ${e.message}`);
void logger.log(`Error: ${e.message}`);
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
}
}

View File

@@ -108,7 +108,7 @@ class DatabaseTreeDataProvider extends DisposableObject
}
private handleDidChangeDatabaseItem = (event: DatabaseChangedEvent): void => {
// Note that events from the databse manager are instances of DatabaseChangedEvent
// Note that events from the database manager are instances of DatabaseChangedEvent
// and events fired by the UI are instances of DatabaseItem
// When event.item is undefined, then the entire tree is refreshed.
@@ -179,7 +179,7 @@ class DatabaseTreeDataProvider extends DisposableObject
public set sortOrder(newSortOrder: SortOrder) {
this._sortOrder = newSortOrder;
this._onDidChangeTreeData.fire();
this._onDidChangeTreeData.fire(undefined);
}
}
@@ -234,7 +234,7 @@ export class DatabaseUI extends DisposableObject {
}
init() {
logger.log('Registering database panel commands.');
void logger.log('Registering database panel commands.');
this.push(
commandRunnerWithProgress(
'codeQL.setCurrentDatabase',
@@ -295,7 +295,7 @@ export class DatabaseUI extends DisposableObject {
'codeQLDatabases.chooseDatabaseLgtm',
this.handleChooseDatabaseLgtm,
{
title: 'Adding database from LGTM',
title: 'Adding database from LGTM. Choose a language from the dropdown, if requested.',
})
);
this.push(
@@ -348,6 +348,12 @@ export class DatabaseUI extends DisposableObject {
this.handleOpenFolder
)
);
this.push(
commandRunner(
'codeQLDatabases.addDatabaseSource',
this.handleAddSource
)
);
this.push(
commandRunner(
'codeQLDatabases.removeOrphanedDatabases',
@@ -369,20 +375,20 @@ export class DatabaseUI extends DisposableObject {
try {
return await this.chooseAndSetDatabase(true, progress, token);
} catch (e) {
showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(e.message);
return undefined;
}
};
handleRemoveOrphanedDatabases = async (): Promise<void> => {
logger.log('Removing orphaned databases from workspace storage.');
void logger.log('Removing orphaned databases from workspace storage.');
let dbDirs = undefined;
if (
!(await fs.pathExists(this.storagePath)) ||
!(await fs.stat(this.storagePath)).isDirectory()
) {
logger.log('Missing or invalid storage directory. Not trying to remove orphaned databases.');
void logger.log('Missing or invalid storage directory. Not trying to remove orphaned databases.');
return;
}
@@ -403,7 +409,7 @@ export class DatabaseUI extends DisposableObject {
dbDirs = await asyncFilter(dbDirs, isLikelyDatabaseRoot);
if (!dbDirs.length) {
logger.log('No orphaned databases found.');
void logger.log('No orphaned databases found.');
return;
}
@@ -412,8 +418,8 @@ export class DatabaseUI extends DisposableObject {
await Promise.all(
dbDirs.map(async dbDir => {
try {
logger.log(`Deleting orphaned database '${dbDir}'.`);
await fs.rmdir(dbDir, { recursive: true } as any); // typings doesn't recognize the options argument
void logger.log(`Deleting orphaned database '${dbDir}'.`);
await fs.remove(dbDir);
} catch (e) {
failures.push(`${path.basename(dbDir)}`);
}
@@ -422,10 +428,9 @@ export class DatabaseUI extends DisposableObject {
if (failures.length) {
const dirname = path.dirname(failures[0]);
showAndLogErrorMessage(
`Failed to delete unused databases:\n ${
failures.join('\n ')
}\n. To delete unused databases, please remove them manually from the storage folder ${dirname}.`
void showAndLogErrorMessage(
`Failed to delete unused databases (${failures.join(', ')
}).\nTo delete unused databases, please remove them manually from the storage folder ${dirname}.`
);
}
};
@@ -438,7 +443,7 @@ export class DatabaseUI extends DisposableObject {
try {
return await this.chooseAndSetDatabase(false, progress, token);
} catch (e) {
showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(e.message);
return undefined;
}
};
@@ -583,8 +588,7 @@ export class DatabaseUI extends DisposableObject {
} catch (e) {
// rethrow and let this be handled by default error handling.
throw new Error(
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${
e.message
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${e.message
}`
);
}
@@ -617,7 +621,7 @@ export class DatabaseUI extends DisposableObject {
});
if (newName) {
this.databaseManager.renameDatabaseItem(databaseItem, newName);
await this.databaseManager.renameDatabaseItem(databaseItem, newName);
}
};
@@ -634,6 +638,24 @@ export class DatabaseUI extends DisposableObject {
}
};
/**
* Adds the source folder of a CodeQL database to the workspace.
* When a database is first added in the "Databases" view, its source folder is added to the workspace.
* If the source folder is removed from the workspace for some reason, we want to be able to re-add it if need be.
*/
private handleAddSource = async (
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined
): Promise<void> => {
if (multiSelect?.length) {
for (const dbItem of multiSelect) {
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
}
} else {
await this.databaseManager.addDatabaseSourceArchiveFolder(databaseItem);
}
};
/**
* Return the current database directory. If we don't already have a
* current database, ask the user for one, and return that, or
@@ -703,7 +725,7 @@ export class DatabaseUI extends DisposableObject {
* 2. If the selected URI is a directory matching db-*, choose the containing directory
* 3. choose the current directory
*
* @param uri a URI that is a datbase folder or inside it
* @param uri a URI that is a database folder or inside it
*
* @return the actual database folder found by using the heuristics above.
*/

View File

@@ -115,7 +115,7 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
const dbAbsolutePath = path.join(parentDirectory, dbRelativePaths[0]);
if (dbRelativePaths.length > 1) {
showAndLogWarningMessage(`Found multiple dataset directories in database, using '${dbAbsolutePath}'.`);
void showAndLogWarningMessage(`Found multiple dataset directories in database, using '${dbAbsolutePath}'.`);
}
return vscode.Uri.file(dbAbsolutePath);
@@ -138,7 +138,7 @@ async function findSourceArchive(
}
}
if (!silent) {
showAndLogInformationMessage(
void showAndLogInformationMessage(
`Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`
);
}
@@ -265,6 +265,11 @@ export interface DatabaseItem {
*/
belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean;
/**
* Whether the database may be affected by test execution for the given path.
*/
isAffectedByTest(testPath: string): Promise<boolean>;
/**
* Gets the state of this database, to be persisted in the workspace state.
*/
@@ -470,6 +475,27 @@ export class DatabaseItemImpl implements DatabaseItem {
return uri.scheme === zipArchiveScheme &&
decodeSourceArchiveUri(uri).sourceArchiveZipPath === this.sourceArchive.fsPath;
}
public async isAffectedByTest(testPath: string): Promise<boolean> {
const databasePath = this.databaseUri.fsPath;
if (!databasePath.endsWith('.testproj')) {
return false;
}
try {
const stats = await fs.stat(testPath);
if (stats.isDirectory()) {
return !path.relative(testPath, databasePath).startsWith('..');
} else {
// database for /one/two/three/test.ql is at /one/two/three/three.testproj
const testdir = path.dirname(testPath);
const testdirbase = path.basename(testdir);
return databasePath == path.join(testdir, testdirbase + '.testproj');
}
} catch {
// No information available for test path - assume database is unaffected.
return false;
}
}
}
/**
@@ -480,7 +506,7 @@ export class DatabaseItemImpl implements DatabaseItem {
function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | undefined> {
return new Promise((res, _rej) => {
const timeout = setTimeout(() => {
logger.log(`Waiting for event ${event} timed out after ${timeoutMs}ms`);
void logger.log(`Waiting for event ${event} timed out after ${timeoutMs}ms`);
res(undefined);
dispose();
}, timeoutMs);
@@ -517,7 +543,7 @@ export class DatabaseManager extends DisposableObject {
qs.onDidStartQueryServer(this.reregisterDatabases.bind(this));
// Let this run async.
this.loadPersistedState();
void this.loadPersistedState();
}
public async openDatabase(
@@ -561,7 +587,7 @@ export class DatabaseManager extends DisposableObject {
}));
}
private async addDatabaseSourceArchiveFolder(item: DatabaseItem) {
public async addDatabaseSourceArchiveFolder(item: DatabaseItem) {
// The folder may already be in workspace state from a previous
// session. If not, add it.
const index = this.getDatabaseWorkspaceFolderIndex(item);
@@ -579,15 +605,15 @@ export class DatabaseManager extends DisposableObject {
const end = (vscode.workspace.workspaceFolders || []).length;
const uri = item.getSourceArchiveExplorerUri();
if (uri === undefined) {
logger.log(`Couldn't obtain file explorer uri for ${item.name}`);
void logger.log(`Couldn't obtain file explorer uri for ${item.name}`);
}
else {
logger.log(`Adding workspace folder for ${item.name} source archive at index ${end}`);
void logger.log(`Adding workspace folder for ${item.name} source archive at index ${end}`);
if ((vscode.workspace.workspaceFolders || []).length < 2) {
// Adding this workspace folder makes the workspace
// multi-root, which may surprise the user. Let them know
// we're doing this.
vscode.window.showInformationMessage(`Adding workspace folder for source archive of database ${item.name}.`);
void vscode.window.showInformationMessage(`Adding workspace folder for source archive of database ${item.name}.`);
}
vscode.workspace.updateWorkspaceFolders(end, 0, {
name: `[${item.name} source archive]`,
@@ -670,7 +696,7 @@ export class DatabaseManager extends DisposableObject {
await databaseItem.refresh();
await this.registerDatabase(progress, token, databaseItem);
if (currentDatabaseUri === database.uri) {
this.setCurrentDatabaseItem(databaseItem, true);
await this.setCurrentDatabaseItem(databaseItem, true);
}
}
catch (e) {
@@ -680,7 +706,7 @@ export class DatabaseManager extends DisposableObject {
}
} catch (e) {
// database list had an unexpected type - nothing to be done?
showAndLogErrorMessage(`Database list loading failed: ${e.message}`);
void showAndLogErrorMessage(`Database list loading failed: ${e.message}`);
}
});
}
@@ -737,7 +763,7 @@ export class DatabaseManager extends DisposableObject {
item: DatabaseItem
) {
this._databaseItems.push(item);
this.updatePersistedDatabaseList();
await this.updatePersistedDatabaseList();
// Add this database item to the allow-list
// Database items reconstituted from persisted state
@@ -754,7 +780,7 @@ export class DatabaseManager extends DisposableObject {
public async renameDatabaseItem(item: DatabaseItem, newName: string) {
item.name = newName;
this.updatePersistedDatabaseList();
await this.updatePersistedDatabaseList();
this._onDidChangeDatabaseItem.fire({
// pass undefined so that the entire tree is rebuilt in order to re-sort
item: undefined,
@@ -774,23 +800,23 @@ export class DatabaseManager extends DisposableObject {
if (index >= 0) {
this._databaseItems.splice(index, 1);
}
this.updatePersistedDatabaseList();
await this.updatePersistedDatabaseList();
// Delete folder from workspace, if it is still there
const folderIndex = (vscode.workspace.workspaceFolders || []).findIndex(
folder => item.belongsToSourceArchiveExplorerUri(folder.uri)
);
if (folderIndex >= 0) {
logger.log(`Removing workspace folder at index ${folderIndex}`);
void logger.log(`Removing workspace folder at index ${folderIndex}`);
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
}
// Delete folder from file system only if it is controlled by the extension
if (this.isExtensionControlledLocation(item.databaseUri)) {
logger.log('Deleting database from filesystem.');
void logger.log('Deleting database from filesystem.');
fs.remove(item.databaseUri.fsPath).then(
() => logger.log(`Deleted '${item.databaseUri.fsPath}'`),
e => logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${e.message}`));
() => void logger.log(`Deleted '${item.databaseUri.fsPath}'`),
e => void logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${e.message}`));
}
// Remove this database item from the allow-list
@@ -808,7 +834,7 @@ export class DatabaseManager extends DisposableObject {
token: vscode.CancellationToken,
dbItem: DatabaseItem,
) {
if (dbItem.contents && (await this.qs.supportsDatabaseRegistration())) {
if (dbItem.contents && (await this.cli.cliConstraints.supportsDatabaseRegistration())) {
const databases: Dataset[] = [{
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: 'default'
@@ -822,7 +848,7 @@ export class DatabaseManager extends DisposableObject {
token: vscode.CancellationToken,
dbItem: DatabaseItem,
) {
if (dbItem.contents && (await this.qs.supportsDatabaseRegistration())) {
if (dbItem.contents && (await this.cli.cliConstraints.supportsDatabaseRegistration())) {
const databases: Dataset[] = [{
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: 'default'
@@ -832,12 +858,12 @@ export class DatabaseManager extends DisposableObject {
}
private updatePersistedCurrentDatabaseItem(): void {
this.ctx.workspaceState.update(CURRENT_DB, this._currentDatabaseItem ?
void this.ctx.workspaceState.update(CURRENT_DB, this._currentDatabaseItem ?
this._currentDatabaseItem.databaseUri.toString(true) : undefined);
}
private updatePersistedDatabaseList(): void {
this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
private async updatePersistedDatabaseList(): Promise<void> {
await this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
}
private isExtensionControlledLocation(uri: vscode.Uri) {
@@ -852,7 +878,7 @@ export class DatabaseManager extends DisposableObject {
}
private async getPrimaryLanguage(dbPath: string) {
if (!(await this.cli.supportsLanguageName())) {
if (!(await this.cli.cliConstraints.supportsLanguageName())) {
// return undefined so that we recalculate on restart until the cli is at a version that
// supports this feature. This recalculation is cheap since we avoid calling into the cli
// unless we know it can return the langauges property.

View File

@@ -59,23 +59,23 @@ export abstract class Discovery<T> extends DisposableObject {
this.discoveryInProgress = false;
this.update(results);
}
});
})
discoveryPromise.catch(err => {
logger.log(`${this.name} failed. Reason: ${err.message}`);
});
.catch(err => {
void logger.log(`${this.name} failed. Reason: ${err.message}`);
})
discoveryPromise.finally(() => {
if (this.retry) {
// Another refresh request came in while we were still running a previous discovery
// operation. Since the discovery results we just computed are now stale, we'll launch
// another discovery operation instead of updating.
// Note that by doing this inside of `finally`, we will relaunch discovery even if the
// initial discovery operation failed.
this.retry = false;
this.launchDiscovery();
}
});
.finally(() => {
if (this.retry) {
// Another refresh request came in while we were still running a previous discovery
// operation. Since the discovery results we just computed are now stale, we'll launch
// another discovery operation instead of updating.
// Note that by doing this inside of `finally`, we will relaunch discovery even if the
// initial discovery operation failed.
this.retry = false;
this.launchDiscovery();
}
});
}
/**

View File

@@ -153,7 +153,7 @@ export class DistributionManager implements DistributionProvider {
// Check config setting, then extension specific distribution, then PATH.
if (this.config.customCodeQlPath) {
if (!await fs.pathExists(this.config.customCodeQlPath)) {
showAndLogErrorMessage(`The CodeQL executable path is specified as "${this.config.customCodeQlPath}" ` +
void showAndLogErrorMessage(`The CodeQL executable path is specified as "${this.config.customCodeQlPath}" ` +
'by a configuration setting, but a CodeQL executable could not be found at that path. Please check ' +
'that a CodeQL executable exists at the specified path or remove the setting.');
return undefined;
@@ -191,7 +191,7 @@ export class DistributionManager implements DistributionProvider {
};
}
}
logger.log('INFO: Could not find CodeQL on path.');
void logger.log('INFO: Could not find CodeQL on path.');
}
return undefined;
@@ -276,7 +276,7 @@ class ExtensionSpecificDistributionManager {
try {
await this.removeDistribution();
} catch (e) {
logger.log('WARNING: Tried to remove corrupted CodeQL CLI at ' +
void logger.log('WARNING: Tried to remove corrupted CodeQL CLI at ' +
`${this.getDistributionStoragePath()} but encountered an error: ${e}.`);
}
}
@@ -313,7 +313,7 @@ class ExtensionSpecificDistributionManager {
progressCallback?: ProgressCallback): Promise<void> {
await this.downloadDistribution(release, progressCallback);
// Store the installed release within the global extension state.
this.storeInstalledRelease(release);
await this.storeInstalledRelease(release);
}
private async downloadDistribution(release: Release,
@@ -321,7 +321,7 @@ class ExtensionSpecificDistributionManager {
try {
await this.removeDistribution();
} catch (e) {
logger.log(`Tried to clean up old version of CLI at ${this.getDistributionStoragePath()} ` +
void logger.log(`Tried to clean up old version of CLI at ${this.getDistributionStoragePath()} ` +
`but encountered an error: ${e}.`);
}
@@ -332,7 +332,7 @@ class ExtensionSpecificDistributionManager {
throw new Error(`Invariant violation: chose a release to install that didn't have ${requiredAssetName}`);
}
if (assets.length > 1) {
logger.log('WARNING: chose a release with more than one asset to install, found ' +
void logger.log('WARNING: chose a release with more than one asset to install, found ' +
assets.map(asset => asset.name).join(', '));
}
@@ -345,7 +345,7 @@ class ExtensionSpecificDistributionManager {
const contentLength = assetStream.headers.get('content-length');
const totalNumBytes = contentLength ? parseInt(contentLength, 10) : undefined;
reportStreamProgress(assetStream.body, 'Downloading CodeQL CLI…', totalNumBytes, progressCallback);
reportStreamProgress(assetStream.body, `Downloading CodeQL CLI ${release.name}`, totalNumBytes, progressCallback);
await new Promise((resolve, reject) =>
assetStream.body.pipe(archiveFile)
@@ -355,7 +355,7 @@ class ExtensionSpecificDistributionManager {
await this.bumpDistributionFolderIndex();
logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
void logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
await extractZipArchive(archivePath, this.getDistributionStoragePath());
} finally {
await fs.remove(tmpDirectory);
@@ -368,7 +368,7 @@ class ExtensionSpecificDistributionManager {
* This should not be called for a distribution that is currently in use, as remove may fail.
*/
private async removeDistribution(): Promise<void> {
this.storeInstalledRelease(undefined);
await this.storeInstalledRelease(undefined);
if (await fs.pathExists(this.getDistributionStoragePath())) {
await fs.remove(this.getDistributionStoragePath());
}
@@ -376,7 +376,7 @@ class ExtensionSpecificDistributionManager {
private async getLatestRelease(): Promise<Release> {
const requiredAssetName = DistributionManager.getRequiredAssetName();
logger.log(`Searching for latest release including ${requiredAssetName}.`);
void logger.log(`Searching for latest release including ${requiredAssetName}.`);
return this.createReleasesApiConsumer().getLatestRelease(
this.versionRange,
this.config.includePrerelease,
@@ -384,11 +384,11 @@ class ExtensionSpecificDistributionManager {
const matchingAssets = release.assets.filter(asset => asset.name === requiredAssetName);
if (matchingAssets.length === 0) {
// For example, this could be a release with no platform-specific assets.
logger.log(`INFO: Ignoring a release with no assets named ${requiredAssetName}`);
void logger.log(`INFO: Ignoring a release with no assets named ${requiredAssetName}`);
return false;
}
if (matchingAssets.length > 1) {
logger.log(`WARNING: Ignoring a release with more than one asset named ${requiredAssetName}`);
void logger.log(`WARNING: Ignoring a release with more than one asset named ${requiredAssetName}`);
return false;
}
return true;
@@ -707,16 +707,14 @@ export async function getExecutableFromDirectory(directory: string, warnWhenNotF
return alternateExpectedLauncherPath;
}
if (warnWhenNotFound) {
logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
void logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
'Will try PATH.');
}
return undefined;
}
function warnDeprecatedLauncher() {
showAndLogWarningMessage(
void showAndLogWarningMessage(
`The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` +
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`
);

View File

@@ -10,24 +10,27 @@ import {
Uri,
window as Window,
env,
window
window,
QuickPickItem
} from 'vscode';
import { LanguageClient } from 'vscode-languageclient';
import * as os from 'os';
import * as path from 'path';
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
import { AstViewer } from './astViewer';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { CodeQLCliServer } from './cli';
import { CodeQLCliServer, CliVersionConstraint } from './cli';
import {
CliConfigListener,
DistributionConfigListener,
isCanary,
MAX_QUERIES,
QueryHistoryConfigListener,
QueryServerConfigListener
} from './config';
import * as languageSupport from './languageSupport';
import { DatabaseManager } from './databases';
import { DatabaseItem, DatabaseManager } from './databases';
import { DatabaseUI } from './databases-ui';
import {
TemplateQueryDefinitionProvider,
@@ -69,6 +72,9 @@ import {
} from './commandRunner';
import { CodeQlStatusBarHandler } from './status-bar';
import { Credentials } from './authentication';
import { runRemoteQuery } from './run-remote-query';
/**
* extension.ts
* ------------
@@ -139,7 +145,7 @@ export interface CodeQLExtensionInterface {
/**
* Returns the CodeQLExtensionInterface, or an empty object if the interface is not
* available afer activation is complete. This will happen if there is no cli
* available after activation is complete. This will happen if there is no cli
* installed when the extension starts. Downloading and installing the cli
* will happen at a later time.
*
@@ -147,14 +153,14 @@ export interface CodeQLExtensionInterface {
*
* @returns CodeQLExtensionInterface
*/
export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionInterface | {}> {
logger.log(`Starting ${extensionId} extension`);
export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionInterface | Record<string, never>> {
void logger.log(`Starting ${extensionId} extension`);
if (extension === undefined) {
throw new Error(`Can't find extension ${extensionId}`);
}
const distributionConfigListener = new DistributionConfigListener();
initializeLogging(ctx);
await initializeLogging(ctx);
await initializeTelemetry(extension, ctx);
languageSupport.install();
@@ -165,7 +171,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
const shouldUpdateOnNextActivationKey = 'shouldUpdateOnNextActivation';
registerErrorStubs([checkForUpdatesCommand], command => (async () => {
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
void helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
}));
interface DistributionUpdateConfig {
@@ -177,7 +183,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
const minSecondsSinceLastUpdateCheck = config.isUserInitiated ? 0 : 86400;
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
helpers.showAndLogInformationMessage : async (message: string) => logger.log(message);
helpers.showAndLogInformationMessage : async (message: string) => void logger.log(message);
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
// We do want to auto update if there is no distribution at all
@@ -185,7 +191,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
switch (result.kind) {
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
logger.log('Didn\'t perform CodeQL CLI update check since a check was already performed within the previous ' +
void logger.log('Didn\'t perform CodeQL CLI update check since a check was already performed within the previous ' +
`${minSecondsSinceLastUpdateCheck} seconds.`);
break;
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
@@ -212,7 +218,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress));
await ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
void helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
}
break;
default:
@@ -244,12 +250,12 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
codeQlInstalled ? 'check for updates to' : 'install') + ' CodeQL CLI';
if (e instanceof GithubRateLimitedError) {
alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
void alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString(env.language)}.`);
} else if (e instanceof GithubApiError) {
alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
void alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
}
alertFunction(`Unable to ${taskDescription}. ` + e);
void alertFunction(`Unable to ${taskDescription}. ` + e);
} finally {
isInstallingOrUpdatingDistribution = false;
}
@@ -259,7 +265,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
const result = await distributionManager.getDistribution();
switch (result.kind) {
case FindDistributionResultKind.CompatibleDistribution:
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.raw})`);
void logger.log(`Found compatible version of CodeQL CLI (version ${result.version.raw})`);
break;
case FindDistributionResultKind.IncompatibleDistribution: {
const fixGuidanceMessage = (() => {
@@ -274,16 +280,20 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
}
})();
helpers.showAndLogWarningMessage(`The current version of the CodeQL CLI (${result.version.raw}) ` +
'is incompatible with this extension. ' + fixGuidanceMessage);
void helpers.showAndLogWarningMessage(
`The current version of the CodeQL CLI (${result.version.raw}) ` +
`is incompatible with this extension. ${fixGuidanceMessage}`
);
break;
}
case FindDistributionResultKind.UnknownCompatibilityDistribution:
helpers.showAndLogWarningMessage('Compatibility with the configured CodeQL CLI could not be determined. ' +
'You may experience problems using the extension.');
void helpers.showAndLogWarningMessage(
'Compatibility with the configured CodeQL CLI could not be determined. ' +
'You may experience problems using the extension.'
);
break;
case FindDistributionResultKind.NoDistribution:
helpers.showAndLogErrorMessage('The CodeQL CLI could not be found.');
void helpers.showAndLogErrorMessage('The CodeQL CLI could not be found.');
break;
default:
assertNever(result);
@@ -293,13 +303,13 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
async function installOrUpdateThenTryActivate(
config: DistributionUpdateConfig
): Promise<CodeQLExtensionInterface | {}> {
): Promise<CodeQLExtensionInterface | Record<string, never>> {
await installOrUpdateDistribution(config);
// Display the warnings even if the extension has already activated.
const distributionResult = await getDistributionDisplayingDistributionWarnings();
let extensionInterface: CodeQLExtensionInterface | {} = {};
let extensionInterface: CodeQLExtensionInterface | Record<string, never> = {};
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
extensionInterface = await activateWithInstalledDistribution(
ctx,
@@ -310,7 +320,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
registerErrorStubs([checkForUpdatesCommand], command => async () => {
const installActionName = 'Install CodeQL CLI';
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, {
const chosenAction = await void helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, {
items: [installActionName]
});
if (chosenAction === installActionName) {
@@ -356,13 +366,13 @@ async function activateWithInstalledDistribution(
// of activation.
errorStubs.forEach((stub) => stub.dispose());
logger.log('Initializing configuration listener...');
void logger.log('Initializing configuration listener...');
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(
distributionManager
);
ctx.subscriptions.push(qlConfigurationListener);
logger.log('Initializing CodeQL cli server...');
void logger.log('Initializing CodeQL cli server...');
const cliServer = new CodeQLCliServer(
distributionManager,
new CliConfigListener(),
@@ -373,12 +383,13 @@ async function activateWithInstalledDistribution(
const statusBar = new CodeQlStatusBarHandler(cliServer, distributionConfigListener);
ctx.subscriptions.push(statusBar);
logger.log('Initializing query server client.');
void logger.log('Initializing query server client.');
const qs = new qsClient.QueryServerClient(
qlConfigurationListener,
cliServer,
{
logger: queryServerLogger,
contextStoragePath: getContextStoragePath(ctx),
},
(task) =>
Window.withProgress(
@@ -389,10 +400,10 @@ async function activateWithInstalledDistribution(
ctx.subscriptions.push(qs);
await qs.startQueryServer();
logger.log('Initializing database manager.');
void logger.log('Initializing database manager.');
const dbm = new DatabaseManager(ctx, qs, cliServer, logger);
ctx.subscriptions.push(dbm);
logger.log('Initializing database panel.');
void logger.log('Initializing database panel.');
const databaseUI = new DatabaseUI(
dbm,
qs,
@@ -402,7 +413,7 @@ async function activateWithInstalledDistribution(
databaseUI.init();
ctx.subscriptions.push(databaseUI);
logger.log('Initializing query history manager.');
void logger.log('Initializing query history manager.');
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
ctx.subscriptions.push(queryHistoryConfigurationListener);
const showResults = async (item: CompletedQuery) =>
@@ -417,11 +428,11 @@ async function activateWithInstalledDistribution(
showResultsForComparison(from, to),
);
ctx.subscriptions.push(qhm);
logger.log('Initializing results panel interface.');
void logger.log('Initializing results panel interface.');
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
logger.log('Initializing compare panel interface.');
void logger.log('Initializing compare panel interface.');
const cmpm = new CompareInterfaceManager(
ctx,
dbm,
@@ -431,7 +442,7 @@ async function activateWithInstalledDistribution(
);
ctx.subscriptions.push(cmpm);
logger.log('Initializing source archive filesystem provider.');
void logger.log('Initializing source archive filesystem provider.');
archiveFilesystemProvider.activate(ctx);
async function showResultsForComparison(
@@ -441,7 +452,7 @@ async function activateWithInstalledDistribution(
try {
await cmpm.showResults(from, to);
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
void helpers.showAndLogErrorMessage(e.message);
}
}
@@ -457,16 +468,18 @@ async function activateWithInstalledDistribution(
selectedQuery: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
databaseItem: DatabaseItem | undefined,
): Promise<void> {
if (qs !== undefined) {
const dbItem = await databaseUI.getDatabaseItem(progress, token);
if (dbItem === undefined) {
// If no databaseItem is specified, use the database currently selected in the Databases UI
databaseItem = databaseItem || await databaseUI.getDatabaseItem(progress, token);
if (databaseItem === undefined) {
throw new Error('Can\'t run query without a selected database');
}
const info = await compileAndRunQueryAgainstDatabase(
cliServer,
qs,
dbItem,
databaseItem,
quickEval,
selectedQuery,
progress,
@@ -480,9 +493,27 @@ async function activateWithInstalledDistribution(
}
}
async function openReferencedFile(
selectedQuery: Uri
): Promise<void> {
if (qs !== undefined) {
if (await cliServer.cliConstraints.supportsResolveQlref()) {
const resolved = await cliServer.resolveQlref(selectedQuery.path);
const uri = Uri.file(resolved.resolvedPath);
await window.showTextDocument(uri, { preview: false });
} else {
void helpers.showAndLogErrorMessage(
'Jumping from a .qlref file to the .ql file it references is not '
+ 'supported with the CLI version you are running.\n'
+ `Please upgrade your CLI to version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_QLREF
} or later to use this feature.`);
}
}
}
ctx.subscriptions.push(tmpDirDisposal);
logger.log('Initializing CodeQL language server.');
void logger.log('Initializing CodeQL language server.');
const client = new LanguageClient(
'CodeQL Language Server',
() => spawnIdeServer(qlConfigurationListener),
@@ -500,20 +531,20 @@ async function activateWithInstalledDistribution(
true
);
logger.log('Initializing QLTest interface.');
void logger.log('Initializing QLTest interface.');
const testExplorerExtension = extensions.getExtension<TestHub>(
testExplorerExtensionId
);
if (testExplorerExtension) {
const testHub = testExplorerExtension.exports;
const testAdapterFactory = new QLTestAdapterFactory(testHub, cliServer);
const testAdapterFactory = new QLTestAdapterFactory(testHub, cliServer, dbm);
ctx.subscriptions.push(testAdapterFactory);
const testUIService = new TestUIService(testHub);
ctx.subscriptions.push(testUIService);
}
logger.log('Registering top-level command palette commands.');
void logger.log('Registering top-level command palette commands.');
ctx.subscriptions.push(
commandRunnerWithProgress(
'codeQL.runQuery',
@@ -521,13 +552,80 @@ async function activateWithInstalledDistribution(
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
) => await compileAndRunQuery(false, uri, progress, token),
) => await compileAndRunQuery(false, uri, progress, token, undefined),
{
title: 'Running query',
cancellable: true
}
)
);
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
}
ctx.subscriptions.push(
commandRunnerWithProgress(
'codeQL.runQueryOnMultipleDatabases',
async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
) => {
let filteredDBs = dbm.databaseItems;
if (filteredDBs.length === 0) {
void helpers.showAndLogErrorMessage('No databases found. Please add a suitable database to your workspace.');
return;
}
// If possible, only show databases with the right language (otherwise show all databases).
const queryLanguage = await helpers.findLanguage(cliServer, uri);
if (queryLanguage) {
filteredDBs = dbm.databaseItems.filter(db => db.language === queryLanguage);
if (filteredDBs.length === 0) {
void helpers.showAndLogErrorMessage(`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`);
return;
}
}
const quickPickItems = filteredDBs.map<DatabaseQuickPickItem>(dbItem => (
{
databaseItem: dbItem,
label: dbItem.name,
description: dbItem.language,
}
));
/**
* Databases that were selected in the quick pick menu.
*/
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
quickPickItems,
{ canPickMany: true, ignoreFocusOut: true }
);
if (quickpick !== undefined) {
// Collect all skipped databases and display them at the end (instead of popping up individual errors)
const skippedDatabases = [];
const errors = [];
for (const item of quickpick) {
try {
await compileAndRunQuery(false, uri, progress, token, item.databaseItem);
} catch (error) {
skippedDatabases.push(item.label);
errors.push(error.message);
}
}
if (skippedDatabases.length > 0) {
void logger.log(`Errors:\n${errors.join('\n')}`);
void helpers.showAndLogWarningMessage(
`The following databases were skipped:\n${skippedDatabases.join('\n')}.\nFor details about the errors, see the logs.`
);
}
} else {
void helpers.showAndLogErrorMessage('No databases selected.');
}
},
{
title: 'Running query on selected databases',
cancellable: true
}
)
);
ctx.subscriptions.push(
commandRunnerWithProgress(
'codeQL.runQueries',
@@ -583,7 +681,7 @@ async function activateWithInstalledDistribution(
});
await Promise.all(queryUris.map(async uri =>
compileAndRunQuery(false, uri, wrappedProgress, token)
compileAndRunQuery(false, uri, wrappedProgress, token, undefined)
.then(() => queriesRemaining--)
));
},
@@ -599,7 +697,7 @@ async function activateWithInstalledDistribution(
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
) => await compileAndRunQuery(true, uri, progress, token),
) => await compileAndRunQuery(true, uri, progress, token, undefined),
{
title: 'Running query',
cancellable: true
@@ -616,6 +714,23 @@ async function activateWithInstalledDistribution(
}
)
);
// The "runRemoteQuery" command is internal-only.
ctx.subscriptions.push(
commandRunner('codeQL.runRemoteQuery', async (
uri: Uri | undefined
) => {
if (isCanary()) {
const credentials = await Credentials.initialize(ctx);
await runRemoteQuery(cliServer, credentials, uri || window.activeTextEditor?.document.uri);
}
})
);
ctx.subscriptions.push(
commandRunner(
'codeQL.openReferencedFile',
openReferencedFile
)
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.restartQueryServer', async (
@@ -623,7 +738,7 @@ async function activateWithInstalledDistribution(
token: CancellationToken
) => {
await qs.restartQueryServer(progress, token);
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', {
void helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', {
outputLogger: queryServerLogger,
});
}, {
@@ -656,7 +771,7 @@ async function activateWithInstalledDistribution(
) =>
databaseUI.handleChooseDatabaseLgtm(progress, token),
{
title: 'Adding database from LGTM',
title: 'Adding database from LGTM. Choose a language from the dropdown, if requested.',
})
);
ctx.subscriptions.push(
@@ -675,11 +790,37 @@ async function activateWithInstalledDistribution(
commandRunner('codeQL.openDocumentation', async () =>
env.openExternal(Uri.parse('https://codeql.github.com/docs/'))));
logger.log('Starting language server.');
ctx.subscriptions.push(
commandRunner('codeQL.copyVersion', async () => {
const text = `CodeQL extension version: ${extension?.packageJSON.version} \nCodeQL CLI version: ${await cliServer.getVersion()} \nPlatform: ${os.platform()} ${os.arch()}`;
await env.clipboard.writeText(text);
void helpers.showAndLogInformationMessage(text);
}));
// 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 helpers.showAndLogInformationMessage(`Authenticated to GitHub as user: ${userInfo.data.login}`);
}
}));
commands.registerCommand('codeQL.showLogs', () => {
logger.show();
});
void logger.log('Starting language server.');
ctx.subscriptions.push(client.start());
// Jump-to-definition and find-references
logger.log('Registering jump-to-definition handlers.');
void logger.log('Registering jump-to-definition handlers.');
languages.registerDefinitionProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryDefinitionProvider(cliServer, qs, dbm)
@@ -711,9 +852,9 @@ async function activateWithInstalledDistribution(
title: 'Calculate AST'
}));
commands.executeCommand('codeQLDatabases.removeOrphanedDatabases');
await commands.executeCommand('codeQLDatabases.removeOrphanedDatabases');
logger.log('Successfully finished extension initialization.');
void logger.log('Successfully finished extension initialization.');
return {
ctx,
@@ -732,11 +873,10 @@ function getContextStoragePath(ctx: ExtensionContext) {
return ctx.storagePath || ctx.globalStoragePath;
}
function initializeLogging(ctx: ExtensionContext): void {
async function initializeLogging(ctx: ExtensionContext): Promise<void> {
const storagePath = getContextStoragePath(ctx);
logger.init(storagePath);
queryServerLogger.init(storagePath);
ideServerLogger.init(storagePath);
await logger.setLogStoragePath(storagePath, false);
await ideServerLogger.setLogStoragePath(storagePath, false);
ctx.subscriptions.push(logger);
ctx.subscriptions.push(queryServerLogger);
ctx.subscriptions.push(ideServerLogger);

View File

@@ -9,7 +9,7 @@ import {
workspace,
env
} from 'vscode';
import { CodeQLCliServer } from './cli';
import { CodeQLCliServer, QlpacksInfo } from './cli';
import { logger } from './logging';
/**
@@ -29,8 +29,13 @@ export async function showAndLogErrorMessage(message: string, {
items = [] as string[],
fullMessage = undefined as (string | undefined)
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showErrorMessage, fullMessage);
return internalShowAndLog(dropLinesExceptInitial(message), items, outputLogger, Window.showErrorMessage, fullMessage);
}
function dropLinesExceptInitial(message: string, n = 2) {
return message.toString().split(/\r?\n/).slice(0, n).join('\n');
}
/**
* Show a warning message and log it to the console
*
@@ -72,7 +77,7 @@ async function internalShowAndLog(
fullMessage?: string
): Promise<string | undefined> {
const label = 'Show Log';
outputLogger.log(fullMessage || message);
void outputLogger.log(fullMessage || message);
const result = await fn(message, label, ...items);
if (result === label) {
outputLogger.show();
@@ -249,31 +254,75 @@ function createRateLimitedResult(): RateLimitedResult {
};
}
export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
export interface QlPacksForLanguage {
/** The name of the pack containing the dbscheme. */
dbschemePack: string;
/** `true` if `dbschemePack` is a library pack. */
dbschemePackIsLibraryPack: boolean;
/**
* The name of the corresponding standard query pack.
* Only defined if `dbschemePack` is a library pack.
*/
queryPack?: string;
}
interface QlPackWithPath {
packName: string;
packDir: string | undefined;
}
async function findDbschemePack(packs: QlPackWithPath[], dbschemePath: string): Promise<{ name: string; isLibraryPack: boolean; }> {
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')) as { dbscheme?: string; library?: boolean; };
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
return {
name: packName,
isLibraryPack: qlpack.library === true
};
}
}
}
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
}
function findStandardQueryPack(qlpacks: QlpacksInfo, dbschemePackName: string): string | undefined {
const matches = dbschemePackName.match(/^codeql\/(?<language>[a-z]+)-all$/);
if (matches) {
const queryPackName = `codeql/${matches.groups!.language}-queries`;
if (qlpacks[queryPackName] !== undefined) {
return queryPackName;
}
}
// Either the dbscheme pack didn't look like one where the queries might be in the query pack, or
// no query pack was found in the search path. Either is OK.
return undefined;
}
export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise<QlPacksForLanguage> {
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
const packs: { packDir: string | undefined; packName: string }[] =
const packs: QlPackWithPath[] =
Object.entries(qlpacks).map(([packName, dirs]) => {
if (dirs.length < 1) {
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
void logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
return { packName, packDir: undefined };
}
if (dirs.length > 1) {
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
void logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
}
return {
packName,
packDir: dirs[0]
};
});
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')) as { dbscheme: string };
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
return packName;
}
}
}
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
const dbschemePack = await findDbschemePack(packs, dbschemePath);
const queryPack = dbschemePack.isLibraryPack ? findStandardQueryPack(qlpacks, dbschemePack.name) : undefined;
return {
dbschemePack: dbschemePack.name,
dbschemePackIsLibraryPack: dbschemePack.isLibraryPack,
queryPack
};
}
export async function getPrimaryDbscheme(datasetFolder: string): Promise<string> {
@@ -287,7 +336,7 @@ export async function getPrimaryDbscheme(datasetFolder: string): Promise<string>
const dbscheme = dbschemes[0];
if (dbschemes.length > 1) {
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
void Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
}
return dbscheme;
}
@@ -362,7 +411,7 @@ export class CachedOperation<U> {
* `cli.CodeQLCliServer.resolveDatabase` and use the first entry in the
* `languages` property.
*
* @see cli.CodeQLCliServer.supportsLanguageName
* @see cli.CliVersionConstraint.supportsLanguageName
* @see cli.CodeQLCliServer.resolveDatabase
*/
const dbSchemeToLanguage = {
@@ -374,6 +423,12 @@ const dbSchemeToLanguage = {
'go.dbscheme': 'go'
};
export const languageToDbScheme = Object.entries(dbSchemeToLanguage).reduce((acc, [k, v]) => {
acc[v] = k;
return acc;
}, {} as { [k: string]: string });
/**
* Returns the initial contents for an empty query, based on the language of the selected
* databse.
@@ -419,3 +474,34 @@ export async function isLikelyDatabaseRoot(maybeRoot: string) {
export function isLikelyDbLanguageFolder(dbPath: string) {
return !!path.basename(dbPath).startsWith('db-');
}
/**
* Finds the language that a query targets.
* If it can't be autodetected, prompt the user to specify the language manually.
*/
export async function findLanguage(
cliServer: CodeQLCliServer,
queryUri: Uri | undefined
): Promise<string | undefined> {
const uri = queryUri || Window.activeTextEditor?.document.uri;
if (uri !== undefined) {
try {
const queryInfo = await cliServer.resolveQueryByLanguage(getOnDiskWorkspaceFolders(), uri);
const language = (Object.keys(queryInfo.byLanguage))[0];
void logger.log(`Detected query language: ${language}`);
return language;
} catch (e) {
void logger.log('Could not autodetect query language. Select language manually.');
}
}
const availableLanguages = Object.keys(await cliServer.resolveLanguages());
const language = await Window.showQuickPick(
availableLanguages,
{ placeHolder: 'Select target language for your query', ignoreFocusOut: true }
);
if (!language) {
// This only happens if the user cancels the quick pick.
void showAndLogErrorMessage('Language not found. Language must be specified manually.');
}
return language;
}

View File

@@ -224,14 +224,14 @@ export async function jumpToLocation(
} catch (e) {
if (e instanceof Error) {
if (e.message.match(/File not found/)) {
Window.showErrorMessage(
void Window.showErrorMessage(
'Original file of this result is not in the database\'s source archive.'
);
} else {
logger.log(`Unable to handleMsgFromView: ${e.message}`);
void logger.log(`Unable to handleMsgFromView: ${e.message}`);
}
} else {
logger.log(`Unable to handleMsgFromView: ${e}`);
void logger.log(`Unable to handleMsgFromView: ${e}`);
}
}
}

View File

@@ -119,7 +119,7 @@ export class InterfaceManager extends DisposableObject {
this.handleSelectionChange.bind(this)
)
);
logger.log('Registering path-step navigation commands.');
void logger.log('Registering path-step navigation commands.');
this.push(
commandRunner(
'codeQLQueryResults.nextPathStep',
@@ -137,16 +137,22 @@ export class InterfaceManager extends DisposableObject {
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
if (kind === DatabaseEventKind.Remove) {
this._diagnosticCollection.clear();
this.postMessage({
t: 'untoggleShowProblems'
});
if (this.isShowingPanel()) {
void this.postMessage({
t: 'untoggleShowProblems'
});
}
}
})
);
}
async navigatePathStep(direction: number): Promise<void> {
this.postMessage({ t: 'navigatePath', direction });
await this.postMessage({ t: 'navigatePath', direction });
}
private isShowingPanel() {
return !!this._panel;
}
// Returns the webview panel, creating it if it doesn't already
@@ -168,6 +174,7 @@ export class InterfaceManager extends DisposableObject {
]
}
));
this._panel.onDidDispose(
() => {
this._panel = undefined;
@@ -200,14 +207,14 @@ export class InterfaceManager extends DisposableObject {
sortState: InterpretedResultsSortState | undefined
): Promise<void> {
if (this._displayedQuery === undefined) {
showAndLogErrorMessage(
void showAndLogErrorMessage(
'Failed to sort results since evaluation info was unknown.'
);
return;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
this._displayedQuery.updateInterpretedSortState(sortState);
await this._displayedQuery.updateInterpretedSortState(sortState);
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
}
@@ -216,7 +223,7 @@ export class InterfaceManager extends DisposableObject {
sortState: RawResultsSortState | undefined
): Promise<void> {
if (this._displayedQuery === undefined) {
showAndLogErrorMessage(
void showAndLogErrorMessage(
'Failed to sort results since evaluation info was unknown.'
);
return;
@@ -294,7 +301,7 @@ export class InterfaceManager extends DisposableObject {
assertNever(msg);
}
} catch (e) {
showAndLogErrorMessage(e.message, {
void showAndLogErrorMessage(e.message, {
fullMessage: e.stack
});
}
@@ -365,7 +372,7 @@ export class InterfaceManager extends DisposableObject {
);
// Address this click asynchronously so we still update the
// query history immediately.
resultPromise.then((result) => {
void resultPromise.then((result) => {
if (result === showButton) {
panel.reveal();
}
@@ -547,24 +554,26 @@ export class InterfaceManager extends DisposableObject {
sourceInfo: cli.SourceInfo | undefined,
sourceLocationPrefix: string,
sortState: InterpretedResultsSortState | undefined
): Promise<Interpretation> {
): Promise<Interpretation | undefined> {
if (!resultsPaths) {
void this.logger.log('No results path. Cannot display interpreted results.');
return undefined;
}
const sarif = await interpretResults(
this.cliServer,
metadata,
resultsPaths,
sourceInfo
);
sarif.runs.forEach(run => {
if (run.results !== undefined) {
sortInterpretedResults(run.results, sortState);
}
});
const numTotalResults = (() => {
if (sarif.runs.length === 0) return 0;
if (sarif.runs[0].results === undefined) return 0;
return sarif.runs[0].results.length;
})();
const numTotalResults = sarif.runs[0]?.results?.length || 0;
const interpretation: Interpretation = {
sarif,
@@ -594,7 +603,7 @@ export class InterfaceManager extends DisposableObject {
throw new Error('Tried to get interpreted results before interpretation finished');
}
if (this._interpretation.sarif.runs.length !== 1) {
this.logger.log(`Warning: SARIF file had ${this._interpretation.sarif.runs.length} runs, expected 1`);
void this.logger.log(`Warning: SARIF file had ${this._interpretation.sarif.runs.length} runs, expected 1`);
}
const interp = this._interpretation;
return {
@@ -633,8 +642,8 @@ export class InterfaceManager extends DisposableObject {
} catch (e) {
// If interpretation fails, accept the error and continue
// trying to render uninterpreted results anyway.
showAndLogErrorMessage(
`Exception during results interpretation: ${e.message}. Will show raw results instead.`
void showAndLogErrorMessage(
`Showing raw results instead of interpreted ones due to an error. ${e.message}`
);
}
}
@@ -666,11 +675,15 @@ export class InterfaceManager extends DisposableObject {
undefined
);
if (!interpretation) {
return;
}
try {
await this.showProblemResultsAsDiagnostics(interpretation, database);
} catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(
void this.logger.log(
`Exception while computing problem results as diagnostics: ${msg}`
);
this._diagnosticCollection.clear();
@@ -684,7 +697,7 @@ export class InterfaceManager extends DisposableObject {
const { sarif, sourceLocationPrefix } = interpretation;
if (!sarif.runs || !sarif.runs[0].results) {
this.logger.log(
void this.logger.log(
'Didn\'t find a run in the sarif results. Error processing sarif?'
);
return;
@@ -695,11 +708,11 @@ export class InterfaceManager extends DisposableObject {
for (const result of sarif.runs[0].results) {
const message = result.message.text;
if (message === undefined) {
this.logger.log('Sarif had result without plaintext message');
void this.logger.log('Sarif had result without plaintext message');
continue;
}
if (!result.locations) {
this.logger.log('Sarif had result without location');
void this.logger.log('Sarif had result without location');
continue;
}
@@ -712,7 +725,7 @@ export class InterfaceManager extends DisposableObject {
}
const resultLocation = tryResolveLocation(sarifLoc, databaseItem);
if (!resultLocation) {
this.logger.log('Sarif location was not resolvable ' + sarifLoc);
void this.logger.log('Sarif location was not resolvable ' + sarifLoc);
continue;
}
const parsedMessage = parseSarifPlainTextMessage(message);

View File

@@ -28,9 +28,16 @@ export interface Logger {
removeAdditionalLogLocation(location: string | undefined): void;
/**
* The base location location where all side log files are stored.
* The base location where all side log files are stored.
*/
getBaseLocation(): string | undefined;
/**
* Sets the location where logs are stored.
* @param storagePath The path where logs are stored.
* @param isCustomLogDirectory Whether the logs are stored in a custom, user-specified directory.
*/
setLogStoragePath(storagePath: string, isCustomLogDirectory: boolean): Promise<void>;
}
export type ProgressReporter = Progress<{ message: string }>;
@@ -40,18 +47,24 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
public readonly outputChannel: OutputChannel;
private readonly additionalLocations = new Map<string, AdditionalLogLocation>();
private additionalLogLocationPath: string | undefined;
isCustomLogDirectory: boolean;
constructor(private title: string) {
super();
this.outputChannel = Window.createOutputChannel(title);
this.push(this.outputChannel);
this.isCustomLogDirectory = false;
}
init(storagePath: string): void {
async setLogStoragePath(storagePath: string, isCustomLogDirectory: boolean): Promise<void> {
this.additionalLogLocationPath = path.join(storagePath, this.title);
// clear out any old state from previous runs
fs.remove(this.additionalLogLocationPath);
this.isCustomLogDirectory = isCustomLogDirectory;
if (!this.isCustomLogDirectory) {
// clear out any old state from previous runs
await fs.remove(this.additionalLogLocationPath);
}
}
/**
@@ -80,7 +93,7 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
this.outputChannel.appendLine(separator);
this.outputChannel.appendLine(msg);
this.outputChannel.appendLine(separator);
additional = new AdditionalLogLocation(logPath);
additional = new AdditionalLogLocation(logPath, !this.isCustomLogDirectory);
this.additionalLocations.set(logPath, additional);
this.track(additional);
}
@@ -112,7 +125,7 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
}
class AdditionalLogLocation extends Disposable {
constructor(private location: string) {
constructor(private location: string, private shouldDeleteLogs: boolean) {
super(() => { /**/ });
}
@@ -128,7 +141,9 @@ class AdditionalLogLocation extends Disposable {
}
async dispose(): Promise<void> {
await fs.remove(this.location);
if (this.shouldDeleteLogs) {
await fs.remove(this.location);
}
}
}

View File

@@ -262,7 +262,7 @@ export interface CompilationTarget {
/**
* Compile as a normal query
*/
query?: {};
query?: Record<string, never>;
/**
* Compile as a quick evaluation
*/
@@ -826,7 +826,7 @@ export interface ResultSet {
/**
* The type returned when the evaluation is complete
*/
export type EvaluationComplete = {};
export type EvaluationComplete = Record<string, never>;
/**
* The result of a single query

View File

@@ -167,10 +167,12 @@ export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
protected update(results: QLTestDiscoveryResults): void {
this._testDirectory = results.testDirectory;
// Watch for changes to any `.ql` or `.qlref` file in any of the QL packs that contain tests.
this.watcher.clear();
// Watch for changes to any `.ql` or `.qlref` file in any of the QL packs that contain tests.
this.watcher.addWatch(new RelativePattern(results.watchPath, '**/*.{ql,qlref}'));
this._onDidChangeTests.fire();
// need to explicitly watch for changes to directories themselves.
this.watcher.addWatch(new RelativePattern(results.watchPath, '**/'));
this._onDidChangeTests.fire(undefined);
}
/**

View File

@@ -193,7 +193,7 @@ export class HistoryTreeDataProvider extends DisposableObject {
public set sortOrder(newSortOrder: SortOrder) {
this._sortOrder = newSortOrder;
this._onDidChangeTreeData.fire();
this._onDidChangeTreeData.fire(undefined);
}
}
@@ -249,7 +249,7 @@ export class QueryHistoryManager extends DisposableObject {
})
);
logger.log('Registering query history panel commands.');
void logger.log('Registering query history panel commands.');
this.push(
commandRunner(
'codeQLQueryHistory.openQuery',
@@ -312,8 +312,14 @@ export class QueryHistoryManager extends DisposableObject {
);
this.push(
commandRunner(
'codeQLQueryHistory.viewSarifResults',
this.handleViewSarifResults.bind(this)
'codeQLQueryHistory.viewCsvAlerts',
this.handleViewCsvAlerts.bind(this)
)
);
this.push(
commandRunner(
'codeQLQueryHistory.viewSarifAlerts',
this.handleViewSarifAlerts.bind(this)
)
);
this.push(
@@ -402,7 +408,7 @@ export class QueryHistoryManager extends DisposableObject {
});
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
this.treeView.reveal(current);
await this.treeView.reveal(current);
await this.invokeCallbackOn(current);
}
}
@@ -470,10 +476,10 @@ export class QueryHistoryManager extends DisposableObject {
const to = await this.findOtherQueryToCompare(from, multiSelect);
if (from && to) {
this.doCompareCallback(from, to);
await this.doCompareCallback(from, to);
}
} catch (e) {
showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(e.message);
}
}
@@ -499,13 +505,13 @@ export class QueryHistoryManager extends DisposableObject {
if (
prevItemClick !== undefined &&
now.valueOf() - prevItemClick.time.valueOf() < DOUBLE_CLICK_TIME &&
singleItem == prevItemClick.item
finalSingleItem == prevItemClick.item
) {
// show original query file on double click
await this.handleOpenQuery(singleItem, [singleItem]);
await this.handleOpenQuery(finalSingleItem, [finalSingleItem]);
} else {
// show results on single click
await this.invokeCallbackOn(singleItem);
await this.invokeCallbackOn(finalSingleItem);
}
}
@@ -520,7 +526,7 @@ export class QueryHistoryManager extends DisposableObject {
if (singleItem.logFileLocation) {
await this.tryOpenExternalFile(singleItem.logFileLocation);
} else {
showAndLogWarningMessage('No log file available');
void showAndLogWarningMessage('No log file available');
}
}
@@ -550,7 +556,7 @@ export class QueryHistoryManager extends DisposableObject {
await vscode.window.showTextDocument(doc, { preview: false });
}
async handleViewSarifResults(
async handleViewSarifAlerts(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
@@ -565,7 +571,7 @@ export class QueryHistoryManager extends DisposableObject {
);
} else {
const label = singleItem.getLabel();
showAndLogInformationMessage(
void showAndLogInformationMessage(
`Query ${label} has no interpreted results.`
);
}
@@ -578,6 +584,24 @@ export class QueryHistoryManager extends DisposableObject {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
if (await singleItem.query.hasCsv()) {
void this.tryOpenExternalFile(singleItem.query.csvPath);
return;
}
await singleItem.query.exportCsvResults(this.qs, singleItem.query.csvPath, () => {
void this.tryOpenExternalFile(
singleItem.query.csvPath
);
});
}
async handleViewCsvAlerts(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
await this.tryOpenExternalFile(
await singleItem.query.ensureCsvProduced(this.qs)
@@ -644,7 +668,7 @@ export class QueryHistoryManager extends DisposableObject {
// We must fire the onDidChangeTreeData event to ensure the current element can be selected
// using `reveal` if the tree view was not visible when the current element was added.
this.treeDataProvider.refresh();
this.treeView.reveal(current);
void this.treeView.reveal(current);
}
}
}
@@ -671,13 +695,13 @@ the file in the file explorer and dragging it into the workspace.`
try {
await vscode.commands.executeCommand('revealFileInOS', uri);
} catch (e) {
showAndLogErrorMessage(e.message);
void showAndLogErrorMessage(e.message);
}
}
} else {
showAndLogErrorMessage(`Could not open file ${fileLocation}`);
logger.log(e.message);
logger.log(e.stack);
void showAndLogErrorMessage(`Could not open file ${fileLocation}`);
void logger.log(e.message);
void logger.log(e.stack);
}
}
}
@@ -729,7 +753,7 @@ the file in the file explorer and dragging it into the workspace.`
private assertSingleQuery(multiSelect: CompletedQuery[] = [], message = 'Please select a single query.') {
if (multiSelect.length > 1) {
showAndLogErrorMessage(
void showAndLogErrorMessage(
message
);
return false;
@@ -797,4 +821,3 @@ the file in the file explorer and dragging it into the workspace.`
this.treeDataProvider.refresh(completedQuery);
}
}

View File

@@ -62,6 +62,9 @@ export class CompletedQuery implements QueryWithResults {
get queryName(): string {
return getQueryName(this.query);
}
get queryFileName(): string {
return getQueryFileName(this.query);
}
get statusString(): string {
switch (this.result.resultType) {
@@ -88,13 +91,14 @@ export class CompletedQuery implements QueryWithResults {
}
interpolate(template: string): string {
const { databaseName, queryName, time, resultCount, statusString } = this;
const { databaseName, queryName, time, resultCount, statusString, queryFileName } = this;
const replacements: { [k: string]: string } = {
t: time,
q: queryName,
d: databaseName,
r: resultCount.toString(),
s: statusString,
f: queryFileName,
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
@@ -152,17 +156,28 @@ export class CompletedQuery implements QueryWithResults {
* Uses metadata if it exists, and defaults to the query file name.
*/
export function getQueryName(query: QueryInfo) {
if (query.quickEvalPosition !== undefined) {
return 'Quick evaluation of ' + getQueryFileName(query);
} else if (query.metadata?.name) {
return query.metadata.name;
} else {
return getQueryFileName(query);
}
}
/**
* Gets the file name for an evaluated query.
* Defaults to the query file name and may contain position information for quick eval queries.
*/
export function getQueryFileName(query: QueryInfo) {
// Queries run through quick evaluation are not usually the entire query file.
// Label them differently and include the line numbers.
if (query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = query.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `Quick evaluation of ${path.basename(fileName)}:${lineInfo}`;
} else if (query.metadata?.name) {
return query.metadata.name;
} else {
return path.basename(query.program.queryPath);
return `${path.basename(fileName)}:${lineInfo}`;
}
return path.basename(query.program.queryPath);
}
@@ -186,14 +201,13 @@ export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) {
if (metadata === undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
let { kind, id, scored } = metadata;
if (kind === undefined) {
if (metadata.kind === undefined) {
throw new Error('Can\'t interpret results without query metadata including kind');
}
if (id === undefined) {
if (metadata.id === undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we use a dummy id.
id = 'dummy-id';
metadata.id = 'dummy-id';
}
return { kind, id, scored };
return metadata;
}

View File

@@ -8,11 +8,13 @@ import { QueryServerConfig } from './config';
import { Logger, ProgressReporter } from './logging';
import { completeQuery, EvaluationResult, progress, ProgressMessage, WithProgressId } from './pure/messages';
import * as messages from './pure/messages';
import { SemVer } from 'semver';
import { ProgressCallback, ProgressTask } from './commandRunner';
import * as fs from 'fs-extra';
import * as helpers from './helpers';
type ServerOpts = {
logger: Logger;
contextStoragePath: string;
}
/** A running query server process and its associated message connection. */
@@ -28,7 +30,7 @@ class ServerProcess implements Disposable {
}
dispose(): void {
this.logger.log('Stopping query server...');
void this.logger.log('Stopping query server...');
this.connection.dispose();
this.child.stdin!.end();
this.child.stderr!.destroy();
@@ -36,7 +38,7 @@ class ServerProcess implements Disposable {
// On Windows, we usually have to terminate the process before closing its stdout.
this.child.stdout!.destroy();
this.logger.log('Stopped query server.');
void this.logger.log('Stopped query server.');
}
}
@@ -50,11 +52,6 @@ type WithProgressReporting = (task: (progress: ProgressReporter, token: Cancella
*/
export class QueryServerClient extends DisposableObject {
/**
* Query Server version where database registration was introduced
*/
private static VERSION_WITH_DB_REGISTRATION = new SemVer('2.4.1');
serverProcess?: ServerProcess;
evaluationResultCallbacks: { [key: number]: (res: EvaluationResult) => void };
progressCallbacks: { [key: number]: ((res: ProgressMessage) => void) | undefined };
@@ -92,6 +89,26 @@ export class QueryServerClient extends DisposableObject {
this.evaluationResultCallbacks = {};
}
async initLogger() {
let storagePath = this.opts.contextStoragePath;
let isCustomLogDirectory = false;
if (this.config.customLogDirectory) {
try {
if (!(await fs.pathExists(this.config.customLogDirectory))) {
await fs.mkdir(this.config.customLogDirectory);
}
void this.logger.log(`Saving query server logs to user-specified directory: ${this.config.customLogDirectory}.`);
storagePath = this.config.customLogDirectory;
isCustomLogDirectory = true;
} catch (e) {
void helpers.showAndLogErrorMessage(`${this.config.customLogDirectory} is not a valid directory. Logs will be stored in a temporary workspace directory instead.`);
}
}
await this.logger.setLogStoragePath(storagePath, isCustomLogDirectory);
}
get logger(): Logger {
return this.opts.logger;
}
@@ -101,7 +118,7 @@ export class QueryServerClient extends DisposableObject {
if (this.serverProcess !== undefined) {
this.disposeAndStopTracking(this.serverProcess);
} else {
this.logger.log('No server process to be stopped.');
void this.logger.log('No server process to be stopped.');
}
}
@@ -133,6 +150,7 @@ export class QueryServerClient extends DisposableObject {
/** Starts a new query server process, sending progress messages to the given reporter. */
private async startQueryServerImpl(progressReporter: ProgressReporter): Promise<void> {
await this.initLogger();
const ramArgs = await this.cliServer.resolveRam(this.config.queryMemoryMb, progressReporter);
const args = ['--threads', this.config.numThreads.toString()].concat(ramArgs);
@@ -145,7 +163,7 @@ export class QueryServerClient extends DisposableObject {
args.push(this.config.cacheSize.toString());
}
if (await this.supportsDatabaseRegistration()) {
if (await this.cliServer.cliConstraints.supportsDatabaseRegistration()) {
args.push('--require-db-registration');
}
@@ -174,9 +192,8 @@ export class QueryServerClient extends DisposableObject {
const connection = createMessageConnection(child.stdout, child.stdin);
connection.onRequest(completeQuery, res => {
if (!(res.runId in this.evaluationResultCallbacks)) {
this.logger.log(`No callback associated with run id ${res.runId}, continuing without executing any callback`);
}
else {
void this.logger.log(`No callback associated with run id ${res.runId}, continuing without executing any callback`);
} else {
const baseLocation = this.logger.getBaseLocation();
if (baseLocation && this.activeQueryName) {
res.logFileLocation = path.join(baseLocation, this.activeQueryName);
@@ -191,7 +208,7 @@ export class QueryServerClient extends DisposableObject {
callback(res);
}
});
this.serverProcess = new ServerProcess(child, connection, this.opts.logger);
this.serverProcess = new ServerProcess(child, connection, this.logger);
// Ensure the server process is disposed together with this client.
this.track(this.serverProcess);
connection.listen();
@@ -202,10 +219,6 @@ export class QueryServerClient extends DisposableObject {
this.evaluationResultCallbacks = {};
}
async supportsDatabaseRegistration() {
return (await this.cliServer.getVersion()).compare(QueryServerClient.VERSION_WITH_DB_REGISTRATION) >= 0;
}
registerCallback(callback: (res: EvaluationResult) => void): number {
const id = this.nextCallback++;
this.evaluationResultCallbacks[id] = callback;

View File

@@ -110,7 +110,7 @@ export async function displayQuickQuery(
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
const dbscheme = await getPrimaryDbscheme(datasetFolder);
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);
const qlpack = (await getQlPackForDbscheme(cliServer, dbscheme)).dbschemePack;
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
const shouldRewrite = await checkShouldRewrite(qlPackFile, qlpack);

View File

@@ -25,6 +25,8 @@ import * as qsClient from './queryserver-client';
import { isQuickQueryPath } from './quick-query';
import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades';
import { ensureMetadataIsComplete } from './query-results';
import { SELECT_QUERY_NAME } from './contextual/locationFinder';
import { DecodedBqrsChunk } from './pure/bqrs-cli-types';
/**
* run-queries.ts
@@ -156,7 +158,7 @@ export class QueryInfo {
compiled = await qs.sendRequest(messages.compileQuery, params, token, progress);
} finally {
qs.logger.log(' - - - COMPILATION DONE - - - ');
void qs.logger.log(' - - - COMPILATION DONE - - - ');
}
return (compiled?.messages || []).filter(msg => msg.severity === messages.Severity.ERROR);
}
@@ -167,14 +169,17 @@ export class QueryInfo {
async canHaveInterpretedResults(): Promise<boolean> {
const hasMetadataFile = await this.dbItem.hasMetadataFile();
if (!hasMetadataFile) {
logger.log('Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.');
void logger.log('Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.');
}
const hasKind = !!this.metadata?.kind;
if (!hasKind) {
logger.log('Cannot produce interpreted results since the query does not have @kind metadata.');
void logger.log('Cannot produce interpreted results since the query does not have @kind metadata.');
}
return hasMetadataFile && hasKind;
const isTable = hasKind && this.metadata?.kind === 'table';
return hasMetadataFile && hasKind && !isTable;
}
/**
@@ -213,6 +218,29 @@ export class QueryInfo {
return this.dilPath;
}
async exportCsvResults(qs: qsClient.QueryServerClient, csvPath: string, onFinish: () => void): Promise<void> {
let stopDecoding = false;
const out = fs.createWriteStream(csvPath);
out.on('finish', onFinish);
out.on('error', () => {
if (!stopDecoding) {
stopDecoding = true;
void showAndLogErrorMessage(`Failed to write CSV results to ${csvPath}`);
}
});
let nextOffset: number | undefined = 0;
while (nextOffset !== undefined && !stopDecoding) {
const chunk: DecodedBqrsChunk = await qs.cliServer.bqrsDecode(this.resultsPaths.resultsPath, SELECT_QUERY_NAME, {
pageSize: 100,
offset: nextOffset,
});
for (const tuple of chunk.tuples)
out.write(tuple.join(',') + '\n');
nextOffset = chunk.next;
}
out.end();
}
async ensureCsvProduced(qs: qsClient.QueryServerClient): Promise<string> {
if (await this.hasCsv()) {
return this.csvPath;
@@ -496,7 +524,7 @@ export async function determineSelectedQuery(selectedResourceUri: Uri | undefine
// then prompt the user to save it first.
if (editor !== undefined && editor.document.uri.fsPath === queryPath) {
if (await promptUserToSaveChanges(editor.document)) {
editor.document.save();
await editor.document.save();
}
}
@@ -559,8 +587,8 @@ export async function compileAndRunQueryAgainstDatabase(
const querySchemaName = path.basename(packConfig.dbscheme);
const dbSchemaName = path.basename(db.contents.dbSchemeUri.fsPath);
if (querySchemaName != dbSchemaName) {
logger.log(`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`);
throw new Error(`The query ${path.basename(queryPath)} cannot be run against the selected database: their target languages are different. Please select a different database and try again.`);
void logger.log(`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`);
throw new Error(`The query ${path.basename(queryPath)} cannot be run against the selected database (${db.name}): their target languages are different. Please select a different database and try again.`);
}
const qlProgram: messages.QlProgram = {
@@ -581,7 +609,7 @@ export async function compileAndRunQueryAgainstDatabase(
metadata = await cliServer.resolveMetadata(qlProgram.queryPath);
} catch (e) {
// Ignore errors and provide no metadata.
logger.log(`Couldn't resolve metadata for ${qlProgram.queryPath}: ${e}`);
void logger.log(`Couldn't resolve metadata for ${qlProgram.queryPath}: ${e}`);
}
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
@@ -609,8 +637,8 @@ export async function compileAndRunQueryAgainstDatabase(
const result = await query.run(qs, upgradeQlo, progress, token);
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const message = result.message || 'Failed to run query';
logger.log(message);
showAndLogErrorMessage(message);
void logger.log(message);
void showAndLogErrorMessage(message);
}
return {
query,
@@ -630,7 +658,7 @@ export async function compileAndRunQueryAgainstDatabase(
// so we include a general description of the problem,
// and direct the user to the output window for the detailed compilation messages.
// However we don't show quick eval errors there so we need to display them anyway.
qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
void qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
const formattedMessages: string[] = [];
@@ -638,21 +666,24 @@ export async function compileAndRunQueryAgainstDatabase(
const message = error.message || '[no error message available]';
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
formattedMessages.push(formatted);
qs.logger.log(formatted);
void qs.logger.log(formatted);
}
if (quickEval && formattedMessages.length <= 3) {
showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n'));
if (quickEval && formattedMessages.length <= 2) {
// If there are more than 2 error messages, they will not be displayed well in a popup
// and will be trimmed by the function displaying the error popup. Accordingly, we only
// try to show the errors if there are 2 or less, otherwise we direct the user to the log.
void showAndLogErrorMessage('Quick evaluation compilation failed: ' + formattedMessages.join('\n'));
} else {
showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
void showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
}
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
}
} finally {
try {
upgradeDir.cleanup();
await upgradeDir.cleanup();
} catch (e) {
qs.logger.log(`Could not clean up the upgrades dir. Reason: ${e.message || e}`);
void qs.logger.log(`Could not clean up the upgrades dir. Reason: ${e.message || e}`);
}
}
}

View File

@@ -0,0 +1,164 @@
import { QuickPickItem, Uri, window } from 'vscode';
import * as yaml from 'js-yaml';
import * as fs from 'fs-extra';
import { findLanguage, showAndLogErrorMessage, showAndLogInformationMessage, showInformationMessageWithAction } from './helpers';
import { Credentials } from './authentication';
import * as cli from './cli';
import { logger } from './logging';
import { getRemoteRepositoryLists } from './config';
interface Config {
repositories: string[];
ref?: string;
language?: string;
}
// Test "controller" repository and workflow.
const OWNER = 'dsp-testing';
const REPO = 'qc-controller';
interface RepoListQuickPickItem extends QuickPickItem {
repoList: string[];
}
/**
* Gets the repositories to run the query against.
*/
async function getRepositories(): Promise<string[] | undefined> {
const repoLists = getRemoteRepositoryLists();
if (repoLists && Object.keys(repoLists).length) {
const quickPickItems = Object.entries(repoLists).map<RepoListQuickPickItem>(([key, value]) => (
{
label: key, // the name of the repository list
repoList: value, // the actual array of repositories
}
));
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
quickPickItems,
{
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.remoteRepositoryLists` setting.',
ignoreFocusOut: true,
});
if (quickpick?.repoList.length) {
void logger.log(`Selected repositories: ${quickpick.repoList.join(', ')}`);
return quickpick.repoList;
} else {
void showAndLogErrorMessage('No repositories selected.');
return;
}
} else {
void logger.log('No repository lists defined. Displaying text input box.');
/**
* This regex matches strings of the form `owner/repo` where:
* - `owner` is made up of alphanumeric characters or single hyphens, starting and ending in an alphanumeric character
* - `repo` is made up of alphanumeric characters, hyphens, or underscores
*/
const repoRegex = /^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+\/[a-zA-Z0-9-_]+$/;
const remoteRepo = await window.showInputBox({
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
placeHolder: '<owner>/<repo>',
prompt: 'Tip: you can save frequently used repositories in the `codeql.remoteRepositoryLists` setting',
ignoreFocusOut: true,
});
if (!remoteRepo) {
void showAndLogErrorMessage('No repositories entered.');
return;
} else if (!repoRegex.test(remoteRepo)) { // Check if user entered invalid input
void showAndLogErrorMessage('Invalid repository format. Must be in the format <owner>/<repo> (e.g. github/codeql)');
return;
}
void logger.log(`Entered repository: ${remoteRepo}`);
return [remoteRepo];
}
}
export async function runRemoteQuery(cliServer: cli.CodeQLCliServer, credentials: Credentials, uri?: Uri) {
if (!uri?.fsPath.endsWith('.ql')) {
return;
}
const queryFile = uri.fsPath;
const query = await fs.readFile(queryFile, 'utf8');
const repositoriesFile = queryFile.substring(0, queryFile.length - '.ql'.length) + '.repositories';
let ref: string | undefined;
let language: string | undefined;
let repositories: string[] | undefined;
// If the user has an explicit `.repositories` file, use that.
// Otherwise, prompt user to select repositories from the `codeQL.remoteRepositoryLists` setting.
if (await fs.pathExists(repositoriesFile)) {
void logger.log(`Found '${repositoriesFile}'. Using information from that file to run ${queryFile}.`);
const config = yaml.safeLoad(await fs.readFile(repositoriesFile, 'utf8')) as Config;
ref = config.ref || 'main';
language = config.language || await findLanguage(cliServer, uri);
repositories = config.repositories;
} else {
ref = 'main';
[language, repositories] = await Promise.all([findLanguage(cliServer, uri), getRepositories()]);
}
if (!language) {
return; // No error message needed, since `findLanguage` already displays one.
}
if (!repositories || repositories.length === 0) {
return; // No error message needed, since `getRepositories` already displays one.
}
await runRemoteQueriesApiRequest(credentials, ref, language, repositories, query);
}
async function runRemoteQueriesApiRequest(credentials: Credentials, ref: string, language: string, repositories: string[], query: string) {
const octokit = await credentials.getOctokit();
const token = await credentials.getToken();
try {
await octokit.request(
'POST /repos/:owner/:repo/code-scanning/codeql/queries',
{
owner: OWNER,
repo: REPO,
data: {
ref: ref,
language: language,
repositories: repositories,
query: query,
token: token,
}
}
);
void showAndLogInformationMessage(`Successfully scheduled runs. [Click here to see the progress](https://github.com/${OWNER}/${REPO}/actions).`);
} catch (error) {
if (typeof error.message === 'string' && error.message.includes('Some repositories were invalid')) {
const invalidRepos = error?.response?.data?.invalid_repos || [];
const reposWithoutDbUploads = error?.response?.data?.repos_without_db_uploads || [];
void logger.log('Unable to run query on some of the specified repositories');
if (invalidRepos.length > 0) {
void logger.log(`Invalid repos: ${invalidRepos.join(', ')}`);
}
if (reposWithoutDbUploads.length > 0) {
void logger.log(`Repos without DB uploads: ${reposWithoutDbUploads.join(', ')}`);
}
if (invalidRepos.length + reposWithoutDbUploads.length === repositories.length) {
// Every repo is invalid in some way
void showAndLogErrorMessage('Unable to run query on any of the specified repositories.');
return;
}
const popupMessage = 'Unable to run query on some of the specified repositories. [See logs for more details](command:codeQL.showLogs).';
const rerunQuery = await showInformationMessageWithAction(popupMessage, 'Rerun on the valid repositories only');
if (rerunQuery) {
const validRepositories = repositories.filter(r => !invalidRepos.includes(r) && !reposWithoutDbUploads.includes(r));
void logger.log(`Rerunning query on set of valid repositories: ${JSON.stringify(validRepositories)}`);
await runRemoteQueriesApiRequest(credentials, ref, language, validRepositories, query);
}
} else {
void showAndLogErrorMessage(error);
}
}
}

View File

@@ -19,8 +19,8 @@ export class CodeQlStatusBarHandler extends DisposableObject {
this.push(this.item);
this.push(workspace.onDidChangeConfiguration(this.handleDidChangeConfiguration, this));
this.push(distributionConfigListener.onDidChangeConfiguration(() => this.updateStatusItem()));
this.item.command = 'codeQL.openDocumentation';
this.updateStatusItem();
this.item.command = 'codeQL.copyVersion';
void this.updateStatusItem();
}
private handleDidChangeConfiguration(e: ConfigurationChangeEvent) {
@@ -37,7 +37,7 @@ export class CodeQlStatusBarHandler extends DisposableObject {
private async updateStatusItem() {
const canary = CANARY_FEATURES.getValue() ? ' (Canary)' : '';
// since getting the verison may take a few seconds, initialize with some
// since getting the version may take a few seconds, initialize with some
// meaningful text.
this.item.text = `CodeQL${canary}`;

View File

@@ -119,7 +119,7 @@ export class TelemetryListener extends ConfigListener {
}
if (LOG_TELEMETRY.getValue<boolean>()) {
logger.log(`Telemetry: ${JSON.stringify(envelope)}`);
void logger.log(`Telemetry: ${JSON.stringify(envelope)}`);
}
return true;
});
@@ -128,7 +128,7 @@ export class TelemetryListener extends ConfigListener {
dispose() {
super.dispose();
this.reporter?.dispose();
void this.reporter?.dispose();
}
sendCommandUsage(name: string, executionTime: number, error?: Error) {
@@ -187,7 +187,7 @@ export class TelemetryListener extends ConfigListener {
private disposeReporter() {
if (this.reporter) {
this.reporter.dispose();
void this.reporter.dispose();
this.reporter = undefined;
}
}
@@ -209,6 +209,8 @@ export let telemetryListener: TelemetryListener;
export async function initializeTelemetry(extension: Extension<any>, ctx: ExtensionContext): Promise<void> {
telemetryListener = new TelemetryListener(extension.id, extension.packageJSON.version, key, ctx);
telemetryListener.initialize();
// do not await initialization, since doing so will sometimes cause a modal popup.
// this is a particular problem during integration tests, which will hang if a modal popup is displayed.
void telemetryListener.initialize();
ctx.subscriptions.push(telemetryListener);
}

View File

@@ -1,3 +1,4 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as vscode from 'vscode';
import {
@@ -17,8 +18,9 @@ import { QLTestFile, QLTestNode, QLTestDirectory, QLTestDiscovery } from './qlte
import { Event, EventEmitter, CancellationTokenSource, CancellationToken } from 'vscode';
import { DisposableObject } from './pure/disposable-object';
import { CodeQLCliServer } from './cli';
import { getOnDiskWorkspaceFolders } from './helpers';
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage, showAndLogWarningMessage } from './helpers';
import { testLogger } from './logging';
import { DatabaseItem, DatabaseManager } from './databases';
/**
* Get the full path of the `.expected` file for the specified QL test.
@@ -57,13 +59,13 @@ function getTestOutputFile(testPath: string, extension: string): string {
* A factory service that creates `QLTestAdapter` objects for workspace folders on demand.
*/
export class QLTestAdapterFactory extends DisposableObject {
constructor(testHub: TestHub, cliServer: CodeQLCliServer) {
constructor(testHub: TestHub, cliServer: CodeQLCliServer, databaseManager: DatabaseManager) {
super();
// this will register a QLTestAdapter for each WorkspaceFolder
this.push(new TestAdapterRegistrar(
testHub,
workspaceFolder => new QLTestAdapter(workspaceFolder, cliServer)
workspaceFolder => new QLTestAdapter(workspaceFolder, cliServer, databaseManager)
));
}
}
@@ -91,7 +93,8 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
constructor(
public readonly workspaceFolder: vscode.WorkspaceFolder,
private readonly cliServer: CodeQLCliServer
private readonly cliServer: CodeQLCliServer,
private readonly databaseManager: DatabaseManager
) {
super();
@@ -182,19 +185,86 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
testLogger.outputChannel.show(true);
this.runningTask = this.track(new CancellationTokenSource());
const token = this.runningTask.token;
this._testStates.fire({ type: 'started', tests: tests } as TestRunStartedEvent);
const currentDatabaseUri = this.databaseManager.currentDatabaseItem?.databaseUri;
const databasesUnderTest: DatabaseItem[] = [];
for (const database of this.databaseManager.databaseItems) {
for (const test of tests) {
if (await database.isAffectedByTest(test)) {
databasesUnderTest.push(database);
break;
}
}
}
await this.removeDatabasesBeforeTests(databasesUnderTest, token);
try {
await this.runTests(tests, this.runningTask.token);
}
catch (e) {
/**/
await this.runTests(tests, token);
} catch (e) {
// CodeQL testing can throw exception even in normal scenarios. For example, if the test run
// produces no output (which is normal), the testing command would throw an exception on
// unexpected EOF during json parsing. So nothing needs to be done here - all the relevant
// error information (if any) should have already been written to the test logger.
}
await this.reopenDatabasesAfterTests(databasesUnderTest, currentDatabaseUri, token);
this._testStates.fire({ type: 'finished' } as TestRunFinishedEvent);
this.clearTask();
}
private async removeDatabasesBeforeTests(
databasesUnderTest: DatabaseItem[], token: vscode.CancellationToken): Promise<void> {
for (const database of databasesUnderTest) {
try {
await this.databaseManager
.removeDatabaseItem(_ => { /* no progress reporting */ }, token, database);
} catch (e) {
// This method is invoked from Test Explorer UI, and testing indicates that Test
// Explorer UI swallows any thrown exception without reporting it to the user.
// So we need to display the error message ourselves and then rethrow.
void showAndLogErrorMessage(`Cannot remove database ${database.name}: ${e}`);
throw e;
}
}
}
private async reopenDatabasesAfterTests(
databasesUnderTest: DatabaseItem[],
currentDatabaseUri: vscode.Uri | undefined,
token: vscode.CancellationToken): Promise<void> {
for (const closedDatabase of databasesUnderTest) {
const uri = closedDatabase.databaseUri;
if (await this.isFileAccessible(uri)) {
try {
const reopenedDatabase = await this.databaseManager
.openDatabase(_ => { /* no progress reporting */ }, token, uri);
await this.databaseManager.renameDatabaseItem(reopenedDatabase, closedDatabase.name);
if (currentDatabaseUri == uri) {
await this.databaseManager.setCurrentDatabaseItem(reopenedDatabase, true);
}
} catch (e) {
// This method is invoked from Test Explorer UI, and testing indicates that Test
// Explorer UI swallows any thrown exception without reporting it to the user.
// So we need to display the error message ourselves and then rethrow.
void showAndLogWarningMessage(`Cannot reopen database ${uri}: ${e}`);
throw e;
}
}
}
}
private async isFileAccessible(uri: vscode.Uri): Promise<boolean> {
try {
await fs.access(uri.fsPath);
return true;
} catch {
return false;
}
}
private clearTask(): void {
if (this.runningTask !== undefined) {
const runningTask = this.runningTask;
@@ -205,7 +275,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
public cancel(): void {
if (this.runningTask !== undefined) {
testLogger.log('Cancelling test run...');
void testLogger.log('Cancelling test run...');
this.runningTask.cancel();
this.clearTask();
}
@@ -224,8 +294,10 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
: 'failed';
let message: string | undefined;
if (event.failureDescription || event.diff?.length) {
message = ['', `${state}: ${event.test}`, event.failureDescription || event.diff?.join('\n'), ''].join('\n');
testLogger.log(message);
message = event.failureStage === 'RESULT'
? ['', `${state}: ${event.test}`, event.failureDescription || event.diff?.join('\n'), ''].join('\n')
: ['', `${event.failureStage?.toLowerCase()} error: ${event.test}`, event.failureDescription || `${event.messages[0].severity}: ${event.messages[0].message}`, ''].join('\n');
void testLogger.log(message);
}
this._testStates.fire({
type: 'test',

View File

@@ -44,7 +44,7 @@ export class TestUIService extends UIService implements TestController {
constructor(private readonly testHub: TestHub) {
super();
logger.log('Registering CodeQL test panel commands.');
void logger.log('Registering CodeQL test panel commands.');
this.registerCommand('codeQLTests.showOutputDifferences', this.showOutputDifferences);
this.registerCommand('codeQLTests.acceptOutput', this.acceptOutput);
@@ -90,7 +90,7 @@ export class TestUIService extends UIService implements TestController {
};
if (!await fs.pathExists(expectedPath)) {
showAndLogWarningMessage(`'${path.basename(expectedPath)}' does not exist. Creating an empty file.`);
void showAndLogWarningMessage(`'${path.basename(expectedPath)}' does not exist. Creating an empty file.`);
await fs.createFile(expectedPath);
}

View File

@@ -103,7 +103,7 @@ async function checkAndConfirmDatabaseUpgrade(
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
}
logger.log(descriptionMessage);
void logger.log(descriptionMessage);
// If the quiet flag is set, do the upgrade without a popup.
@@ -187,35 +187,35 @@ export async function upgradeDatabaseExplicit(
compileUpgradeResult = await compileDatabaseUpgrade(qs, db, finalDbscheme, scripts, currentUpgradeTmp, progress, token);
}
catch (e) {
showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
void showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
return;
}
finally {
qs.logger.log('Done compiling database upgrade.');
void qs.logger.log('Done compiling database upgrade.');
}
if (!compileUpgradeResult.compiledUpgrades) {
const error = compileUpgradeResult.error || '[no error message available]';
showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
void showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
return;
}
await checkAndConfirmDatabaseUpgrade(compileUpgradeResult.compiledUpgrades, db, qs.cliServer.quiet);
try {
qs.logger.log('Running the following database upgrade:');
void qs.logger.log('Running the following database upgrade:');
getUpgradeDescriptions(compileUpgradeResult.compiledUpgrades).map(s => s.description).join('\n');
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades, progress, token);
}
catch (e) {
showAndLogErrorMessage(`Database upgrade failed: ${e}`);
void showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
} finally {
qs.logger.log('Done running database upgrade.');
void qs.logger.log('Done running database upgrade.');
}
} finally {
currentUpgradeTmp.cleanup();
await currentUpgradeTmp.cleanup();
}
}

View File

@@ -3,7 +3,7 @@ import * as React from 'react';
import * as Sarif from 'sarif';
import * as Keys from '../pure/result-keys';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection, emptyQueryResultsMessage } from './result-table-utils';
import { onNavigation, NavigationEvent } from './results';
import { PathTableResultSet } from '../pure/interface-types';
import {
@@ -79,7 +79,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
if (this.props.nonemptyRawResults) {
return <span>No Alerts. See <a href='#' onClick={this.props.showRawResults}>raw results</a>.</span>;
} else {
return <span>No Alerts</span>;
return emptyQueryResultsMessage();
}
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { ResultTableProps, className } from './result-table-utils';
import { ResultTableProps, className, emptyQueryResultsMessage } from './result-table-utils';
import { RAW_RESULTS_LIMIT, RawResultsSortState } from '../pure/interface-types';
import { RawTableResultSet } from '../pure/interface-types';
import RawTableHeader from './RawTableHeader';
@@ -12,7 +12,7 @@ export type RawTableProps = ResultTableProps & {
offset: number;
};
export class RawTable extends React.Component<RawTableProps, {}> {
export class RawTable extends React.Component<RawTableProps, Record<string, never>> {
constructor(props: RawTableProps) {
super(props);
}
@@ -21,6 +21,10 @@ export class RawTable extends React.Component<RawTableProps, {}> {
const { resultSet, databaseUri } = this.props;
let dataRows = resultSet.rows;
if (dataRows.length === 0) {
return emptyQueryResultsMessage();
}
let numTruncatedResults = 0;
if (dataRows.length > RAW_RESULTS_LIMIT) {
numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT;

View File

@@ -140,3 +140,9 @@ export function nextSortDirection(direction: SortDirection | undefined, includeU
return assertNever(direction);
}
}
export function emptyQueryResultsMessage(): JSX.Element {
return <div className='vscode-codeql__empty-query-message'><span>
This query returned no results. If this isn&apos;t what you were expecting, and for effective query-writing tips, check out the <a href="https://codeql.github.com/docs/codeql-language-guides/">CodeQL language guides</a>.
</span></div>;
}

View File

@@ -82,7 +82,7 @@ export class ResultTables
private getResultSets(): ResultSet[] {
const resultSets: ResultSet[] =
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2783
this.props.rawResultSets.map((rs) => ({ t: 'RawResultSet', ...rs }));
@@ -328,13 +328,15 @@ export class ResultTables
}
private vscodeMessageHandler(evt: MessageEvent) {
// sanitize origin
const origin = evt.origin.replace(/\n|\r/g, '');
evt.origin === window.origin
? this.handleMessage(evt.data as IntoResultsViewMsg)
: console.error(`Invalid event origin ${evt.origin}`);
: console.error(`Invalid event origin ${origin}`);
}
}
class ResultTable extends React.Component<ResultTableProps, {}> {
class ResultTable extends React.Component<ResultTableProps, Record<string, never>> {
constructor(props: ResultTableProps) {
super(props);

View File

@@ -71,7 +71,7 @@ export const onNavigation = new EventHandlerList<NavigationEvent>();
/**
* A minimal state container for displaying results.
*/
class App extends React.Component<{}, ResultsViewState> {
class App extends React.Component<Record<string, never>, ResultsViewState> {
constructor(props: any) {
super(props);
this.state = {
@@ -102,7 +102,7 @@ class App extends React.Component<{}, ResultsViewState> {
queryPath: msg.queryPath,
});
this.loadResults();
void this.loadResults();
break;
case 'showInterpretedPage':
this.updateStateWithNewResultsInfo({
@@ -134,7 +134,7 @@ class App extends React.Component<{}, ResultsViewState> {
queryName: msg.queryName,
queryPath: msg.queryPath,
});
this.loadResults();
void this.loadResults();
break;
case 'resultsUpdating':
this.setState({
@@ -307,9 +307,11 @@ class App extends React.Component<{}, ResultsViewState> {
}
private vscodeMessageHandler(evt: MessageEvent) {
// sanitize origin
const origin = evt.origin.replace(/\n|\r/g, '');
evt.origin === window.origin
? this.handleMessage(evt.data as IntoResultsViewMsg)
: console.error(`Invalid event origin ${evt.origin}`);
: console.error(`Invalid event origin ${origin}`);
}
}

View File

@@ -15,6 +15,10 @@
display: flex;
padding: 0.5em 0;
align-items: center;
top: 0;
background-color: var(--vscode-editorGutter-background);
position: sticky;
z-index: 1;
}
.vscode-codeql__table-selection-header-item {
@@ -221,3 +225,16 @@ td.vscode-codeql__path-index-cell {
.vscode-codeql__compare-body > tbody {
vertical-align: top;
}
.vscode-codeql__empty-query-message {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.vscode-codeql__empty-query-message > span {
max-width: 80%;
font-size: 14px;
text-align: center;
}

View File

@@ -1,5 +1,19 @@
module.exports = {
env: {
mocha: true
},
rules: {
"@typescript-eslint/ban-types": [
"error",
{
// For a full list of the default banned types, see:
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md
extendDefaults: true,
"types": {
// Don't complain about the `Function` type in test files. (Default is `true`.)
"Function": false,
}
}
]
}
}

View File

@@ -0,0 +1,3 @@
import javascript
select 1

View File

@@ -25,7 +25,7 @@ describe('Databases', function() {
beforeEach(async () => {
try {
const extension = await extensions.getExtension<CodeQLExtensionInterface | {}>('GitHub.vscode-codeql')!.activate();
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
if ('databaseManager' in extension) {
databaseManager = extension.databaseManager;
} else {

View File

@@ -27,7 +27,7 @@ export default function(mocha: Mocha) {
if (!fs.existsSync(dbLoc)) {
try {
await new Promise((resolve, reject) => {
fetch(DB_URL).then(response => {
return fetch(DB_URL).then(response => {
const dest = fs.createWriteStream(dbLoc);
response.body.pipe(dest);
@@ -68,7 +68,7 @@ export default function(mocha: Mocha) {
// ensure etension is cleaned up.
(mocha.options as any).globalTeardown.push(
async () => {
const extension = await extensions.getExtension<CodeQLExtensionInterface | {}>('GitHub.vscode-codeql')!.activate();
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
// This shuts down the extension and can only be run after all tests have completed.
// If this is not called, then the test process will hang.
if ('dispose' in extension) {

View File

@@ -45,7 +45,7 @@ describe('Queries', function() {
sandbox = sinon.createSandbox();
try {
const extension = await extensions.getExtension<CodeQLExtensionInterface | {}>('GitHub.vscode-codeql')!.activate();
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
if ('databaseManager' in extension) {
databaseManager = extension.databaseManager;
cli = extension.cliServer;
@@ -150,7 +150,7 @@ describe('Queries', function() {
fs.readFileSync(qlpackFile, 'utf8')
);
// Should have chosen the js libraries
expect(qlpackContents.libraryPathDependencies[0]).to.eq('codeql-javascript');
expect(qlpackContents.libraryPathDependencies[0]).to.include('javascript');
});
it('should avoid creating a quick query', async () => {

View File

@@ -34,7 +34,7 @@ class Checkpoint<T> {
this.res = () => { /**/ };
this.rej = () => { /**/ };
this.promise = new Promise((res, rej) => {
this.res = res as () => {};
this.res = res as () => Record<string, never>;
this.rej = rej;
});
}
@@ -104,7 +104,7 @@ describe('using the query server', function() {
beforeEach(async () => {
try {
const extension = await extensions.getExtension<CodeQLExtensionInterface | {}>('GitHub.vscode-codeql')!.activate();
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
if ('cliServer' in extension && 'qs' in extension) {
cliServer = extension.cliServer;
qs = extension.qs;
@@ -119,7 +119,7 @@ describe('using the query server', function() {
it('should be able to start the query server', async function() {
await qs.startQueryServer();
queryServerStarted.resolve();
await queryServerStarted.resolve();
});
for (const queryTestCase of queryTestCases) {
@@ -129,7 +129,7 @@ describe('using the query server', function() {
const parsedResults = new Checkpoint<void>();
it('should register the database if necessary', async () => {
if (await qs.supportsDatabaseRegistration()) {
if (await cliServer.cliConstraints.supportsDatabaseRegistration()) {
await qs.sendRequest(messages.registerDatabases, { databases: [db] }, token, (() => { /**/ }) as any);
}
});
@@ -160,10 +160,10 @@ describe('using the query server', function() {
};
const result = await qs.sendRequest(messages.compileQuery, params, token, () => { /**/ });
expect(result.messages!.length).to.equal(0);
compilationSucceeded.resolve();
await compilationSucceeded.resolve();
}
catch (e) {
compilationSucceeded.reject(e);
await compilationSucceeded.reject(e);
}
});
@@ -171,7 +171,7 @@ describe('using the query server', function() {
try {
await compilationSucceeded.done();
const callbackId = qs.registerCallback(_res => {
evaluationSucceeded.resolve();
void evaluationSucceeded.resolve();
});
const queryToRun: messages.QueryToRun = {
resultsPath: RESULTS_PATH,
@@ -190,7 +190,7 @@ describe('using the query server', function() {
await qs.sendRequest(messages.runQueries, params, token, () => { /**/ });
}
catch (e) {
evaluationSucceeded.reject(e);
await evaluationSucceeded.reject(e);
}
});
@@ -203,7 +203,7 @@ describe('using the query server', function() {
const decoded = await cliServer.bqrsDecode(RESULTS_PATH, resultSet.name);
actualResultSets[resultSet.name] = decoded.tuples;
}
parsedResults.resolve();
await parsedResults.resolve();
});
it(`should have correct results for query ${queryName}`, async function() {

View File

@@ -1,22 +1,27 @@
import { expect } from 'chai';
import { extensions } from 'vscode';
import { extensions, Uri } from 'vscode';
import * as path from 'path';
import { SemVer } from 'semver';
import { CodeQLCliServer } from '../../cli';
import { CodeQLCliServer, QueryInfoByLanguage } from '../../cli';
import { CodeQLExtensionInterface } from '../../extension';
import { skipIfNoCodeQL } from '../ensureCli';
import { getOnDiskWorkspaceFolders } from '../../helpers';
import { getOnDiskWorkspaceFolders, getQlPackForDbscheme, languageToDbScheme } from '../../helpers';
import { resolveQueries } from '../../contextual/queryResolver';
import { KeyType } from '../../contextual/keyType';
/**
* Perform proper integration tests by running the CLI
*/
describe('Use cli', function() {
const supportedLanguages = ['cpp', 'csharp', 'go', 'java', 'javascript', 'python'];
this.timeout(60000);
let cli: CodeQLCliServer;
beforeEach(async () => {
const extension = await extensions.getExtension<CodeQLExtensionInterface | {}>('GitHub.vscode-codeql')!.activate();
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
if ('cliServer' in extension) {
cli = extension.cliServer;
} else {
@@ -24,13 +29,15 @@ describe('Use cli', function() {
}
});
it('should have the correct version of the cli', async () => {
expect(
(await cli.getVersion()).toString()
).to.eq(
new SemVer(process.env.CLI_VERSION || '').toString()
);
});
if (process.env.CLI_VERSION !== 'nightly') {
it('should have the correct version of the cli', async () => {
expect(
(await cli.getVersion()).toString()
).to.eq(
new SemVer(process.env.CLI_VERSION || '').toString()
);
});
}
it('should resolve ram', async () => {
const result = await (cli as any).resolveRam(8192);
@@ -43,11 +50,49 @@ describe('Use cli', function() {
it('should resolve query packs', async function() {
skipIfNoCodeQL(this);
const qlpacks = await cli.resolveQlpacks(getOnDiskWorkspaceFolders());
// should have a bunch of qlpacks. just check that a few known ones exist
expect(qlpacks['codeql-cpp']).not.to.be.undefined;
expect(qlpacks['codeql-csharp']).not.to.be.undefined;
expect(qlpacks['codeql-java']).not.to.be.undefined;
expect(qlpacks['codeql-javascript']).not.to.be.undefined;
expect(qlpacks['codeql-python']).not.to.be.undefined;
// Depending on the version of the CLI, the qlpacks may have different names
// (e.g. "codeql/javascript-all" vs "codeql-javascript"),
// so we just check that the expected languages are included.
for (const expectedLanguage of supportedLanguages) {
expect((Object.keys(qlpacks)).includes(expectedLanguage));
}
});
it('should resolve languages', async function() {
skipIfNoCodeQL(this);
const languages = await cli.resolveLanguages();
for (const expectedLanguage of supportedLanguages) {
expect(languages).to.have.property(expectedLanguage).that.is.not.undefined;
}
});
it('should resolve query by language', async function() {
skipIfNoCodeQL(this);
const queryPath = path.join(__dirname, 'data', 'simple-javascript-query.ql');
const queryInfo: QueryInfoByLanguage = await cli.resolveQueryByLanguage(getOnDiskWorkspaceFolders(), Uri.file(queryPath));
expect((Object.keys(queryInfo.byLanguage))[0]).to.eql('javascript');
});
supportedLanguages.forEach(lang => {
if (lang === 'go') {
// The codeql-go submodule is not available in the integration tests.
return;
}
it(`should resolve printAST queries for ${lang}`, async function() {
skipIfNoCodeQL(this);
const pack = await getQlPackForDbscheme(cli, languageToDbScheme[lang]);
expect(pack.dbschemePack).to.contain(lang);
if (pack.dbschemePackIsLibraryPack) {
expect(pack.queryPack).to.contain(lang);
}
const result = await resolveQueries(cli, pack, KeyType.PrintAstQuery);
// It doesn't matter what the name or path of the query is, only
// that we have found exactly one query.
expect(result.length).to.eq(1);
});
});
});

View File

@@ -6,10 +6,15 @@ import { workspace } from 'vscode';
/**
* This module ensures that the proper CLI is available for tests of the extension.
* There are two environment variables to control this module:
* There are three environment variables to control this module:
*
* - CLI_VERSION: The version of the CLI to install. Defaults to the most recent
* version. Note that for now, we must maintain the default version by hand.
* This may be set to `nightly`, in which case the `NIGHTLY_URL` variable must
* also be set.
*
* - NIGHTLY_URL: The URL for a nightly release of the CodeQL CLI that will be
* used if `CLI_VERSION` is set to `nightly`.
*
* - CLI_BASE_DIR: The base dir where the CLI will be downloaded and unzipped.
* The download location is `${CLI_BASE_DIR}/assets` and the unzip loction is
@@ -39,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.4.2';
const CLI_VERSION = process.env.CLI_VERSION || 'v2.6.1';
process.env.CLI_VERSION = CLI_VERSION;
// Base dir where CLIs will be downloaded into
@@ -133,6 +138,11 @@ export function skipIfNoCodeQL(context: Mocha.Context) {
* Url to download from
*/
function getCliDownloadUrl(assetName: string) {
if (CLI_VERSION === 'nightly') {
if (!process.env.NIGHTLY_URL)
throw new Error('Nightly CLI was specified but no URL to download it from was given!');
return process.env.NIGHTLY_URL + `/${assetName}`;
}
return `https://github.com/github/codeql-cli-binaries/releases/download/${CLI_VERSION}/${assetName}`;
}

View File

@@ -79,6 +79,7 @@ export async function runTestsInDirectory(testsRoot: string, useCli = false): Pr
.filter(f => f.endsWith('.helper.js'))
.forEach(f => {
console.log(`Executing helper ${f}`);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const helper = require(path.resolve(testsRoot, f)).default;
helper(mocha);
});

View File

@@ -29,7 +29,7 @@ describe('config listeners', function() {
});
interface TestConfig<T> {
clazz: new () => {};
clazz: new () => unknown;
settings: {
name: string;
property: string;
@@ -48,6 +48,10 @@ describe('config listeners', function() {
name: 'codeQL.runningTests.numberOfThreads',
property: 'numberTestThreads',
values: [1, 0]
}, {
name: 'codeQL.runningQueries.maxPaths',
property: 'maxPaths',
values: [0, 1]
}]
},
{

View File

@@ -67,11 +67,13 @@ describe('databases', () => {
} as unknown as ExtensionContext,
{
sendRequest: sendRequestSpy,
supportsDatabaseRegistration: supportsDatabaseRegistrationSpy,
onDidStartQueryServer: () => { /**/ }
} as unknown as QueryServerClient,
{
supportsLanguageName: supportsLanguageNameSpy,
cliConstraints: {
supportsLanguageName: supportsLanguageNameSpy,
supportsDatabaseRegistration: supportsDatabaseRegistrationSpy,
},
resolveDatabase: resolveDatabaseSpy
} as unknown as CodeQLCliServer,
{} as Logger,
@@ -177,7 +179,7 @@ describe('databases', () => {
expect(spy).to.have.been.calledWith(mockEvent);
});
it('should add a database item source archive', async function() {
it('should add a database item source archive', async function () {
const mockDbItem = createMockDB();
mockDbItem.name = 'xxx';
await (databaseManager as any).addDatabaseSourceArchiveFolder(mockDbItem);
@@ -412,6 +414,72 @@ describe('databases', () => {
expect(result).to.eq('');
});
describe('isAffectedByTest', () => {
const directoryStats = new fs.Stats();
const fileStats = new fs.Stats();
before(() => {
sinon.stub(directoryStats, 'isDirectory').returns(true);
sinon.stub(fileStats, 'isDirectory').returns(false);
});
it('should return true for testproj database in test directory', async () => {
sandbox.stub(fs, 'stat').resolves(directoryStats);
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/dir/dir.testproj'));
expect(await db.isAffectedByTest('/path/to/dir')).to.true;
});
it('should return false for non-existent test directory', async () => {
sandbox.stub(fs, 'stat').throws('Simulated Error: ENOENT');
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/dir/dir.testproj'));
expect(await db.isAffectedByTest('/path/to/dir')).to.false;
});
it('should return false for non-testproj database in test directory', async () => {
sandbox.stub(fs, 'stat').resolves(directoryStats);
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/dir/dir.proj'));
expect(await db.isAffectedByTest('/path/to/dir')).to.false;
});
it('should return false for testproj database outside test directory', async () => {
sandbox.stub(fs, 'stat').resolves(directoryStats);
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/other/dir.testproj'));
expect(await db.isAffectedByTest('/path/to/dir')).to.false;
});
it('should return false for testproj database for prefix directory', async () => {
sandbox.stub(fs, 'stat').resolves(directoryStats);
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/dir/dir.testproj'));
// /path/to/d is a prefix of /path/to/dir/dir.testproj, but
// /path/to/dir/dir.testproj is not under /path/to/d
expect(await db.isAffectedByTest('/path/to/d')).to.false;
});
it('should return true for testproj database for test file', async () => {
sandbox.stub(fs, 'stat').resolves(fileStats);
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/dir/dir.testproj'));
expect(await db.isAffectedByTest('/path/to/dir/test.ql')).to.true;
});
it('should return false for non-existent test file', async () => {
sandbox.stub(fs, 'stat').throws('Simulated Error: ENOENT');
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/dir/dir.testproj'));
expect(await db.isAffectedByTest('/path/to/dir/test.ql')).to.false;
});
it('should return false for non-testproj database for test file', async () => {
sandbox.stub(fs, 'stat').resolves(fileStats);
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/dir/dir.proj'));
expect(await db.isAffectedByTest('/path/to/dir/test.ql')).to.false;
});
it('should return false for testproj database not matching test file', async () => {
sandbox.stub(fs, 'stat').resolves(fileStats);
const db = createMockDB(sourceLocationUri(), Uri.file('/path/to/dir/dir.testproj'));
expect(await db.isAffectedByTest('/path/to/test.ql')).to.false;
});
});
function createMockDB(
// source archive location must be a real(-ish) location since
// tests will add this to the workspace location

View File

@@ -19,23 +19,23 @@ This test uses an AST generated from this file (already BQRS-decoded in ../data/
int interrupt_init(void)
{
return 0;
return 0;
}
void enable_interrupts(void)
{
return;
return;
}
int disable_interrupts(void)
{
return 0;
return 0;
}
*/
describe('AstBuilder', () => {
let mockCli: CodeQLCliServer;
let overrides: Record<string, object | undefined>;
let overrides: Record<string, Record<string, unknown> | undefined>;
beforeEach(() => {
mockCli = {
@@ -135,7 +135,7 @@ describe('AstBuilder', () => {
};
const astBuilder = createAstBuilder();
expect(astBuilder.getRoots()).to.be.rejectedWith('AST is invalid');
await expect(astBuilder.getRoots()).to.be.rejectedWith('AST is invalid');
});
function createAstBuilder() {

View File

@@ -18,28 +18,48 @@ describe('queryResolver', () => {
let writeFileSpy: sinon.SinonSpy;
let getQlPackForDbschemeSpy: sinon.SinonStub;
let getPrimaryDbschemeSpy: sinon.SinonStub;
let mockCli: Record<string, sinon.SinonStub>;
let mockCli: Record<string, sinon.SinonStub | Record<string, sinon.SinonStub>>;
beforeEach(() => {
mockCli = {
resolveQueriesInSuite: sinon.stub()
resolveQueriesInSuite: sinon.stub(),
cliConstraints: {
supportsAllowLibraryPacksInResolveQueries: sinon.stub().returns(true),
}
};
module = createModule();
});
describe('resolveQueries', () => {
it('should resolve a query', async () => {
mockCli.resolveQueriesInSuite.returns(['a', 'b']);
const result = await module.resolveQueries(mockCli, 'my-qlpack', KeyType.DefinitionQuery);
const result = await module.resolveQueries(mockCli, { dbschemePack: 'my-qlpack' }, KeyType.DefinitionQuery);
expect(result).to.deep.equal(['a', 'b']);
expect(writeFileSpy.getCall(0).args[0]).to.match(/.qls$/);
expect(yaml.safeLoad(writeFileSpy.getCall(0).args[1])).to.deep.equal({
qlpack: 'my-qlpack',
expect(yaml.safeLoad(writeFileSpy.getCall(0).args[1])).to.deep.equal([{
from: 'my-qlpack',
queries: '.',
include: {
kind: 'definitions',
'tags contain': 'ide-contextual-queries/local-definitions'
}
});
}]);
});
it('should resolve a query from the queries pack if this is an old CLI', async () => {
// pretend this is an older CLI
(mockCli.cliConstraints as any).supportsAllowLibraryPacksInResolveQueries.returns(false);
mockCli.resolveQueriesInSuite.returns(['a', 'b']);
const result = await module.resolveQueries(mockCli, { dbschemePackIsLibraryPack: true, dbschemePack: 'my-qlpack', queryPack: 'my-qlpack2' }, KeyType.DefinitionQuery);
expect(result).to.deep.equal(['a', 'b']);
expect(writeFileSpy.getCall(0).args[0]).to.match(/.qls$/);
expect(yaml.safeLoad(writeFileSpy.getCall(0).args[1])).to.deep.equal([{
from: 'my-qlpack2',
queries: '.',
include: {
kind: 'definitions',
'tags contain': 'ide-contextual-queries/local-definitions'
}
}]);
});
it('should throw an error when there are no queries found', async () => {
@@ -48,12 +68,12 @@ describe('queryResolver', () => {
// TODO: Figure out why chai-as-promised isn't failing the test on an
// unhandled rejection.
try {
await module.resolveQueries(mockCli, 'my-qlpack', KeyType.DefinitionQuery);
await module.resolveQueries(mockCli, { dbschemePack: 'my-qlpack' }, KeyType.DefinitionQuery);
// should reject
expect(true).to.be.false;
} catch (e) {
expect(e.message).to.eq(
'Couldn\'t find any queries tagged ide-contextual-queries/local-definitions for qlpack my-qlpack'
'Couldn\'t find any queries tagged ide-contextual-queries/local-definitions in any of the following packs: my-qlpack.'
);
}
});

View File

@@ -69,7 +69,7 @@ describe('databaseFetcher', function() {
it('should fail on a nonexistent project', async () => {
quickPickSpy.resolves('javascript');
const lgtmUrl = 'https://lgtm.com/projects/g/github/hucairz';
expect(convertToDatabaseUrl(lgtmUrl)).to.rejectedWith(/Invalid LGTM URL/);
await expect(convertToDatabaseUrl(lgtmUrl)).to.rejectedWith(/Invalid LGTM URL/);
});
});

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai';
import 'mocha';
import { ExtensionContext, Memento, window } from 'vscode';
import { EnvironmentVariableCollection, EnvironmentVariableMutator, Event, ExtensionContext, ExtensionMode, Memento, SecretStorage, SecretStorageChangeEvent, Uri, window } from 'vscode';
import * as yaml from 'js-yaml';
import * as tmp from 'tmp';
import * as path from 'path';
@@ -146,9 +146,10 @@ describe('helpers', () => {
});
class MockExtensionContext implements ExtensionContext {
extensionMode: ExtensionMode = 3;
subscriptions: { dispose(): unknown }[] = [];
workspaceState: Memento = new MockMemento();
globalState: Memento = new MockMemento();
globalState = new MockGlobalStorage();
extensionPath = '';
asAbsolutePath(_relativePath: string): string {
throw new Error('Method not implemented.');
@@ -156,6 +157,38 @@ describe('helpers', () => {
storagePath = '';
globalStoragePath = '';
logPath = '';
extensionUri = Uri.parse('');
environmentVariableCollection = new MockEnvironmentVariableCollection();
secrets = new MockSecretStorage();
storageUri = Uri.parse('');
globalStorageUri = Uri.parse('');
logUri = Uri.parse('');
extension: any;
}
class MockEnvironmentVariableCollection implements EnvironmentVariableCollection {
persistent = false;
replace(_variable: string, _value: string): void {
throw new Error('Method not implemented.');
}
append(_variable: string, _value: string): void {
throw new Error('Method not implemented.');
}
prepend(_variable: string, _value: string): void {
throw new Error('Method not implemented.');
}
get(_variable: string): EnvironmentVariableMutator | undefined {
throw new Error('Method not implemented.');
}
forEach(_callback: (variable: string, mutator: EnvironmentVariableMutator, collection: EnvironmentVariableCollection) => any, _thisArg?: any): void {
throw new Error('Method not implemented.');
}
delete(_variable: string): void {
throw new Error('Method not implemented.');
}
clear(): void {
throw new Error('Method not implemented.');
}
}
class MockMemento implements Memento {
@@ -184,6 +217,25 @@ describe('helpers', () => {
}
}
class MockGlobalStorage extends MockMemento {
public setKeysForSync(_keys: string[]): void {
return;
}
}
class MockSecretStorage implements SecretStorage {
get(_key: string): Thenable<string | undefined> {
throw new Error('Method not implemented.');
}
store(_key: string, _value: string): Thenable<void> {
throw new Error('Method not implemented.');
}
delete(_key: string): Thenable<void> {
throw new Error('Method not implemented.');
}
onDidChange!: Event<SecretStorageChangeEvent>;
}
it('should report stream progress', () => {
const spy = sandbox.spy();
const mockReadable = {

View File

@@ -268,7 +268,7 @@ describe('query-history', () => {
});
});
function createMockQueryHistory(allHistory: {}[]) {
function createMockQueryHistory(allHistory: Record<string, unknown>[]) {
return {
assertSingleQuery: (QueryHistoryManager.prototype as any).assertSingleQuery,
findOtherQueryToCompare: (QueryHistoryManager.prototype as any).findOtherQueryToCompare,

View File

@@ -54,6 +54,23 @@ describe('CompletedQuery', () => {
expect(completedQuery.queryName).to.eq('Quick evaluation of yz:1');
});
it('should get the query file name', () => {
const completedQuery = mockCompletedQuery();
// from the query path
expect(completedQuery.queryFileName).to.eq('stu');
// from quick eval position
(completedQuery.query as any).quickEvalPosition = {
line: 1,
endLine: 2,
fileName: '/home/users/yz'
};
expect(completedQuery.queryFileName).to.eq('yz:1-2');
(completedQuery.query as any).quickEvalPosition.endLine = 1;
expect(completedQuery.queryFileName).to.eq('yz:1');
});
it('should get the label', () => {
const completedQuery = mockCompletedQuery();
expect(completedQuery.getLabel()).to.eq('ghi');

View File

@@ -24,6 +24,23 @@ describe('run-queries', () => {
expect(info.dataset).to.eq('file:///abc');
});
it('should check if interpreted results can be created', async () => {
const info = createMockQueryInfo();
(info.dbItem.hasMetadataFile as sinon.SinonStub).returns(true);
expect(await info.canHaveInterpretedResults()).to.eq(true);
(info.dbItem.hasMetadataFile as sinon.SinonStub).returns(false);
expect(await info.canHaveInterpretedResults()).to.eq(false);
(info.dbItem.hasMetadataFile as sinon.SinonStub).returns(true);
info.metadata!.kind = undefined;
expect(await info.canHaveInterpretedResults()).to.eq(false);
info.metadata!.kind = 'table';
expect(await info.canHaveInterpretedResults()).to.eq(false);
});
describe('compile', () => {
it('should compile', async () => {
const info = createMockQueryInfo();
@@ -73,9 +90,14 @@ describe('run-queries', () => {
{
contents: {
datasetUri: 'file:///abc'
}
},
hasMetadataFile: sinon.stub()
} as unknown as DatabaseItem,
'my-scheme' // queryDbscheme
'my-scheme', // queryDbscheme,
undefined,
{
kind: 'problem'
}
);
}

View File

@@ -1,24 +1,61 @@
import 'vscode-test';
import 'mocha';
import * as sinon from 'sinon';
import * as fs from 'fs-extra';
import { Uri, WorkspaceFolder } from 'vscode';
import { expect } from 'chai';
import { QLTestAdapter } from '../../test-adapter';
import { CodeQLCliServer } from '../../cli';
import { DatabaseItem, DatabaseItemImpl, DatabaseManager, FullDatabaseOptions } from '../../databases';
describe('test-adapter', () => {
let adapter: QLTestAdapter;
let fakeDatabaseManager: DatabaseManager;
let currentDatabaseItem: DatabaseItem | undefined;
let databaseItems: DatabaseItem[] = [];
let openDatabaseSpy: sinon.SinonStub;
let removeDatabaseItemSpy: sinon.SinonStub;
let renameDatabaseItemSpy: sinon.SinonStub;
let setCurrentDatabaseItemSpy: sinon.SinonStub;
let runTestsSpy: sinon.SinonStub;
let resolveTestsSpy: sinon.SinonStub;
let resolveQlpacksSpy: sinon.SinonStub;
let sandox: sinon.SinonSandbox;
const preTestDatabaseItem = new DatabaseItemImpl(
Uri.file('/path/to/test/dir/dir.testproj'),
undefined,
{ displayName: 'custom display name' } as unknown as FullDatabaseOptions,
(_) => { /* no change event listener */ }
);
const postTestDatabaseItem = new DatabaseItemImpl(
Uri.file('/path/to/test/dir/dir.testproj'),
undefined,
{ displayName: 'default name' } as unknown as FullDatabaseOptions,
(_) => { /* no change event listener */ }
);
beforeEach(() => {
sandox = sinon.createSandbox();
mockRunTests();
openDatabaseSpy = sandox.stub().resolves(postTestDatabaseItem);
removeDatabaseItemSpy = sandox.stub().resolves();
renameDatabaseItemSpy = sandox.stub().resolves();
setCurrentDatabaseItemSpy = sandox.stub().resolves();
resolveQlpacksSpy = sandox.stub().resolves({});
resolveTestsSpy = sandox.stub().resolves([]);
fakeDatabaseManager = {
currentDatabaseItem: undefined,
databaseItems: undefined,
openDatabase: openDatabaseSpy,
removeDatabaseItem: removeDatabaseItemSpy,
renameDatabaseItem: renameDatabaseItemSpy,
setCurrentDatabaseItem: setCurrentDatabaseItemSpy,
} as unknown as DatabaseManager;
sandox.stub(fakeDatabaseManager, 'currentDatabaseItem').get(() => currentDatabaseItem);
sandox.stub(fakeDatabaseManager, 'databaseItems').get(() => databaseItems);
sandox.stub(preTestDatabaseItem, 'isAffectedByTest').resolves(true);
adapter = new QLTestAdapter({
name: 'ABC',
uri: Uri.parse('file:/ab/c')
@@ -26,7 +63,8 @@ describe('test-adapter', () => {
runTests: runTestsSpy,
resolveQlpacks: resolveQlpacksSpy,
resolveTests: resolveTestsSpy
} as unknown as CodeQLCliServer);
} as unknown as CodeQLCliServer,
fakeDatabaseManager);
});
afterEach(() => {
@@ -58,7 +96,7 @@ describe('test-adapter', () => {
type: 'test',
state: 'errored',
test: gPath,
message: `\nerrored: ${gPath}\npqr\nxyz\n`,
message: `\ncompilation error: ${gPath}\nERROR: abc\n`,
decorations: [
{ line: 1, message: 'abc' }
]
@@ -74,12 +112,33 @@ describe('test-adapter', () => {
expect(listenerSpy).to.have.callCount(5);
});
it('should reregister testproj databases around test run', async () => {
sandox.stub(fs, 'access').resolves();
currentDatabaseItem = preTestDatabaseItem;
databaseItems = [preTestDatabaseItem];
await adapter.run(['/path/to/test/dir']);
removeDatabaseItemSpy.getCall(0).calledBefore(runTestsSpy.getCall(0));
openDatabaseSpy.getCall(0).calledAfter(runTestsSpy.getCall(0));
renameDatabaseItemSpy.getCall(0).calledAfter(openDatabaseSpy.getCall(0));
setCurrentDatabaseItemSpy.getCall(0).calledAfter(openDatabaseSpy.getCall(0));
sinon.assert.calledOnceWithExactly(
removeDatabaseItemSpy, sinon.match.any, sinon.match.any, preTestDatabaseItem);
sinon.assert.calledOnceWithExactly(
openDatabaseSpy, sinon.match.any, sinon.match.any, preTestDatabaseItem.databaseUri);
sinon.assert.calledOnceWithExactly(
renameDatabaseItemSpy, postTestDatabaseItem, preTestDatabaseItem.name);
sinon.assert.calledOnceWithExactly(
setCurrentDatabaseItemSpy, postTestDatabaseItem, true);
});
function mockRunTests() {
// runTests is an async generator function. This is not directly supported in sinon
// However, we can pretend the same thing by just returning an async array.
runTestsSpy = sandox.stub();
runTestsSpy.returns(
(async function*() {
(async function* () {
yield Promise.resolve({
test: Uri.parse('file:/ab/c/d.ql').fsPath,
pass: true,
@@ -90,14 +149,16 @@ describe('test-adapter', () => {
pass: false,
diff: ['pqr', 'xyz'],
// a compile error
failureStage: 'COMPILATION',
messages: [
{ position: { line: 1 }, message: 'abc' }
{ position: { line: 1 }, message: 'abc', severity: 'ERROR' }
]
});
yield Promise.resolve({
test: Uri.parse('file:/ab/c/e/f/h.ql').fsPath,
pass: false,
diff: ['jkh', 'tuv'],
failureStage: 'RESULT',
messages: []
});
})()

View File

@@ -105,7 +105,7 @@ async function main() {
}
}
main();
void main();
function getLaunchArgs(dir: TestDir) {
@@ -128,6 +128,14 @@ function getLaunchArgs(dir: TestDir) {
return [
'--disable-gpu',
path.resolve(__dirname, '../../test/data'),
// explicitly disable extensions that are known to interfere with the CLI integration tests
'--disable-extension',
'eamodio.gitlens',
'--disable-extension',
'github.codespaces',
'--disable-extension',
'github.copilot',
process.env.TEST_CODEQL_PATH!
];

View File

@@ -48,7 +48,7 @@ describe('OutputChannelLogger tests', () => {
});
it('should create a side log in the workspace area', async () => {
logger.init(tempFolders.storagePath.name);
await logger.setLogStoragePath(tempFolders.storagePath.name, false);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -65,7 +65,7 @@ describe('OutputChannelLogger tests', () => {
});
it('should delete side logs on dispose', async () => {
logger.init(tempFolders.storagePath.name);
await logger.setLogStoragePath(tempFolders.storagePath.name, false);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -79,8 +79,23 @@ describe('OutputChannelLogger tests', () => {
expect(mockOutputChannel.dispose).to.have.been.calledWith();
});
it('should not delete side logs on dispose in a custom directory', async () => {
await logger.setLogStoragePath(tempFolders.storagePath.name, true);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
await logger.dispose();
// need to wait for disposable-object to dispose
await waitABit();
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
expect(mockOutputChannel.dispose).to.have.been.calledWith();
});
it('should remove an additional log location', async () => {
logger.init(tempFolders.storagePath.name);
await logger.setLogStoragePath(tempFolders.storagePath.name, false);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -94,11 +109,36 @@ describe('OutputChannelLogger tests', () => {
expect(fs.readFileSync(path.join(testLoggerFolder, 'second'), 'utf8')).to.equal('yyy\n');
});
it('should delete an existing folder on init', async () => {
it('should not remove an additional log location in a custom directory', async () => {
await logger.setLogStoragePath(tempFolders.storagePath.name, true);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
await logger.removeAdditionalLogLocation('first');
// need to wait for disposable-object to dispose
await waitABit();
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
expect(fs.readFileSync(path.join(testLoggerFolder, 'second'), 'utf8')).to.equal('yyy\n');
});
it('should delete an existing folder when setting the log storage path', async () => {
fs.createFileSync(path.join(tempFolders.storagePath.name, 'test-logger', 'xxx'));
logger.init(tempFolders.storagePath.name);
await logger.setLogStoragePath(tempFolders.storagePath.name, false);
// should be empty dir
await waitABit();
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
expect(fs.existsSync(testLoggerFolder)).to.be.false;
});
it('should not delete an existing folder when setting the log storage path for a custom directory', async () => {
fs.createFileSync(path.join(tempFolders.storagePath.name, 'test-logger', 'xxx'));
await logger.setLogStoragePath(tempFolders.storagePath.name, true);
// should not be empty dir
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
expect(fs.readdirSync(testLoggerFolder).length).to.equal(1);
});
@@ -130,7 +170,7 @@ describe('OutputChannelLogger tests', () => {
});
}
function waitABit(ms = 50): Promise<void> {
function waitABit(ms = 50): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
});