Compare commits

...

150 Commits

Author SHA1 Message Date
jcreedcmu
a472786d93 Merge pull request #290 from jcreedcmu/jcreed/bump-1.1.0
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
Fix package.json for minor version bump.
2020-03-17 16:21:58 -04:00
Jason Reed
bb6faaedbe Fix package.json for minor version bump. 2020-03-17 16:13:52 -04:00
jcreedcmu
91fcd4e26c Merge pull request #288 from jcreedcmu/jcreed/v1.1.0
Update CHANGELOG.md for release.
2020-03-17 16:05:54 -04:00
jcreedcmu
61f182342f Update extensions/ql-vscode/CHANGELOG.md
Co-Authored-By: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-03-17 15:59:16 -04:00
Jason Reed
416a87fe1d Update CHANGELOG.md for release. 2020-03-17 15:50:49 -04:00
Andrew Eisenberg
0bd835958b Merge pull request #281 from aeisenberg/aeisenberg/eslint
chore: Introduce eslint
2020-03-17 11:58:04 -07:00
Andrew Eisenberg
8e73c64e63 Merge pull request #282 from aeisenberg/aeisenberg/error-message
fix: Readable error message for invalid file
2020-03-17 09:24:21 -07:00
Andrew Eisenberg
443abea7d7 chore: Introduce eslint
Adds eslint support and fixes linting problems in a few files.

This change adds an npm task, but does not enforce linting for builds.

The idea is to slowly fix linting problems over time.

Closes #238.
2020-03-17 09:14:02 -07:00
Andrew Eisenberg
a72b22cd61 Change error message
Co-Authored-By: jcreedcmu <jcreed@gmail.com>
2020-03-17 08:54:35 -07:00
jcreedcmu
8286850651 Merge pull request #230 from jcreedcmu/jcreed/quick-query-bug
Fix weird behavior of Quick Query in non-multi-root workspaces
2020-03-17 09:19:32 -04:00
Andrew Eisenberg
3d8843f64b fix: Readable error message for invalid file
Display an understandable error message when trying to Run Query on
a qll file. Closes 201
2020-03-12 16:13:53 -07:00
jcreedcmu
1a4d72995f Merge pull request #280 from aeisenberg/aeisenberg/ignore-temp
chore: Remove rush temp files
2020-03-11 14:11:30 -04:00
Andrew Eisenberg
5fa3c62763 chore: Remove rush temp files
The .rush/temp folder should not be committed.

See https://rushjs.io/pages/commands/rush_build/
2020-03-11 11:04:01 -07:00
jcreedcmu
585266160a Merge pull request #276 from jcreedcmu/jcreed/prep-jump-to-def
Add bqrs types and ability to run template queries.
2020-03-11 13:18:17 -04:00
Jason Reed
55d3db05dc Make absent fields consistently 'never' 2020-03-11 10:40:10 -04:00
Jason Reed
d21cd4447c Fix some jsdoc comments. 2020-03-11 10:13:42 -04:00
jcreedcmu
a86adbd965 Merge pull request #279 from jcreedcmu/jcreed/hide-empty-test-dirs
Only create nonempty test suites
2020-03-11 09:21:48 -04:00
Jason Reed
4968ad8a90 Only create nonempty test suites
Fixes #269.
2020-03-11 09:04:59 -04:00
Andrew Eisenberg
b577c12d1c Merge pull request #274 from aeisenberg/show-log
Add "Show log" button for all messages
2020-03-10 10:26:05 -07:00
Andrew Eisenberg
f399da75d0 Migrate to using showAndLogInformationMessage
Also, changes the showAndLog* signatures to accept an optional
logger argument.
2020-03-10 10:01:51 -07:00
Andrew Eisenberg
7638900552 Merge pull request #275 from aeisenberg/update-pnpm
Update pnpm, rush and add sinon as a dependency
2020-03-10 09:21:40 -07:00
Andrew Eisenberg
e1c1fc3672 Update pnpm, rush and add sinon as a dependency
Note that pnpm no longer uses shrinkwrap.yaml.

I'm not entirely happy with this solution because it makes a change
to the rush.ts build script in order to handle peer dependencies
coming from pnpm. Seems to work, though.
2020-03-10 08:57:26 -07:00
alexet
fd728202ed Add cli bqrs -types 2020-03-10 09:43:41 -04:00
Jason Reed
8d9a470208 Allow running queries with templates 2020-03-10 09:43:06 -04:00
jcreedcmu
0b79cce512 Merge pull request #272 from jcreedcmu/jcreed/enums-revert
Add more types and documentation to protocol file
2020-03-10 09:03:17 -04:00
Andrew Eisenberg
a2a2aafa98 Update changelog 2020-03-09 11:47:08 -07:00
Andrew Eisenberg
84144157e7 Add "Show log" button for all messages
This change ensures that "Show log" is available on all messages
from the extension. It's important to note that the only place that
was specifying an "item" before was doing it incorrectly. That's
been fixed.

Closes #287
2020-03-09 11:39:10 -07:00
Andrew Eisenberg
059a75c5a4 Merge pull request #266 from aeisenberg/timeout
Display timeout warning and display canceled queries
2020-03-09 10:22:50 -07:00
Jason Reed
c15b1cd3ea Add more types and documentation to protocol file
Reduce the chances that someone reading this file forgets the intent
of the extensible 'enum' namespaces by redundantly putting it loudly
at the beginning.

Add some eponymous type aliases for these 'enum' types so that code
can refer to them.
2020-03-09 13:10:34 -04:00
jcreedcmu
dfab5900a6 Merge pull request #271 from github/revert-270-jcreed/enums
Revert "Convert namespaces to enums in messages.ts"
2020-03-09 13:09:34 -04:00
Andrew Eisenberg
c2e0f251e8 Addresses comments on #266 2020-03-09 09:44:33 -07:00
Henry Mercer
2150281062 Revert "Convert namespaces to enums in messages.ts" 2020-03-09 16:32:10 +00:00
Andrew Eisenberg
dfd1645576 Merge pull request #267 from aeisenberg/docs
Update contributing documentation and launch config
2020-03-09 09:26:43 -07:00
Henry Mercer
bbbea29407 Merge pull request #270 from jcreedcmu/jcreed/enums
Convert namespaces to enums in messages.ts
2020-03-09 16:14:57 +00:00
Jason Reed
d120388266 Convert namespaces to enums in messages.ts 2020-03-09 12:02:56 -04:00
Andrew Eisenberg
ce0f8add9f Merge branch 'master' into docs 2020-03-09 09:02:21 -07:00
Andrew Eisenberg
2d975de118 Merge pull request #265 from aeisenberg/settings
Change vscode settings to hide generated files
2020-03-09 07:44:03 -07:00
jcreedcmu
9377279b05 Merge pull request #173 from dbartol/dbartol/QLTest
Implement QL Test support
2020-03-09 08:16:28 -04:00
Andrew Eisenberg
1efa9f1082 Update contributing documentation and launch config
Adds section in CONTRIBUTING.md to document how to run tests. Also,
fixes some markdown linting warnings.

And fixes the launch config for running unit tests.
2020-03-07 16:36:09 -08:00
Andrew Eisenberg
2489095d25 fix: Display queries that are canceled during compilation
This change converts a cancelled query into a synthetic query result
that is displayed in query history. 

Also includes some light refactoring.

Closes #250.
2020-03-06 15:51:41 -08:00
Andrew Eisenberg
a4e02f6b42 fix: Display message when query times out
Also, add the message to the log.

Closes #251
2020-03-06 13:03:51 -08:00
Andrew Eisenberg
afe0a65fc5 Change vscode settings to hide generated files 2020-03-06 13:00:24 -08:00
Dave Bartolomeo
7fc501f795 Revert "Remove unnecessary? import hack"
This reverts commit 39805bc4a1.
2020-03-06 12:50:29 -05:00
Dave Bartolomeo
39805bc4a1 Remove unnecessary? import hack 2020-03-06 12:36:01 -05:00
Henry Mercer
35f619e97a Merge pull request #262 from jcreedcmu/jcreed/retry-tests
Retry integration tests when they fail
2020-03-05 17:57:01 +00:00
Jason Reed
87e563e24e Review comments. 2020-03-05 12:47:02 -05:00
Jason Reed
3a1219bb64 Retry integration tests when they fail 2020-03-05 09:26:26 -05:00
Henry Mercer
222cafb73c Merge pull request #260 from alexet/tm-test
Add compiled grammar to top level for use in linguist
2020-03-04 18:44:03 +00:00
Alexander Eyers-Taylor
2435a0b2f7 Update syntaxes/README.md
Co-Authored-By: jcreedcmu <jcreed@gmail.com>
2020-03-04 18:28:20 +00:00
alexet
dd9f0e811b Add compiled grammar to top level for use in linguist 2020-03-04 18:28:20 +00:00
Henry Mercer
ed076afde7 Merge pull request #256 from github/version/bump-to-v1.0.7
Bump version to v1.0.7
2020-03-04 15:17:16 +00:00
Dave Bartolomeo
370444c364 Sort tests by name 2020-03-03 15:33:42 -05:00
Dave Bartolomeo
984ba73080 Merge from master 2020-03-03 15:13:15 -05:00
jcreedcmu
c4aa9d9396 Merge pull request #255 from jcreedcmu/jcreed/1.0.6-release
Update CHANGELOG
2020-02-28 12:40:51 -05:00
Henry Mercer
bfb7d99c20 Update extensions/ql-vscode/CHANGELOG.md
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
2020-02-28 17:15:11 +00:00
jcreedcmu
7ba8aa8181 Update extensions/ql-vscode/CHANGELOG.md
Co-Authored-By: Henry Mercer <henry.mercer@me.com>
2020-02-28 12:14:08 -05:00
github-actions[bot]
735f70276a Bump version to v1.0.7 2020-02-28 17:06:29 +00:00
Jason Reed
233907a19f Update CHANGELOG 2020-02-28 12:04:12 -05:00
jcreedcmu
018e9c0ae7 Merge pull request #252 from jcreedcmu/jcreed/version-filter
Relax CLI version constraint to allow minor version increases
2020-02-28 12:03:27 -05:00
Jason Reed
585b694f52 Relax version constraint to allow minor version increases 2020-02-27 15:04:28 -05:00
Aditya Sharad
2c4cf1bab3 Merge pull request #254 from jcreedcmu/jcreed/fix-actions
Fix version bump PR creation during release workflow
2020-02-27 11:18:47 -08:00
jcreedcmu
4eeedb6ad4 Merge pull request #244 from jcreedcmu/jcreed/restart-query-server
Add command for restarting the query server.
2020-02-25 15:08:49 -05:00
jcreedcmu
895398fe40 Update extensions/ql-vscode/src/extension.ts
Co-Authored-By: Henry Mercer <henry.mercer@me.com>
2020-02-25 12:05:28 -05:00
Jason Reed
9c129f53ea Remove unnecessary dismiss button 2020-02-25 09:26:08 -05:00
Jason Reed
54039823d3 Add restartQueryServer to activation events 2020-02-25 09:11:50 -05:00
Jason Reed
ef0623c605 Switch to master branch just before version bump PR creation 2020-02-24 09:07:43 -05:00
Jason Reed
7429af3e27 Fix create pull request step
- Bump create-pull-request version
- Actually checkout master branch
2020-02-21 09:10:12 -05:00
Jason Reed
88033c12f1 Use actions/checkout@v2 2020-02-21 08:30:09 -05:00
Jason Reed
71898ac4ce Add command for restarting the query server.
Include a convenience button to show the query server log in case the
reason the user wants to restart the server is that it's acting
unexpectedly and they want to investigate why.
2020-02-19 08:58:59 -05:00
Dave Bartolomeo
f2c525b56d Fix references to renamed type 2020-02-18 11:51:55 -07:00
Dave Bartolomeo
afcc05fb03 Remove unused interface 2020-02-14 18:07:22 -07:00
Dave Bartolomeo
1b7d0da277 Fix typo 2020-02-14 17:54:18 -07:00
Dave Bartolomeo
90a975321f Merge from master 2020-02-14 17:45:27 -07:00
jcreedcmu
e57a685424 Merge pull request #241 from jcreedcmu/jcreed/refs-tags
Trim prefix from git ref name during release
2020-02-14 09:33:46 -05:00
Jason Reed
54fc90a673 Trim prefix from git ref name during release 2020-02-14 07:27:46 -05:00
Henry Mercer
ca67d30810 Merge pull request #240 from github/jcreed/changelog
Update changelog date.
2020-02-13 18:14:46 +00:00
Jason Reed
35e311d399 Update changelog date.
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
2020-02-13 12:56:44 -05:00
jcreedcmu
457ae9a611 Merge pull request #239 from jcreedcmu/jcreed/1.0.5-bump
Bump released version to 1.0.5
2020-02-13 12:43:20 -05:00
Jason Reed
b9d9d239c8 Bump released version to 1.0.5 2020-02-13 12:27:46 -05:00
Henry Mercer
ae8cab3eed Merge pull request #232 from jcreedcmu/jcreed/sort-interpreted
Support sorting for interpreted alerts
2020-02-13 14:48:43 +00:00
Jason Reed
d5b35a46ca Add CHANGELOG message. 2020-02-13 09:25:46 -05:00
Jason Reed
c18de5bb8c Make JSDoc 2020-02-13 09:16:59 -05:00
Jason Reed
7a782517f0 change to jsdoc 2020-02-13 09:07:26 -05:00
Jason Reed
cf377a7830 Factor out common code when updating sort results 2020-02-13 08:47:43 -05:00
Jason Reed
ecc80886d3 More formatting, commenting 2020-02-13 08:17:16 -05:00
Jason Reed
b3552cd4a1 Document meaning of undefined sortState 2020-02-13 08:13:29 -05:00
Jason Reed
58e69c899e Use switch instead of conditional. 2020-02-13 08:06:44 -05:00
Jason Reed
5c90e5fd19 Formatting, naming consistency. 2020-02-13 08:04:28 -05:00
Jason Reed
256890fd6c Make table headers behave more like UI elements than raw text
Mouseover should give the same cursor as links, and not lead to header
text accidentally being selected.
2020-02-12 15:16:20 -05:00
Jason Reed
6bf691ef51 Remove Location header
Just cycle through ascending, descending, and no sort on Message column.
2020-02-12 15:16:14 -05:00
Jason Reed
c9fd8d41d5 Remove unprincipled sort-by-location
Leave it so that clicking on the Location column goes back to sorting
by location, but reflecting this as looking as the same as the default
'unsorted' view.
2020-02-12 12:06:09 -05:00
Jason Reed
6eb873d1b9 Change sorting interface to table header on alerts. 2020-02-12 11:58:27 -05:00
Henry Mercer
42c8ff5cfc Merge pull request #231 from github/pr-template-docs-cc
Add steps to the pull request checklist to inform the documentation team
2020-02-12 14:09:14 +00:00
Henry Mercer
0b3fc98a61 Add docs cc step to pull request checklist 2020-02-12 12:06:25 +00:00
jcreedcmu
19113b72ec Merge pull request #233 from jcreedcmu/jcreed/revert-actions-change
Revert "Exclude documentation from CI workflow"
2020-02-12 06:57:19 -05:00
Jason Reed
64b1a7c1d9 Revert "Exclude documentation from CI workflow"
This reverts commit c95ac8e6ea.
2020-02-12 06:46:11 -05:00
Jason Reed
68f14d19a0 Sort alerts according to UI-chosen sort order 2020-02-11 17:46:50 -05:00
Jason Reed
d325463efd Create UI element to pick sort order 2020-02-11 17:35:25 -05:00
Jason Reed
d135507a77 Organize sort state for interpreted results
Rename existing sort state for raw results, and make some state for
keeping track of sort state for interpreted results.
2020-02-11 16:56:41 -05:00
Jason Reed
81a6b23e81 Fix interpretation path bug 2020-02-11 16:53:17 -05:00
Jason Reed
9aaffb9a89 Fix weird behavior of Quick Query in non-multi-root workspaces 2020-02-11 11:40:14 -05:00
jcreedcmu
99d0e39914 Merge pull request #229 from jcreedcmu/jcreed/path-ignore-for-ci
Exclude documentation from CI workflow (WIP don't merge)
2020-02-10 13:26:38 -05:00
Jason Reed
c95ac8e6ea Exclude documentation from CI workflow 2020-02-10 10:10:10 -05:00
Henry Mercer
2f7282e714 Merge pull request #228 from github/jcreedcmu-patch-1
Add VS Marketplace badge
2020-02-07 19:07:30 +00:00
jcreedcmu
d35193188b Add VS Marketplace badge 2020-02-07 09:32:22 -05:00
jcreedcmu
47ba8d98f7 Merge pull request #222 from jcreedcmu/jcreed/pr/184
Create diagnostics messages using sarif.
2020-02-06 12:14:00 -05:00
Jason Reed
5b2b34a704 Fix LGTM alert about missing await 2020-02-06 09:45:17 -05:00
Jason Reed
96174005c9 Fix rebase-induced type error 2020-02-06 09:29:57 -05:00
Jason Reed
ed801a7f49 rename queries.ts -> run-queries.ts 2020-02-06 09:26:41 -05:00
Jason Reed
a36b810c62 Refactor query results and query history 2020-02-06 09:26:41 -05:00
Jason Reed
6fee8b3eb4 Extract db upgrade code from queries.ts into its own file. 2020-02-06 09:25:06 -05:00
Jason Reed
75a15e2427 Add SARIF parsing test 2020-02-06 09:25:06 -05:00
alexet
bd4f56e90f Switch back to computing the file names in one place. 2020-02-06 09:25:06 -05:00
alexet
29f6ec9996 Use sarif for problems view over doing it manually. 2020-02-06 09:25:06 -05:00
alexet
752c7b2d6b Move sarif parsing code to a location that can be shared. 2020-02-06 09:25:06 -05:00
Henry Mercer
d6b7889694 Merge pull request #226 from github/alexet-templates
Add issue templates
2020-02-04 19:27:23 +00:00
Alexander Eyers-Taylor
b1530c74f3 Fix templates 2020-02-04 19:16:42 +00:00
Alexander Eyers-Taylor
4a72ecb29a Update issue templates with suggestions 2020-02-04 19:13:50 +00:00
Aditya Sharad
8e10f474a1 Merge pull request #225 from henrymercer/add-pr-template
Add pull request template
2020-02-04 11:10:16 -08:00
Alexander Eyers-Taylor
89595921ff Update .github/ISSUE_TEMPLATE/new-extension-release.md
Co-Authored-By: Aditya Sharad <6874315+adityasharad@users.noreply.github.com>
2020-02-04 19:08:28 +00:00
Alexander Eyers-Taylor
75e069cf12 Add issue templates 2020-02-04 19:01:56 +00:00
Henry Mercer
f6bcc10cd8 Add pull request template to remind us to update the changelog 2020-02-04 18:56:33 +00:00
Henry Mercer
6e34055206 Add changelog entry for #224 2020-02-04 18:40:16 +00:00
jcreedcmu
5cb2589807 Merge pull request #224 from henrymercer/failed-query-history-icon
Display a failed icon next to failed query history items
2020-02-04 13:15:36 -05:00
Henry Mercer
a8532af0ae Display failure icon next to failed query history items 2020-02-04 17:47:14 +00:00
Henry Mercer
2f848afcfc Merge pull request #223 from github/jcreedcmu-patch-1
Update release instructions
2020-02-04 15:39:08 +00:00
jcreedcmu
1da526ac9b Update CONTRIBUTING.md
Co-Authored-By: Henry Mercer <henrymercer@github.com>
2020-02-04 10:30:46 -05:00
jcreedcmu
11df0d8139 Update release instructions 2020-02-04 10:18:58 -05:00
jcreedcmu
2f41c30908 Merge pull request #220 from jcreedcmu/jcreed/long-dbupgrade-msg
Truncate long database upgrade messages in native dialog box
2020-01-28 14:28:32 -05:00
Jason Reed
e5b0117a63 Review comments. 2020-01-28 14:02:51 -05:00
Jason Reed
3e60a118e9 Address review comments.
- Decrease line limit
- Adjust constant doc
- Add button to show log when database upgrade list is truncated
2020-01-28 13:27:39 -05:00
Jason Reed
d56f51b510 Truncate long database upgrade messages in native dialog box. 2020-01-28 11:05:20 -05:00
Jason Reed
20c312e3c5 Pin nodejs version during Actions build.
This solves the problem of whatever node/npm ubuntu-latest happens to
have in /usr/local/bin/{node,npm} producing `Error: Cannot find module
'semver'` errors when it is misconfigured in the image.

(cf. https://askubuntu.com/questions/1152570/npm-cant-find-module-semver-error-in-ubuntu-19-04)
2020-01-28 11:02:48 -05:00
jcreedcmu
40e7657238 Merge pull request #219 from jcreedcmu/jcreed/version-bump-1.0.5
Version bump -> 1.0.5
2020-01-24 10:51:29 -05:00
Jason Reed
6769f55162 Version bump -> 1.0.5 2020-01-24 10:36:02 -05:00
jcreedcmu
9a92780c98 Merge pull request #218 from jcreedcmu/jcreed/1.0.4
Add date to changelog for release.
2020-01-24 10:29:37 -05:00
Dave Bartolomeo
edc1f1c2ab Merge remote-tracking branch 'upstream/master' into dbartol/QLTest 2020-01-13 11:19:24 -07:00
Dave Bartolomeo
0947a35332 Implement cancellation of test run 2020-01-06 16:09:34 -07:00
Dave Bartolomeo
207743e7b7 Merge remote-tracking branch 'upstream/master' into dbartol/QLTest 2020-01-03 11:49:15 -07:00
Dave Bartolomeo
de2a6cc0b7 Make QLTest use codeql test run instead of odasa qltest 2020-01-03 11:48:48 -07:00
Dave Bartolomeo
55d1a4aa1c Merge from master 2019-12-16 15:41:16 -07:00
Dave Bartolomeo
2f9a31484c Merge from master 2019-12-04 09:53:06 -07:00
Dave Bartolomeo
9e6100f383 Merge remote-tracking branch 'upstream/master' into dbartol/QLTest 2019-11-22 10:35:04 -07:00
Dave Bartolomeo
7d325e3832 Fix file-watching bug in QLTest discovery 2019-11-21 14:13:55 -07:00
Dave Bartolomeo
cbe3c055b6 odasa -> Semmle Core 2019-11-20 17:19:29 -07:00
Dave Bartolomeo
e37807c45e Better error message when odasa is not configured properly 2019-11-20 17:12:29 -07:00
Dave Bartolomeo
be72e9b67a Activate when Test Explorer view is opened 2019-11-20 17:11:43 -07:00
Dave Bartolomeo
85f7ff1d11 Attempt to fix pure test failure 2019-11-20 16:28:03 -07:00
Dave Bartolomeo
ddf42d81d1 Fix LGTM alerts 2019-11-20 14:55:08 -07:00
Dave Bartolomeo
444aca3bae Implement QL Test support (using odasa for now)
The PR contains the initial implementing of QL Test support in CodeQL for Visual Studio Code. Because QL Test support isn't quite ready in the CLI yet, this PR uses `odasa` to run the tests for now. As CLI support comes online, it should be straightforward to swap out the implementation to use the CLI.

The treeview UI for the tests is implemented via the `hbenl.vscode-test-explorer` extension. This extension is open source, and appears to be actively maintained. It's used by a couple dozen existing extensions for tests for various languages. The extension doesn't really do anything on its own, so taking it as a dependency isn't introducing any unwanted UI clutter. Note that I did have to remove the `--disable-extensions` argument from `launch.json`, because otherwise the test explorer extension gets disabled, preventing our own extension from loading.

The UI will display a root node for each QL pack that contains tests, with the actual test directories and files as descendants of that root node. We consider only those QL packs in the workspace; QL packs on the default CodeQL search path are ignored. We use `codeql resolve qlpacks` to find the packs, and then watch all `qlpack.yml` files in the workspace for changes in order to refresh the pack discovery when necessary. Ideally, we'd have the CLI return a set of path patterns to watch, but for now the current implementation works fine.

To discover the tests within a given pack, we walk the pack's directory tree manually for now, until the relevant CLI command is available. Because we do not yet have a mechanism in `qlpack.yml` to specify whether or not the pack contains tests, we assume that any pack whose name ends with "-tests" to contain nothing but tests, and any other pack to contain no tests. This is sufficient for the tests in the QL repo. As with QL pack discovery, we watch the file system for changes in `.ql` and `.qlref` files in order to refresh the tree of tests if anything changes.

To actually run the tests, we just invoke `odasa qltest` with the appropriate arguments. This code is pretty much a straight copy-and-paste from the repo where I've had a private version of QL Test support for several months. Once we can run tests via the CLI, this will all be deleted.

The `test-ui.ts` file implements a couple of additional commands for the context menu of the test treeview. You can accept the output of a failing test (copying the `.actual` file to the `.expected` file), and you can bring up a diff view of the `.expected` and `.actual` files).

This PR includes a couple of related utility classes. `UIService` makes it a little easier to implement a service that handles VS Code commands. `Discovery` is a base class that handles most of the work that is shared between the different kinds of discovery that we do, like avoiding running multiple discovery operations simultaneously if we get a storm of file change notifications.
2019-11-20 14:42:33 -07:00
78 changed files with 6570 additions and 2450 deletions

View File

@@ -4,3 +4,4 @@ indent_size = 2
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

20
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,18 @@
---
name: New extension release
about: Create an issue with a checklist for the release steps (write access required
for the steps)
title: Release Checklist for version xx.xx.xx
labels: ''
assignees: ''
---
- [ ] Update this issue title to refer to the version of the release
- [ ] Trigger a release build on Actions by adding a new tag on master of the format `vxx.xx.xx`
- [ ] Monitor the status of the release build in the `Release` workflow in the Actions tab.
- [ ] Download the VSIX from the draft GitHub release that is created when the release build finishes.
- [ ] Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
- [ ] Click the `...` menu in the CodeQL row and click **Update**.
- [ ] Drag the `.vsix` file you downloaded from the GitHub release into the Marketplace and click **Upload**.
- [ ] Publish the draft GitHub release and confirm the new release is marked as the latest release at https://github.com/github/vscode-codeql/releases.

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
<!-- Thank you for submitting a pull request. Please read our pull request guidelines before
submitting your pull request:
https://github.com/github/vscode-codeql/blob/master/CONTRIBUTING.md#submitting-a-pull-request.
-->
Replace this with a description of the changes your pull request makes.
## Checklist
- [ ] [CHANGELOG.md](../extensions/ql-vscode/CHANGELOG.md) has been updated to incorporate all user visible changes made by this pull request.
- [ ] Issues have been created for any UI or other user-facing changes made by this pull request.
- [ ] `@github/product-docs-dsp` has been cc'd in all issues for UI or other user-facing changes made by this pull request.

View File

@@ -10,10 +10,14 @@ jobs:
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: '10.18.1'
- name: Build
run: |
cd build
@@ -42,10 +46,14 @@ jobs:
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: '10.18.1'
# We have to build the dependencies in `lib` before running any tests.
- name: Build
run: |
@@ -86,4 +94,4 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
cd extensions/ql-vscode
npm run integration
npm run integration

View File

@@ -27,13 +27,11 @@ jobs:
# TODO Share steps with the main workflow.
steps:
- name: Checkout
uses: actions/checkout@master
uses: actions/checkout@v2
# The checkout action does not fetch the master branch.
# Fetch the master branch so that we can base the version bump PR against master.
- name: Fetch master branch
run: |
git fetch --depth=1 origin master:master
- uses: actions/setup-node@v1
with:
node-version: '10.18.1'
- name: Build
run: |
@@ -53,8 +51,8 @@ jobs:
VSIX_PATH="$(ls dist/*.vsix)"
echo "::set-output name=vsix_path::$VSIX_PATH"
# Transform the GitHub ref so it can be used in a filename.
# This is mainly needed for testing branches that modify this workflow.
REF_NAME="$(echo ${{ github.ref }} | sed -e 's:/:-:g')"
# The last sed invocation is used for testing branches that modify this workflow.
REF_NAME="$(echo ${{ github.ref }} | sed -e 's:^refs/tags/::' | sed -e 's:/:-:g')"
echo "::set-output name=ref_name::$REF_NAME"
# Uploading artifacts is not necessary to create a release.
@@ -95,6 +93,13 @@ jobs:
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
asset_content_type: application/zip
# The checkout action does not fetch the master branch.
# Fetch the master branch so that we can base the version bump PR against master.
- name: Fetch master branch
run: |
git fetch --depth=1 origin master:master
git checkout master
- name: Bump patch version
id: bump-patch-version
if: success()
@@ -106,7 +111,7 @@ jobs:
echo "::set-output name=next_version::$NEXT_VERSION"
- name: Create version bump PR
uses: peter-evans/create-pull-request@c202684c928d4c9f18394b2ad11df905c5d8b40c # v2.1.2
uses: peter-evans/create-pull-request@c7b64af0a489eae91f7890f2c1b63d13cc2d8ab7 # v2.4.2
if: success()
with:
token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ artifacts/
# Rush files
/common/temp/**
package-deps.json
**/.rush/temp

45
.vscode/launch.json vendored
View File

@@ -8,8 +8,7 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"--disable-extensions"
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql"
],
"stopOnEntry": false,
"sourceMaps": true,
@@ -24,25 +23,30 @@
},
{
"name": "Launch Unit Tests (vscode-codeql)",
"type": "extensionHost",
"type": "node",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"--extensionTestsPath=${workspaceRoot}/extensions/ql-vscode/out/test",
"--disable-extensions"
"program": "${workspaceFolder}/extensions/ql-vscode/node_modules/mocha/bin/_mocha",
"showAsyncStacks": true,
"cwd": "${workspaceFolder}/extensions/ql-vscode",
"runtimeArgs": [
"--inspect=9229"
],
"args": [
"--exit",
"-u",
"bdd",
"--colors",
"--diff",
"-r",
"ts-node/register",
"test/pure-tests/**/*.ts"
],
"port": 9229,
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/dist/vscode-codeql/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-bqrs/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-io/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-io-node/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-vscode-utils/out/**/*.js",
"${workspaceRoot}/extensions/ql-vscode/out/test/**/*.js"
],
"preLaunchTask": "Build"
"preLaunchTask": "Build",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"name": "Launch Integration Tests - No Workspace (vscode-codeql)",
@@ -51,8 +55,7 @@
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"--extensionTestsPath=${workspaceRoot}/extensions/ql-vscode/out/vscode-tests/no-workspace/index",
"--disable-extensions"
"--extensionTestsPath=${workspaceRoot}/extensions/ql-vscode/out/vscode-tests/no-workspace/index"
],
"stopOnEntry": false,
"sourceMaps": true,
@@ -70,7 +73,7 @@
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"--extensionTestsPath=${workspaceRoot}/extensions/ql-vscode/out/vscode-tests/minimal-workspace/index",
"${workspaceRoot}/extensions/ql-vscode/test/data",
"${workspaceRoot}/extensions/ql-vscode/test/data"
],
"stopOnEntry": false,
"sourceMaps": true,
@@ -81,4 +84,4 @@
"preLaunchTask": "Build"
}
]
}
}

32
.vscode/settings.json vendored
View File

@@ -1,14 +1,36 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
"**/out": true, // set this to true to hide the "out" folder with the compiled JS files
"**/dist": true,
"**/node_modules": true,
"common/temp": true,
"**/.vscode-test": true
},
"files.watcherExclude": {
"**/.git/**": true,
"**/node_modules/*/**": true
"**/out": true,
"**/dist": true,
"**/node_modules": true,
"common/temp": true,
"**/.vscode-test": true
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
"**/out": true, // set this to false to include "out" folder in search results
"**/dist": true,
"**/node_modules": true,
"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": "./common/temp/node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"eslint.options": {
// This is necessary so that eslint can properly resolve its plugins
"resolvePluginsRelativeTo": "./extensions/ql-vscode"
}
}

11
.vscode/tasks.json vendored
View File

@@ -28,7 +28,7 @@
"file": 1,
"location": 2,
"message": 3
},
}
},
"$ts-webpack"
]
@@ -100,6 +100,15 @@
"clear": true
},
"problemMatcher": []
},
{
"type": "npm",
"script": "watch",
"path": "extensions/ql-vscode/",
"problemMatcher": [
"$gulp-tsc"
],
"group": "build"
}
]
}

View File

@@ -1,4 +1,4 @@
## Contributing
# Contributing
[fork]: https://github.com/github/vscode-codeql/fork
[pr]: https://github.com/github/vscode-codeql/compare
@@ -13,19 +13,19 @@ Please note that this project is released with a [Contributor Code of Conduct][c
## Submitting a pull request
0. [Fork][fork] and clone the repository
0. Set up a local build
0. Create a new branch: `git checkout -b my-branch-name`
0. Make your change
0. Push to your fork and [submit a pull request][pr]
0. Pat yourself on the back and wait for your pull request to be reviewed and merged.
1. [Fork][fork] and clone the repository
1. Set up a local build
1. Create a new branch: `git checkout -b my-branch-name`
1. Make your change
1. Push to your fork and [submit a pull request][pr]
1. Pat yourself on the back and wait for your pull request to be reviewed and merged.
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
- Follow the [style guide][style].
- Write tests. Tests that don't require the VS Code API are located [here](extensions/ql-vscode/test). Integration tests that do require the VS Code API are located [here](extensions/ql-vscode/src/vscode-tests).
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
* Follow the [style guide][style].
* Write tests. Tests that don't require the VS Code API are located [here](extensions/ql-vscode/test). Integration tests that do require the VS Code API are located [here](extensions/ql-vscode/src/vscode-tests).
* Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
* Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
## Setting up a local build
@@ -42,12 +42,22 @@ If you plan on building from the command line, it's easiest if Rush is installed
npm install -g @microsoft/rush
```
To get started, run:
```shell
rush update && rush build
```
Note that when you run the `rush` command from the globally installed version, it will examine the
`rushVersion` property in the repo's `rush.json`, and if it differs from the globally installed
version, it will download, cache, and run the version of Rush specified in the `rushVersion`
property.
If you plan on only building via VS Code tasks, you don't need Rush installed at all, since those
A few more things to know about using rush:
* Avoid running `npm` for any commands that install/link dependencies
* Instead use the *rush* equivalent: `rush add <package>`, `rush update`, etc.
* If you plan on only building via VS Code tasks, you don't need Rush installed at all, since those
tasks run `common/scripts/install-run-rush.js` to bootstrap a locally installed and cached copy of
Rush.
@@ -66,7 +76,7 @@ a single-project repo. With Rush, you need to do an "update" instead:
##### From the command line
```shell
$ rush update
rush update
```
#### Building all projects (instead of `gulp`)
@@ -100,6 +110,8 @@ force a full rebuild of all projects:
rush rebuild --verbose
```
Note that `rush rebuild` performs a complete rebuild, whereas `rush build` performs an incremental build and in many cases will not need to do anything at all.
### Installing
You can install the `.vsix` file from within VS Code itself, from the Extensions container in the sidebar:
@@ -118,6 +130,18 @@ $ vscode/scripts/code-cli.sh --install-extension dist/vscode-codeql-*.vsix # if
You can use VS Code to debug the extension without explicitly installing it. Just open this directory as a workspace in VS Code, and hit `F5` to start a debugging session.
### Running the unit/integration tests
Ensure the `CODEQL_PATH` environment variable is set to point to the `codeql` cli executable.
Outside of vscode, run:
```shell
npm run test && npm run integration
```
Alternatively, you can run the tests inside of vscode. There are several vscode launch configurations defined that run the unit and integration tests. They can all be found in the debug view.
## Releasing (write access required)
1. Trigger a release build on Actions by adding a new tag on master of the format `vxx.xx.xx`
@@ -126,10 +150,10 @@ You can use VS Code to debug the extension without explicitly installing it. Jus
1. Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
1. Click the `...` menu in the CodeQL row and click **Update**.
1. Drag the `.vsix` file you downloaded from the GitHub release into the Marketplace and click **Upload**.
1. Publish the GitHub release.
1. Publish the draft GitHub release and confirm the new release is marked as the latest release at <https://github.com/github/vscode-codeql/releases>.
## Resources
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)
* [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
* [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
* [GitHub Help](https://help.github.com)

View File

@@ -7,6 +7,7 @@ The extension is released. You can download it from the [Visual Studio Marketpla
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/CHANGELOG.md).
[![CI status badge](https://github.com/github/vscode-codeql/workflows/Build%20Extension/badge.svg)](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amaster)
[![VS Marketplace badge](https://vsmarketplacebadge.apphb.com/version/github.vscode-codeql.svg)](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql)
## Features

View File

@@ -14,4 +14,4 @@
"build-release": "rush install && rush build --release"
},
"author": "GitHub"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,67 @@
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for license information.
Object.defineProperty(exports, "__esModule", { value: true });
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where the Rush command may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it.
// An example usage would be:
//
// node common/scripts/install-run-rush.js install
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
const path = require("path");
const fs = require("fs");
const install_run_1 = require("./install-run");
const PACKAGE_NAME = '@microsoft/rush';
function getRushVersion() {
const rushJsonFolder = install_run_1.findRushJsonFolder();
const rushJsonPath = path.join(rushJsonFolder, install_run_1.RUSH_JSON_FILENAME);
try {
const rushJsonContents = fs.readFileSync(rushJsonPath, 'utf-8');
// Use a regular expression to parse out the rushVersion value because rush.json supports comments,
// but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script.
const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/);
return rushJsonMatches[1];
}
catch (e) {
throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` +
'The \'rushVersion\' field is either not assigned in rush.json or was specified ' +
'using an unexpected syntax.');
}
}
function run() {
const [nodePath, /* Ex: /bin/node */ scriptPath, /* /repo/common/scripts/install-run-rush.js */ ...packageBinArgs /* [build, --to, myproject] */] = process.argv;
if (!nodePath || !scriptPath) {
throw new Error('Unexpected exception: could not detect node path or script path');
}
if (process.argv.length < 3) {
console.log('Usage: install-run-rush.js <command> [args...]');
console.log('Example: install-run-rush.js build --to myproject');
process.exit(1);
}
install_run_1.runWithErrorAndStatusCode(() => {
const version = getRushVersion();
console.log(`The rush.json configuration requests Rush version ${version}`);
return install_run_1.installAndRun(PACKAGE_NAME, version, 'rush', packageBinArgs);
});
}
run();
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for license information.
Object.defineProperty(exports, "__esModule", { value: true });
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where the Rush command may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it.
// An example usage would be:
//
// node common/scripts/install-run-rush.js install
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
const path = require("path");
const fs = require("fs");
const install_run_1 = require("./install-run");
const PACKAGE_NAME = '@microsoft/rush';
const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION';
function _getRushVersion() {
const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION];
if (rushPreviewVersion !== undefined) {
console.log(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`);
return rushPreviewVersion;
}
const rushJsonFolder = install_run_1.findRushJsonFolder();
const rushJsonPath = path.join(rushJsonFolder, install_run_1.RUSH_JSON_FILENAME);
try {
const rushJsonContents = fs.readFileSync(rushJsonPath, 'utf-8');
// Use a regular expression to parse out the rushVersion value because rush.json supports comments,
// but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script.
const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/);
return rushJsonMatches[1];
}
catch (e) {
throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` +
'The \'rushVersion\' field is either not assigned in rush.json or was specified ' +
'using an unexpected syntax.');
}
}
function _run() {
const [nodePath, /* Ex: /bin/node */ scriptPath, /* /repo/common/scripts/install-run-rush.js */ ...packageBinArgs /* [build, --to, myproject] */] = process.argv;
// Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the
// appropriate binary inside the rush package to run
const scriptName = path.basename(scriptPath);
const bin = scriptName.toLowerCase() === 'install-run-rushx.js' ? 'rushx' : 'rush';
if (!nodePath || !scriptPath) {
throw new Error('Unexpected exception: could not detect node path or script path');
}
if (process.argv.length < 3) {
console.log(`Usage: ${scriptName} <command> [args...]`);
if (scriptName === 'install-run-rush.js') {
console.log(`Example: ${scriptName} build --to myproject`);
}
else {
console.log(`Example: ${scriptName} custom-command`);
}
process.exit(1);
}
install_run_1.runWithErrorAndStatusCode(() => {
const version = _getRushVersion();
console.log(`The rush.json configuration requests Rush version ${version}`);
return install_run_1.installAndRun(PACKAGE_NAME, version, bin, packageBinArgs);
});
}
_run();
//# sourceMappingURL=install-run-rush.js.map

View File

@@ -0,0 +1,18 @@
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for license information.
Object.defineProperty(exports, "__esModule", { value: true });
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where the Rush command may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the
// rushx command.
//
// An example usage would be:
//
// node common/scripts/install-run-rushx.js custom-command
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
require("./install-run-rush");
//# sourceMappingURL=install-run-rushx.js.map

View File

@@ -1,399 +1,433 @@
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for license information.
Object.defineProperty(exports, "__esModule", { value: true });
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where a Node tool may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the specified
// version of the specified tool (if not already installed), and then pass a command-line to it.
// An example usage would be:
//
// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
const childProcess = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
exports.RUSH_JSON_FILENAME = 'rush.json';
const INSTALLED_FLAG_FILENAME = 'installed.flag';
const NODE_MODULES_FOLDER_NAME = 'node_modules';
const PACKAGE_JSON_FILENAME = 'package.json';
/**
* Parse a package specifier (in the form of name\@version) into name and version parts.
*/
function parsePackageSpecifier(rawPackageSpecifier) {
rawPackageSpecifier = (rawPackageSpecifier || '').trim();
const separatorIndex = rawPackageSpecifier.lastIndexOf('@');
let name;
let version = undefined;
if (separatorIndex === 0) {
// The specifier starts with a scope and doesn't have a version specified
name = rawPackageSpecifier;
}
else if (separatorIndex === -1) {
// The specifier doesn't have a version
name = rawPackageSpecifier;
}
else {
name = rawPackageSpecifier.substring(0, separatorIndex);
version = rawPackageSpecifier.substring(separatorIndex + 1);
}
if (!name) {
throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`);
}
return { name, version };
}
/**
* Resolve a package specifier to a static version
*/
function resolvePackageVersion(rushCommonFolder, { name, version }) {
if (!version) {
version = '*'; // If no version is specified, use the latest version
}
if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) {
// If the version contains only characters that we recognize to be used in static version specifiers,
// pass the version through
return version;
}
else {
// version resolves to
try {
const rushTempFolder = ensureAndJoinPath(rushCommonFolder, 'temp');
const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
syncNpmrc(sourceNpmrcFolder, rushTempFolder);
const npmPath = getNpmPath();
// This returns something that looks like:
// @microsoft/rush@3.0.0 '3.0.0'
// @microsoft/rush@3.0.1 '3.0.1'
// ...
// @microsoft/rush@3.0.20 '3.0.20'
// <blank line>
const npmVersionSpawnResult = childProcess.spawnSync(npmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier'], {
cwd: rushTempFolder,
stdio: []
});
if (npmVersionSpawnResult.status !== 0) {
throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`);
}
const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString();
const versionLines = npmViewVersionOutput.split('\n').filter((line) => !!line);
const latestVersion = versionLines[versionLines.length - 1];
if (!latestVersion) {
throw new Error('No versions found for the specified version range.');
}
const versionMatches = latestVersion.match(/^.+\s\'(.+)\'$/);
if (!versionMatches) {
throw new Error(`Invalid npm output ${latestVersion}`);
}
return versionMatches[1];
}
catch (e) {
throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`);
}
}
}
let _npmPath = undefined;
/**
* Get the absolute path to the npm executable
*/
function getNpmPath() {
if (!_npmPath) {
try {
if (os.platform() === 'win32') {
// We're on Windows
const whereOutput = childProcess.execSync('where npm', { stdio: [] }).toString();
const lines = whereOutput.split(os.EOL).filter((line) => !!line);
// take the last result, we are looking for a .cmd command
// see https://github.com/Microsoft/web-build-tools/issues/759
_npmPath = lines[lines.length - 1];
}
else {
// We aren't on Windows - assume we're on *NIX or Darwin
_npmPath = childProcess.execSync('which npm', { stdio: [] }).toString();
}
}
catch (e) {
throw new Error(`Unable to determine the path to the NPM tool: ${e}`);
}
_npmPath = _npmPath.trim();
if (!fs.existsSync(_npmPath)) {
throw new Error('The NPM executable does not exist');
}
}
return _npmPath;
}
exports.getNpmPath = getNpmPath;
let _rushJsonFolder;
/**
* Find the absolute path to the folder containing rush.json
*/
function findRushJsonFolder() {
if (!_rushJsonFolder) {
let basePath = __dirname;
let tempPath = __dirname;
do {
const testRushJsonPath = path.join(basePath, exports.RUSH_JSON_FILENAME);
if (fs.existsSync(testRushJsonPath)) {
_rushJsonFolder = basePath;
break;
}
else {
basePath = tempPath;
}
} while (basePath !== (tempPath = path.dirname(basePath))); // Exit the loop when we hit the disk root
if (!_rushJsonFolder) {
throw new Error('Unable to find rush.json.');
}
}
return _rushJsonFolder;
}
exports.findRushJsonFolder = findRushJsonFolder;
/**
* Create missing directories under the specified base directory, and return the resolved directory.
*
* Does not support "." or ".." path segments.
* Assumes the baseFolder exists.
*/
function ensureAndJoinPath(baseFolder, ...pathSegments) {
let joinedPath = baseFolder;
try {
for (let pathSegment of pathSegments) {
pathSegment = pathSegment.replace(/[\\\/]/g, '+');
joinedPath = path.join(joinedPath, pathSegment);
if (!fs.existsSync(joinedPath)) {
fs.mkdirSync(joinedPath);
}
}
}
catch (e) {
throw new Error(`Error building local installation folder (${path.join(baseFolder, ...pathSegments)}): ${e}`);
}
return joinedPath;
}
/**
* As a workaround, _syncNpmrc() copies the .npmrc file to the target folder, and also trims
* unusable lines from the .npmrc file. If the source .npmrc file not exist, then _syncNpmrc()
* will delete an .npmrc that is found in the target folder.
*
* Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in
* the .npmrc file to provide different authentication tokens for different registry.
* However, if the environment variable is undefined, it expands to an empty string, which
* produces a valid-looking mapping with an invalid URL that causes an error. Instead,
* we'd prefer to skip that line and continue looking in other places such as the user's
* home directory.
*
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc()
*/
function syncNpmrc(sourceNpmrcFolder, targetNpmrcFolder) {
const sourceNpmrcPath = path.join(sourceNpmrcFolder, '.npmrc');
const targetNpmrcPath = path.join(targetNpmrcFolder, '.npmrc');
try {
if (fs.existsSync(sourceNpmrcPath)) {
let npmrcFileLines = fs.readFileSync(sourceNpmrcPath).toString().split('\n');
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
const resultLines = [];
// Trim out lines that reference environment variables that aren't defined
for (const line of npmrcFileLines) {
// This finds environment variable tokens that look like "${VAR_NAME}"
const regex = /\$\{([^\}]+)\}/g;
const environmentVariables = line.match(regex);
let lineShouldBeTrimmed = false;
if (environmentVariables) {
for (const token of environmentVariables) {
// Remove the leading "${" and the trailing "}" from the token
const environmentVariableName = token.substring(2, token.length - 1);
if (!process.env[environmentVariableName]) {
lineShouldBeTrimmed = true;
break;
}
}
}
if (lineShouldBeTrimmed) {
// Example output:
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
}
else {
resultLines.push(line);
}
}
fs.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL));
}
else if (fs.existsSync(targetNpmrcPath)) {
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
fs.unlinkSync(targetNpmrcPath);
}
}
catch (e) {
throw new Error(`Error syncing .npmrc file: ${e}`);
}
}
/**
* Detects if the package in the specified directory is installed
*/
function isPackageAlreadyInstalled(packageInstallFolder) {
try {
const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
if (!fs.existsSync(flagFilePath)) {
return false;
}
const fileContents = fs.readFileSync(flagFilePath).toString();
return fileContents.trim() === process.version;
}
catch (e) {
return false;
}
}
/**
* Removes the following files and directories under the specified folder path:
* - installed.flag
* -
* - node_modules
*/
function cleanInstallFolder(rushCommonFolder, packageInstallFolder) {
try {
const flagFile = path.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);
if (fs.existsSync(flagFile)) {
fs.unlinkSync(flagFile);
}
const packageLockFile = path.resolve(packageInstallFolder, 'package-lock.json');
if (fs.existsSync(packageLockFile)) {
fs.unlinkSync(packageLockFile);
}
const nodeModulesFolder = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);
if (fs.existsSync(nodeModulesFolder)) {
const rushRecyclerFolder = ensureAndJoinPath(rushCommonFolder, 'temp', 'rush-recycler', `install-run-${Date.now().toString()}`);
fs.renameSync(nodeModulesFolder, rushRecyclerFolder);
}
}
catch (e) {
throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);
}
}
function createPackageJson(packageInstallFolder, name, version) {
try {
const packageJsonContents = {
'name': 'ci-rush',
'version': '0.0.0',
'dependencies': {
[name]: version
},
'description': 'DON\'T WARN',
'repository': 'DON\'T WARN',
'license': 'MIT'
};
const packageJsonPath = path.join(packageInstallFolder, PACKAGE_JSON_FILENAME);
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2));
}
catch (e) {
throw new Error(`Unable to create package.json: ${e}`);
}
}
/**
* Run "npm install" in the package install folder.
*/
function installPackage(packageInstallFolder, name, version) {
try {
console.log(`Installing ${name}...`);
const npmPath = getNpmPath();
const result = childProcess.spawnSync(npmPath, ['install'], {
stdio: 'inherit',
cwd: packageInstallFolder,
env: process.env
});
if (result.status !== 0) {
throw new Error('"npm install" encountered an error');
}
console.log(`Successfully installed ${name}@${version}`);
}
catch (e) {
throw new Error(`Unable to install package: ${e}`);
}
}
/**
* Get the ".bin" path for the package.
*/
function getBinPath(packageInstallFolder, binName) {
const binFolderPath = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
const resolvedBinName = (os.platform() === 'win32') ? `${binName}.cmd` : binName;
return path.resolve(binFolderPath, resolvedBinName);
}
/**
* Write a flag file to the package's install directory, signifying that the install was successful.
*/
function writeFlagFile(packageInstallFolder) {
try {
const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
fs.writeFileSync(flagFilePath, process.version);
}
catch (e) {
throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);
}
}
function installAndRun(packageName, packageVersion, packageBinName, packageBinArgs) {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = path.join(rushJsonFolder, 'common');
const packageInstallFolder = ensureAndJoinPath(rushCommonFolder, 'temp', 'install-run', `${packageName}@${packageVersion}`);
if (!isPackageAlreadyInstalled(packageInstallFolder)) {
// The package isn't already installed
cleanInstallFolder(rushCommonFolder, packageInstallFolder);
const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
syncNpmrc(sourceNpmrcFolder, packageInstallFolder);
createPackageJson(packageInstallFolder, packageName, packageVersion);
installPackage(packageInstallFolder, packageName, packageVersion);
writeFlagFile(packageInstallFolder);
}
const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`;
const statusMessageLine = new Array(statusMessage.length + 1).join('-');
console.log(os.EOL + statusMessage + os.EOL + statusMessageLine + os.EOL);
const binPath = getBinPath(packageInstallFolder, packageBinName);
const result = childProcess.spawnSync(binPath, packageBinArgs, {
stdio: 'inherit',
cwd: process.cwd(),
env: process.env
});
return result.status;
}
exports.installAndRun = installAndRun;
function runWithErrorAndStatusCode(fn) {
process.exitCode = 1;
try {
const exitCode = fn();
process.exitCode = exitCode;
}
catch (e) {
console.error(os.EOL + os.EOL + e.toString() + os.EOL + os.EOL);
}
}
exports.runWithErrorAndStatusCode = runWithErrorAndStatusCode;
function run() {
const [nodePath, /* Ex: /bin/node */ scriptPath, /* /repo/common/scripts/install-run-rush.js */ rawPackageSpecifier, /* qrcode@^1.2.0 */ packageBinName, /* qrcode */ ...packageBinArgs /* [-f, myproject/lib] */] = process.argv;
if (!nodePath) {
throw new Error('Unexpected exception: could not detect node path');
}
if (path.basename(scriptPath).toLowerCase() !== 'install-run.js') {
// If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control
// to the script that (presumably) imported this file
return;
}
if (process.argv.length < 4) {
console.log('Usage: install-run.js <package>@<version> <command> [args...]');
console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io');
process.exit(1);
}
runWithErrorAndStatusCode(() => {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = ensureAndJoinPath(rushJsonFolder, 'common');
const packageSpecifier = parsePackageSpecifier(rawPackageSpecifier);
const name = packageSpecifier.name;
const version = resolvePackageVersion(rushCommonFolder, packageSpecifier);
if (packageSpecifier.version !== version) {
console.log(`Resolved to ${name}@${version}`);
}
return installAndRun(name, version, packageBinName, packageBinArgs);
});
}
run();
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for license information.
Object.defineProperty(exports, "__esModule", { value: true });
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where a Node tool may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the specified
// version of the specified tool (if not already installed), and then pass a command-line to it.
// An example usage would be:
//
// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
const childProcess = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
exports.RUSH_JSON_FILENAME = 'rush.json';
const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER';
const INSTALLED_FLAG_FILENAME = 'installed.flag';
const NODE_MODULES_FOLDER_NAME = 'node_modules';
const PACKAGE_JSON_FILENAME = 'package.json';
/**
* Parse a package specifier (in the form of name\@version) into name and version parts.
*/
function _parsePackageSpecifier(rawPackageSpecifier) {
rawPackageSpecifier = (rawPackageSpecifier || '').trim();
const separatorIndex = rawPackageSpecifier.lastIndexOf('@');
let name;
let version = undefined;
if (separatorIndex === 0) {
// The specifier starts with a scope and doesn't have a version specified
name = rawPackageSpecifier;
}
else if (separatorIndex === -1) {
// The specifier doesn't have a version
name = rawPackageSpecifier;
}
else {
name = rawPackageSpecifier.substring(0, separatorIndex);
version = rawPackageSpecifier.substring(separatorIndex + 1);
}
if (!name) {
throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`);
}
return { name, version };
}
/**
* As a workaround, copyAndTrimNpmrcFile() copies the .npmrc file to the target folder, and also trims
* unusable lines from the .npmrc file.
*
* Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in
* the .npmrc file to provide different authentication tokens for different registry.
* However, if the environment variable is undefined, it expands to an empty string, which
* produces a valid-looking mapping with an invalid URL that causes an error. Instead,
* we'd prefer to skip that line and continue looking in other places such as the user's
* home directory.
*
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._copyNpmrcFile()
*/
function _copyAndTrimNpmrcFile(sourceNpmrcPath, targetNpmrcPath) {
console.log(`Copying ${sourceNpmrcPath} --> ${targetNpmrcPath}`); // Verbose
let npmrcFileLines = fs.readFileSync(sourceNpmrcPath).toString().split('\n');
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
const resultLines = [];
// Trim out lines that reference environment variables that aren't defined
for (const line of npmrcFileLines) {
// This finds environment variable tokens that look like "${VAR_NAME}"
const regex = /\$\{([^\}]+)\}/g;
const environmentVariables = line.match(regex);
let lineShouldBeTrimmed = false;
if (environmentVariables) {
for (const token of environmentVariables) {
// Remove the leading "${" and the trailing "}" from the token
const environmentVariableName = token.substring(2, token.length - 1);
if (!process.env[environmentVariableName]) {
lineShouldBeTrimmed = true;
break;
}
}
}
if (lineShouldBeTrimmed) {
// Example output:
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
}
else {
resultLines.push(line);
}
}
fs.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL));
}
/**
* syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file.
* If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder.
*
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc()
*/
function _syncNpmrc(sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish) {
const sourceNpmrcPath = path.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');
const targetNpmrcPath = path.join(targetNpmrcFolder, '.npmrc');
try {
if (fs.existsSync(sourceNpmrcPath)) {
_copyAndTrimNpmrcFile(sourceNpmrcPath, targetNpmrcPath);
}
else if (fs.existsSync(targetNpmrcPath)) {
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
console.log(`Deleting ${targetNpmrcPath}`); // Verbose
fs.unlinkSync(targetNpmrcPath);
}
}
catch (e) {
throw new Error(`Error syncing .npmrc file: ${e}`);
}
}
let _npmPath = undefined;
/**
* Get the absolute path to the npm executable
*/
function getNpmPath() {
if (!_npmPath) {
try {
if (os.platform() === 'win32') {
// We're on Windows
const whereOutput = childProcess.execSync('where npm', { stdio: [] }).toString();
const lines = whereOutput.split(os.EOL).filter((line) => !!line);
// take the last result, we are looking for a .cmd command
// see https://github.com/microsoft/rushstack/issues/759
_npmPath = lines[lines.length - 1];
}
else {
// We aren't on Windows - assume we're on *NIX or Darwin
_npmPath = childProcess.execSync('which npm', { stdio: [] }).toString();
}
}
catch (e) {
throw new Error(`Unable to determine the path to the NPM tool: ${e}`);
}
_npmPath = _npmPath.trim();
if (!fs.existsSync(_npmPath)) {
throw new Error('The NPM executable does not exist');
}
}
return _npmPath;
}
exports.getNpmPath = getNpmPath;
function _ensureFolder(folderPath) {
if (!fs.existsSync(folderPath)) {
const parentDir = path.dirname(folderPath);
_ensureFolder(parentDir);
fs.mkdirSync(folderPath);
}
}
/**
* Create missing directories under the specified base directory, and return the resolved directory.
*
* Does not support "." or ".." path segments.
* Assumes the baseFolder exists.
*/
function _ensureAndJoinPath(baseFolder, ...pathSegments) {
let joinedPath = baseFolder;
try {
for (let pathSegment of pathSegments) {
pathSegment = pathSegment.replace(/[\\\/]/g, '+');
joinedPath = path.join(joinedPath, pathSegment);
if (!fs.existsSync(joinedPath)) {
fs.mkdirSync(joinedPath);
}
}
}
catch (e) {
throw new Error(`Error building local installation folder (${path.join(baseFolder, ...pathSegments)}): ${e}`);
}
return joinedPath;
}
function _getRushTempFolder(rushCommonFolder) {
const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME];
if (rushTempFolder !== undefined) {
_ensureFolder(rushTempFolder);
return rushTempFolder;
}
else {
return _ensureAndJoinPath(rushCommonFolder, 'temp');
}
}
/**
* Resolve a package specifier to a static version
*/
function _resolvePackageVersion(rushCommonFolder, { name, version }) {
if (!version) {
version = '*'; // If no version is specified, use the latest version
}
if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) {
// If the version contains only characters that we recognize to be used in static version specifiers,
// pass the version through
return version;
}
else {
// version resolves to
try {
const rushTempFolder = _getRushTempFolder(rushCommonFolder);
const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
_syncNpmrc(sourceNpmrcFolder, rushTempFolder);
const npmPath = getNpmPath();
// This returns something that looks like:
// @microsoft/rush@3.0.0 '3.0.0'
// @microsoft/rush@3.0.1 '3.0.1'
// ...
// @microsoft/rush@3.0.20 '3.0.20'
// <blank line>
const npmVersionSpawnResult = childProcess.spawnSync(npmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier'], {
cwd: rushTempFolder,
stdio: []
});
if (npmVersionSpawnResult.status !== 0) {
throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`);
}
const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString();
const versionLines = npmViewVersionOutput.split('\n').filter((line) => !!line);
const latestVersion = versionLines[versionLines.length - 1];
if (!latestVersion) {
throw new Error('No versions found for the specified version range.');
}
const versionMatches = latestVersion.match(/^.+\s\'(.+)\'$/);
if (!versionMatches) {
throw new Error(`Invalid npm output ${latestVersion}`);
}
return versionMatches[1];
}
catch (e) {
throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`);
}
}
}
let _rushJsonFolder;
/**
* Find the absolute path to the folder containing rush.json
*/
function findRushJsonFolder() {
if (!_rushJsonFolder) {
let basePath = __dirname;
let tempPath = __dirname;
do {
const testRushJsonPath = path.join(basePath, exports.RUSH_JSON_FILENAME);
if (fs.existsSync(testRushJsonPath)) {
_rushJsonFolder = basePath;
break;
}
else {
basePath = tempPath;
}
} while (basePath !== (tempPath = path.dirname(basePath))); // Exit the loop when we hit the disk root
if (!_rushJsonFolder) {
throw new Error('Unable to find rush.json.');
}
}
return _rushJsonFolder;
}
exports.findRushJsonFolder = findRushJsonFolder;
/**
* Detects if the package in the specified directory is installed
*/
function _isPackageAlreadyInstalled(packageInstallFolder) {
try {
const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
if (!fs.existsSync(flagFilePath)) {
return false;
}
const fileContents = fs.readFileSync(flagFilePath).toString();
return fileContents.trim() === process.version;
}
catch (e) {
return false;
}
}
/**
* Removes the following files and directories under the specified folder path:
* - installed.flag
* -
* - node_modules
*/
function _cleanInstallFolder(rushTempFolder, packageInstallFolder) {
try {
const flagFile = path.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);
if (fs.existsSync(flagFile)) {
fs.unlinkSync(flagFile);
}
const packageLockFile = path.resolve(packageInstallFolder, 'package-lock.json');
if (fs.existsSync(packageLockFile)) {
fs.unlinkSync(packageLockFile);
}
const nodeModulesFolder = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);
if (fs.existsSync(nodeModulesFolder)) {
const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler', `install-run-${Date.now().toString()}`);
fs.renameSync(nodeModulesFolder, rushRecyclerFolder);
}
}
catch (e) {
throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);
}
}
function _createPackageJson(packageInstallFolder, name, version) {
try {
const packageJsonContents = {
'name': 'ci-rush',
'version': '0.0.0',
'dependencies': {
[name]: version
},
'description': 'DON\'T WARN',
'repository': 'DON\'T WARN',
'license': 'MIT'
};
const packageJsonPath = path.join(packageInstallFolder, PACKAGE_JSON_FILENAME);
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2));
}
catch (e) {
throw new Error(`Unable to create package.json: ${e}`);
}
}
/**
* Run "npm install" in the package install folder.
*/
function _installPackage(packageInstallFolder, name, version) {
try {
console.log(`Installing ${name}...`);
const npmPath = getNpmPath();
const result = childProcess.spawnSync(npmPath, ['install'], {
stdio: 'inherit',
cwd: packageInstallFolder,
env: process.env
});
if (result.status !== 0) {
throw new Error('"npm install" encountered an error');
}
console.log(`Successfully installed ${name}@${version}`);
}
catch (e) {
throw new Error(`Unable to install package: ${e}`);
}
}
/**
* Get the ".bin" path for the package.
*/
function _getBinPath(packageInstallFolder, binName) {
const binFolderPath = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
const resolvedBinName = (os.platform() === 'win32') ? `${binName}.cmd` : binName;
return path.resolve(binFolderPath, resolvedBinName);
}
/**
* Write a flag file to the package's install directory, signifying that the install was successful.
*/
function _writeFlagFile(packageInstallFolder) {
try {
const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
fs.writeFileSync(flagFilePath, process.version);
}
catch (e) {
throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);
}
}
function installAndRun(packageName, packageVersion, packageBinName, packageBinArgs) {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = path.join(rushJsonFolder, 'common');
const rushTempFolder = _getRushTempFolder(rushCommonFolder);
const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`);
if (!_isPackageAlreadyInstalled(packageInstallFolder)) {
// The package isn't already installed
_cleanInstallFolder(rushTempFolder, packageInstallFolder);
const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
_syncNpmrc(sourceNpmrcFolder, packageInstallFolder);
_createPackageJson(packageInstallFolder, packageName, packageVersion);
_installPackage(packageInstallFolder, packageName, packageVersion);
_writeFlagFile(packageInstallFolder);
}
const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`;
const statusMessageLine = new Array(statusMessage.length + 1).join('-');
console.log(os.EOL + statusMessage + os.EOL + statusMessageLine + os.EOL);
const binPath = _getBinPath(packageInstallFolder, packageBinName);
const result = childProcess.spawnSync(binPath, packageBinArgs, {
stdio: 'inherit',
cwd: process.cwd(),
env: process.env
});
if (result.status !== null) {
return result.status;
}
else {
throw result.error || new Error('An unknown error occurred.');
}
}
exports.installAndRun = installAndRun;
function runWithErrorAndStatusCode(fn) {
process.exitCode = 1;
try {
const exitCode = fn();
process.exitCode = exitCode;
}
catch (e) {
console.error(os.EOL + os.EOL + e.toString() + os.EOL + os.EOL);
}
}
exports.runWithErrorAndStatusCode = runWithErrorAndStatusCode;
function _run() {
const [nodePath, /* Ex: /bin/node */ scriptPath, /* /repo/common/scripts/install-run-rush.js */ rawPackageSpecifier, /* qrcode@^1.2.0 */ packageBinName, /* qrcode */ ...packageBinArgs /* [-f, myproject/lib] */] = process.argv;
if (!nodePath) {
throw new Error('Unexpected exception: could not detect node path');
}
if (path.basename(scriptPath).toLowerCase() !== 'install-run.js') {
// If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control
// to the script that (presumably) imported this file
return;
}
if (process.argv.length < 4) {
console.log('Usage: install-run.js <package>@<version> <command> [args...]');
console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io');
process.exit(1);
}
runWithErrorAndStatusCode(() => {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common');
const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier);
const name = packageSpecifier.name;
const version = _resolvePackageVersion(rushCommonFolder, packageSpecifier);
if (packageSpecifier.version !== version) {
console.log(`Resolved to ${name}@${version}`);
}
return installAndRun(name, version, packageBinName, packageBinArgs);
});
}
_run();
//# sourceMappingURL=install-run.js.map

View File

@@ -0,0 +1,37 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
modules: true,
},
},
plugins: ['@typescript-eslint'],
env: {
node: true,
es6: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'@typescript-eslint/no-use-before-define': 0,
'@typescript-eslint/no-unused-vars': ["warn", {
"vars": "all",
"args": "none",
"ignoreRestSiblings": false
}],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"prefer-const": ["warn", {"destructuring": "all"}],
"indent": "off",
"@typescript-eslint/indent": ["error", 2, {
"SwitchCase": 1,
"FunctionDeclaration": { "body": 1, "parameters": 1 }
}],
"@typescript-eslint/no-throw-literal": "error"
},
};

View File

@@ -1,5 +1,27 @@
# CodeQL for Visual Studio Code: Changelog
## 1.1.0 - 17 March 2020
- Add functionality for testing custom CodeQL queries by using the VS
Code Test Explorer extension and `codeql test`. See the documentation for
more details.
- Add a "Show log" button to all information, error, and warning
popups that will display the CodeQL extension log.
- Display a message when a query times out.
- Show canceled queries in query history.
- Improve error messages when attempting to run non-query files.
## 1.0.6 - 28 February 2020
- Add command to restart query server.
- Enable support for future minor upgrades to the CodeQL CLI.
## 1.0.5 - 13 February 2020
- Add an icon next to any failed query runs in the query history
view.
- Add the ability to sort alerts by alert message.
## 1.0.4 - 24 January 2020
- Disable word-based autocomplete by default.

View File

@@ -15,7 +15,8 @@ export function compileView(cb: (err?: Error) => void) {
hash: false,
entrypoints: false,
timings: false,
modules: false
modules: false,
errors: true
}));
if (stats.hasErrors()) {
cb(new Error('Compilation errors detected.'));

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.0.4",
"version": "1.1.0",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -18,16 +18,21 @@
"categories": [
"Programming Languages"
],
"extensionDependencies": [
"hbenl.vscode-test-explorer"
],
"activationEvents": [
"onLanguage:ql",
"onView:codeQLDatabases",
"onView:codeQLQueryHistory",
"onView:test-explorer",
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.chooseDatabase",
"onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQLDatabases.chooseDatabase",
"onCommand:codeQLDatabases.setCurrentDatabase",
"onCommand:codeQL.quickQuery",
"onCommand:codeQL.restartQueryServer",
"onWebviewPanel:resultsView",
"onFileSystem:codeql-zip-archive"
],
@@ -131,6 +136,14 @@
"type": "string",
"default": "[%t] %q on %d - %s",
"description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, and %s is a status string."
},
"codeQL.runningTests.numberOfThreads": {
"scope": "window",
"type": "integer",
"default": 1,
"minimum": 1,
"maximum": 1024,
"description": "Number of threads for running CodeQL tests."
}
}
},
@@ -206,6 +219,18 @@
{
"command": "codeQLQueryHistory.setLabel",
"title": "Set Label"
},
{
"command": "codeQL.restartQueryServer",
"title": "CodeQL: Restart Query Server"
},
{
"command": "codeQLTests.showOutputDifferences",
"title": "CodeQL: Show Test Output Differences"
},
{
"command": "codeQLTests.acceptOutput",
"title": "CodeQL: Accept Test Output"
}
],
"menus": {
@@ -246,6 +271,16 @@
"command": "codeQLQueryHistory.setLabel",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLTests.showOutputDifferences",
"group": "qltest@1",
"when": "view == test-explorer && viewItem == testWithSource"
},
{
"command": "codeQLTests.acceptOutput",
"group": "qltest@2",
"when": "view == test-explorer && viewItem == testWithSource"
}
],
"explorer/context": [
@@ -340,9 +375,11 @@
"integration": "node ./out/vscode-tests/run-integration-tests.js",
"update-vscode": "node ./node_modules/vscode/bin/install",
"postinstall": "node ./node_modules/vscode/bin/install",
"format": "tsfmt -r"
"format": "tsfmt -r",
"lint": "eslint . --ext .ts,.tsx"
},
"dependencies": {
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"fs-extra": "^8.1.0",
"glob-promise": "^3.4.0",
@@ -354,12 +391,16 @@
"semmle-io-node": "^0.0.1",
"semmle-vscode-utils": "^0.0.1",
"tmp": "^0.1.0",
"tree-kill": "~1.2.2",
"unzipper": "~0.10.5",
"vscode-jsonrpc": "^4.0.0",
"vscode-languageclient": "^5.2.1"
"vscode-languageclient": "^5.2.1",
"vscode-test-adapter-api": "~1.7.0",
"vscode-test-adapter-util": "~0.7.0"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/child-process-promise": "^2.2.1",
"@types/classnames": "~2.2.9",
"@types/fs-extra": "^8.0.0",
"@types/glob": "^7.1.1",
@@ -380,14 +421,15 @@
"@types/xml2js": "~0.4.4",
"build-tasks": "^0.0.1",
"chai": "^4.2.0",
"child-process-promise": "^2.2.1",
"css-loader": "~3.1.0",
"glob": "^7.1.4",
"gulp": "^4.0.2",
"gulp-sourcemaps": "^2.6.5",
"gulp-typescript": "^5.0.1",
"mocha": "~6.2.1",
"mocha-sinon": "~2.1.0",
"npm-run-all": "^4.1.5",
"sinon": "~9.0.0",
"style-loader": "~0.23.1",
"through2": "^3.0.1",
"ts-loader": "^5.4.5",
@@ -399,6 +441,9 @@
"vsce": "^1.65.0",
"vscode-test": "^1.0.0",
"webpack": "^4.38.0",
"webpack-cli": "^3.3.2"
"webpack-cli": "^3.3.2",
"eslint": "~6.8.0",
"@typescript-eslint/eslint-plugin": "~2.23.0",
"@typescript-eslint/parser": "~2.23.0"
}
}

View File

@@ -0,0 +1,79 @@
export const PAGE_SIZE = 1000;
export type ColumnKind = "f" | "i" | "s" | "b" | "d" | "e";
export interface Column {
name?: string;
kind: ColumnKind;
}
export interface ResultSetSchema {
name: string;
rows: number;
columns: Column[];
pagination?: PaginationInfo;
}
export function getResultSetSchema(resultSetName: string, resultSets: BQRSInfo): ResultSetSchema | undefined {
for (const schema of resultSets["result-sets"]) {
if (schema.name === resultSetName) {
return schema;
}
}
return undefined;
}
export interface PaginationInfo {
"step-size": number;
offsets: number[];
}
export interface BQRSInfo {
"result-sets": ResultSetSchema[];
}
export interface EntityValue {
url?: UrlValue;
label?: string;
}
export interface LineColumnLocation {
uri: string;
startLine: number;
startColumn: number;
endLine: number;
endColumn: number;
charOffset: never;
charLength: never;
}
export interface OffsetLengthLocation {
uri: string;
startLine: never;
startColumn: never;
endLine: never;
endColumn: never;
charOffset: number;
charLength: number;
}
export interface WholeFileLocation {
uri: string;
startLine: never;
startColumn: never;
endLine: never;
endColumn: never;
charOffset: never;
charLength: never;
}
export type UrlValue = LineColumnLocation | OffsetLengthLocation | WholeFileLocation | string;
export type ColumnValue = EntityValue | number | string | boolean;
export interface DecodedBqrsChunk {
tuples: ColumnValue[][];
next?: number;
}

View File

@@ -1,13 +1,19 @@
import * as child_process from "child_process";
/* 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';
import * as path from 'path';
import * as sarif from 'sarif';
import { Readable } from 'stream';
import { StringDecoder } from 'string_decoder';
import * as tk from 'tree-kill';
import * as util from 'util';
import { Logger, ProgressReporter } from "./logging";
import { Disposable } from "vscode";
import { DistributionProvider } from "./distribution";
import { SortDirection } from "./interface-types";
import { assertNever } from "./helpers-pure";
import { CancellationToken, Disposable } from 'vscode';
import { BQRSInfo, DecodedBqrsChunk } from "./bqrs-cli-types";
import { DistributionProvider } from './distribution';
import { assertNever } from './helpers-pure';
import { QueryMetadata, SortDirection } from './interface-types';
import { Logger, ProgressReporter } from './logging';
/**
* The version of the SARIF format that we are using.
@@ -23,10 +29,10 @@ const LOGGING_FLAGS = ['-v', '--log-to-stderr'];
* The expected output of `codeql resolve library-path`.
*/
export interface QuerySetup {
libraryPath: string[],
dbscheme: string,
relativeName?: string,
compilationCache?: string
libraryPath: string[];
dbscheme: string;
relativeName?: string;
compilationCache?: string;
}
/**
@@ -55,16 +61,6 @@ export interface UpgradesInfo {
*/
export type QlpacksInfo = { [name: string]: string[] };
/**
* The expected output of `codeql resolve metadata`.
*/
export interface QueryMetadata {
name?: string,
description?: string,
id?: string,
kind?: string
}
// `codeql bqrs interpret` requires both of these to be present or
// both absent.
export interface SourceInfo {
@@ -72,6 +68,31 @@ export interface SourceInfo {
sourceLocationPrefix: string;
}
/**
* The expected output of `codeql resolve tests`.
*/
export type ResolvedTests = string[];
/**
* Options for `codeql test run`.
*/
export interface TestRunOptions {
cancellationToken?: CancellationToken;
logger?: Logger;
}
/**
* Event fired by `codeql test run`.
*/
export interface TestCompleted {
test: string;
pass: boolean;
messages: string[];
compilationMs: number;
evaluationMs: number;
expected: string;
}
/**
* This class manages a cli server started by `codeql execute cli-server` to
* run commands without the overhead of starting a new java
@@ -101,11 +122,11 @@ export class CodeQLCliServer implements Disposable {
}
dispose() {
dispose(): void {
this.killProcessIfRunning();
}
killProcessIfRunning() {
killProcessIfRunning(): void {
if (this.process) {
// Tell the Java CLI server process to shut down.
this.logger.log('Sending shutdown request');
@@ -132,8 +153,8 @@ export class CodeQLCliServer implements Disposable {
/**
* Restart the server when the current command terminates
*/
private restartCliServer() {
let callback = () => {
private restartCliServer(): void {
const callback = (): void => {
try {
this.killProcessIfRunning();
} finally {
@@ -151,19 +172,27 @@ export class CodeQLCliServer implements Disposable {
}
/**
* Get the path to the CodeQL CLI distribution, or throw an exception if not found.
*/
private async getCodeQlPath(): Promise<string> {
const codeqlPath = await this.config.getCodeQlPathWithoutVersionCheck();
if (!codeqlPath) {
throw new Error('Failed to find CodeQL distribution.');
}
return codeqlPath;
}
/**
* Launch the cli server
*/
private async launchProcess(): Promise<child_process.ChildProcessWithoutNullStreams> {
const config = await this.config.getCodeQlPathWithoutVersionCheck();
if (!config) {
throw new Error("Failed to find codeql distribution")
}
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => {})
const config = await this.getCodeQlPath();
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => { /**/ })
}
private async runCodeQlCliInternal(command: string[], commandArgs: string[], description: string): Promise<string> {
let stderrBuffers: Buffer[] = [];
const stderrBuffers: Buffer[] = [];
if (this.commandInProcess) {
throw new Error("runCodeQlCliInternal called while cli was running")
}
@@ -176,7 +205,7 @@ export class CodeQLCliServer implements Disposable {
// Grab the process so that typescript know that it is always defined.
const process = this.process;
// The array of fragments of stdout
let stdoutBuffers: Buffer[] = [];
const stdoutBuffers: Buffer[] = [];
// Compute the full args array
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
@@ -205,9 +234,9 @@ export class CodeQLCliServer implements Disposable {
process.stdin.write(this.nullBuffer)
});
// Join all the data together
let fullBuffer = Buffer.concat(stdoutBuffers);
const fullBuffer = Buffer.concat(stdoutBuffers);
// Make sure we remove the terminator;
let data = fullBuffer.toString("utf8", 0, fullBuffer.length - 1);
const data = fullBuffer.toString("utf8", 0, fullBuffer.length - 1);
this.logger.log(`CLI command succeeded.`);
return data;
} catch (err) {
@@ -236,13 +265,94 @@ export class CodeQLCliServer implements Disposable {
/**
* Run the next command in the queue
*/
private runNext() {
private runNext(): void {
const callback = this.commandQueue.shift();
if (callback) {
callback();
}
}
/**
* Runs an asynchronous CodeQL CLI command without invoking the CLI server, returning any events
* fired by the command as an asynchronous generator.
*
* @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 cancellationToken CancellationToken to terminate the test process.
* @param logger Logger to write text output from the command.
* @returns The sequence of async events produced by the command.
*/
private async* runAsyncCodeQlCliCommandInternal(
command: string[],
commandArgs: string[],
cancellationToken?: CancellationToken,
logger?: Logger
): AsyncGenerator<string, void, unknown> {
// Add format argument first, in case commandArgs contains positional parameters.
const args = [
...command,
'--format', 'jsonz',
...commandArgs
];
// Spawn the CodeQL process
const codeqlPath = await this.getCodeQlPath();
const childPromise = cpp.spawn(codeqlPath, args);
const child = childPromise.childProcess;
let cancellationRegistration: Disposable | undefined = undefined;
try {
if (cancellationToken !== undefined) {
cancellationRegistration = cancellationToken.onCancellationRequested(_e => {
tk(child.pid);
});
}
if (logger !== undefined) {
// The human-readable output goes to stderr.
logStream(child.stderr!, logger);
}
for await (const event of await splitStreamAtSeparators(child.stdout!, ['\0'])) {
yield event;
}
await childPromise;
}
finally {
if (cancellationRegistration !== undefined) {
cancellationRegistration.dispose();
}
}
}
/**
* Runs an asynchronous CodeQL CLI command without invoking the CLI server, returning any events
* fired by the command as an asynchronous generator.
*
* @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 cancellationToken CancellationToken to terminate the test process.
* @param logger Logger to write text output from the command.
* @returns The sequence of async events produced by the command.
*/
public async* runAsyncCodeQlCliCommand<EventType>(
command: string[],
commandArgs: string[],
description: string,
cancellationToken?: CancellationToken,
logger?: Logger
): AsyncGenerator<EventType, void, unknown> {
for await (const event of await this.runAsyncCodeQlCliCommandInternal(command, commandArgs,
cancellationToken, logger)) {
try {
yield JSON.parse(event) as EventType;
} catch (err) {
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`)
}
}
}
/**
* Runs a CodeQL CLI command on the server, returning the output as a string.
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
@@ -258,7 +368,7 @@ export class CodeQLCliServer implements Disposable {
return new Promise((resolve, reject) => {
// Construct the command that actually does the work
const callback = () => {
const callback = (): void => {
try {
this.runCodeQlCliInternal(command, commandArgs, description).then(resolve, reject);
} catch (err) {
@@ -308,6 +418,40 @@ export class CodeQLCliServer implements Disposable {
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, "Resolving library paths");
}
/**
* Finds all available QL tests in a given directory.
* @param testPath Root of directory tree to search for tests.
* @returns The list of tests that were found.
*/
public async resolveTests(testPath: string): Promise<ResolvedTests> {
const subcommandArgs = [
testPath
];
return await this.runJsonCodeQlCliCommand<ResolvedTests>(['resolve', 'tests'], subcommandArgs, 'Resolving tests');
}
/**
* Runs QL tests.
* @param testPaths Full paths of the tests to run.
* @param workspaces Workspace paths to use as search paths for QL packs.
* @param options Additional options.
*/
public async* runTests(
testPaths: string[], workspaces: string[], options: TestRunOptions
): AsyncGenerator<TestCompleted, void, unknown> {
const subcommandArgs = [
'--additional-packs', workspaces.join(path.delimiter),
'--threads', '8',
...testPaths
];
for await (const event of await this.runAsyncCodeQlCliCommand<TestCompleted>(['test', 'run'],
subcommandArgs, 'Run CodeQL Tests', options.cancellationToken, options.logger)) {
yield event;
}
}
/**
* Gets the metadata for a query.
* @param queryPath The path to the query.
@@ -320,6 +464,7 @@ export class CodeQLCliServer implements Disposable {
* Gets the RAM setting for the query server.
* @param queryMemoryMb The maximum amount of RAM to use, in MB.
* Leave `undefined` for CodeQL to choose a limit based on the available system memory.
* @param progressReporter The progress reporter to send progress information to.
* @returns String arguments that can be passed to the CodeQL query server,
* indicating how to split the given RAM limit between heap and off-heap memory.
*/
@@ -330,9 +475,41 @@ export class CodeQLCliServer implements Disposable {
}
return await this.runJsonCodeQlCliCommand<string[]>(['resolve', 'ram'], args, "Resolving RAM settings", progressReporter);
}
/**
* Gets the headers (and optionally pagination info) of a bqrs.
* @param bqrsPath The path to the bqrs.
* @param pageSize The page size to precompute offsets into the binary file for.
*/
async bqrsInfo(bqrsPath: string, pageSize?: number): Promise<BQRSInfo> {
const subcommandArgs = (
pageSize ? ["--paginate-rows", pageSize.toString()] : []
).concat(
bqrsPath
);
return await this.runJsonCodeQlCliCommand<BQRSInfo>(['bqrs', 'info'], subcommandArgs, "Reading bqrs header");
}
/**
* Gets the results from a bqrs.
* @param bqrsPath The path to the bqrs.
* @param resultSet The result set to get.
* @param pageSize How many results to get.
* @param offset The 0-based index of the first result to get.
*/
async bqrsDecode(bqrsPath: string, resultSet: string, pageSize?: number, offset?: number): Promise<DecodedBqrsChunk> {
const subcommandArgs = [
"--entities=url,string",
"--result-set", resultSet,
].concat(
pageSize ? ["--rows", pageSize.toString()] : []
).concat(
offset ? ["--start-at", offset.toString()] : []
).concat([bqrsPath]);
return await this.runJsonCodeQlCliCommand<DecodedBqrsChunk>(['bqrs', 'decode'], subcommandArgs, "Reading bqrs data");
}
async interpretBqrs(metadata: { kind: string, id: string }, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
async interpretBqrs(metadata: { kind: string; id: string }, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
const args = [
`-t=kind=${metadata.kind}`,
`-t=id=${metadata.id}`,
@@ -419,11 +596,16 @@ export class CodeQLCliServer implements Disposable {
/**
* Gets information about available qlpacks
* @param searchPath A list of directories to search for qlpacks
* @param additionalPacks A list of directories to search for qlpacks before searching in `searchPath`.
* @param searchPath A list of directories to search for packs not found in `additionalPacks`. If undefined,
* the default CLI search path is used.
* @returns A dictionary mapping qlpack name to the directory it comes from
*/
resolveQlpacks(searchPath: string[]): Promise<QlpacksInfo> {
const args = ['--additional-packs', searchPath.join(path.delimiter)];
resolveQlpacks(additionalPacks: string[], searchPath?: string[]): Promise<QlpacksInfo> {
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
if (searchPath !== undefined) {
args.push('--search-path', searchPath.join(path.delimiter));
}
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
['resolve', 'qlpacks'],
@@ -513,3 +695,101 @@ export async function runCodeQlCliCommand(codeQlPath: string, command: string[],
throw new Error(`${description} failed: ${err.stderr || err}`)
}
}
/**
* Buffer to hold state used when splitting a text stream into lines.
*/
class SplitBuffer {
private readonly decoder = new StringDecoder('utf8');
private readonly maxSeparatorLength: number;
private buffer = '';
private searchIndex = 0;
constructor(private readonly separators: readonly string[]) {
this.maxSeparatorLength = separators.map(s => s.length).reduce((a, b) => Math.max(a, b), 0);
}
/**
* Append new text data to the buffer.
* @param chunk The chunk of data to append.
*/
public addChunk(chunk: Buffer): void {
this.buffer += this.decoder.write(chunk);
}
/**
* Signal that the end of the input stream has been reached.
*/
public end(): void {
this.buffer += this.decoder.end();
this.buffer += this.separators[0]; // Append a separator to the end to ensure the last line is returned.
}
/**
* Extract the next full line from the buffer, if one is available.
* @returns The text of the next available full line (without the separator), or `undefined` if no
* line is available.
*/
public getNextLine(): string | undefined {
while (this.searchIndex <= (this.buffer.length - this.maxSeparatorLength)) {
for (const separator of this.separators) {
if (this.buffer.startsWith(separator, this.searchIndex)) {
const line = this.buffer.substr(0, this.searchIndex);
this.buffer = this.buffer.substr(this.searchIndex + separator.length);
this.searchIndex = 0;
return line;
}
}
this.searchIndex++;
}
return undefined;
}
}
/**
* Splits a text stream into lines based on a list of valid line separators.
* @param stream The text stream to split. This stream will be fully consumed.
* @param separators The list of strings that act as line separators.
* @returns A sequence of lines (not including separators).
*/
async function* splitStreamAtSeparators(
stream: Readable, separators: string[]
): AsyncGenerator<string, void, unknown> {
const buffer = new SplitBuffer(separators);
for await (const chunk of stream) {
buffer.addChunk(chunk);
let line: string | undefined;
do {
line = buffer.getNextLine();
if (line !== undefined) {
yield line;
}
} while (line !== undefined);
}
buffer.end();
let line: string | undefined;
do {
line = buffer.getNextLine();
if (line !== undefined) {
yield line;
}
} while (line !== undefined);
}
/**
* Standard line endings for splitting human-readable text.
*/
const lineEndings = ['\r\n', '\r', '\n'];
/**
* Log a text stream to a `Logger` interface.
* @param stream The stream to log.
* @param logger The logger that will consume the stream output.
*/
async function logStream(stream: Readable, logger: Logger): Promise<void> {
for await (const line of await splitStreamAtSeparators(stream, lineEndings)) {
logger.log(line);
}
}

View File

@@ -1,12 +1,13 @@
import * as path from 'path';
import { DisposableObject } from "semmle-vscode-utils";
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from "vscode";
import { DisposableObject } from 'semmle-vscode-utils';
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
import * as cli from './cli';
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from "./databases";
import { logger } from "./logging";
import { clearCacheInDatabase, upgradeDatabase, UserCancellationException } from "./queries";
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
import { getOnDiskWorkspaceFolders } from './helpers';
import { logger } from './logging';
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
import * as qsClient from './queryserver-client';
import { getOnDiskWorkspaceFolders } from "./helpers";
import { upgradeDatabase } from './upgrades';
type ThemableIconPath = { light: string, dark: string } | string;

View File

@@ -571,7 +571,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);
showAndLogErrorMessage(`Database list loading failed: ${e.message}`);
}
}

View File

@@ -0,0 +1,87 @@
import { DisposableObject } from 'semmle-vscode-utils';
/**
* Base class for "discovery" operations, which scan the file system to find specific kinds of
* files. This class automatically prevents more than one discovery operation from running at the
* same time.
*/
export abstract class Discovery<T> extends DisposableObject {
private retry = false;
private discoveryInProgress = false;
constructor() {
super();
}
/**
* Force the discovery process to run. Normally invoked by the derived class when a relevant file
* system change is detected.
*/
public refresh(): void {
// We avoid having multiple discovery operations in progress at the same time. Otherwise, if we
// got a storm of refresh requests due to, say, the copying or deletion of a large directory
// tree, we could potentially spawn a separate simultaneous discovery operation for each
// individual file change notification.
// Our approach is to spawn a discovery operation immediately upon receiving the first refresh
// request. If we receive any additional refresh requests before the first one is complete, we
// record this fact by setting `this.retry = true`. When the original discovery operation
// completes, we discard its results and spawn another one to account for that additional
// changes that have happened since.
// The means that for the common case of a single file being modified, we'll complete the
// discovery and update as soon as possible. If multiple files are being modified, we'll
// probably wind up doing discovery at least twice.
// We could choose to delay the initial discovery request by a second or two to wait for any
// other change notifications that might be coming along. However, this would create more
// latency in the common case, in order to save a bit of latency in the uncommon case.
if (this.discoveryInProgress) {
// There's already a discovery operation in progress. Tell it to restart when it's done.
this.retry = true;
}
else {
// No discovery in progress, so start one now.
this.discoveryInProgress = true;
this.launchDiscovery();
}
}
/**
* Starts the asynchronous discovery operation by invoking the `discover` function. When the
* discovery operation completes, the `update` function will be invoked with the results of the
* discovery.
*/
private launchDiscovery(): void {
const discoveryPromise = this.discover();
discoveryPromise.then(results => {
if (!this.retry) {
// Update any listeners with the results of the discovery.
this.discoveryInProgress = false;
this.update(results);
}
});
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();
}
});
}
/**
* Overridden by the derived class to spawn the actual discovery operation, returning the results.
*/
protected abstract discover(): Promise<T>;
/**
* Overridden by the derived class to atomically update the `Discovery` object with the results of
* the discovery operation, and to notify any listeners that the discovery results may have
* changed.
* @param results The discovery results returned by the `discover` function.
*/
protected abstract update(results: T): void;
}

View File

@@ -39,9 +39,9 @@ const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
* This applies to both extension-managed and CLI distributions.
*/
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
description: "2.0.*",
description: "2.*.*",
isVersionCompatible: (v: Version) => {
return v.majorVersion === 2 && v.minorVersion === 0
return v.majorVersion === 2 && v.minorVersion >= 0
}
}

View File

@@ -1,5 +1,5 @@
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
import { ErrorCodes, LanguageClient, ResponseError } from 'vscode-languageclient';
import { LanguageClient } from 'vscode-languageclient';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
import { DatabaseManager } from './databases';
@@ -12,12 +12,16 @@ import * as helpers from './helpers';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager, WebviewReveal } from './interface';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { compileAndRunQueryAgainstDatabase, EvaluationInfo, tmpDirDisposal, UserCancellationException } from './queries';
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
import { CompletedQuery } from './query-results';
import { QueryHistoryManager } from './query-history';
import * as qsClient from './queryserver-client';
import { CodeQLCliServer } from './cli';
import { assertNever } from './helpers-pure';
import { displayQuickQuery } from './quick-query';
import { TestHub, testExplorerExtensionId } from 'vscode-test-adapter-api';
import { QLTestAdapterFactory } from './test-adapter';
import { TestUIService } from './test-ui';
/**
* extension.ts
@@ -52,7 +56,7 @@ let isInstallingOrUpdatingDistribution = false;
*
* @param excludedCommands List of commands for which we should not register error stubs.
*/
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => void) {
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => void): void {
// Remove existing stubs
errorStubs.forEach(stub => stub.dispose());
@@ -197,7 +201,9 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
} 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.`, installActionName);
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, {
items: [installActionName]
});
if (chosenAction === installActionName) {
installOrUpdateThenTryActivate({
isUserInitiated: true,
@@ -223,7 +229,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
});
}
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager) {
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager): Promise<void> {
beganMainExtensionActivation = true;
// Remove any error stubs command handlers left over from first part
// of activation.
@@ -254,17 +260,17 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
const qhm = new QueryHistoryManager(
ctx,
queryHistoryConfigurationListener,
async item => showResultsForInfo(item.info, WebviewReveal.Forced)
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced)
);
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
archiveFilesystemProvider.activate(ctx);
async function showResultsForInfo(info: EvaluationInfo, forceReveal: WebviewReveal): Promise<void> {
await intm.showResults(info, forceReveal, false);
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
await intm.showResults(query, forceReveal, false);
}
async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined) {
async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined): Promise<void> {
if (qs !== undefined) {
try {
const dbItem = await databaseUI.getDatabaseItem();
@@ -272,27 +278,23 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
throw new Error('Can\'t run query without a selected database');
}
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
await showResultsForInfo(info, WebviewReveal.NotForced);
qhm.push(info);
}
catch (e) {
const item = qhm.addQuery(info);
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
} catch (e) {
if (e instanceof UserCancellationException) {
logger.log(e.message);
}
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
logger.log(e.message);
}
else if (e instanceof Error)
helpers.showAndLogWarningMessage(e.message);
} else if (e instanceof Error) {
helpers.showAndLogErrorMessage(e.message);
else
} else {
throw e;
}
}
}
}
ctx.subscriptions.push(tmpDirDisposal);
let client = new LanguageClient('CodeQL Language Server', () => spawnIdeServer(qlConfigurationListener), {
const client = new LanguageClient('CodeQL Language Server', () => spawnIdeServer(qlConfigurationListener), {
documentSelector: [
{ language: 'ql', scheme: 'file' },
{ language: 'yaml', scheme: 'file', pattern: '**/qlpack.yml' }
@@ -304,9 +306,23 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
outputChannel: ideServerLogger.outputChannel
}, true);
const testExplorerExtension = extensions.getExtension<TestHub>(testExplorerExtensionId);
if (testExplorerExtension) {
const testHub = testExplorerExtension.exports;
const testAdapterFactory = new QLTestAdapterFactory(testHub, cliServer);
ctx.subscriptions.push(testAdapterFactory);
const testUIService = new TestUIService(testHub);
ctx.subscriptions.push(testUIService);
}
ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)));
ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)));
ctx.subscriptions.push(commands.registerCommand('codeQL.quickQuery', async () => displayQuickQuery(ctx, cliServer, databaseUI)));
ctx.subscriptions.push(commands.registerCommand('codeQL.restartQueryServer', async () => {
await qs.restartQueryServer();
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger });
}));
ctx.subscriptions.push(client.start());
}

View File

@@ -1,7 +1,7 @@
import * as path from 'path';
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
import { logger } from './logging';
import { EvaluationInfo } from './queries';
import { QueryInfo } from './run-queries';
export interface ProgressUpdate {
/**
@@ -47,37 +47,59 @@ export function withProgress<R>(
* Show an error message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param options.outputLogger The output logger that will receive the message
* @param options.items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showErrorMessage(message, ...items);
export async function showAndLogErrorMessage(message: string, {
outputLogger = logger,
items = [] as string[]
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showErrorMessage);
}
/**
* Show a warning message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param options.outputLogger The output logger that will receive the message
* @param options.items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showWarningMessage(message, ...items);
export async function showAndLogWarningMessage(message: string, {
outputLogger = logger,
items = [] as string[]
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showWarningMessage);
}
/**
* Show an information message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param options.outputLogger The output logger that will receive the message
* @param options.items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showInformationMessage(message, ...items);
export async function showAndLogInformationMessage(message: string, {
outputLogger = logger,
items = [] as string[]
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showInformationMessage);
}
type ShowMessageFn = (message: string, ...items: string[]) => Thenable<string | undefined>;
async function internalShowAndLog(message: string, items: string[], outputLogger = logger,
fn: ShowMessageFn): Promise<string | undefined> {
const label = 'Show Log';
outputLogger.log(message);
const result = await fn(message, label, ...items);
if (result === label) {
outputLogger.show();
}
return result;
}
/**
@@ -121,17 +143,17 @@ export function getOnDiskWorkspaceFolders() {
* Gets a human-readable name for an evaluated query.
* Uses metadata if it exists, and defaults to the query file name.
*/
export function getQueryName(info: EvaluationInfo) {
export function getQueryName(query: QueryInfo) {
// Queries run through quick evaluation are not usually the entire query file.
// Label them differently and include the line numbers.
if (info.query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = info.query.quickEvalPosition;
if (query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = query.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `Quick evaluation of ${path.basename(fileName)}:${lineInfo}`;
} else if (info.query.metadata && info.query.metadata.name) {
return info.query.metadata.name;
} else if (query.metadata && query.metadata.name) {
return query.metadata.name;
} else {
return path.basename(info.query.program.queryPath);
return path.basename(query.program.queryPath);
}
}

View File

@@ -16,6 +16,14 @@ export interface DatabaseInfo {
databaseUri: string;
}
/** Arbitrary query metadata */
export interface QueryMetadata {
name?: string,
description?: string,
id?: string,
kind?: string
}
export interface PreviousExecution {
queryName: string;
time: string;
@@ -26,17 +34,22 @@ export interface PreviousExecution {
export interface Interpretation {
sourceLocationPrefix: string;
numTruncatedResults: number;
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
*/
sortState?: InterpretedResultsSortState;
sarif: sarif.Log;
}
export interface ResultsInfo {
export interface ResultsPaths {
resultsPath: string;
interpretedResultsPath: string;
}
export interface SortedResultSetInfo {
resultsPath: string;
sortState: SortState;
sortState: RawResultsSortState;
}
export type SortedResultsMap = { [resultSet: string]: SortedResultSetInfo };
@@ -53,10 +66,11 @@ export interface ResultsUpdatingMsg {
export interface SetStateMsg {
t: 'setState';
resultsPath: string;
origResultsPaths: ResultsPaths;
sortedResultsMap: SortedResultsMap;
interpretation: undefined | Interpretation;
database: DatabaseInfo;
kind?: string;
metadata?: QueryMetadata
/**
* Whether to keep displaying the old results while rendering the new results.
*
@@ -75,7 +89,12 @@ export interface NavigatePathMsg {
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
export type FromResultsViewMsg = ViewSourceFileMsg | ToggleDiagnostics | ChangeSortMsg | ResultViewLoaded;
export type FromResultsViewMsg =
| ViewSourceFileMsg
| ToggleDiagnostics
| ChangeRawResultsSortMsg
| ChangeInterpretedResultsSortMsg
| ResultViewLoaded;
interface ViewSourceFileMsg {
t: 'viewSourceFile';
@@ -86,7 +105,8 @@ interface ViewSourceFileMsg {
interface ToggleDiagnostics {
t: 'toggleDiagnostics';
databaseUri: string;
resultsPath: string;
metadata?: QueryMetadata
origResultsPaths: ResultsPaths;
visible: boolean;
kind?: string;
};
@@ -99,13 +119,34 @@ export enum SortDirection {
asc, desc
}
export interface SortState {
export interface RawResultsSortState {
columnIndex: number;
direction: SortDirection;
sortDirection: SortDirection;
}
interface ChangeSortMsg {
export type InterpretedResultsSortColumn =
'alert-message';
export interface InterpretedResultsSortState {
sortBy: InterpretedResultsSortColumn;
sortDirection: SortDirection;
}
interface ChangeRawResultsSortMsg {
t: 'changeSort';
resultSetName: string;
sortState?: SortState;
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
*/
sortState?: RawResultsSortState;
}
interface ChangeInterpretedResultsSortMsg {
t: 'changeInterpretedSort';
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
*/
sortState?: InterpretedResultsSortState;
}

View File

@@ -1,20 +1,21 @@
import * as crypto from 'crypto';
import * as path from 'path';
import * as bqrs from 'semmle-bqrs';
import { CustomResultSets, FivePartLocation, LocationStyle, LocationValue, PathProblemQueryResults, ProblemQueryResults, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
import { FileReader } from 'semmle-io-node';
import * as Sarif from 'sarif';
import { FivePartLocation, LocationStyle, LocationValue, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
import { DisposableObject } from 'semmle-vscode-utils';
import * as vscode from 'vscode';
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Position, Range, Uri, window as Window, workspace } from 'vscode';
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Range, Uri, window as Window, workspace } from 'vscode';
import * as cli from './cli';
import { CodeQLCliServer } from './cli';
import { DatabaseItem, DatabaseManager } from './databases';
import * as helpers from './helpers';
import { showAndLogErrorMessage } from './helpers';
import { assertNever } from './helpers-pure';
import { FromResultsViewMsg, Interpretation, IntoResultsViewMsg, ResultsInfo, SortedResultSetInfo, SortedResultsMap, INTERPRETED_RESULTS_PER_RUN_LIMIT } from './interface-types';
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types';
import { Logger } from './logging';
import * as messages from './messages';
import { EvaluationInfo, interpretResults, QueryInfo, tmpDir } from './queries';
import { CompletedQuery, interpretResults } from './query-results';
import { QueryInfo, tmpDir } from './run-queries';
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
/**
* interface.ts
@@ -41,7 +42,11 @@ export enum WebviewReveal {
* Returns HTML to populate the given webview.
* Uses a content security policy that only loads the given script.
*/
function getHtmlForWebview(webview: vscode.Webview, scriptUriOnDisk: vscode.Uri, stylesheetUriOnDisk: vscode.Uri) {
function getHtmlForWebview(
webview: vscode.Webview,
scriptUriOnDisk: vscode.Uri,
stylesheetUriOnDisk: vscode.Uri
): void {
// Convert the on-disk URIs into webview URIs.
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk);
@@ -85,25 +90,67 @@ export function webviewUriToFileUri(webviewUri: string): Uri {
return Uri.file(path);
}
function sortMultiplier(sortDirection: SortDirection): number {
switch (sortDirection) {
case SortDirection.asc: return 1;
case SortDirection.desc: return -1;
}
}
function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedResultsSortState | undefined): void {
if (sortState !== undefined) {
const multiplier = sortMultiplier(sortState.sortDirection);
switch (sortState.sortBy) {
case 'alert-message':
results.sort((a, b) =>
a.message.text === undefined ? 0 :
b.message.text === undefined ? 0 :
multiplier * (a.message.text?.localeCompare(b.message.text)));
break;
default:
assertNever(sortState.sortBy);
}
}
}
export class InterfaceManager extends DisposableObject {
private _displayedEvaluationInfo?: EvaluationInfo;
private _displayedQuery?: CompletedQuery;
private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false;
private _panelLoadedCallBacks: (() => void)[] = [];
private readonly _diagnosticCollection = languages.createDiagnosticCollection(`codeql-query-results`);
constructor(public ctx: vscode.ExtensionContext, private databaseManager: DatabaseManager,
public cliServer: CodeQLCliServer, public logger: Logger) {
private readonly _diagnosticCollection = languages.createDiagnosticCollection(
`codeql-query-results`
);
constructor(
public ctx: vscode.ExtensionContext,
private databaseManager: DatabaseManager,
public cliServer: CodeQLCliServer,
public logger: Logger
) {
super();
this.push(this._diagnosticCollection);
this.push(vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange.bind(this)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.nextPathStep', this.navigatePathStep.bind(this, 1)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.previousPathStep', this.navigatePathStep.bind(this, -1)));
this.push(
vscode.window.onDidChangeTextEditorSelection(
this.handleSelectionChange.bind(this)
)
);
this.push(
vscode.commands.registerCommand(
"codeQLQueryResults.nextPathStep",
this.navigatePathStep.bind(this, 1)
)
);
this.push(
vscode.commands.registerCommand(
"codeQLQueryResults.previousPathStep",
this.navigatePathStep.bind(this, -1)
)
);
}
navigatePathStep(direction: number) {
navigatePathStep(direction: number): void {
this.postMessage({ t: "navigatePath", direction });
}
@@ -112,9 +159,9 @@ export class InterfaceManager extends DisposableObject {
getPanel(): vscode.WebviewPanel {
if (this._panel == undefined) {
const { ctx } = this;
const panel = this._panel = Window.createWebviewPanel(
'resultsView', // internal name
'CodeQL Query Results', // user-visible name
const panel = (this._panel = Window.createWebviewPanel(
"resultsView", // internal name
"CodeQL Query Results", // user-visible name
{ viewColumn: vscode.ViewColumn.Beside, preserveFocus: true },
{
enableScripts: true,
@@ -122,50 +169,96 @@ export class InterfaceManager extends DisposableObject {
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.file(tmpDir.name),
vscode.Uri.file(path.join(this.ctx.extensionPath, 'out'))
vscode.Uri.file(path.join(this.ctx.extensionPath, "out"))
]
}
));
this._panel.onDidDispose(
() => {
this._panel = undefined;
},
null,
ctx.subscriptions
);
const scriptPathOnDisk = vscode.Uri.file(
ctx.asAbsolutePath("out/resultsView.js")
);
const stylesheetPathOnDisk = vscode.Uri.file(
ctx.asAbsolutePath("out/resultsView.css")
);
getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
stylesheetPathOnDisk
);
panel.webview.onDidReceiveMessage(
async e => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
);
this._panel.onDidDispose(() => { this._panel = undefined; }, null, ctx.subscriptions);
const scriptPathOnDisk = vscode.Uri
.file(ctx.asAbsolutePath('out/resultsView.js'));
const stylesheetPathOnDisk = vscode.Uri
.file(ctx.asAbsolutePath('out/resultsView.css'));
getHtmlForWebview(panel.webview, scriptPathOnDisk, stylesheetPathOnDisk);
panel.webview.onDidReceiveMessage(async (e) => this.handleMsgFromView(e), undefined, ctx.subscriptions);
}
return this._panel;
}
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
private async changeSortState(
update: (query: CompletedQuery) => Promise<void>
): Promise<void> {
if (this._displayedQuery === undefined) {
showAndLogErrorMessage(
"Failed to sort results since evaluation info was unknown."
);
return;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: "resultsUpdating" });
await update(this._displayedQuery);
await this.showResults(
this._displayedQuery,
WebviewReveal.NotForced,
true
);
}
private async handleMsgFromView(
msg: FromResultsViewMsg
): Promise<void> {
switch (msg.t) {
case 'viewSourceFile': {
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
case "viewSourceFile": {
const databaseItem = this.databaseManager.findDatabaseItem(
Uri.parse(msg.databaseUri)
);
if (databaseItem !== undefined) {
try {
await showLocation(msg.loc, databaseItem);
}
catch (e) {
} catch (e) {
if (e instanceof Error) {
if (e.message.match(/File not found/)) {
vscode.window.showErrorMessage(`Original file of this result is not in the database's source archive.`);
vscode.window.showErrorMessage(
`Original file of this result is not in the database's source archive.`
);
} else {
this.logger.log(
`Unable to handleMsgFromView: ${e.message}`
);
}
else {
this.logger.log(`Unable to handleMsgFromView: ${e.message}`);
}
}
else {
} else {
this.logger.log(`Unable to handleMsgFromView: ${e}`);
}
}
}
break;
}
case 'toggleDiagnostics': {
case "toggleDiagnostics": {
if (msg.visible) {
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
const databaseItem = this.databaseManager.findDatabaseItem(
Uri.parse(msg.databaseUri)
);
if (databaseItem !== undefined) {
await this.showResultsAsDiagnostics(msg.resultsPath, msg.kind, databaseItem);
await this.showResultsAsDiagnostics(
msg.origResultsPaths,
msg.metadata,
databaseItem
);
}
} else {
// TODO: Only clear diagnostics on the same database.
@@ -178,17 +271,20 @@ export class InterfaceManager extends DisposableObject {
this._panelLoadedCallBacks.forEach(cb => cb());
this._panelLoadedCallBacks = [];
break;
case 'changeSort': {
if (this._displayedEvaluationInfo === undefined) {
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
break;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await this._displayedEvaluationInfo.query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState);
await this.showResults(this._displayedEvaluationInfo, WebviewReveal.NotForced, true);
case "changeSort":
await this.changeSortState(query =>
query.updateSortState(
this.cliServer,
msg.resultSetName,
msg.sortState
)
);
break;
case "changeInterpretedSort":
await this.changeSortState(query =>
query.updateInterpretedSortState(this.cliServer, msg.sortState)
);
break;
}
default:
assertNever(msg);
}
@@ -199,51 +295,63 @@ export class InterfaceManager extends DisposableObject {
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve, _reject) => {
return new Promise(resolve => {
if (this._panelLoaded) {
resolve();
} else {
this._panelLoadedCallBacks.push(resolve)
this._panelLoadedCallBacks.push(resolve);
}
})
});
}
/**
* Show query results in webview panel.
* @param info Evaluation info for the executed query.
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
* @param forceReveal Force the webview panel to be visible and
* Appropriate when the user has just performed an explicit
* UI interaction requesting results, e.g. clicking on a query
* history entry.
*/
public async showResults(info: EvaluationInfo, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
if (info.result.resultType !== messages.QueryResultType.SUCCESS) {
* Show query results in webview panel.
* @param results Evaluation info for the executed query.
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
* @param forceReveal Force the webview panel to be visible and
* Appropriate when the user has just performed an explicit
* UI interaction requesting results, e.g. clicking on a query
* history entry.
*/
public async showResults(
results: CompletedQuery,
forceReveal: WebviewReveal,
shouldKeepOldResultsWhileRendering = false
): Promise<void> {
if (results.result.resultType !== messages.QueryResultType.SUCCESS) {
return;
}
const interpretation = await this.interpretResultsInfo(info.query, info.query.resultsInfo);
const interpretation = await this.interpretResultsInfo(
results.query,
results.interpretedResultsSortState
);
const sortedResultsMap: SortedResultsMap = {};
info.query.sortedResultsInfo.forEach((v, k) =>
sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v));
results.sortedResultsInfo.forEach(
(v, k) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
v
))
);
this._displayedEvaluationInfo = info;
this._displayedQuery = results;
const panel = this.getPanel();
await this.waitForPanelLoaded();
if (forceReveal === WebviewReveal.Forced) {
panel.reveal(undefined, true);
}
else if (!panel.visible) {
} else if (!panel.visible) {
// The results panel exists, (`.getPanel()` guarantees it) but
// is not visible; it's in a not-currently-viewed tab. Show a
// more asynchronous message to not so abruptly interrupt
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const queryName = helpers.getQueryName(info);
const showButton = "View Results";
const queryName = results.queryName;
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${(queryName.length > 0) ? `${queryName}` : ''}.`,
`Finished running query ${
queryName.length > 0 ? `${queryName}` : ""
}.`,
showButton
);
// Address this click asynchronously so we still update the
@@ -256,135 +364,219 @@ export class InterfaceManager extends DisposableObject {
}
await this.postMessage({
t: 'setState',
t: "setState",
interpretation,
resultsPath: this.convertPathToWebviewUri(info.query.resultsInfo.resultsPath),
origResultsPaths: results.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath
),
sortedResultsMap,
database: info.database,
database: results.database,
shouldKeepOldResultsWhileRendering,
kind: info.query.metadata ? info.query.metadata.kind : undefined
metadata: results.query.metadata
});
}
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsInfo): Promise<Interpretation | undefined> {
private async getTruncatedResults(
metadata: QueryMetadata | undefined,
resultsPaths: ResultsPaths,
sourceInfo: cli.SourceInfo | undefined,
sourceLocationPrefix: string,
sortState: InterpretedResultsSortState | undefined
): Promise<Interpretation> {
const sarif = await interpretResults(
this.cliServer,
metadata,
resultsPaths.resultsPath,
sourceInfo
);
// For performance reasons, limit the number of results we try
// to serialize and send to the webview. TODO: possibly also
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach(run => {
if (run.results !== undefined) {
sortInterpretedResults(run.results, sortState);
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults +=
run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(
0,
INTERPRETED_RESULTS_PER_RUN_LIMIT
);
}
}
});
return {
sarif,
sourceLocationPrefix,
numTruncatedResults,
sortState
};
}
private async interpretResultsInfo(
query: QueryInfo,
sortState: InterpretedResultsSortState | undefined
): Promise<Interpretation | undefined> {
let interpretation: Interpretation | undefined = undefined;
if (query.hasInterpretedResults()
&& query.quickEvalPosition === undefined // never do results interpretation if quickEval
if (
(await query.hasInterpretedResults()) &&
query.quickEvalPosition === undefined // never do results interpretation if quickEval
) {
try {
const sourceLocationPrefix = await query.dbItem.getSourceLocationPrefix(this.cliServer);
const sourceLocationPrefix = await query.dbItem.getSourceLocationPrefix(
this.cliServer
);
const sourceArchiveUri = query.dbItem.sourceArchive;
const sourceInfo = sourceArchiveUri === undefined ?
undefined :
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
const sarif = await interpretResults(this.cliServer, query, resultsInfo, sourceInfo);
// For performance reasons, limit the number of results we try
// to serialize and send to the webview. TODO: possibly also
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach(run => {
if (run.results !== undefined) {
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
}
}
});
interpretation = { sarif, sourceLocationPrefix, numTruncatedResults };
}
catch (e) {
const sourceInfo =
sourceArchiveUri === undefined
? undefined
: {
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix
};
interpretation = await this.getTruncatedResults(
query.metadata,
query.resultsPaths,
sourceInfo,
sourceLocationPrefix,
sortState
);
} catch (e) {
// If interpretation fails, accept the error and continue
// trying to render uninterpreted results anyway.
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
this.logger.log(
`Exception during results interpretation: ${e.message}. Will show raw results instead.`
);
}
}
return interpretation;
}
private async showResultsAsDiagnostics(resultsPath: string, kind: string | undefined,
database: DatabaseItem) {
private async showResultsAsDiagnostics(
resultsInfo: ResultsPaths,
metadata: QueryMetadata | undefined,
database: DatabaseItem
): Promise<void> {
const sourceLocationPrefix = await database.getSourceLocationPrefix(
this.cliServer
);
const sourceArchiveUri = database.sourceArchive;
const sourceInfo =
sourceArchiveUri === undefined
? undefined
: {
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix
};
const interpretation = await this.getTruncatedResults(
metadata,
resultsInfo,
sourceInfo,
sourceLocationPrefix,
undefined
);
// URIs from the webview have the vscode-resource scheme, so convert into a filesystem URI first.
const resultsPathOnDisk = webviewUriToFileUri(resultsPath).fsPath;
const fileReader = await FileReader.open(resultsPathOnDisk);
try {
const resultSets = await bqrs.open(fileReader);
try {
switch (kind || 'problem') {
case 'problem': {
const customResults = bqrs.createCustomResultSets<ProblemQueryResults>(resultSets, ProblemQueryResults);
await this.showProblemResultsAsDiagnostics(customResults, database);
}
break;
case 'path-problem': {
const customResults = bqrs.createCustomResultSets<PathProblemQueryResults>(resultSets, PathProblemQueryResults);
await this.showProblemResultsAsDiagnostics(customResults, database);
}
break;
default:
throw new Error(`Unrecognized query kind '${kind}'.`);
}
}
catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
this._diagnosticCollection.clear();
}
}
finally {
fileReader.dispose();
await this.showProblemResultsAsDiagnostics(
interpretation,
database
);
} catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(
`Exception while computing problem results as diagnostics: ${msg}`
);
this._diagnosticCollection.clear();
}
}
private async showProblemResultsAsDiagnostics(results: CustomResultSets<ProblemQueryResults>,
databaseItem: DatabaseItem): Promise<void> {
private async showProblemResultsAsDiagnostics(
interpretation: Interpretation,
databaseItem: DatabaseItem
): Promise<void> {
const { sarif, sourceLocationPrefix } = interpretation;
if (!sarif.runs || !sarif.runs[0].results) {
this.logger.log(
"Didn't find a run in the sarif results. Error processing sarif?"
);
return;
}
const diagnostics: [Uri, ReadonlyArray<Diagnostic>][] = [];
for await (const problemRow of results.problems.readTuples()) {
const codeLocation = resolveLocation(problemRow.element.location, databaseItem);
let message: string;
const references = problemRow.references;
if (references) {
let referenceIndex = 0;
message = problemRow.message.replace(/\$\@/g, sub => {
if (referenceIndex < references.length) {
const replacement = references[referenceIndex].text;
referenceIndex++;
return replacement;
}
else {
return sub;
}
});
for (const result of sarif.runs[0].results) {
const message = result.message.text;
if (message === undefined) {
this.logger.log("Sarif had result without plaintext message");
continue;
}
else {
message = problemRow.message;
if (!result.locations) {
this.logger.log("Sarif had result without location");
continue;
}
const diagnostic = new Diagnostic(codeLocation.range, message, DiagnosticSeverity.Warning);
if (problemRow.references) {
const relatedInformation: DiagnosticRelatedInformation[] = [];
for (const reference of problemRow.references) {
const referenceLocation = tryResolveLocation(reference.element.location, databaseItem);
const sarifLoc = parseSarifLocation(
result.locations[0],
sourceLocationPrefix
);
if (sarifLoc.t == "NoLocation") {
continue;
}
const resultLocation = tryResolveLocation(sarifLoc, databaseItem);
if (!resultLocation) {
this.logger.log("Sarif location was not resolvable " + sarifLoc);
continue;
}
const parsedMessage = parseSarifPlainTextMessage(message);
const relatedInformation: DiagnosticRelatedInformation[] = [];
const relatedLocationsById: { [k: number]: Sarif.Location } = {};
for (const loc of result.relatedLocations || []) {
relatedLocationsById[loc.id!] = loc;
}
const resultMessageChunks: string[] = [];
for (const section of parsedMessage) {
if (typeof section === "string") {
resultMessageChunks.push(section);
} else {
resultMessageChunks.push(section.text);
const sarifChunkLoc = parseSarifLocation(
relatedLocationsById[section.dest],
sourceLocationPrefix
);
if (sarifChunkLoc.t == "NoLocation") {
continue;
}
const referenceLocation = tryResolveLocation(
sarifChunkLoc,
databaseItem
);
if (referenceLocation) {
const related = new DiagnosticRelatedInformation(referenceLocation,
reference.text);
const related = new DiagnosticRelatedInformation(
referenceLocation,
section.text
);
relatedInformation.push(related);
}
}
diagnostic.relatedInformation = relatedInformation;
}
diagnostics.push([
codeLocation.uri,
[diagnostic]
]);
}
const diagnostic = new Diagnostic(
resultLocation.range,
resultMessageChunks.join(""),
DiagnosticSeverity.Warning
);
diagnostic.relatedInformation = relatedInformation;
diagnostics.push([resultLocation.uri, [diagnostic]]);
}
this._diagnosticCollection.set(diagnostics);
}
@@ -392,18 +584,22 @@ export class InterfaceManager extends DisposableObject {
return fileUriToWebviewUri(this.getPanel(), Uri.file(path));
}
private convertPathPropertiesToWebviewUris(info: SortedResultSetInfo): SortedResultSetInfo {
private convertPathPropertiesToWebviewUris(
info: SortedResultSetInfo
): SortedResultSetInfo {
return {
resultsPath: this.convertPathToWebviewUri(info.resultsPath),
sortState: info.sortState
};
}
private handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent) {
private handleSelectionChange(
event: vscode.TextEditorSelectionChangeEvent
): void {
if (event.kind === vscode.TextEditorSelectionChangeKind.Command) {
return; // Ignore selection events we caused ourselves.
}
let editor = vscode.window.activeTextEditor;
const editor = vscode.window.activeTextEditor;
if (editor !== undefined) {
editor.setDecorations(shownLocationDecoration, []);
editor.setDecorations(shownLocationLineDecoration, []);
@@ -429,9 +625,9 @@ async function showLocation(loc: ResolvableLocationValue, databaseItem: Database
const doc = await workspace.openTextDocument(resolvedLocation.uri);
const editorsWithDoc = Window.visibleTextEditors.filter(e => e.document === doc);
const editor = editorsWithDoc.length > 0
? editorsWithDoc[0]
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
let range = resolvedLocation.range;
? editorsWithDoc[0]
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
const range = resolvedLocation.range;
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
// trigger based on where we place the cursor/selection, and will compete for the user's attention.
// For reference:
@@ -442,9 +638,9 @@ async function showLocation(loc: ResolvableLocationValue, databaseItem: Database
// For single-line ranges, select the whole range, mainly to disable bracket highlighting.
// For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks.
// Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting.
let selectionEnd = (range.start.line === range.end.line)
? range.end
: range.start;
const selectionEnd = (range.start.line === range.end.line)
? range.end
: range.start;
editor.selection = new vscode.Selection(range.start, selectionEnd);
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
editor.setDecorations(shownLocationDecoration, [range]);
@@ -479,22 +675,6 @@ function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: Database
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Resolve the specified CodeQL location to a URI into the source archive.
* @param loc CodeQL location to resolve
* @param databaseItem Database in which to resolve the file location.
*/
function resolveLocation(loc: LocationValue | undefined, databaseItem: DatabaseItem): Location {
const resolvedLocation = tryResolveLocation(loc, databaseItem);
if (resolvedLocation) {
return resolvedLocation;
}
else {
// Return a fake position in the source archive directory itself.
return new Location(databaseItem.resolveSourceFile(undefined), new Position(0, 0));
}
}
/**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`.

View File

@@ -6,13 +6,19 @@ export interface Logger {
log(message: string): void;
/** Writes the given log message, not followed by a newline. */
logWithoutTrailingNewline(message: string): void;
/**
* Reveal this channel in the UI.
*
* @param preserveFocus When `true` the channel will not take focus.
*/
show(preserveFocus?: boolean): void;
}
export type ProgressReporter = Progress<{ message: string }>;
/** A logger that writes messages to an output channel in the Output tab. */
export class OutputChannelLogger extends DisposableObject implements Logger {
outputChannel: OutputChannel;
public readonly outputChannel: OutputChannel;
constructor(title: string) {
super();
@@ -28,6 +34,9 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
this.outputChannel.append(message);
}
show(preserveFocus?: boolean) {
this.outputChannel.show(preserveFocus);
}
}
/** The global logger for the extension. */
@@ -38,3 +47,6 @@ export const queryServerLogger = new OutputChannelLogger('CodeQL Query Server');
/** The logger for messages from the language server. */
export const ideServerLogger = new OutputChannelLogger('CodeQL Language Server');
/** The logger for messages from tests. */
export const testLogger = new OutputChannelLogger('CodeQL Tests');

View File

@@ -1,6 +1,17 @@
/**
* Types for messages exchanged during jsonrpc communication with the
* the CodeQL query server.
*
* This file exists in the queryserver and in the vscode extension, and
* should be kept in sync between them.
*
* A note about the namespaces below, which look like they are
* essentially enums, namely Severity, ResultColumnKind, and
* QueryResultType. By design, for the sake of extensibility, clients
* receiving messages of this protocol are supposed to accept any
* number for any of these types. We commit to the given meaning of
* the numbers listed in constants in the namespaces, and we commit to
* the fact that any unknown QueryResultType value counts as an error.
*/
import * as rpc from 'vscode-jsonrpc';
@@ -294,10 +305,13 @@ export interface CompilationMessage {
/**
* The severity of the message
*/
severity: number;
severity: Severity;
}
export type Severity = number;
/**
* Severity of different messages
* Severity of different messages. This namespace is intentionally not
* an enum, see "for the sake of extensibility" comment above.
*/
export namespace Severity {
/**
@@ -333,7 +347,7 @@ export interface ResultColumn {
* The kind of the column. See `ResultColumnKind`
* for the current possible meanings
*/
kind: number;
kind: ResultColumnKind;
/**
* The name of the column.
* This may be compiler generated for complex select expressions.
@@ -341,8 +355,10 @@ export interface ResultColumn {
name: string;
}
export type ResultColumnKind = number;
/**
* The kind of a result column.
* The kind of a result column. This namespace is intentionally not an enum, see "for the sake of
* extensibility" comment above.
*/
export namespace ResultColumnKind {
/**
@@ -619,6 +635,8 @@ export interface EvaluateQueriesParams {
useSequenceHint: boolean;
}
export type TemplateDefinitions = { [key: string]: TemplateSource; }
/**
* A single query that should be run
*/
@@ -642,7 +660,7 @@ export interface QueryToRun {
/**
* Values to set for each template
*/
templateValues?: { [key: string]: TemplateSource; };
templateValues?: TemplateDefinitions;
/**
* Whether templates without values in the templateValues
* map should be set to the empty set or give an error.
@@ -748,7 +766,7 @@ export interface EvaluationResult {
* The type of the result. See QueryResultType for
* possible meanings. Any other result should be interpreted as an error.
*/
resultType: number;
resultType: QueryResultType;
/**
* The wall clock time it took to evaluate the query.
* The time is from when we initially tried to evaluate the query
@@ -762,8 +780,10 @@ export interface EvaluationResult {
message?: string;
}
export type QueryResultType = number;
/**
* The result of running a query,
* The result of running a query. This namespace is intentionally not
* an enum, see "for the sake of extensibility" comment above.
*/
export namespace QueryResultType {
/**
@@ -818,7 +838,7 @@ export interface RunUpgradeResult {
* The type of the result. See QueryResultType for
* possible meanings. Any other result should be interpreted as an error.
*/
resultType: number;
resultType: QueryResultType;
/**
* The error message if an error occurred
*/

View File

@@ -0,0 +1,59 @@
import { EventEmitter, Event, Uri, WorkspaceFolder, RelativePattern } from 'vscode';
import { MultiFileSystemWatcher } from 'semmle-vscode-utils';
import { CodeQLCliServer, QlpacksInfo } from './cli';
import { Discovery } from './discovery';
export interface QLPack {
name: string;
uri: Uri;
};
/**
* Service to discover all available QL packs in a workspace folder.
*/
export class QLPackDiscovery extends Discovery<QlpacksInfo> {
private readonly _onDidChangeQLPacks = this.push(new EventEmitter<void>());
private readonly watcher = this.push(new MultiFileSystemWatcher());
private _qlPacks: readonly QLPack[] = [];
constructor(private readonly workspaceFolder: WorkspaceFolder,
private readonly cliServer: CodeQLCliServer) {
super();
// Watch for any changes to `qlpack.yml` files in this workspace folder.
// TODO: The CLI server should tell us what paths to watch for.
this.watcher.addWatch(new RelativePattern(this.workspaceFolder, '**/qlpack.yml'));
this.watcher.addWatch(new RelativePattern(this.workspaceFolder, '**/.codeqlmanifest.json'));
this.push(this.watcher.onDidChange(this.handleQLPackFileChanged, this));
this.refresh();
}
public get onDidChangeQLPacks(): Event<void> { return this._onDidChangeQLPacks.event; }
public get qlPacks(): readonly QLPack[] { return this._qlPacks; }
private handleQLPackFileChanged(_uri: Uri): void {
this.refresh();
}
protected discover(): Promise<QlpacksInfo> {
// Only look for QL packs in this workspace folder.
return this.cliServer.resolveQlpacks([this.workspaceFolder.uri.fsPath], []);
}
protected update(results: QlpacksInfo): void {
const qlPacks: QLPack[] = [];
for (const id in results) {
qlPacks.push(...results[id].map(fsPath => {
return {
name: id,
uri: Uri.file(fsPath)
};
}));
}
this._qlPacks = qlPacks;
this._onDidChangeQLPacks.fire();
}
}

View File

@@ -0,0 +1,222 @@
import * as path from 'path';
import { QLPackDiscovery } from './qlpack-discovery';
import { Discovery } from './discovery';
import { EventEmitter, Event, Uri, RelativePattern } from 'vscode';
import { MultiFileSystemWatcher } from 'semmle-vscode-utils';
import { CodeQLCliServer } from './cli';
/**
* A node in the tree of tests. This will be either a `QLTestDirectory` or a `QLTestFile`.
*/
export abstract class QLTestNode {
constructor(private _path: string, private _name: string) {
}
public get path(): string {
return this._path;
}
public get name(): string {
return this._name;
}
public abstract get children(): readonly QLTestNode[];
public abstract finish(): void;
}
/**
* A directory containing one or more QL tests or other test directories.
*/
export class QLTestDirectory extends QLTestNode {
private _children: QLTestNode[] = [];
constructor(_path: string, _name: string) {
super(_path, _name);
}
public get children(): readonly QLTestNode[] {
return this._children;
}
public addChild(child: QLTestNode): void {
this._children.push(child);
}
public createDirectory(relativePath: string): QLTestDirectory {
const dirName = path.dirname(relativePath);
if (dirName === '.') {
return this.createChildDirectory(relativePath);
}
else {
const parent = this.createDirectory(dirName);
return parent.createDirectory(path.basename(relativePath));
}
}
public finish(): void {
this._children.sort((a, b) => a.name.localeCompare(b.name));
for (const child of this._children) {
child.finish();
}
}
private createChildDirectory(name: string): QLTestDirectory {
const existingChild = this._children.find((child) => child.name === name);
if (existingChild !== undefined) {
return <QLTestDirectory>existingChild;
}
else {
const newChild = new QLTestDirectory(path.join(this.path, name), name);
this.addChild(newChild);
return newChild;
}
}
}
/**
* A single QL test. This will be either a `.ql` file or a `.qlref` file.
*/
export class QLTestFile extends QLTestNode {
constructor(_path: string, _name: string) {
super(_path, _name);
}
public get children(): readonly QLTestNode[] {
return [];
}
public finish(): void {
}
}
/**
* The results of discovering QL tests.
*/
interface QLTestDiscoveryResults {
/**
* The root test directory for each QL pack that contains tests.
*/
testDirectories: QLTestDirectory[];
/**
* The list of file system paths to watch. If any of these paths changes, the discovery results
* may be out of date.
*/
watchPaths: string[];
}
/**
* Discovers all QL tests contained in the QL packs in a given workspace folder.
*/
export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
private readonly _onDidChangeTests = this.push(new EventEmitter<void>());
private readonly watcher: MultiFileSystemWatcher = this.push(new MultiFileSystemWatcher());
private _testDirectories: QLTestDirectory[] = [];
constructor(private readonly qlPackDiscovery: QLPackDiscovery,
private readonly cliServer: CodeQLCliServer) {
super();
this.push(this.qlPackDiscovery.onDidChangeQLPacks(this.handleDidChangeQLPacks, this));
this.push(this.watcher.onDidChange(this.handleDidChange, this));
this.refresh();
}
/**
* Event to be fired when the set of discovered tests may have changed.
*/
public get onDidChangeTests(): Event<void> { return this._onDidChangeTests.event; }
/**
* The root test directory for each QL pack that contains tests.
*/
public get testDirectories(): QLTestDirectory[] { return this._testDirectories; }
private handleDidChangeQLPacks(): void {
this.refresh();
}
private handleDidChange(uri: Uri): void {
if (!QLTestDiscovery.ignoreTestPath(uri.fsPath)) {
this.refresh();
}
}
protected async discover(): Promise<QLTestDiscoveryResults> {
const testDirectories: QLTestDirectory[] = [];
const watchPaths: string[] = [];
const qlPacks = this.qlPackDiscovery.qlPacks;
for (const qlPack of qlPacks) {
//HACK: Assume that only QL packs whose name ends with '-tests' contain tests.
if (qlPack.name.endsWith('-tests')) {
watchPaths.push(qlPack.uri.fsPath);
const testPackage = await this.discoverTests(qlPack.uri.fsPath, qlPack.name);
if (testPackage !== undefined) {
testDirectories.push(testPackage);
}
}
}
return {
testDirectories: testDirectories,
watchPaths: watchPaths
};
}
protected update(results: QLTestDiscoveryResults): void {
this._testDirectories = results.testDirectories;
// Watch for changes to any `.ql` or `.qlref` file in any of the QL packs that contain tests.
this.watcher.clear();
results.watchPaths.forEach(watchPath => {
this.watcher.addWatch(new RelativePattern(watchPath, '**/*.{ql,qlref}'));
});
this._onDidChangeTests.fire();
}
/**
* Discover all QL tests in the specified directory and its subdirectories.
* @param fullPath The full path of the test directory.
* @param name The display name to use for the returned `TestDirectory` object.
* @returns A `QLTestDirectory` object describing the contents of the directory, or `undefined` if
* no tests were found.
*/
private async discoverTests(fullPath: string, name: string): Promise<QLTestDirectory | undefined> {
const resolvedTests = (await this.cliServer.resolveTests(fullPath))
.filter((testPath) => !QLTestDiscovery.ignoreTestPath(testPath));
if (resolvedTests.length === 0) {
return undefined;
}
else {
const rootDirectory = new QLTestDirectory(fullPath, name);
for (const testPath of resolvedTests) {
const relativePath = path.normalize(path.relative(fullPath, testPath));
const dirName = path.dirname(relativePath);
const parentDirectory = rootDirectory.createDirectory(dirName);
parentDirectory.addChild(new QLTestFile(testPath, path.basename(testPath)));
}
rootDirectory.finish();
return rootDirectory;
}
}
/**
* Determine if the specified QL test should be ignored based on its filename.
* @param testPath Path to the test file.
*/
private static ignoreTestPath(testPath: string): boolean {
switch (path.extname(testPath).toLowerCase()) {
case '.ql':
case '.qlref':
return path.basename(testPath).startsWith('__');
default:
return false;
}
}
}

View File

@@ -1,9 +1,10 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { ExtensionContext, window as Window } from 'vscode';
import { EvaluationInfo } from './queries';
import * as helpers from './helpers';
import * as messages from './messages';
import { CompletedQuery } from './query-results';
import { QueryHistoryConfig } from './config';
import { QueryWithResults } from './run-queries';
/**
* query-history.ts
* ------------
@@ -19,71 +20,14 @@ export type QueryHistoryItemOptions = {
}
/**
* One item in the user-displayed list of queries that have been run.
* Path to icon to display next to a failed query history item.
*/
export class QueryHistoryItem {
queryName: string;
time: string;
databaseName: string;
info: EvaluationInfo;
constructor(
info: EvaluationInfo,
public config: QueryHistoryConfig,
public options: QueryHistoryItemOptions = info.historyItemOptions,
) {
this.queryName = helpers.getQueryName(info);
this.databaseName = info.database.name;
this.info = info;
this.time = new Date().toLocaleString();
}
get statusString(): string {
switch (this.info.result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OOM:
return `out of memory`;
case messages.QueryResultType.SUCCESS:
return `finished in ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return `failed`;
}
}
interpolate(template: string): string {
const { databaseName, queryName, time, statusString } = this;
const replacements: { [k: string]: string } = {
t: time,
q: queryName,
d: databaseName,
s: statusString,
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
getLabel(): string {
if (this.options.label !== undefined)
return this.options.label;
return this.config.format;
}
toString(): string {
return this.interpolate(this.getLabel());
}
}
const FAILED_QUERY_HISTORY_ITEM_ICON: string = 'media/red-x.svg';
/**
* Tree data provider for the query history view.
*/
class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryItem> {
class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery> {
/**
* XXX: This idiom for how to get a `.fire()`-able event emitter was
@@ -91,21 +35,20 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
* involved and I hope there's something better that can be done
* instead.
*/
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryItem | undefined> = new vscode.EventEmitter<QueryHistoryItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<QueryHistoryItem | undefined> = this._onDidChangeTreeData.event;
private _onDidChangeTreeData: vscode.EventEmitter<CompletedQuery | undefined> = new vscode.EventEmitter<CompletedQuery | undefined>();
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this._onDidChangeTreeData.event;
private history: QueryHistoryItem[] = [];
private history: CompletedQuery[] = [];
/**
* When not undefined, must be reference-equal to an item in `this.databases`.
*/
private current: QueryHistoryItem | undefined;
private current: CompletedQuery | undefined;
constructor() {
this.history = [];
constructor(private ctx: ExtensionContext) {
}
getTreeItem(element: QueryHistoryItem): vscode.TreeItem {
getTreeItem(element: CompletedQuery): vscode.TreeItem {
const it = new vscode.TreeItem(element.toString());
it.command = {
@@ -114,10 +57,14 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
arguments: [element],
};
if (!element.didRunSuccessfully) {
it.iconPath = path.join(this.ctx.extensionPath, FAILED_QUERY_HISTORY_ITEM_ICON);
}
return it;
}
getChildren(element?: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem[]> {
getChildren(element?: CompletedQuery): vscode.ProviderResult<CompletedQuery[]> {
if (element == undefined) {
return this.history;
}
@@ -126,25 +73,25 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
}
}
getParent(_element: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem> {
getParent(_element: CompletedQuery): vscode.ProviderResult<CompletedQuery> {
return null;
}
getCurrent(): QueryHistoryItem | undefined {
getCurrent(): CompletedQuery | undefined {
return this.current;
}
push(item: QueryHistoryItem): void {
push(item: CompletedQuery): void {
this.current = item;
this.history.push(item);
this.refresh();
}
setCurrentItem(item: QueryHistoryItem) {
setCurrentItem(item: CompletedQuery) {
this.current = item;
}
remove(item: QueryHistoryItem) {
remove(item: CompletedQuery) {
if (this.current === item)
this.current = undefined;
const index = this.history.findIndex(i => i === item);
@@ -173,19 +120,19 @@ const DOUBLE_CLICK_TIME = 500;
export class QueryHistoryManager {
treeDataProvider: HistoryTreeDataProvider;
ctx: ExtensionContext;
treeView: vscode.TreeView<QueryHistoryItem>;
selectedCallback: ((item: QueryHistoryItem) => void) | undefined;
lastItemClick: { time: Date, item: QueryHistoryItem } | undefined;
treeView: vscode.TreeView<CompletedQuery>;
selectedCallback: ((item: CompletedQuery) => void) | undefined;
lastItemClick: { time: Date, item: CompletedQuery } | undefined;
async invokeCallbackOn(queryHistoryItem: QueryHistoryItem) {
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
if (this.selectedCallback !== undefined) {
const sc = this.selectedCallback;
await sc(queryHistoryItem);
}
}
async handleOpenQuery(queryHistoryItem: QueryHistoryItem): Promise<void> {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.info.query.program.queryPath));
async handleOpenQuery(queryHistoryItem: CompletedQuery): Promise<void> {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.query.program.queryPath));
const editor = await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
const queryText = queryHistoryItem.options.queryText;
if (queryText !== undefined) {
@@ -195,7 +142,7 @@ export class QueryHistoryManager {
}
}
async handleRemoveHistoryItem(queryHistoryItem: QueryHistoryItem) {
async handleRemoveHistoryItem(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.remove(queryHistoryItem);
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
@@ -204,7 +151,7 @@ export class QueryHistoryManager {
}
}
async handleSetLabel(queryHistoryItem: QueryHistoryItem) {
async handleSetLabel(queryHistoryItem: CompletedQuery) {
const response = await vscode.window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
@@ -221,7 +168,7 @@ export class QueryHistoryManager {
}
}
async handleItemClicked(queryHistoryItem: QueryHistoryItem) {
async handleItemClicked(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.setCurrentItem(queryHistoryItem);
const now = new Date();
@@ -243,11 +190,11 @@ export class QueryHistoryManager {
constructor(
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
selectedCallback?: (item: QueryHistoryItem) => Promise<void>
selectedCallback?: (item: CompletedQuery) => Promise<void>
) {
this.ctx = ctx;
this.selectedCallback = selectedCallback;
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider();
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider(ctx);
this.treeView = Window.createTreeView('codeQLQueryHistory', { treeDataProvider });
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
@@ -269,10 +216,11 @@ export class QueryHistoryManager {
});
}
push(evaluationInfo: EvaluationInfo) {
const item = new QueryHistoryItem(evaluationInfo, this.queryHistoryConfigListener);
addQuery(info: QueryWithResults): CompletedQuery {
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
this.treeDataProvider.push(item);
this.updateTreeViewSelectionIfVisible();
return item;
}
/**

View File

@@ -0,0 +1,147 @@
import { QueryWithResults, tmpDir, QueryInfo } from "./run-queries";
import * as messages from './messages';
import * as helpers from './helpers';
import * as cli from './cli';
import * as sarif from 'sarif';
import * as fs from 'fs-extra';
import * as path from 'path';
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState } from "./interface-types";
import { QueryHistoryConfig } from "./config";
import { QueryHistoryItemOptions } from "./query-history";
export class CompletedQuery implements QueryWithResults {
readonly time: string;
readonly query: QueryInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
options: QueryHistoryItemOptions;
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Map<string, SortedResultSetInfo>;
/**
* How we're currently sorting alerts. This is not mere interface
* state due to truncation; on re-sort, we want to read in the file
* again, sort it, and only ship off a reasonable number of results
* to the webview. Undefined means to use whatever order is in the
* sarif file.
*/
interpretedResultsSortState: InterpretedResultsSortState | undefined;
constructor(
evalaution: QueryWithResults,
public config: QueryHistoryConfig,
) {
this.query = evalaution.query;
this.result = evalaution.result;
this.database = evalaution.database;
this.time = new Date().toLocaleString();
this.sortedResultsInfo = new Map();
this.options = evalaution.options;
}
get databaseName(): string {
return this.database.name;
}
get queryName(): string {
return helpers.getQueryName(this.query);
}
/**
* Holds if this query should produce interpreted results.
*/
canInterpretedResults(): Promise<boolean> {
return this.query.dbItem.hasMetadataFile();
}
get statusString(): string {
switch (this.result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OOM:
return `out of memory`;
case messages.QueryResultType.SUCCESS:
return `finished in ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return this.result.message ? `failed: ${this.result.message}` : 'failed';
}
}
interpolate(template: string): string {
const { databaseName, queryName, time, statusString } = this;
const replacements: { [k: string]: string } = {
t: time,
q: queryName,
d: databaseName,
s: statusString,
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
getLabel(): string {
if (this.options.label !== undefined)
return this.options.label;
return this.config.format;
}
get didRunSuccessfully(): boolean {
return this.result.resultType === messages.QueryResultType.SUCCESS;
}
toString(): string {
return this.interpolate(this.getLabel());
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: RawResultsSortState | undefined): Promise<void> {
if (sortState === undefined) {
this.sortedResultsInfo.delete(resultSetName);
return;
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${this.query.queryID}-${resultSetName}.bqrs`),
sortState
};
await server.sortBqrs(this.query.resultsPaths.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.sortDirection]);
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
}
async updateInterpretedSortState(_server: cli.CodeQLCliServer, sortState: InterpretedResultsSortState | undefined): Promise<void> {
this.interpretedResultsSortState = sortState;
}
}
/**
* Call cli command to interpret results.
*/
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPath: string, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
const interpretedResultsPath = resultsPath + ".interpreted.sarif"
if (await fs.pathExists(interpretedResultsPath)) {
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));
}
if (metadata === undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
let { kind, id } = metadata;
if (kind === undefined) {
throw new Error('Can\'t interpret results without query metadata including kind');
}
if (id === undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we use a dummy id.
id = "dummy-id";
}
return await server.interpretBqrs({ kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
}

View File

@@ -1,5 +1,7 @@
import * as cp from 'child_process';
import { DisposableObject } from 'semmle-vscode-utils';
// Import from the specific module within `semmle-vscode-utils`, rather than via `index.ts`, because
// we avoid taking an accidental runtime dependency on `vscode` this way.
import { DisposableObject } from 'semmle-vscode-utils/out/disposable-object';
import { Disposable } from 'vscode';
import { CancellationToken, createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
import * as cli from './cli';
@@ -56,7 +58,10 @@ export class QueryServerClient extends DisposableObject {
super();
// When the query server configuration changes, restart the query server.
if (config.onDidChangeQueryServerConfiguration !== undefined) {
this.push(config.onDidChangeQueryServerConfiguration(async () => await this.restartQueryServer(), this));
this.push(config.onDidChangeQueryServerConfiguration(async () => {
this.logger.log('Restarting query server due to configuration changes...');
await this.restartQueryServer();
}, this));
}
this.withProgressReporting = withProgressReporting;
this.nextCallback = 0;
@@ -77,12 +82,15 @@ export class QueryServerClient extends DisposableObject {
}
/** Restarts the query server by disposing of the current server process and then starting a new one. */
private async restartQueryServer() {
this.logger.log('Restarting query server due to configuration changes...');
async restartQueryServer() {
this.stopQueryServer();
await this.startQueryServer();
}
async showLog() {
this.logger.show();
}
/** Starts a new query server process, sending progress messages to the status bar. */
async startQueryServer() {
// Use an arrow function to preserve the value of `this`.

View File

@@ -8,10 +8,11 @@ import { CodeQLCliServer } from './cli';
import { DatabaseUI } from './databases-ui';
import * as helpers from './helpers';
import { logger } from './logging';
import { UserCancellationException } from './queries';
import { UserCancellationException } from './run-queries';
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
const QUICK_QUERY_WORKSPACE_FOLDER_NAME = 'Quick Queries';
export function isQuickQueryPath(queryPath: string): boolean {
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
@@ -75,6 +76,17 @@ async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQLCliServer, databaseUI: DatabaseUI) {
try {
const workspaceFolders = workspace.workspaceFolders || [];
const queriesDir = await getQuickQueriesDir(ctx);
function updateQuickQueryDir(index: number, len: number) {
workspace.updateWorkspaceFolders(
index,
len,
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
);
}
// If there is already a quick query open, don't clobber it, just
// show it.
const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME);
@@ -83,19 +95,29 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL
return;
}
const queriesDir = await getQuickQueriesDir(ctx);
// We need this folder in workspace folders so the language server
// knows how to find its qlpack.yml
if (workspace.workspaceFolders === undefined
|| !workspace.workspaceFolders.some(folder => folder.uri.fsPath === queriesDir)) {
workspace.updateWorkspaceFolders(
(workspace.workspaceFolders || []).length,
0,
{ uri: Uri.file(queriesDir), name: "Quick Queries" }
);
// We need to have a multi-root workspace to make quick query work
// at all. Changing the workspace from single-root to multi-root
// causes a restart of the whole extension host environment, so we
// basically can't do anything that survives that restart.
//
// So if we are currently in a single-root workspace (of which the
// only reliable signal appears to be `workspace.workspaceFile`
// being undefined) just let the user know that they're in for a
// restart.
if (workspace.workspaceFile === undefined) {
const makeMultiRoot = await helpers.showBinaryChoiceDialog('Quick query requires multiple folders in the workspace. Reload workspace as multi-folder workspace?');
if (makeMultiRoot) {
updateQuickQueryDir(workspaceFolders.length, 0);
}
return;
}
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME)
if (index === -1)
updateQuickQueryDir(workspaceFolders.length, 0);
else
updateQuickQueryDir(index, 1);
// We're going to infer which qlpack to use from the current database
const dbItem = await databaseUI.getDatabaseItem();
if (dbItem === undefined) {

View File

@@ -1,22 +1,24 @@
import * as crypto from 'crypto';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as sarif from 'sarif';
import * as tmp from 'tmp';
import { promisify } from 'util';
import * as vscode from 'vscode';
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
import * as cli from './cli';
import { DatabaseItem, getUpgradesDirectories } from './databases';
import * as helpers from './helpers';
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo } from './interface-types';
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './interface-types';
import { logger } from './logging';
import * as messages from './messages';
import * as qsClient from './queryserver-client';
import { promisify } from 'util';
import { QueryHistoryItemOptions } from './query-history';
import * as qsClient from './queryserver-client';
import { isQuickQueryPath } from './quick-query';
import { upgradeDatabase } from './upgrades';
/**
* queries.ts
* run-queries.ts
* -------------
*
* Compiling and running QL queries.
@@ -24,7 +26,7 @@ import { isQuickQueryPath } from './quick-query';
// XXX: Tmp directory should be configuarble.
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
export const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
export const tmpDirDisposal = {
dispose: () => {
upgradesTmpDir.removeCallback();
@@ -32,7 +34,6 @@ export const tmpDirDisposal = {
}
};
export class UserCancellationException extends Error { }
/**
@@ -42,30 +43,27 @@ export class UserCancellationException extends Error { }
* output and results.
*/
export class QueryInfo {
compiledQueryPath: string;
resultsInfo: ResultsInfo;
private static nextQueryId = 0;
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Map<string, SortedResultSetInfo>;
dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
queryId: number;
readonly compiledQueryPath: string;
readonly resultsPaths: ResultsPaths;
readonly dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
readonly queryID: number;
constructor(
public program: messages.QlProgram,
public dbItem: DatabaseItem,
public queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
public quickEvalPosition?: messages.Position,
public metadata?: cli.QueryMetadata,
public readonly program: messages.QlProgram,
public readonly dbItem: DatabaseItem,
public readonly queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
public readonly quickEvalPosition?: messages.Position,
public readonly metadata?: QueryMetadata,
public readonly templates?: messages.TemplateDefinitions,
) {
this.queryId = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryId}.qlo`);
this.resultsInfo = {
resultsPath: path.join(tmpDir.name, `results${this.queryId}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryId}.sarif`)
this.queryID = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
this.resultsPaths = {
resultsPath: path.join(tmpDir.name, `results${this.queryID}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryID}.sarif`),
};
this.sortedResultsInfo = new Map();
if (dbItem.contents === undefined) {
throw new Error('Can\'t run query on invalid database.');
}
@@ -80,9 +78,10 @@ export class QueryInfo {
const callbackId = qs.registerCallback(res => { result = res });
const queryToRun: messages.QueryToRun = {
resultsPath: this.resultsInfo.resultsPath,
resultsPath: this.resultsPaths.resultsPath,
qlo: vscode.Uri.file(this.compiledQueryPath).toString(),
allowUnknownTemplates: true,
templateValues: this.templates,
id: callbackId,
timeoutSecs: qs.config.timeoutSecs,
}
@@ -108,13 +107,19 @@ export class QueryInfo {
} finally {
qs.unRegisterCallback(callbackId);
}
return result || { evaluationTime: 0, message: "No result from server", queryId: -1, runId: callbackId, resultType: messages.QueryResultType.OTHER_ERROR };
return result || {
evaluationTime: 0,
message: "No result from server",
queryId: -1,
runId: callbackId,
resultType: messages.QueryResultType.OTHER_ERROR
};
}
async compile(
qs: qsClient.QueryServerClient,
): Promise<messages.CompilationMessage[]> {
let compiled: messages.CheckQueryResult;
let compiled: messages.CheckQueryResult | undefined;
try {
const params: messages.CompileQueryParams = {
compilationOptions: {
@@ -145,8 +150,7 @@ export class QueryInfo {
} finally {
qs.logger.log(" - - - COMPILATION DONE - - - ");
}
return (compiled.messages || []).filter(msg => msg.severity == 0);
return (compiled?.messages || []).filter(msg => msg.severity === messages.Severity.ERROR);
}
/**
@@ -159,220 +163,13 @@ export class QueryInfo {
}
return hasMetadataFile;
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
if (sortState === undefined) {
this.sortedResultsInfo.delete(resultSetName);
return;
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${this.queryId}-${resultSetName}.bqrs`),
sortState
};
await server.sortBqrs(this.resultsInfo.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.direction]);
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
}
}
/**
* Call cli command to interpret results.
*/
export async function interpretResults(server: cli.CodeQLCliServer, queryInfo: QueryInfo, resultsInfo: ResultsInfo, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
if (await fs.pathExists(resultsInfo.interpretedResultsPath)) {
return JSON.parse(await fs.readFile(resultsInfo.interpretedResultsPath, 'utf8'));
}
const { metadata } = queryInfo;
if (metadata == undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
let { kind, id } = metadata;
if (kind == undefined) {
throw new Error('Can\'t interpret results without query metadata including kind');
}
if (id == undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we invent one
// based on the query path.
//
// Just to be careful, sanitize to remove '/' since SARIF (section
// 3.27.5 "ruleId property") says that it has special meaning.
id = queryInfo.program.queryPath.replace(/\//g, '-');
}
return await server.interpretBqrs({ kind, id }, resultsInfo.resultsPath, resultsInfo.interpretedResultsPath, sourceInfo);
}
export interface EvaluationInfo {
query: QueryInfo;
result: messages.EvaluationResult;
database: DatabaseInfo;
historyItemOptions: QueryHistoryItemOptions;
}
/**
* Checks whether the given database can be upgraded to the given target DB scheme,
* and whether the user wants to proceed with the upgrade.
* Reports errors to both the user and the console.
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
*/
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.UpgradeParams | undefined> {
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
return;
}
const params: messages.UpgradeParams = {
fromDbscheme: db.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme.fsPath,
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
};
let checkUpgradeResult: messages.CheckUpgradeResult;
try {
qs.logger.log('Checking database upgrade...');
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
return;
}
finally {
qs.logger.log('Done checking database upgrade.');
}
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
if (checkedUpgrades === undefined) {
const error = checkUpgradeResult.upgradeError || '[no error message available]';
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
return;
}
if (checkedUpgrades.scripts.length === 0) {
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
return;
}
let curSha = checkedUpgrades.initialSha;
let descriptionMessage = '';
for (const script of checkedUpgrades.scripts) {
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
curSha = script.newSha;
}
const targetSha = checkedUpgrades.targetSha;
if (curSha != targetSha) {
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
// A modal dialog would be rendered better, but is more intrusive.
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
return;
}
logger.log(descriptionMessage);
// Ask the user to confirm the upgrade.
const shouldUpgrade = await helpers.showBinaryChoiceDialog(`Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${descriptionMessage}`);
if (shouldUpgrade) {
return params;
}
else {
throw new UserCancellationException('User cancelled the database upgrade.');
}
}
/**
* Command handler for 'Upgrade Database'.
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
* First performs a dry-run and prompts the user to confirm the upgrade.
* Reports errors during compilation and evaluation of upgrades to the user.
*/
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.RunUpgradeResult | undefined> {
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
if (upgradeParams === undefined) {
return;
}
let compileUpgradeResult: messages.CompileUpgradeResult;
try {
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
}
catch (e) {
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
const error = compileUpgradeResult.error || '[no error message available]';
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
return;
}
try {
qs.logger.log('Running the following database upgrade:');
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
}
finally {
qs.logger.log('Done running database upgrade.')
}
}
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CheckUpgradeResult> {
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Checking for database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
}
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CompileUpgradeResult> {
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
}
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
}
const database: messages.Dataset = {
dbDir: db.contents.datasetUri.fsPath,
workingSet: 'default'
};
const params: messages.RunUpgradeParams = {
db: database,
timeoutSecs: qs.config.timeoutSecs,
toRun: upgrades
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
export interface QueryWithResults {
readonly query: QueryInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
readonly options: QueryHistoryItemOptions;
}
export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbItem: DatabaseItem):
@@ -537,7 +334,11 @@ async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefine
if (queryUri.scheme !== 'file') {
throw new Error('Can only run queries that are on disk.');
}
const queryPath = queryUri.fsPath;
const queryPath = queryUri.fsPath || '';
if (!queryPath.endsWith('.ql')) {
throw new Error('The selected resource is not a CodeQL query file; It should have the extension ".ql".');
}
// Whether we chose the file from the active editor or from a context menu,
// if the same file is open with unsaved changes in the active editor,
@@ -567,8 +368,9 @@ export async function compileAndRunQueryAgainstDatabase(
qs: qsClient.QueryServerClient,
db: DatabaseItem,
quickEval: boolean,
selectedQueryUri: vscode.Uri | undefined
): Promise<EvaluationInfo> {
selectedQueryUri: vscode.Uri | undefined,
templates?: messages.TemplateDefinitions,
): Promise<QueryWithResults> {
if (!db.contents || !db.contents.dbSchemeUri) {
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
@@ -612,7 +414,7 @@ export async function compileAndRunQueryAgainstDatabase(
};
// Read the query metadata if possible, to use in the UI.
let metadata: cli.QueryMetadata | undefined;
let metadata: QueryMetadata | undefined;
try {
metadata = await cliServer.resolveMetadata(qlProgram.queryPath);
} catch (e) {
@@ -620,13 +422,27 @@ export async function compileAndRunQueryAgainstDatabase(
logger.log(`Couldn't resolve metadata for ${qlProgram.queryPath}: ${e}`);
}
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata);
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
await checkDbschemeCompatibility(cliServer, qs, query);
const errors = await query.compile(qs);
let errors;
try {
errors = await query.compile(qs);
} catch (e) {
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
} else {
throw e;
}
}
if (errors.length == 0) {
const result = await query.run(qs);
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const message = result.message || 'Failed to run query';
logger.log(message);
helpers.showAndLogErrorMessage(message);
}
return {
query,
result,
@@ -634,7 +450,7 @@ export async function compileAndRunQueryAgainstDatabase(
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
historyItemOptions
options: historyItemOptions
};
} else {
// Error dialogs are limited in size and scrollability,
@@ -660,20 +476,31 @@ export async function compileAndRunQueryAgainstDatabase(
" and choose CodeQL Query Server from the dropdown.");
}
return {
query,
result: {
evaluationTime: 0,
resultType: messages.QueryResultType.OTHER_ERROR,
queryId: -1,
runId: -1,
message: "Query had compilation errors"
},
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
historyItemOptions,
};
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
}
}
function createSyntheticResult(
query: QueryInfo,
db: DatabaseItem,
historyItemOptions: QueryHistoryItemOptions,
message: string,
resultType: number
) {
return {
query,
result: {
evaluationTime: 0,
resultType: resultType,
queryId: -1,
runId: -1,
message
},
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
options: historyItemOptions,
};
}

View File

@@ -0,0 +1,124 @@
import * as Sarif from "sarif"
import * as path from "path"
import { LocationStyle, ResolvableLocationValue } from "semmle-bqrs";
export interface SarifLink {
dest: number
text: string
}
type ParsedSarifLocation =
| ResolvableLocationValue
// Resolvable locations have a `file` field, but it will sometimes include
// a source location prefix, which contains build-specific information the user
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
& { userVisibleFile: string }
| { t: 'NoLocation', hint: string };
export type SarifMessageComponent = string | SarifLink
/**
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
export function unescapeSarifText(message: string): string {
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
}
export function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
let results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
// Technically we could have any uri in the target but we don't output that yet.
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
let result: RegExpExecArray | null;
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
return results;
}
/**
* Computes a path normalized to reflect conventional normalization
* of windows paths into zip archive paths.
* @param sourceLocationPrefix The source location prefix of a database. May be
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
* directory separators are normalized, but drive letters `C:` may appear.
*/
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
}
export function parseSarifLocation(loc: Sarif.Location, sourceLocationPrefix: string): ParsedSarifLocation {
const physicalLocation = loc.physicalLocation;
if (physicalLocation === undefined)
return { t: 'NoLocation', hint: 'no physical location' };
if (physicalLocation.artifactLocation === undefined)
return { t: 'NoLocation', hint: 'no artifact location' };
if (physicalLocation.artifactLocation.uri === undefined)
return { t: 'NoLocation', hint: 'artifact location has no uri' };
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
const userVisibleFile = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
uri;
if (physicalLocation.region === undefined) {
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
// TODO: Do we get here if we provide a non-filesystem URL?
return {
t: LocationStyle.WholeFile,
file: effectiveLocation,
userVisibleFile,
};
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const lineStart = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const colEnd = region.endColumn! - 1;
return {
t: LocationStyle.FivePart,
file: effectiveLocation,
userVisibleFile,
lineStart,
colStart,
lineEnd,
colEnd,
};
}
}

View File

@@ -0,0 +1,239 @@
import * as path from 'path';
import * as vscode from 'vscode';
import {
TestAdapter,
TestLoadStartedEvent,
TestLoadFinishedEvent,
TestRunStartedEvent,
TestRunFinishedEvent,
TestSuiteEvent,
TestEvent,
TestSuiteInfo,
TestInfo,
TestHub
} from 'vscode-test-adapter-api';
import { TestAdapterRegistrar } from 'vscode-test-adapter-util';
import { QLTestFile, QLTestNode, QLTestDirectory, QLTestDiscovery } from './qltest-discovery';
import { Event, EventEmitter, CancellationTokenSource, CancellationToken } from 'vscode';
import { DisposableObject } from 'semmle-vscode-utils';
import { QLPackDiscovery } from './qlpack-discovery';
import { CodeQLCliServer } from './cli';
import { getOnDiskWorkspaceFolders } from './helpers';
import { testLogger } from './logging';
/**
* Get the full path of the `.expected` file for the specified QL test.
* @param testPath The full path to the test file.
*/
export function getExpectedFile(testPath: string): string {
return getTestOutputFile(testPath, '.expected');
}
/**
* Get the full path of the `.actual` file for the specified QL test.
* @param testPath The full path to the test file.
*/
export function getActualFile(testPath: string): string {
return getTestOutputFile(testPath, '.actual');
}
/**
* Get the directory containing the specified QL test.
* @param testPath The full path to the test file.
*/
export function getTestDirectory(testPath: string): string {
return path.dirname(testPath);
}
/**
* Gets the the full path to a particular output file of the specified QL test.
* @param testPath The full path to the QL test.
* @param extension The file extension of the output file.
*/
function getTestOutputFile(testPath: string, extension: string): string {
return changeExtension(testPath, extension);
}
/**
* A factory service that creates `QLTestAdapter` objects for workspace folders on demand.
*/
export class QLTestAdapterFactory extends DisposableObject {
constructor(testHub: TestHub, cliServer: CodeQLCliServer) {
super();
// this will register a QLTestAdapter for each WorkspaceFolder
this.push(new TestAdapterRegistrar(
testHub,
workspaceFolder => new QLTestAdapter(workspaceFolder, cliServer)
));
}
}
/**
* Change the file extension of the specified path.
* @param p The original file path.
* @param ext The new extension, including the `.`.
*/
function changeExtension(p: string, ext: string): string {
return p.substr(0, p.length - path.extname(p).length) + ext;
}
/**
* Test adapter for QL tests.
*/
export class QLTestAdapter extends DisposableObject implements TestAdapter {
private readonly qlPackDiscovery: QLPackDiscovery;
private readonly qlTestDiscovery: QLTestDiscovery;
private readonly _tests = this.push(
new EventEmitter<TestLoadStartedEvent | TestLoadFinishedEvent>());
private readonly _testStates = this.push(
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
TestEvent>());
private readonly _autorun = this.push(new EventEmitter<void>());
private runningTask?: vscode.CancellationTokenSource = undefined;
constructor(
public readonly workspaceFolder: vscode.WorkspaceFolder,
private readonly cliServer: CodeQLCliServer
) {
super();
this.qlPackDiscovery = this.push(new QLPackDiscovery(workspaceFolder, cliServer));
this.qlTestDiscovery = this.push(new QLTestDiscovery(this.qlPackDiscovery, cliServer));
this.push(this.qlTestDiscovery.onDidChangeTests(this.discoverTests, this));
}
public get tests(): Event<TestLoadStartedEvent | TestLoadFinishedEvent> {
return this._tests.event;
}
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
TestEvent> {
return this._testStates.event;
}
public get autorun(): Event<void> | undefined {
return this._autorun.event;
}
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]):
(TestSuiteInfo | TestInfo)[] {
return testNodes.map((childNode) => {
return QLTestAdapter.createTestOrSuiteInfo(childNode);
});
}
private static createTestOrSuiteInfo(testNode: QLTestNode): TestSuiteInfo | TestInfo {
if (testNode instanceof QLTestFile) {
return QLTestAdapter.createTestInfo(testNode);
}
else if (testNode instanceof QLTestDirectory) {
return QLTestAdapter.createTestSuiteInfo(testNode, testNode.name);
}
else {
throw new Error('Unexpected test type.');
}
}
private static createTestInfo(testFile: QLTestFile): TestInfo {
return {
type: 'test',
id: testFile.path,
label: testFile.name,
tooltip: testFile.path,
file: testFile.path
};
}
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string):
TestSuiteInfo {
return {
type: 'suite',
id: testDirectory.path,
label: label,
children: QLTestAdapter.createTestOrSuiteInfos(testDirectory.children),
tooltip: testDirectory.path
};
}
public async load(): Promise<void> {
this.discoverTests();
}
private discoverTests(): void {
this._tests.fire(<TestLoadStartedEvent>{ type: 'started' });
const testDirectories = this.qlTestDiscovery.testDirectories;
const children = testDirectories.map(
testDirectory => QLTestAdapter.createTestSuiteInfo(testDirectory, testDirectory.name)
);
const testSuite: TestSuiteInfo = {
type: 'suite',
label: 'CodeQL',
id: '.',
children
};
this._tests.fire(<TestLoadFinishedEvent>{
type: 'finished',
suite: children.length > 0 ? testSuite : undefined
});
}
public async run(tests: string[]): Promise<void> {
if (this.runningTask !== undefined) {
throw new Error('Tests already running.');
}
testLogger.outputChannel.clear();
testLogger.outputChannel.show(true);
this.runningTask = this.track(new CancellationTokenSource());
this._testStates.fire(<TestRunStartedEvent>{ type: 'started', tests: tests });
const testAdapter = this;
try {
await this.runTests(tests, this.runningTask.token);
}
catch (e) {
}
testAdapter._testStates.fire(<TestRunFinishedEvent>{ type: 'finished' });
testAdapter.clearTask();
}
private clearTask(): void {
if (this.runningTask !== undefined) {
const runningTask = this.runningTask;
this.runningTask = undefined;
this.disposeAndStopTracking(runningTask);
}
}
public cancel(): void {
if (this.runningTask !== undefined) {
testLogger.log('Cancelling test run...');
this.runningTask.cancel();
this.clearTask();
}
}
private async runTests(tests: string[], cancellationToken: CancellationToken): Promise<void> {
const workspacePaths = await getOnDiskWorkspaceFolders();
for await (const event of await this.cliServer.runTests(tests, workspacePaths, {
cancellationToken: cancellationToken,
logger: testLogger
})) {
this._testStates.fire({
type: 'test',
state: event.pass ? 'passed' : 'failed',
test: event.test
});
}
}
}

View File

@@ -0,0 +1,9 @@
import { TestSuiteInfo, TestInfo } from 'vscode-test-adapter-api';
/**
* Tree view node for a test, suite, or collection. This object is passed as the argument to the
* command handler of a context menu item for a tree view item.
*/
export interface TestTreeNode {
readonly info: TestSuiteInfo | TestInfo;
}

View File

@@ -0,0 +1,85 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { Uri, TextDocumentShowOptions, commands, window } from 'vscode';
import { TestTreeNode } from './test-tree-node';
import { DisposableObject, UIService } from 'semmle-vscode-utils';
import { TestHub, TestController, TestAdapter, TestRunStartedEvent, TestRunFinishedEvent, TestEvent, TestSuiteEvent } from 'vscode-test-adapter-api';
import { QLTestAdapter, getExpectedFile, getActualFile } from './test-adapter';
/**
* Test event listener. Currently unused, but left in to keep the plumbing hooked up for future use.
*/
class QLTestListener extends DisposableObject {
constructor(adapter: TestAdapter) {
super();
this.push(adapter.testStates(this.onTestStatesEvent, this));
}
private onTestStatesEvent(_e: TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent): void {
}
}
/**
* Service that implements all UI and commands for QL tests.
*/
export class TestUIService extends UIService implements TestController {
private readonly listeners: Map<TestAdapter, QLTestListener> = new Map();
constructor(private readonly testHub: TestHub) {
super();
this.registerCommand('codeQLTests.showOutputDifferences', this.showOutputDifferences);
this.registerCommand('codeQLTests.acceptOutput', this.acceptOutput);
testHub.registerTestController(this);
}
public dispose(): void {
this.testHub.unregisterTestController(this);
super.dispose();
}
public registerTestAdapter(adapter: TestAdapter): void {
this.listeners.set(adapter, new QLTestListener(adapter));
}
public unregisterTestAdapter(adapter: TestAdapter): void {
if (adapter instanceof QLTestAdapter) {
this.listeners.delete(adapter);
}
}
private async acceptOutput(node: TestTreeNode): Promise<void> {
const testId = node.info.id;
const stat = await fs.lstat(testId);
if (stat.isFile()) {
const expectedPath = getExpectedFile(testId);
const actualPath = getActualFile(testId);
await fs.copy(actualPath, expectedPath, { overwrite: true });
}
}
private async showOutputDifferences(node: TestTreeNode): Promise<void> {
const testId = node.info.id;
const stat = await fs.lstat(testId);
if (stat.isFile()) {
const expectedPath = getExpectedFile(testId);
const expectedUri = Uri.file(expectedPath);
const actualPath = getActualFile(testId);
const options: TextDocumentShowOptions = {
preserveFocus: true,
preview: true
};
if (await fs.pathExists(actualPath)) {
const actualUri = Uri.file(actualPath);
await commands.executeCommand<void>('vscode.diff', expectedUri, actualUri,
`Expected vs. Actual for ${path.basename(testId)}`, options);
}
else {
await window.showTextDocument(expectedUri, options);
}
}
}
}

View File

@@ -0,0 +1,198 @@
import * as vscode from 'vscode';
import { DatabaseItem } from './databases';
import * as helpers from './helpers';
import { logger } from './logging';
import * as messages from './messages';
import * as qsClient from './queryserver-client';
import { upgradesTmpDir, UserCancellationException } from './run-queries';
/**
* Maximum number of lines to include from database upgrade message,
* to work around the fact that we can't guarantee a scrollable text
* box for it when displaying in dialog boxes.
*/
const MAX_UPGRADE_MESSAGE_LINES = 10;
/**
* Checks whether the given database can be upgraded to the given target DB scheme,
* and whether the user wants to proceed with the upgrade.
* Reports errors to both the user and the console.
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
*/
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.UpgradeParams | undefined> {
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
return;
}
const params: messages.UpgradeParams = {
fromDbscheme: db.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme.fsPath,
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
};
let checkUpgradeResult: messages.CheckUpgradeResult;
try {
qs.logger.log('Checking database upgrade...');
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
return;
}
finally {
qs.logger.log('Done checking database upgrade.');
}
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
if (checkedUpgrades === undefined) {
const error = checkUpgradeResult.upgradeError || '[no error message available]';
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
return;
}
if (checkedUpgrades.scripts.length === 0) {
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
return;
}
let curSha = checkedUpgrades.initialSha;
let descriptionMessage = '';
for (const script of checkedUpgrades.scripts) {
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
curSha = script.newSha;
}
const targetSha = checkedUpgrades.targetSha;
if (curSha != targetSha) {
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
// A modal dialog would be rendered better, but is more intrusive.
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
return;
}
logger.log(descriptionMessage);
// Ask the user to confirm the upgrade.
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
let dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
let messageLines = descriptionMessage.split('\n');
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
messageLines = messageLines.slice(0, MAX_UPGRADE_MESSAGE_LINES);
messageLines.push(`The list of upgrades was truncated, click "No, Show Changes" to see the full list.`);
dialogOptions.push(showLogItem);
}
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join("\n")}`;
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
if (chosenItem === showLogItem) {
logger.outputChannel.show();
}
if (chosenItem === yesItem) {
return params;
}
else {
throw new UserCancellationException('User cancelled the database upgrade.');
}
}
/**
* Command handler for 'Upgrade Database'.
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
* First performs a dry-run and prompts the user to confirm the upgrade.
* Reports errors during compilation and evaluation of upgrades to the user.
*/
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.RunUpgradeResult | undefined> {
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
if (upgradeParams === undefined) {
return;
}
let compileUpgradeResult: messages.CompileUpgradeResult;
try {
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
}
catch (e) {
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
const error = compileUpgradeResult.error || '[no error message available]';
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
return;
}
try {
qs.logger.log('Running the following database upgrade:');
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
}
finally {
qs.logger.log('Done running database upgrade.')
}
}
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CheckUpgradeResult> {
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Checking for database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
}
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CompileUpgradeResult> {
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
}
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
}
const database: messages.Dataset = {
dbDir: db.contents.datasetUri.fsPath,
workingSet: 'default'
};
const params: messages.RunUpgradeParams = {
db: database,
timeoutSecs: qs.config.timeoutSecs,
toRun: upgrades
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
}

View File

@@ -0,0 +1,8 @@
module.exports = {
ecmaFeatures: {
jsx: true,
},
env: {
browser: true
},
}

View File

@@ -2,10 +2,12 @@ import * as path from 'path';
import * as React from 'react';
import * as Sarif from 'sarif';
import * as Keys from '../result-keys';
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
import { LocationStyle } from 'semmle-bqrs';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent } from './results';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results';
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState {
@@ -13,64 +15,6 @@ export interface PathTableState {
selectedPathNode: undefined | Keys.PathNode;
}
interface SarifLink {
dest: number
text: string
}
type ParsedSarifLocation =
| ResolvableLocationValue
// Resolvable locations have a `file` field, but it will sometimes include
// a source location prefix, which contains build-specific information the user
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
& { userVisibleFile: string }
| { t: 'NoLocation', hint: string };
type SarifMessageComponent = string | SarifLink
/**
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
function unescapeSarifText(message: string): string {
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
}
function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
let results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
// Technically we could have any uri in the target but we don't output that yet.
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
let result: RegExpExecArray | null;
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
return results;
}
/**
* Computes a path normalized to reflect conventional normalization
* of windows paths into zip archive paths.
* @param sourceLocationPrefix The source location prefix of a database. May be
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
* directory separators are normalized, but drive letters `C:` may appear.
*/
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
}
export class PathTable extends React.Component<PathTableProps, PathTableState> {
constructor(props: PathTableProps) {
super(props);
@@ -100,9 +44,41 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
e.preventDefault();
}
sortClass(column: InterpretedResultsSortColumn): string {
const sortState = this.props.resultSet.sortState;
if (sortState !== undefined && sortState.sortBy === column) {
return sortState.sortDirection === SortDirection.asc ? 'sort-asc' : 'sort-desc';
}
else {
return 'sort-none';
}
}
getNextSortState(column: InterpretedResultsSortColumn): InterpretedResultsSortState | undefined {
const oldSortState = this.props.resultSet.sortState;
const prevDirection = oldSortState && oldSortState.sortBy === column ? oldSortState.sortDirection : undefined;
const nextDirection = nextSortDirection(prevDirection, true);
return nextDirection === undefined ? undefined :
{ sortBy: column, sortDirection: nextDirection };
}
toggleSortStateForColumn(column: InterpretedResultsSortColumn): void {
vscode.postMessage({
t: 'changeInterpretedSort',
sortState: this.getNextSortState(column),
});
}
render(): JSX.Element {
const { databaseUri, resultSet } = this.props;
const header = <thead>
<tr>
<th colSpan={2}></th>
<th className={this.sortClass('alert-message') + ' vscode-codeql__alert-message-cell'} colSpan={3} onClick={() => this.toggleSortStateForColumn('alert-message')}>Message</th>
</tr>
</thead>;
const rows: JSX.Element[] = [];
const { numTruncatedResults, sourceLocationPrefix } = resultSet;
@@ -122,7 +98,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
result.push(<span>{part} </span>);
} else {
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
undefined);
undefined);
result.push(<span>{renderedLocation} </span>);
}
} return result;
@@ -150,7 +126,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
return renderNonLocation(text, parsedLoc.hint);
case LocationStyle.FivePart:
case LocationStyle.WholeFile:
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
}
return undefined;
}
@@ -288,6 +264,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
}
return <table className={className}>
{header}
<tbody>{rows}</tbody>
</table>;
}
@@ -323,64 +300,3 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
onNavigation.removeListener(this.handleNavigationEvent);
}
}
function parseSarifLocation(loc: Sarif.Location, sourceLocationPrefix: string): ParsedSarifLocation {
const physicalLocation = loc.physicalLocation;
if (physicalLocation === undefined)
return { t: 'NoLocation', hint: 'no physical location' };
if (physicalLocation.artifactLocation === undefined)
return { t: 'NoLocation', hint: 'no artifact location' };
if (physicalLocation.artifactLocation.uri === undefined)
return { t: 'NoLocation', hint: 'artifact location has no uri' };
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
const userVisibleFile = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
uri;
if (physicalLocation.region === undefined) {
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
// TODO: Do we get here if we provide a non-filesystem URL?
return {
t: LocationStyle.WholeFile,
file: effectiveLocation,
userVisibleFile,
};
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const lineStart = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const colEnd = region.endColumn! - 1;
return {
t: LocationStyle.FivePart,
file: effectiveLocation,
userVisibleFile,
lineStart,
colStart,
lineEnd,
colEnd,
};
}
}

View File

@@ -1,12 +1,11 @@
import * as React from "react";
import { renderLocation, ResultTableProps, zebraStripe, className } from "./result-table-utils";
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
import { RawTableResultSet, ResultValue, vscode } from "./results";
import { assertNever } from "../helpers-pure";
import { SortDirection, SortState, RAW_RESULTS_LIMIT } from "../interface-types";
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
export type RawTableProps = ResultTableProps & {
resultSet: RawTableResultSet,
sortState?: SortState;
resultSet: RawTableResultSet;
sortState?: RawResultsSortState;
};
export class RawTable extends React.Component<RawTableProps, {}> {
@@ -55,7 +54,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
<th key={-1}><b>#</b></th>,
...resultSet.schema.columns.map((col, index) => {
const displayName = col.name || `[${index}]`;
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.direction : undefined;
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.sortDirection : undefined;
return <th className={"sort-" + (sortDirection !== undefined ? SortDirection[sortDirection] : "none")} key={index} onClick={() => this.toggleSortStateForColumn(index)}><b>{displayName}</b></th>;
})
]
@@ -68,13 +67,13 @@ export class RawTable extends React.Component<RawTableProps, {}> {
</table>;
}
private toggleSortStateForColumn(index: number) {
private toggleSortStateForColumn(index: number): void {
const sortState = this.props.sortState;
const prevDirection = sortState && sortState.columnIndex === index ? sortState.direction : undefined;
const prevDirection = sortState && sortState.columnIndex === index ? sortState.sortDirection : undefined;
const nextDirection = nextSortDirection(prevDirection);
const nextSortState = nextDirection === undefined ? undefined : {
columnIndex: index,
direction: nextDirection
sortDirection: nextDirection
};
vscode.postMessage({
t: 'changeSort',
@@ -84,7 +83,6 @@ export class RawTable extends React.Component<RawTableProps, {}> {
}
}
/**
* Render one column of a tuple.
*/
@@ -99,15 +97,3 @@ function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element {
return renderLocation(v.location, v.label, databaseUri);
}
}
function nextSortDirection(direction: SortDirection | undefined): SortDirection {
switch (direction) {
case SortDirection.asc:
return SortDirection.desc;
case SortDirection.desc:
case undefined:
return SortDirection.asc;
default:
return assertNever(direction);
}
}

View File

@@ -1,17 +1,20 @@
import * as React from 'react';
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
import { SortState } from '../interface-types';
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
import { ResultSet, vscode } from './results';
import { assertNever } from '../helpers-pure';
export interface ResultTableProps {
resultSet: ResultSet;
databaseUri: string;
metadata?: QueryMetadata;
resultsPath: string | undefined;
sortState?: SortState;
sortState?: RawResultsSortState;
}
export const className = 'vscode-codeql__result-table';
export const tableSelectionHeaderClassName = 'vscode-codeql__table-selection-header';
export const alertExtrasClassName = `${className}-alert-extras`;
export const toggleDiagnosticsClassName = `${className}-toggle-diagnostics`;
export const evenRowClassName = 'vscode-codeql__result-table-row--even';
export const oddRowClassName = 'vscode-codeql__result-table-row--odd';
@@ -31,7 +34,7 @@ export function jumpToLocationHandler(
};
}
export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string) {
export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string): void {
vscode.postMessage({
t: 'viewSourceFile',
loc,
@@ -79,6 +82,23 @@ export function zebraStripe(index: number, ...otherClasses: string[]): { classNa
*/
export function selectableZebraStripe(isSelected: boolean, index: number, ...otherClasses: string[]): { className: string } {
return isSelected
? { className: [selectedRowClassName, ...otherClasses].join(' ') }
: zebraStripe(index, ...otherClasses)
? { className: [selectedRowClassName, ...otherClasses].join(' ') }
: zebraStripe(index, ...otherClasses)
}
/**
* Returns the next sort direction when cycling through sort directions while clicking.
* if `includeUndefined` is true, include `undefined` in the cycle.
*/
export function nextSortDirection(direction: SortDirection | undefined, includeUndefined?: boolean): SortDirection | undefined {
switch (direction) {
case SortDirection.asc:
return SortDirection.desc;
case SortDirection.desc:
return includeUndefined ? undefined : SortDirection.asc;
case undefined:
return SortDirection.asc;
default:
return assertNever(direction);
}
}

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import { DatabaseInfo, Interpretation, SortState } from '../interface-types';
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState } from '../interface-types';
import { PathTable } from './alert-table';
import { RawTable } from './raw-results-table';
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName } from './result-table-utils';
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils';
import { ResultSet, vscode } from './results';
/**
@@ -12,9 +12,11 @@ export interface ResultTablesProps {
rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined;
database: DatabaseInfo;
resultsPath: string | undefined;
kind: string | undefined;
sortStates: Map<string, SortState>;
metadata?: QueryMetadata;
resultsPath: string;
origResultsPaths: ResultsPaths;
sortStates: Map<string, RawResultsSortState>;
interpretedSortState?: InterpretedResultsSortState;
isLoadingNewResults: boolean;
}
@@ -88,38 +90,44 @@ export class ResultTables
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSets[0].schema.name].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
}
private onChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
this.setState({ selectedTable: event.target.value });
}
render(): React.ReactNode {
const { selectedTable } = this.state;
const resultSets = this.getResultSets();
const { database, resultsPath, kind } = this.props;
private alertTableExtras(): JSX.Element | undefined {
const { database, resultsPath, metadata, origResultsPaths } = this.props;
// Only show the Problems view display checkbox for the alerts table.
const diagnosticsCheckBox = selectedTable === ALERTS_TABLE_NAME ?
const displayProblemsAsAlertsToggle =
<div className={toggleDiagnosticsClassName}>
<input type="checkbox" id="toggle-diagnostics" name="toggle-diagnostics" onChange={(e) => {
if (resultsPath !== undefined) {
vscode.postMessage({
t: 'toggleDiagnostics',
resultsPath: resultsPath,
origResultsPaths: origResultsPaths,
databaseUri: database.databaseUri,
visible: e.target.checked,
kind: kind
metadata: metadata
});
}
}} />
<label htmlFor="toggle-diagnostics">Show results in Problems view</label>
</div> : undefined;
</div>;
return <div className={alertExtrasClassName}>
{displayProblemsAsAlertsToggle}
</div>
}
render(): React.ReactNode {
const { selectedTable } = this.state;
const resultSets = this.getResultSets();
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
const numberOfResults = resultSet && renderResultCountString(resultSet);
return <div>
<div className={tableSelectionHeaderClassName}>
<select value={selectedTable} onChange={this.onChange}>
<select value={selectedTable} onChange={this.onTableSelectionChange}>
{
resultSets.map(resultSet =>
<option key={resultSet.schema.name} value={resultSet.schema.name}>
@@ -129,7 +137,7 @@ export class ResultTables
}
</select>
{numberOfResults}
{diagnosticsCheckBox}
{selectedTable === ALERTS_TABLE_NAME ? this.alertTableExtras() : undefined}
{
this.props.isLoadingNewResults ?
<span className={UPDATING_RESULTS_TEXT_CLASS_NAME}>Updating results</span>
@@ -157,11 +165,9 @@ class ResultTable extends React.Component<ResultTableProps, {}> {
const { resultSet } = this.props;
switch (resultSet.t) {
case 'RawResultSet': return <RawTable
resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} sortState={this.props.sortState} />;
{...this.props} resultSet={resultSet} />;
case 'SarifResultSet': return <PathTable
resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} />;
{...this.props} resultSet={resultSet} />;
}
}
}

View File

@@ -3,9 +3,9 @@ import * as Rdom from 'react-dom';
import * as bqrs from 'semmle-bqrs';
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
import { assertNever } from '../helpers-pure';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState, NavigatePathMsg } from '../interface-types';
import { ResultTables } from './result-tables';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types';
import { EventHandlers as EventHandlerList } from './event-handler-list';
import { ResultTables } from './result-tables';
/**
* results.tsx
@@ -24,8 +24,8 @@ declare const acquireVsCodeApi: () => VsCodeApi;
export const vscode = acquireVsCodeApi();
export interface ResultElement {
label: string,
location?: LocationValue
label: string;
location?: LocationValue;
}
export interface ResultUri {
@@ -37,7 +37,7 @@ export type ResultValue = ResultElement | ResultUri | string;
export type ResultRow = ResultValue[];
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
export type PathTableResultSet = { t: 'SarifResultSet', readonly schema: ResultSetSchema, name: string } & Interpretation;
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
export type ResultSet =
| RawTableResultSet
@@ -58,7 +58,7 @@ async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint
if (done) {
return;
}
yield value;
yield value!;
}
}
@@ -127,7 +127,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
interface ResultsInfo {
resultsPath: string;
kind: string | undefined;
origResultsPaths: ResultsPaths;
database: DatabaseInfo;
interpretation: Interpretation | undefined;
sortedResultsMap: Map<string, SortedResultSetInfo>;
@@ -135,11 +135,12 @@ interface ResultsInfo {
* See {@link SetStateMsg.shouldKeepOldResultsWhileRendering}.
*/
shouldKeepOldResultsWhileRendering: boolean;
metadata?: QueryMetadata;
}
interface Results {
resultSets: readonly ResultSet[];
sortStates: Map<string, SortState>;
sortStates: Map<string, RawResultsSortState>;
database: DatabaseInfo;
}
@@ -186,11 +187,12 @@ class App extends React.Component<{}, ResultsViewState> {
case 'setState':
this.updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath,
kind: msg.kind,
origResultsPaths: msg.origResultsPaths,
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
database: msg.database,
interpretation: msg.interpretation,
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering,
metadata: msg.metadata
});
this.loadResults();
@@ -210,7 +212,7 @@ class App extends React.Component<{}, ResultsViewState> {
private updateStateWithNewResultsInfo(resultsInfo: ResultsInfo): void {
this.setState(prevState => {
const stateWithDisplayedResults = (displayedResults: ResultsState) => ({
const stateWithDisplayedResults = (displayedResults: ResultsState): ResultsViewState => ({
displayedResults,
isExpectingResultsUpdate: prevState.isExpectingResultsUpdate,
nextResultsInfo: resultsInfo
@@ -243,7 +245,7 @@ class App extends React.Component<{}, ResultsViewState> {
}
let results: Results | null = null;
let statusText: string = '';
let statusText = '';
try {
results = {
resultSets: await this.getResultSets(resultsInfo),
@@ -296,21 +298,23 @@ class App extends React.Component<{}, ResultsViewState> {
}));
}
private getSortStates(resultsInfo: ResultsInfo): Map<string, SortState> {
private getSortStates(resultsInfo: ResultsInfo): Map<string, RawResultsSortState> {
const entries = Array.from(resultsInfo.sortedResultsMap.entries());
return new Map(entries.map(([key, sortedResultSetInfo]) =>
[key, sortedResultSetInfo.sortState]));
}
render() {
render(): JSX.Element {
const displayedResults = this.state.displayedResults;
if (displayedResults.results !== null) {
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
return <ResultTables rawResultSets={displayedResults.results.resultSets}
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
database={displayedResults.results.database}
resultsPath={displayedResults.resultsInfo ? displayedResults.resultsInfo.resultsPath : undefined}
kind={displayedResults.resultsInfo ? displayedResults.resultsInfo.kind : undefined}
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
resultsPath={displayedResults.resultsInfo.resultsPath}
metadata={displayedResults.resultsInfo ? displayedResults.resultsInfo.metadata : undefined}
sortStates={displayedResults.results.sortStates}
interpretedSortState={displayedResults.resultsInfo.interpretation?.sortState}
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
}
else {
@@ -318,12 +322,12 @@ class App extends React.Component<{}, ResultsViewState> {
}
}
componentDidMount() {
componentDidMount(): void {
this.vscodeMessageHandler = evt => this.handleMessage(evt.data as IntoResultsViewMsg);
window.addEventListener('message', this.vscodeMessageHandler);
}
componentWillUnmount() {
componentWillUnmount(): void {
if (this.vscodeMessageHandler) {
window.removeEventListener('message', this.vscodeMessageHandler);
}
@@ -337,4 +341,4 @@ Rdom.render(
document.getElementById('root')
);
vscode.postMessage({ t: "resultViewLoaded" })
vscode.postMessage({ t: "resultViewLoaded" })

View File

@@ -13,12 +13,16 @@
border: 0;
}
.vscode-codeql__result-table-toggle-diagnostics {
.vscode-codeql__result-table-alert-extras {
display: inline-block;
text-align: left;
margin-left: auto;
}
.vscode-codeql__result-table-toggle-diagnostics {
display: inline-block;
}
/* Keep the checkbox and its label in horizontal alignment. */
.vscode-codeql__result-table-toggle-diagnostics label,
.vscode-codeql__result-table-toggle-diagnostics input {
@@ -26,7 +30,7 @@
vertical-align: middle;
}
.vscode-codeql__result-table-toggle-diagnostics input {
margin: 3px 3px 1px 3px;
margin: 3px 3px 1px 13px;
}
@@ -41,6 +45,13 @@
opacity: 0.6;
}
.vscode-codeql__result-table .sort-asc,
.vscode-codeql__result-table .sort-desc,
.vscode-codeql__result-table .sort-none {
cursor: pointer;
user-select: none;
}
.vscode-codeql__result-table .sort-none::after {
/* Want to take up the same space as the other sort directions */
content: " ▲";
@@ -108,8 +119,14 @@ td.vscode-codeql__path-index-cell {
text-align: right;
}
td.vscode-codeql__location-cell {
text-align: right;
/* Both of these are !important to override the
.vscode-codeql__result-table th { text-align: center } above */
.vscode-codeql__alert-message-cell {
text-align: left !important;
}
.vscode-codeql__location-cell {
text-align: right !important;
}
.vscode-codeql__vertical-rule {

View File

@@ -0,0 +1,5 @@
module.exports = {
env: {
mocha: true
}
}

View File

@@ -58,4 +58,4 @@ export function runTestsInDirectory(testsRoot: string): Promise<void> {
}
});
});
}
}

View File

@@ -10,7 +10,7 @@ describe('launching with a minimal workspace', async () => {
it('should not activate the extension at first', () => {
assert(ext!.isActive === false);
});
it('should activate the extension when a .ql file is opened', async function () {
it('should activate the extension when a .ql file is opened', async function() {
const folders = vscode.workspace.workspaceFolders;
assert(folders && folders.length === 1);
const folderPath = folders![0].uri.fsPath;
@@ -23,4 +23,4 @@ describe('launching with a minimal workspace', async () => {
assert(ext!.isActive);
}, 1000);
});
});
});

View File

@@ -1,4 +1,4 @@
import { runTestsInDirectory } from '../index-template';
export function run(): Promise<void> {
return runTestsInDirectory(__dirname);
}
}

View File

@@ -10,4 +10,4 @@ describe('launching with no specified workspace', () => {
it('should not activate the extension at first', () => {
assert(ext!.isActive === false);
});
});
});

View File

@@ -14,7 +14,7 @@ describe("archive filesystem provider", () => {
});
});
describe('source archive uri encoding', function () {
describe('source archive uri encoding', function() {
const testCases: { name: string, input: ZipFileReference }[] = [
{
name: 'mixed case and unicode',
@@ -30,7 +30,7 @@ describe('source archive uri encoding', function () {
}
];
for (const testCase of testCases) {
it(`should work round trip with ${testCase.name}`, function () {
it(`should work round trip with ${testCase.name}`, function() {
const output = decodeSourceArchiveUri(encodeSourceArchiveUri(testCase.input));
expect(output).to.eql(testCase.input);
});

View File

@@ -151,8 +151,8 @@ describe("Release version ordering", () => {
patchVersion,
prereleaseVersion,
rawString: `${majorVersion}.${minorVersion}.${patchVersion}` +
prereleaseVersion ? `-${prereleaseVersion}` : "" +
buildMetadata ? `+${buildMetadata}` : ""
prereleaseVersion ? `-${prereleaseVersion}` : "" +
buildMetadata ? `+${buildMetadata}` : ""
};
}

View File

@@ -1,4 +1,4 @@
import { runTestsInDirectory } from '../index-template';
export function run(): Promise<void> {
return runTestsInDirectory(__dirname);
}
}

View File

@@ -0,0 +1,39 @@
import 'mocha';
import { expect } from "chai";
import { parseSarifPlainTextMessage } from '../../sarif-utils';
describe('parsing sarif', () => {
it('should be able to parse a simple message from the spec', async function() {
const message = "Tainted data was used. The data came from [here](3)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Tainted data was used. The data came from ",
{ dest: 3, text: "here" }, "."
]);
});
it('should be able to parse a complex message from the spec', async function() {
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\]](1)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Prohibited term used in ",
{ dest: 1, text: "para[0]\\spans[2]" }, "."
]);
});
it('should be able to parse a broken complex message from the spec', async function() {
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\](1)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Prohibited term used in [para[0]\\spans[2](1)."
]);
});
it('should be able to parse a message with extra escaping the spec', async function() {
const message = "Tainted data was used. The data came from \\[here](3)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Tainted data was used. The data came from [here](3)."
]);
});
});

View File

@@ -4,7 +4,7 @@ import * as tmp from "tmp";
import { window, ViewColumn, Uri } from "vscode";
import { fileUriToWebviewUri, webviewUriToFileUri } from '../../interface';
describe('webview uri conversion', function () {
describe('webview uri conversion', function() {
const fileSuffix = '.bqrs';
function setupWebview(filePrefix: string) {
@@ -21,7 +21,7 @@ describe('webview uri conversion', function () {
]
}
);
after(function () {
after(function() {
panel.dispose();
tmpFile.removeCallback();
});
@@ -34,15 +34,15 @@ describe('webview uri conversion', function () {
panel
}
}
it('should correctly round trip from filesystem to webview and back', function () {
it('should correctly round trip from filesystem to webview and back', function() {
const { fileUriOnDisk, panel } = setupWebview('');
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
const reconstructedFileUri = webviewUriToFileUri(webviewUri);
expect(reconstructedFileUri.toString(true)).to.equal(fileUriOnDisk.toString(true));
});
it("does not double-encode # in URIs", function () {
it("does not double-encode # in URIs", function() {
const { fileUriOnDisk, panel } = setupWebview('#');
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
const parsedUri = Uri.parse(webviewUri);

View File

@@ -1,6 +1,41 @@
import * as path from 'path';
import { runTests } from 'vscode-test';
// A subset of the fields in TestOptions from vscode-test, which we
// would simply use instead, but for the fact that it doesn't export
// it.
type Suite = {
extensionDevelopmentPath: string,
extensionTestsPath: string,
launchArgs: string[]
};
/**
* Run an integration test suite `suite` at most `tries` times, or
* until it succeeds, whichever comes first.
*
* TODO: Presently there is no way to distinguish a legitimately
* failed test run from the test runner being terminated by a signal.
* If in the future there arises a way to distinguish these cases
* (e.g. https://github.com/microsoft/vscode-test/pull/56) only retry
* in the terminated-by-signal case.
*/
async function runTestsWithRetry(suite: Suite, tries: number): Promise<void> {
for (let t = 0; t < tries; t++) {
try {
// Download and unzip VS Code if necessary, and run the integration test suite.
await runTests(suite);
return;
} catch (err) {
console.error(`Exception raised while running tests: ${err}`);
if (t < tries - 1)
console.log('Retrying...');
}
}
console.error(`Tried running suite ${tries} time(s), still failed, giving up.`);
process.exit(1);
}
/**
* Integration test runner. Launches the VSCode Extension Development Host with this extension installed.
* See https://github.com/microsoft/vscode-test/blob/master/sample/test/runTest.ts
@@ -32,11 +67,10 @@ async function main() {
];
for (const integrationTestSuite of integrationTestSuites) {
// Download and unzip VS Code if necessary, and run the integration test suite.
await runTests(integrationTestSuite);
await runTestsWithRetry(integrationTestSuite, 3);
}
} catch (err) {
console.error('Failed to run tests');
console.error(`Unexpected exception while running tests: ${err}`);
process.exit(1);
}
}

View File

@@ -0,0 +1,5 @@
module.exports = {
env: {
mocha: true
}
}

View File

@@ -3,7 +3,6 @@ import 'mocha';
import { LocationStyle, StringLocation, tryGetWholeFileLocation } from 'semmle-bqrs';
describe('processing string locations', function () {
it('should detect Windows whole-file locations', function () {
const loc: StringLocation = {
t: LocationStyle.String,

View File

@@ -10,7 +10,7 @@ import { CancellationTokenSource } from 'vscode-jsonrpc';
import * as messages from '../../src/messages';
import * as qsClient from '../../src/queryserver-client';
import * as cli from '../../src/cli';
import { ProgressReporter } from '../../src/logging';
import { ProgressReporter, Logger } from '../../src/logging';
declare module "url" {
@@ -75,8 +75,8 @@ const queryTestCases: QueryTestCase[] = [
}
];
describe('using the query server', function () {
before(function () {
describe('using the query server', function() {
before(function() {
if (process.env["CODEQL_PATH"] === undefined) {
console.log('The environment variable CODEQL_PATH is not set. The query server tests, which require the CodeQL CLI, will be skipped.');
this.skip();
@@ -100,13 +100,14 @@ describe('using the query server', function () {
}
});
it('should be able to start the query server', async function () {
it('should be able to start the query server', async function() {
const consoleProgressReporter: ProgressReporter = {
report: (v: {message: string}) => console.log(`progress reporter says ${v.message}`)
report: (v: { message: string }) => console.log(`progress reporter says ${v.message}`)
};
const logger = {
const logger: Logger = {
log: (s: string) => console.log('logger says', s),
logWithoutTrailingNewline: (s: string) => console.log('logger says', s)
logWithoutTrailingNewline: (s: string) => console.log('logger says', s),
show: () => { },
};
cliServer = new cli.CodeQLCliServer({
async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
@@ -137,7 +138,7 @@ describe('using the query server', function () {
const evaluationSucceeded = new Checkpoint<void>();
const parsedResults = new Checkpoint<void>();
it(`should be able to compile query ${queryName}`, async function () {
it(`should be able to compile query ${queryName}`, async function() {
await queryServerStarted.done();
expect(fs.existsSync(queryTestCase.queryPath)).to.be.true;
try {
@@ -169,7 +170,7 @@ describe('using the query server', function () {
}
});
it(`should be able to run query ${queryName}`, async function () {
it(`should be able to run query ${queryName}`, async function() {
try {
await compilationSucceeded.done();
const callbackId = qs.registerCallback(_res => {
@@ -201,7 +202,7 @@ describe('using the query server', function () {
});
const actualResultSets: ResultSets = {};
it(`should be able to parse results of query ${queryName}`, async function () {
it(`should be able to parse results of query ${queryName}`, async function() {
let fileReader: FileReader | undefined;
try {
await evaluationSucceeded.done();
@@ -222,7 +223,7 @@ describe('using the query server', function () {
}
});
it(`should have correct results for query ${queryName}`, async function () {
it(`should have correct results for query ${queryName}`, async function() {
await parsedResults.done();
expect(actualResultSets!).not.to.be.empty;
expect(Object.keys(actualResultSets!).sort()).to.eql(Object.keys(queryTestCase.expectedResultSets).sort());

View File

@@ -1,3 +1,3 @@
{
"extends": "./node_modules/typescript-config/extension.tsconfig.json"
}
}

View File

@@ -1 +1,4 @@
export * from './disposable-object';
export * from './multi-file-system-watcher';
export * from './ui-service';

View File

@@ -0,0 +1,65 @@
import { DisposableObject } from './disposable-object';
import { EventEmitter, Event, Uri, GlobPattern, workspace } from 'vscode';
/**
* A collection of `FileSystemWatcher` objects. Disposing this object disposes all of the individual
* `FileSystemWatcher` objects and their event registrations.
*/
class WatcherCollection extends DisposableObject {
constructor() {
super();
}
/**
* Create a `FileSystemWatcher` and add it to the collection.
* @param pattern The pattern to watch.
* @param listener The event listener to be invoked when a watched file is created, changed, or
* deleted.
* @param thisArgs The `this` argument for the event listener.
*/
public addWatcher(pattern: GlobPattern, listener: (e: Uri) => any, thisArgs: any): void {
const watcher = workspace.createFileSystemWatcher(pattern);
this.push(watcher.onDidCreate(listener, thisArgs));
this.push(watcher.onDidChange(listener, thisArgs));
this.push(watcher.onDidDelete(listener, thisArgs));
}
}
/**
* A class to watch multiple patterns in the file system at the same time, reporting all
* notifications via a single event.
*/
export class MultiFileSystemWatcher extends DisposableObject {
private readonly _onDidChange = this.push(new EventEmitter<Uri>());
private watchers = this.track(new WatcherCollection());
constructor() {
super();
}
/**
* Event to be fired when any watched file is created, changed, or deleted.
*/
public get onDidChange(): Event<Uri> { return this._onDidChange.event; }
/**
* Adds a new pattern to watch.
* @param pattern The pattern to watch.
*/
public addWatch(pattern: GlobPattern): void {
this.watchers.addWatcher(pattern, this.handleDidChange, this);
}
/**
* Deletes all existing watchers.
*/
public clear(): void {
this.disposeAndStopTracking(this.watchers);
this.watchers = this.track(new WatcherCollection());
}
private handleDidChange(uri: Uri): void {
this._onDidChange.fire(uri);
}
}

View File

@@ -0,0 +1,27 @@
import { commands, TreeDataProvider, window } from 'vscode';
import { DisposableObject } from './disposable-object';
/**
* A VS Code service that interacts with the UI, including handling commands.
*/
export class UIService extends DisposableObject {
protected constructor() {
super();
}
/**
* Registers a command handler with Visual Studio Code.
* @param command The ID of the command to register.
* @param callback Callback function to implement the command.
* @remarks The command handler is automatically unregistered when the service is disposed.
*/
protected registerCommand(command: string, callback: (...args: any[]) => any): void {
this.push(commands.registerCommand(command, callback, this));
}
protected registerTreeDataProvider<T>(viewId: string, treeDataProvider: TreeDataProvider<T>):
void {
this.push(window.registerTreeDataProvider<T>(viewId, treeDataProvider));
}
}

View File

@@ -4,10 +4,10 @@
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json",
"rushVersion": "5.11.2",
"pnpmVersion": "2.15.1",
"rushVersion": "5.20.0",
"pnpmVersion": "4.8.0",
"pnpmOptions": {
"strictPeerDependencies": true,
"strictPeerDependencies": true
},
"nodeSupportedVersionRange": ">=10.13.0 <13.0.0",
"ensureConsistentVersions": true,
@@ -55,4 +55,4 @@
"shouldPublish": true
}
]
}
}

3
syntaxes/README.md Normal file
View File

@@ -0,0 +1,3 @@
This folder contains a compiled version of the textmate grammar for use with systems that need a compiled version of the grammar in the repository such as linguist. It also contains a patch for the grammar to make it work with linguist.
To update the grammar, first build the extension, then run "./updateSyntax".

1416
syntaxes/ql.tmLanguage.json Normal file

File diff suppressed because it is too large Load Diff

4
syntaxes/updateSyntax Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
perl -0777 -pe 's/{\s*"include"\s*:\s*"text.html.markdown#[a-zA-Z_]+\"\s*}\s*,//igs' ../extensions/ql-vscode/out/syntaxes/ql.tmLanguage.json > ./ql.tmLanguage.json

View File

@@ -16,7 +16,7 @@
},
"dependencies": {
"@microsoft/node-core-library": "~3.13.0",
"@microsoft/rush-lib": "~5.11.2",
"@microsoft/rush-lib": "~5.20.0",
"ansi-colors": "^4.0.1",
"child-process-promise": "^2.2.1",
"fs-extra": "^8.1.0",
@@ -44,4 +44,4 @@
"typescript-config": "^0.0.1",
"typescript-formatter": "^7.2.2"
}
}
}

View File

@@ -84,6 +84,8 @@ export class RushContext {
}
else {
pkg = await this.getShrinkwrapPackage(name, version);
// Ensure a proper version number. pnpm uses syntax like 3.4.0_glob@7.1.6 for peer dependencies
version = version.split('_')[0];
packagePath = path.join(this.packageRepository, name, version, 'package');
if (!await fs.pathExists(packagePath)) {
throw new Error(`Package '${name}:${version}' not found in package repository.`);

View File

@@ -1,4 +1,5 @@
{
"newLineCharacter": "\n",
"indentStyle": 2,
"insertSpaceAfterCommaDelimiter": true,
"insertSpaceAfterSemicolonInForStatements": true,
@@ -15,4 +16,4 @@
"insertSpaceBeforeFunctionParenthesis": false,
"placeOpenBraceOnNewLineForFunctions": false,
"placeOpenBraceOnNewLineForControlBlocks": false
}
}