Compare commits

...

144 Commits

Author SHA1 Message Date
Andrew Eisenberg
4bb48879ec Update changelog in preparation for v1.2.2 release (#433)
Some checks failed
Code Scanning - CodeQL / codeql (push) Has been cancelled
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
* Update changelog in preparation for v1.2.2 release

Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-06-08 11:17:31 -07:00
Andrew Eisenberg
46e7382832 Clarify log message (#430) 2020-06-05 13:28:41 -07:00
jcreedcmu
91bd7f5971 Merge pull request #401 from jcreedcmu/jcreed/pagination
Implement pagination for BQRS results.
2020-06-05 10:23:27 -04:00
jcreedcmu
109c8755c3 Merge pull request #421 from jcreedcmu/jcreed/fix-release-asset-search
Download platform-specific releases if they are available.
2020-06-05 09:50:51 -04:00
jcreedcmu
218a14a4a1 Update extensions/ql-vscode/src/distribution.ts
Co-authored-by: Henry Mercer <henry.mercer@me.com>
2020-06-05 09:01:07 -04:00
jcreedcmu
71efe355f0 Update extensions/ql-vscode/src/distribution.ts
Co-authored-by: Henry Mercer <henry.mercer@me.com>
2020-06-05 09:00:54 -04:00
jcreedcmu
f7eee72b93 Update extensions/ql-vscode/src/distribution.ts
Co-authored-by: Henry Mercer <henry.mercer@me.com>
2020-06-05 09:00:39 -04:00
jcreedcmu
3bc884f45d Update extensions/ql-vscode/src/distribution.ts
Co-authored-by: Henry Mercer <henry.mercer@me.com>
2020-06-05 08:32:53 -04:00
Andrew Eisenberg
ddf382d690 Update changelog 2020-06-04 07:42:01 -07:00
Andrew Eisenberg
b84c429882 Fix bad indentation on paste
I don't fully understand why this is working
differently, but these changes enable proper
behavior on pasting ql into the editor.
2020-06-04 07:42:01 -07:00
Jason Reed
73a0bcacc8 Don't update release.assets in place. 2020-06-04 10:23:38 -04:00
jcreedcmu
60f47e8ee3 Update extensions/ql-vscode/src/distribution.ts
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2020-06-03 14:55:00 -04:00
Jason Reed
c29f4d4c79 Download platform-specific releases if they are available. 2020-06-03 09:58:33 -04:00
Henry Mercer
71f74cb620 Merge pull request #427 from henrymercer/fix-semver-comparison
Use semver package for semantic version comparison and precedence checking
2020-06-02 22:01:09 +01:00
Henry Mercer
c4766e464b Add additional tests for choosing the latest release of the CodeQL CLI 2020-06-02 18:37:44 +01:00
Henry Mercer
eba67f8f4f Apply suggestions from review 2020-06-02 18:28:37 +01:00
Henry Mercer
b7a97d34e5 Apply suggestions from code review
Co-authored-by: Andrew Eisenberg <aeisenberg@github.com>
2020-06-02 10:21:16 +01:00
Henry Mercer
18a9e2794e Update handling of prerelease versions of the CodeQL CLI.
Suppose a user has the includePrereleases config option set, installs an
extension-managed prerelease, then decides they no longer want
prereleases and disables includePrereleases.
In this case, we should prompt the user to downgrade the CLI to a
non-prerelease version.
However, if the user is managing their own CLI, we will allow them to
use prereleases without incompatibility prompts.
2020-06-01 22:26:48 +01:00
Henry Mercer
8208940532 Introduce release compatibility check before selecting the most recent 2020-06-01 22:18:09 +01:00
Henry Mercer
71d4038744 Use version ranges instead of version constraint for simplicity 2020-06-01 22:18:09 +01:00
Henry Mercer
034d8b7c68 Use semver package for version comparison and precedence checking 2020-06-01 22:18:08 +01:00
Henry Mercer
e686b421ec Merge pull request #426 from github/revert-425-fix-semver-comparison
Revert "Use semver package for semantic version comparison and precedence checking"
2020-06-01 21:20:34 +01:00
Henry Mercer
9191873eb1 Revert "Use semver package for semantic version comparison and precedence checking" 2020-06-01 21:11:10 +01:00
jcreedcmu
d924e9f649 Merge pull request #425 from henrymercer/fix-semver-comparison
Use semver package for semantic version comparison and precedence checking
2020-06-01 15:56:02 -04:00
Henry Mercer
e911bf4854 Introduce release compatibility check before selecting the most recent 2020-06-01 20:41:41 +01:00
Henry Mercer
7b9e540332 Use version ranges instead of version constraint for simplicity 2020-06-01 20:41:25 +01:00
Henry Mercer
577ce95cb1 Use semver package for version comparison and precedence checking 2020-06-01 20:41:00 +01:00
jcreedcmu
63c8afab44 Merge pull request #422 from jcreedcmu/jcreed/retry-harder-on-windows
Chore: Retry tests more aggressively on windows
2020-06-01 12:39:51 -04:00
Jason Reed
7777f9d643 Retry tests more aggressively on windows
There are some flaky CI test failures that manifest only as a message
like

    [main 2020-06-01T16:09:47.671Z] [VS Code]: render process crashed!

(and only afaict on windows) which I am not sure how to detect at the
moment. If that message is occurring in the exception caught at this
stage, we can check for it.
2020-06-01 12:23:57 -04:00
jcreedcmu
6505e97b98 Merge pull request #420 from jcreedcmu/jcreed/fix-release-asset-search
Only look for codeql.zip assets
2020-06-01 09:25:08 -04:00
Jason Reed
a6fc0d5493 Only look for codeql.zip assets
There are now multiple release assets available. Make sure we don't
throw an error when looking for the codeql distribution.
2020-06-01 09:01:31 -04:00
jcreedcmu
572e74e079 Merge pull request #416 from github/version/bump-to-v1.2.2
Bump version to v1.2.2
2020-05-29 14:06:49 -04:00
github-actions[bot]
c2de5fc9b6 Bump version to v1.2.2 2020-05-29 17:46:23 +00:00
jcreedcmu
728b8ca0fd Merge pull request #415 from jcreedcmu/jcreed/v1.2.1
Some checks failed
Code Scanning - CodeQL / codeql (push) Has been cancelled
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
Update CHANGELOG for release
2020-05-29 13:34:04 -04:00
Jason Reed
edd5734de8 Update CHANGELOG for release 2020-05-29 13:32:29 -04:00
jcreedcmu
88a4cc528e Merge pull request #414 from aeisenberg/aeisenberg/lgtm-explore
Update link to lgtm to go to the explore section
2020-05-29 13:31:59 -04:00
Andrew Eisenberg
a732f19a3d Update link to lgtm to go to the explore section
Makes it easier for new users to search for projects.
2020-05-29 10:29:04 -07:00
jcreedcmu
18c9333f37 Merge pull request #413 from aeisenberg/aeisenberg/rush
Add node v14 to allowed node versions
2020-05-29 13:21:52 -04:00
Andrew Eisenberg
010000b878 Add node v14 to allowed node versions 2020-05-29 10:20:13 -07:00
jcreedcmu
7b5f7499b4 Merge pull request #411 from aeisenberg/aeisenberg/readme-lgtm
Update the download from LGTM section in the readme
2020-05-29 11:32:41 -04:00
Andrew Eisenberg
292bec2ea5 Update extensions/ql-vscode/README.md
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-05-29 08:18:17 -07:00
Andrew Eisenberg
910a877d06 Update extensions/ql-vscode/README.md
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-05-29 08:18:11 -07:00
Andrew Eisenberg
80023f1304 Update extensions/ql-vscode/README.md
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-05-29 08:18:04 -07:00
Andrew Eisenberg
8e8247e986 Update extensions/ql-vscode/README.md
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-05-29 08:17:57 -07:00
Andrew Eisenberg
d92e0b5568 Update extensions/ql-vscode/README.md
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-05-29 08:17:50 -07:00
Andrew Eisenberg
d3c1e7688e Update extensions/ql-vscode/README.md
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-05-29 08:17:42 -07:00
Andrew Eisenberg
3e9c58869c Update extensions/ql-vscode/README.md
Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-05-29 08:17:34 -07:00
Andrew Eisenberg
c0a8c7affd Update the download from LGTM section in the readme 2020-05-28 13:12:21 -07:00
Andrew Eisenberg
f2575e4d4a Better error handling for downloading dbs at invalid URLs
We do our best to extract a readable error message from the
response.
2020-05-28 11:53:41 -07:00
Andrew Eisenberg
87315b8f33 Update changelog 2020-05-28 11:53:41 -07:00
Andrew Eisenberg
a338683a71 Add unit tests for databaseFetcher 2020-05-28 11:53:41 -07:00
Andrew Eisenberg
a541b11a37 Add more flexibility with Uri parsing for LGTM
Ensure that providers other than `g` are accepted and that subpages
are ignored.
2020-05-28 11:53:41 -07:00
Andrew Eisenberg
e2771a8922 Remove code scanning on pull request 2020-05-28 11:53:41 -07:00
Andrew Eisenberg
16e09b7ae9 Add better error handling
* ensure error appears when an invalid URL is entered
* ensure error messages are understandable by users
2020-05-28 11:53:41 -07:00
Andrew Eisenberg
1c1dbc95c7 Allow download from lgtm
Adds a new command to download databases from lgtm. It's working,
though need to create better error messages on failure.
2020-05-28 11:53:41 -07:00
jcreedcmu
dd9fafc27c Merge pull request #407 from jcreedcmu/jcreed/view-sarif
Allow viewing SARIF from query history view
2020-05-28 08:08:56 -04:00
jcreedcmu
7172505e25 Merge pull request #328 from jcreedcmu/jcreed/restart-on-segfault
Retry integration tests only on segfault
2020-05-27 14:10:18 -04:00
Jason Reed
7b99bdfc88 Address review comments. 2020-05-27 14:08:03 -04:00
Jason Reed
bb16454ab7 Only show 'view SARIF' if SARIF exists. 2020-05-27 11:57:31 -04:00
Jason Reed
70529a81f3 Add "View SARIF" command to query history context menu. 2020-05-27 11:30:50 -04:00
Jason Reed
7db6bc8228 Remove dead code. 2020-05-27 10:59:32 -04:00
Jason Reed
41fab207dc Retry integration tests only on segfault 2020-05-27 10:52:31 -04:00
Jason Reed
a8bad9ecb8 Upgrade vscode-test dependency to 1.4.0 2020-05-27 10:52:31 -04:00
jcreedcmu
17901bee0c Merge pull request #406 from shati-patel/qldoc
Update capitalization of "QLDoc" in CHANGELOG.md
2020-05-27 10:34:36 -04:00
Shati Patel
e7d041af68 Update CHANGELOG.md 2020-05-27 10:09:22 +01:00
jcreedcmu
9afd676c1e Merge pull request #403 from aeisenberg/aeisenberg/codeql-action
Introduce codeql code scanning action
2020-05-26 17:05:04 -04:00
Andrew Eisenberg
7bf719f632 Introduce codeql code scanning action 2020-05-26 14:00:28 -07:00
Jason Reed
c90dae89c1 Fix LGTM warning. 2020-05-26 16:53:20 -04:00
Jason Reed
110cf0ddc0 Implement pagination for BQRS results. 2020-05-26 16:30:10 -04:00
Andrew Eisenberg
32622b1b9f Update changelog 2020-05-26 12:01:22 -07:00
Andrew Eisenberg
8262ecf990 Fix syntax error in ql library 2020-05-26 12:01:22 -07:00
Andrew Eisenberg
0817abd6ac Use Uri.file instead of Uri.parse 2020-05-26 12:01:22 -07:00
Andrew Eisenberg
821ec9b8f7 Add tests for database uri fixing 2020-05-26 12:01:22 -07:00
Andrew Eisenberg
b0328b03a0 Allow users more flexibility when opening a DB
Closes #383.

See the heuristics in the issue.
2020-05-26 12:01:22 -07:00
Andrew Eisenberg
2d7d6fb873 Update changelog 2020-05-26 11:39:57 -07:00
Andrew Eisenberg
b7201c04dc Add onEnterRules for language config
This change provides proper indent/outdent for block comments. Through
onEnterRules. Because onEnterRules are not exactly API, I had to use
a back door to implement them.

Also, it tweaks the language-configuration.json by adding more support
for things like word boundaries and auto-closing pairs.

Since QL has similar syntactical items as JavaScriot, I started with
the JS lang config and removed single quotes and back ticks.
2020-05-26 11:39:57 -07:00
Andrew Eisenberg
8db488563b Add more tests for the archive-filesystem-provider 2020-05-26 10:50:03 -07:00
jcreedcmu
fac5f98d80 Merge pull request #382 from jcreedcmu/jcreed/zip-logging
Log more details when source archive entries aren't found
2020-05-26 12:38:40 -04:00
jcreedcmu
fccec96926 Merge pull request #389 from jcreedcmu/jcreed/no-defs-err
Add error message when there are no definitions/references queries available.
2020-05-26 12:38:24 -04:00
Jason Reed
8cadd3dcab Add error message when no definitions queries. 2020-05-21 14:45:55 -04:00
jcreedcmu
d9e1a6f82a Merge pull request #385 from github/jcreedcmu-patch-1
Update README.md
2020-05-21 11:06:15 -04:00
Bas van Schaik
f47a88dcb1 Update README.md 2020-05-21 16:02:31 +01:00
Bas van Schaik
8cab3e9c6f Update README.md 2020-05-21 16:02:02 +01:00
jcreedcmu
165f3957ed Update README.md
Include instructions about how to update libraries
2020-05-21 10:58:00 -04:00
Jason Reed
3e4eeeb8fd Log more details when source archive entries aren't found 2020-05-20 08:49:58 -04:00
jcreedcmu
038e0a3c63 Merge pull request #381 from dbartol/dbartol/publish
Avoid need for `build` directory
2020-05-20 07:51:58 -04:00
Dave Bartolomeo
3e7084f65d Remove build directory 2020-05-19 17:13:30 -04:00
Dave Bartolomeo
18bb4b0231 Avoid need for build directory 2020-05-19 17:03:56 -04:00
github-actions[bot]
8cb5661330 Bump version to v1.2.1 2020-05-19 12:48:50 -07:00
jcreedcmu
f6f2b99c67 Merge pull request #379 from jcreedcmu/jcreed/v1.2.0-take-2
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
Increment version for minor release
2020-05-19 14:28:49 -04:00
Jason Reed
b2c82029f6 Increment version for minor release 2020-05-19 14:26:16 -04:00
jcreedcmu
d18b524c81 Merge pull request #378 from jcreedcmu/jcreed/v1.2.0
Update CHANGELOG.md for release.
2020-05-19 14:25:07 -04:00
Jason Reed
6be2c8bb95 Update CHANGELOG.md for release. 2020-05-19 14:22:12 -04:00
jcreedcmu
c289f1f66f Merge pull request #368 from jcreedcmu/jcreed/jump-to-def-release
Remove feature flag guard around source archive jump-to-definition
2020-05-19 14:17:33 -04:00
jcreedcmu
c2717d7725 Merge branch 'master' into jcreed/jump-to-def-release 2020-05-19 14:14:59 -04:00
jcreedcmu
74e42b86a6 Merge pull request #375 from jcreedcmu/jcreed/more-logging
Logging: More chatty logging during extension initialization.
2020-05-19 14:14:00 -04:00
jcreedcmu
6db514843b Merge branch 'master' into jcreed/jump-to-def-release 2020-05-19 13:57:20 -04:00
jcreedcmu
c8d64e4c35 Merge branch 'master' into jcreed/more-logging 2020-05-19 13:56:10 -04:00
jcreedcmu
0e4c3be404 Merge pull request #377 from jcreedcmu/jcreed/prefix
Fix naming and availability in command palette of various commands
2020-05-19 13:54:10 -04:00
Jason Reed
dd1bdf54bb Add integrity check for commands in package.json
Attempt to enforce some regularity in how we name commands, and fix
one command that was showing up improperly in the command palette.
2020-05-19 12:20:45 -04:00
Jason Reed
c01772848c Add all db-getting commands (dl, folder, zip) to command palette 2020-05-19 11:32:54 -04:00
Jason Reed
ab09cdb66d Make capitalization consistent 2020-05-19 11:02:32 -04:00
Jason Reed
d92edfb058 Remove database panel icon commands from command palette
This corrects what is an unfortunately common accidental antipattern,
where creating a command meant just to be the handler of a user
interface button ends up in the command palette unless you explicitly
set `"when": "false"` in the command palette section of the
configuration.

Also enforce the naming convention that commands prefixed with
`codeQLDatabases.` are those meant for the databases panel only, while
prefixing `codeQL.` means that it's meant to be directly accessible
through the command palette.
2020-05-19 10:59:35 -04:00
jcreedcmu
1e86e08851 Merge pull request #376 from shati-patel/choose-db
Update README with new command for adding database
2020-05-19 09:14:38 -04:00
Shati Patel
c505996ca0 Update README with new command for adding database 2020-05-19 13:19:58 +01:00
Jason Reed
0796893017 Logging: More chatty logging during extension initialization.
Mainly intentded to make it easier to debug the cause of
command-palette commands being undefined.
2020-05-18 13:24:00 -04:00
jcreedcmu
6fdfade1ed Merge pull request #374 from aeisenberg/path-fix
Fix paths on windows when opening archive databases
2020-05-15 19:35:04 -04:00
Andrew Eisenberg
e31f8b73ac Fix paths on windows when opening archive databases 2020-05-15 16:30:50 -07:00
jcreedcmu
f38d0fd08e Merge pull request #372 from github/version/bump-to-v1.1.6
Bump version to v1.1.6
2020-05-15 15:39:14 -04:00
github-actions[bot]
579aba5abb Bump version to v1.1.6 2020-05-15 19:00:31 +00:00
jcreedcmu
31066be29e Merge pull request #371 from aeisenberg/aeisenberg/release
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
Update changelog for release
2020-05-15 14:55:47 -04:00
Andrew Eisenberg
3bbecb248b Update changelog for release 2020-05-15 11:53:58 -07:00
jcreedcmu
691c9af1f7 Merge pull request #370 from aeisenberg/aeisenberg/import-notify
Refactor how we import database archives
2020-05-15 13:43:10 -04:00
Andrew Eisenberg
a137a72e02 Refactor how we import database archives
Now, importing an archive includes a progress monitor and will also
display a message when complete. Error handling is also more uniform.
2020-05-15 10:39:40 -07:00
Jason Reed
a98e3bc9ae Fix docs, comments. 2020-05-15 12:42:46 -04:00
Jason Reed
4ffab3c16d Remove feature flag guard around source archive jump-to-definition 2020-05-15 12:42:46 -04:00
Andrew Eisenberg
bb3aa79dad Update changelog 2020-05-15 08:27:51 -07:00
Andrew Eisenberg
7f34fcaa1c Update commands for importing databases
1. Add commands for importing an archive, folder, or from internet
2. Add new icons for all of them
3. Ensure that each command can only retrieve databases through a 
   single mechanism
2020-05-15 08:27:51 -07:00
Andrew Eisenberg
e42a39e5ec Look for either .dbinfo or codeql-database.yml 2020-05-15 08:27:51 -07:00
Andrew Eisenberg
bd22878ec8 Update changelog 2020-05-15 08:27:51 -07:00
Andrew Eisenberg
8dd1b9f44e Augments the add database command to handle zip files
The add database command can now add databases by zip file. When a
file is selected, the zip file is attempted to be extracted into a
directory managed by the extension.

Once extracted, a database is searched for, by looking for a .dbinfo
file.

Crucially, we are using the same infrastructure to download a database
as we are to add a database by zip file.
2020-05-15 08:27:51 -07:00
jcreedcmu
2da70d774d Merge pull request #367 from jbj/link-style
Make links variable-width without underline
2020-05-14 12:58:51 -04:00
Jonas Jensen
2fddc9cff1 Make links variable-width without underline
These are two independent changes.
- The monospace font used in links made the text larger so that less
  text could fit on the screen. It also suggested that all link text was
  an code snippet, which it isn't. The advantage of a fixed-width font,
  vertical alignment, was not put to any use.
- Underlining of links made it almost impossible to distinguish a space
  from an underscore.
2020-05-14 09:59:44 +02:00
jcreedcmu
11d9bdc8e1 Merge pull request #365 from aeisenberg/lint-semi
Add semi-colon linting
2020-05-13 15:40:47 -04:00
Andrew Eisenberg
7d23a833b1 Add semi-colon linting 2020-05-13 12:06:53 -07:00
Andrew Eisenberg
258322057f Ensure the extension dir exists before writing to it 2020-05-13 11:47:43 -07:00
jcreedcmu
6ded193891 Merge pull request #362 from github/version/bump-to-v1.1.5
Bump version to v1.1.5
2020-05-13 14:44:41 -04:00
jcreedcmu
bb6b90646f Merge pull request #363 from aeisenberg/lint-fixes
Fixes to how we lint automatically
2020-05-13 14:42:57 -04:00
Andrew Eisenberg
fece068800 Rename npm script for clarity 2020-05-13 11:36:05 -07:00
Andrew Eisenberg
de8b7d44cd Avoid linting indentation rules
This is already handled by tsfmt and eslint has
some slight differences.
2020-05-13 11:29:32 -07:00
Andrew Eisenberg
432c5c9ae7 Move linting from precommit to prepush
It's a longer action, so we don't want to run it on each commit.
2020-05-13 11:27:34 -07:00
github-actions[bot]
59433af8be Bump version to v1.1.5 2020-05-13 17:30:08 +00:00
Andrew Eisenberg
c6928d3159 Update changelog
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-05-13 10:25:06 -07:00
Andrew Eisenberg
fd26e02ed3 Update changelog 2020-05-13 08:10:38 -07:00
Andrew Eisenberg
de381804f6 Fix lint 2020-05-13 08:10:38 -07:00
Andrew Eisenberg
2f92477bd9 Move storagePath calculation to extension.ts 2020-05-13 08:10:38 -07:00
Andrew Eisenberg
926ab92dfe Add command to download, unzip, and open databases
New command that requests a URL and allows a user to install a
database from that url.

Closes #357
2020-05-13 08:10:38 -07:00
Andrew Eisenberg
36484fcea6 Formatting 2020-05-13 08:10:38 -07:00
Andrew Eisenberg
89e7b03d4a Add format and lint on commit 2020-05-12 10:15:48 -07:00
Andrew Eisenberg
c3e3390647 Extract BQRS locations from string results 2020-05-08 11:49:46 -07:00
Andrew Eisenberg
010ae64da3 Use inline-source-map
This gets a better debugging experience for webview.
2020-05-08 11:49:46 -07:00
Andrew Eisenberg
bd3702121f Never run format on save
This can lead to lots of non-semantic whitespace changes.
2020-05-08 11:49:46 -07:00
jcreedcmu
043d17d454 Merge pull request #356 from github/version/bump-to-v1.1.4
Bump version to v1.1.4
2020-05-08 12:49:35 -04:00
github-actions[bot]
1c7cad0151 Bump version to v1.1.4 2020-05-08 16:43:45 +00:00
71 changed files with 2744 additions and 2080 deletions

21
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: "Code Scanning - CodeQL"
on:
push:
schedule:
- cron: '0 0 * * 0'
jobs:
codeql:
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -18,11 +18,12 @@ jobs:
with:
node-version: '10.18.1'
- name: Install dependencies
run: node common/scripts/install-run-rush.js install
shell: bash
- name: Build
run: |
cd build
npm install
npm run build-ci
run: node common/scripts/install-run-rush.js build
shell: bash
- name: Prepare artifacts
@@ -55,11 +56,12 @@ jobs:
node-version: '10.18.1'
# We have to build the dependencies in `lib` before running any tests.
- name: Install dependencies
run: node common/scripts/install-run-rush.js install
shell: bash
- name: Build
run: |
cd build
npm install
npm run build-ci
run: node common/scripts/install-run-rush.js build
shell: bash
- name: Lint

View File

@@ -33,12 +33,12 @@ jobs:
with:
node-version: '10.18.1'
- name: Install dependencies
run: node common/scripts/install-run-rush.js install
shell: bash
- name: Build
run: |
cd build
npm install
# Release build instead of dev build.
npm run build-release
run: node common/scripts/install-run-rush.js build --release
shell: bash
- name: Prepare artifacts

View File

@@ -32,5 +32,7 @@
"eslint.options": {
// This is necessary so that eslint can properly resolve its plugins
"resolvePluginsRelativeTo": "./extensions/ql-vscode"
}
},
// Force this to false since this will cause too many changes on each commit
"editor.formatOnSave": false
}

View File

@@ -1,12 +0,0 @@
GitHub Actions Build directory
===
The point of this directory is to allow us to do a local installation *of* the rush
tool, since
- installing globally is not permitted on github actions
- installing locally in the root directory of the repo creates `node_modules` there,
and rush itself gives error messages since it thinks `node_modules` is not supposed
to exist, since rush is supposed to be managing subproject dependencies.
Running rush from a subdirectory searches parent directories for `rush.json`
and does the build starting from that file's location.

1293
build/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +0,0 @@
{
"name": "build",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"@microsoft/rush": "^5.10.3"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rush update && rush build",
"build-ci": "rush install && rush build",
"build-release": "rush install && rush build --release"
},
"author": "GitHub"
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,11 +26,9 @@ module.exports = {
"@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/indent": "off",
"@typescript-eslint/no-throw-literal": "error",
"no-useless-escape": 0
"no-useless-escape": 0,
"semi": 2
},
};

View File

@@ -1,5 +1,37 @@
# CodeQL for Visual Studio Code: Changelog
## 1.2.2 - 8 June 2020
- Fix auto-indentation rules.
- Add ability to download platform-specific releases of the CodeQL CLI if they are available.
- Fix handling of downloading prerelease versions of the CodeQL CLI.
- Add pagination for displaying non-interpreted results.
## 1.2.1 - 29 May 2020
- Better formatting and autoindentation when adding QLDoc comments to `.ql` and `.qll` files.
- Allow for more flexibility when opening a database in the workspace. A user can now choose the actual database folder, or the nested `db-*` folder.
- Add query history menu command for viewing corresponding SARIF file.
- Add ability for users to download databases directly from LGTM.com.
## 1.2.0 - 19 May 2020
- Enable 'Go to Definition' and 'Go to References' on source archive
files in CodeQL databases. This is handled by a CodeQL query.
- Fix adding database archive files on Windows.
- Enable adding remote and local database archive files from the
command palette.
## 1.1.5 - 15 May 2020
- Links in results are no longer underlined and monospaced.
- Add the ability to choose a database either from an archive, a folder, or from the internet.
- New icons for commands on the databases view.
## 1.1.4 - 13 May 2020
- Add the ability to download and install databases archives from the internet.
## 1.1.3 - 8 May 2020
- Add a suggestion in alerts view to view raw results, when there are

View File

@@ -2,10 +2,10 @@
This project is an extension for Visual Studio Code that adds rich language support for [CodeQL](https://help.semmle.com/codeql) and allows you to easily find problems in codebases. In particular, the extension:
* Enables you to use CodeQL to query databases generated from source code.
* Shows the flow of data through the results of path queries, which is essential for triaging security results.
* Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/Semmle/ql).
* Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
- Enables you to use CodeQL to query databases generated from source code.
- Shows the flow of data through the results of path queries, which is essential for triaging security results.
- Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/github/codeql).
- Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
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).
@@ -14,18 +14,18 @@ To see what has changed in the last few versions of the extension, see the [Chan
The information in this `README` file describes the quickest way to start using CodeQL.
For information about other configurations, see the separate [CodeQL help](https://help.semmle.com/codeql/codeql-for-vscode.html).
**Quick start: Installing and configuring the extension**
### Quick start: Installing and configuring the extension
1. [Install the extension](#installing-the-extension).
1. [Check access to the CodeQL CLI](#checking-access-to-the-codeql-cli).
1. [Clone the CodeQL starter workspace](#cloning-the-codeql-starter-workspace).
**Quick start: Using CodeQL**
### Quick start: Using CodeQL
1. [Import a database from LGTM](#importing-a-database-from-lgtm).
1. [Run a query](#running-a-query).
-----
---
## Quick start: Installing and configuring the extension
@@ -49,11 +49,26 @@ If you have any difficulty with CodeQL CLI access, see the **CodeQL Extension Lo
### Cloning the CodeQL starter workspace
When you're working with CodeQL, you need access to the standard CodeQL libraries and queries.
Initially, we recommend that you clone and use the ready-to-use starter workspace, https://github.com/github/vscode-codeql-starter/.
Initially, we recommend that you clone and use the ready-to-use [starter workspace](https://github.com/github/vscode-codeql-starter/).
This includes libraries and queries for the main supported languages, with folders set up ready for your custom queries. After cloning the workspace (use `git clone --recursive`), you can use it in the same way as any other VS Code workspace—with the added advantage that you can easily update the CodeQL libraries.
For information about configuring an existing workspace for CodeQL, [see the documentation](https://help.semmle.com/codeql/codeql-for-vscode/procedures/setting-up.html#updating-an-existing-workspace-for-codeql).
## Upgrading CodeQL standard libraries
You can easily keep up-to-date with the latest changes to the [CodeQL standard libraries](https://github.com/github/codeql).
If you're using the [CodeQL starter workspace](https://github.com/github/vscode-codeql-starter/), you can pull in the latest standard libraries by running:
```shell
git pull
git submodule update --recursive
```
in the starter workspace directory.
If you're using your own clone of the CodeQL standard libraries, you can do a `git pull` from where you have the libraries checked out.
## Quick start: Using CodeQL
You can find all the commands contributed by the extension in the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**) by typing `CodeQL`, many of them are also accessible through the interface, and via keyboard shortcuts.
@@ -62,16 +77,13 @@ You can find all the commands contributed by the extension in the Command Palett
While you can use the [CodeQL CLI to create your own databases](https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html), the simplest way to start is by downloading a database from LGTM.com.
1. Log in to LGTM.com.
1. Find a project you're interested in and display the **Integrations** tab (for example, [Apache Kafka](https://lgtm.com/projects/g/apache/kafka/ci/)).
1. Scroll to the **CodeQL databases for local analysis** section at the bottom of the page.
1. Download databases for the languages that you want to explore.
1. Unzip the databases.
1. For each database that you want to import:
1. In the VS Code sidebar, go to **CodeQL** > **Databases** and click **+**.
1. Browse to the unzipped database folder (the parent folder that contains `db-<language>` and `src`) and select **Choose database** to add it.
When the import is complete, each CodeQL database is displayed in the CodeQL sidebar under **Databases**.
1. Open [LGTM.com](https://lgtm.com/#explore) in your browser.
1. Search for a project you're interested in, for example [Apache Kafka](https://lgtm.com/projects/g/apache/kafka).
1. Copy the link to that project, for example `https://lgtm.com/projects/g/apache/kafka`.
1. In VS Code, open the Command Palette and choose the **CodeQL: Download Database from LGTM** command.
1. Paste the link you copied earlier.
1. Select the language for the database you want to download (only required if the project has databases for multiple languages).
1. Once the CodeQL database has been imported, it is displayed in the Databases view.
### Running a query
@@ -79,7 +91,7 @@ The instructions below assume that you're using the CodeQL starter workspace, or
1. Expand the `ql` folder and locate a query to run. The standard queries are grouped by target language and then type, for example: `ql/java/ql/src/Likely Bugs`.
1. Open a query (`.ql`) file.
3. Right-click in the query window and select **CodeQL: Run Query**. Alternatively, open the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**), type `Run Query`, then select **CodeQL: Run Query**.
1. Right-click in the query window and select **CodeQL: Run Query**. Alternatively, open the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**), type `Run Query`, then select **CodeQL: Run Query**.
The CodeQL extension runs the query on the current database using the CLI and reports progress in the bottom right corner of the application.
When the results are ready, they're displayed in the CodeQL Query Results view. Use the dropdown menu to choose between different forms of result output.
@@ -90,10 +102,10 @@ If there are any problems running a query, a notification is displayed in the bo
For more information about the CodeQL extension, [see the documentation](https://help.semmle.com/codeql/codeql-for-vscode.html). Otherwise, you could:
* [Create a database for a different codebase](https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html).
* [Try out variant analysis](https://help.semmle.com/QL/learn-ql/ql-training.html).
* [Learn more about CodeQL](https://help.semmle.com/QL/learn-ql/).
* [Read how security researchers use CodeQL to find CVEs](https://securitylab.github.com/research).
- [Create a database for a different codebase](https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html).
- [Try out variant analysis](https://help.semmle.com/QL/learn-ql/ql-training.html).
- [Learn more about CodeQL](https://help.semmle.com/QL/learn-ql/).
- [Read how security researchers use CodeQL to find CVEs](https://securitylab.github.com/research).
## License

View File

@@ -10,7 +10,7 @@ export const config: webpack.Configuration = {
path: path.resolve(__dirname, '..', 'out'),
filename: "[name].js"
},
devtool: 'source-map',
devtool: "inline-source-map",
resolve: {
extensions: ['.js', '.ts', '.tsx', '.json']
},

View File

@@ -1,72 +1,34 @@
{
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "//",
// symbols used for start and end a block comment. Remove this entry if your language does not support block comments
"blockComment": [
"/*",
"*/"
]
},
// symbols used as brackets
"brackets": [
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
]
],
// symbols that are auto closed when typing
"autoClosingPairs": [
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
],
[
"\"",
"\""
],
[
"'",
"'"
]
],
// symbols that that can be used to surround a selection
"surroundingPairs": [
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
],
[
"\"",
"\""
],
[
"'",
"'"
]
]
}
"comments": {
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
"autoCloseBefore": ";:.=}])> \n\t",
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["'", "'"],
["\"", "\""]
],
"folding": {
"markers": {
"start": "^\\s*//\\s*#?region\\b",
"end": "^\\s*//\\s*#?endregion\\b"
}
},
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\.\\<\\>\\/\\?\\s]+)"
}

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 1H1.5L1 1.5V4.5L1.5 5H2V13.5L2.5 14H13.5L14 13.5V5H14.5L15 4.5V1.5L14.5 1ZM13.5 4H2.5H2V2H14V4H13.5ZM3 13V5H13V13H3ZM11 7H5V8H11V7Z" fill="#C5C5C5"/>
<line y2="12" x2="8" y1="12" x1="16" stroke-width="1" stroke="green" fill="none"/>
<line y2="8" x2="12" y1="16" x1="12" stroke-width="1" stroke="green" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9565 6H12.0064C12.8004 6 13.5618 6.31607 14.1232 6.87868C14.6846 7.44129 15 8.20435 15 9C15 9.79565 14.6846 10.5587 14.1232 11.1213C13.5618 11.6839 12.8004 12 12.0064 12V11C12.5357 11 13.0434 10.7893 13.4176 10.4142C13.7919 10.0391 14.0021 9.53044 14.0021 9C14.0021 8.46957 13.7919 7.96086 13.4176 7.58579C13.0434 7.21072 12.5357 7 12.0064 7H11.0924L10.9687 6.143C10.8938 5.60541 10.6456 5.10711 10.2618 4.72407C9.87801 4.34103 9.37977 4.09427 8.84303 4.02143C8.30629 3.94859 7.76051 4.05365 7.2889 4.3206C6.81729 4.58754 6.44573 5.00173 6.23087 5.5L5.89759 6.262L5.08933 6.073C4.90382 6.02699 4.71364 6.0025 4.52255 6C3.86093 6 3.22641 6.2634 2.75858 6.73224C2.29075 7.20108 2.02792 7.83696 2.02792 8.5C2.02792 9.16304 2.29075 9.79893 2.75858 10.2678C3.22641 10.7366 3.86093 11 4.52255 11H5.02148V12H4.52255C4.02745 12.0043 3.5371 11.903 3.08403 11.7029C2.63096 11.5028 2.22553 11.2084 1.89461 10.8394C1.5637 10.4703 1.31488 10.0349 1.16465 9.56211C1.01442 9.08932 0.966217 8.58992 1.02324 8.09704C1.08026 7.60416 1.24121 7.12906 1.4954 6.70326C1.74959 6.27745 2.09121 5.91068 2.49762 5.62727C2.90402 5.34385 3.36591 5.15027 3.85264 5.05937C4.33938 4.96847 4.83984 4.98232 5.32083 5.1C5.6241 4.40501 6.14511 3.82799 6.80496 3.45635C7.4648 3.08472 8.22753 2.9387 8.9776 3.04044C9.72768 3.14217 10.4242 3.4861 10.9618 4.02014C11.4993 4.55418 11.8485 5.24923 11.9565 6ZM6.70719 11.1214L8.0212 12.4354V7H9.01506V12.3992L10.2929 11.1214L11 11.8285L8.85356 13.9749H8.14645L6.00008 11.8285L6.70719 11.1214Z" fill="#C5C5C5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 14H12.5L12.98 13.63L15.61 6.63L15.13 6H13V3.5L12.5 3H6.70996L5.84998 2.15002L5.5 2H0.5L0 2.5V13.5L0.5 14ZM1 3H5.29004L6.15002 3.84998L6.5 4H12V6H8.5L8.15002 6.15002L7.29004 7H2.5L2.03003 7.33997L1.03003 10.42L1 3ZM12.13 13H1.18994L2.85999 8H7.5L7.84998 7.84998L8.70996 7H14.5L12.13 13Z" fill="#C5C5C5"/>
<line y2="12" x2="8" y1="12" x1="16" stroke-width="1" stroke="green" fill="none"/>
<line y2="8" x2="12" y1="16" x1="12" stroke-width="1" stroke="green" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M16.010 6.49c-3.885 0-7.167 0.906-9.328 2.813-0.063-0.12-0.109-0.219-0.188-0.339-0.224-0.365-0.438-0.776-1.104-1.188-0.411-0.26-0.87-0.438-1.349-0.516-0.208-0.021-0.422-0.021-0.63 0l0.135-0.016c-1.214 0-1.922 0.724-2.385 1.354-0.458 0.625-0.755 1.328-0.948 2.099-0.38 1.542-0.385 3.536 1.083 5.026 0.766 0.781 1.667 1.151 2.484 1.37 0.156 0.042 0.297 0.052 0.448 0.083 0.531 2.521 2.104 4.656 4.208 5.839v0.005c1.24 0.693 2.417 1.010 3.297 1.349 1.234 0.479 2.536 1 4.052 1.135l0.078 0.005h0.198c1.745 0 3.063-0.703 4.203-1.141 0.875-0.333 2.052-0.641 3.302-1.344 0.578-0.323 1.115-0.719 1.594-1.172 1.318-1.234 2.229-2.839 2.625-4.599 1.115-0.182 2.141-0.719 2.922-1.536 1.464-1.484 1.458-3.479 1.078-5.021-0.193-0.771-0.49-1.474-0.948-2.099-0.458-0.63-1.172-1.354-2.385-1.354l0.135 0.016c-0.208-0.021-0.422-0.021-0.63 0-0.479 0.078-0.938 0.255-1.344 0.516-0.667 0.411-0.88 0.823-1.104 1.182-0.073 0.12-0.12 0.219-0.188 0.333-2.156-1.901-5.432-2.802-9.313-2.802zM16.042 8.313c4.745 0 8.016 1.422 9.411 3.964 0.839-0.323 1.453-2.521 2.146-2.948 0.563-0.344 0.885-0.26 0.885-0.26 1.271 0 2.578 3.729 0.953 5.38-0.859 0.875-2.443 1.12-3.229 1.057-0.063 2.542-1.542 4.833-3.5 5.932-1 0.563-2.068 0.854-3.063 1.234-1.229 0.469-2.38 1.016-3.547 1.016h-0.125c-1.161-0.099-2.318-0.542-3.547-1.016-0.995-0.38-2.068-0.682-3.063-1.24-1.948-1.099-3.427-3.391-3.49-5.927-0.781 0.068-2.385-0.177-3.245-1.057-1.625-1.651-0.318-5.38 0.948-5.38 0 0 0.328-0.083 0.885 0.26 0.698 0.427 1.318 2.646 2.161 2.953 1.391-2.547 4.667-3.969 9.417-3.969zM10.875 11.422c-2.276-0.042-4.146 1.792-4.146 4.068 0 2.281 1.87 4.115 4.146 4.073 5.328-0.099 5.328-8.047 0-8.141zM21.208 11.422c-5.427 0-5.427 8.141 0 8.141s5.427-8.141 0-8.141zM11.453 13.708c2.349 0.063 2.349 3.552 0 3.615-1.182 0-2.042-1.115-1.75-2.255 0.318 0.771 1.469 0.547 1.464-0.292 0-0.406-0.318-0.745-0.729-0.76 0.302-0.203 0.656-0.313 1.016-0.307zM20.641 13.708c2.344 0.063 2.344 3.552 0 3.615-1.182 0-2.047-1.115-1.755-2.255 0.229 0.552 0.979 0.641 1.328 0.146 0.344-0.49 0.010-1.167-0.589-1.193 0.297-0.208 0.651-0.313 1.016-0.313zM15.359 19.906c-0.318 0.026-0.5 0.193-0.5 0.635 0 0.281 0.182 0.484 0.5 0.484 0.229 0 0.266-0.323 0.047-0.375-0.031-0.005-0.172-0.057-0.172-0.182 0-0.12 0-0.167 0.24-0.198 0.104-0.016 0.156-0.141 0.125-0.24s-0.125-0.135-0.24-0.125zM16.724 19.906c-0.115-0.005-0.208 0.026-0.24 0.125s0.021 0.224 0.125 0.24c0.24 0.031 0.24 0.078 0.24 0.198 0 0.125-0.141 0.177-0.172 0.182-0.219 0.052-0.182 0.375 0.042 0.375 0.323 0 0.51-0.203 0.51-0.484 0-0.443-0.188-0.609-0.505-0.635z" fill="#C5C5C5"/>
<line y2="24" x2="16" y1="26" x1="32" stroke-width="2" stroke="green" fill="none"/>
<line y2="16" x2="24" y1="32" x1="24" stroke-width="1" stroke="green" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 1H1.5L1 1.5V4.5L1.5 5H2V13.5L2.5 14H13.5L14 13.5V5H14.5L15 4.5V1.5L14.5 1ZM13.5 4H2.5H2V2H14V4H13.5ZM3 13V5H13V13H3ZM11 7H5V8H11V7Z" fill="#424242"/>
<line y2="12" x2="8" y1="12" x1="16" stroke-width="1" stroke="green" fill="none"/>
<line y2="8" x2="12" y1="16" x1="12" stroke-width="1" stroke="green" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9565 6H12.0064C12.8004 6 13.5618 6.31607 14.1232 6.87868C14.6846 7.44129 15 8.20435 15 9C15 9.79565 14.6846 10.5587 14.1232 11.1213C13.5618 11.6839 12.8004 12 12.0064 12V11C12.5357 11 13.0434 10.7893 13.4176 10.4142C13.7919 10.0391 14.0021 9.53044 14.0021 9C14.0021 8.46957 13.7919 7.96086 13.4176 7.58579C13.0434 7.21072 12.5357 7 12.0064 7H11.0924L10.9687 6.143C10.8938 5.60541 10.6456 5.10711 10.2618 4.72407C9.87801 4.34103 9.37977 4.09427 8.84303 4.02143C8.30629 3.94859 7.76051 4.05365 7.2889 4.3206C6.81729 4.58754 6.44573 5.00173 6.23087 5.5L5.89759 6.262L5.08933 6.073C4.90382 6.02699 4.71364 6.0025 4.52255 6C3.86093 6 3.22641 6.2634 2.75858 6.73224C2.29075 7.20108 2.02792 7.83696 2.02792 8.5C2.02792 9.16304 2.29075 9.79893 2.75858 10.2678C3.22641 10.7366 3.86093 11 4.52255 11H5.02148V12H4.52255C4.02745 12.0043 3.5371 11.903 3.08403 11.7029C2.63096 11.5028 2.22553 11.2084 1.89461 10.8394C1.5637 10.4703 1.31488 10.0349 1.16465 9.56211C1.01442 9.08932 0.966217 8.58992 1.02324 8.09704C1.08026 7.60416 1.24121 7.12906 1.4954 6.70326C1.74959 6.27745 2.09121 5.91068 2.49762 5.62727C2.90402 5.34385 3.36591 5.15027 3.85264 5.05937C4.33938 4.96847 4.83984 4.98232 5.32083 5.1C5.6241 4.40501 6.14511 3.82799 6.80496 3.45635C7.4648 3.08472 8.22753 2.9387 8.9776 3.04044C9.72768 3.14217 10.4242 3.4861 10.9618 4.02014C11.4993 4.55418 11.8485 5.24923 11.9565 6ZM6.70719 11.1214L8.0212 12.4354V7H9.01506V12.3992L10.2929 11.1214L11 11.8285L8.85356 13.9749H8.14645L6.00008 11.8285L6.70719 11.1214Z" fill="#424242"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M0.499817 14H12.4998L12.9798 13.63L15.6098 6.63L15.1298 6H12.9998V3.5L12.4998 3H6.70978L5.84979 2.15002L5.49982 2H0.499817L-0.000183105 2.5V13.5L0.499817 14ZM0.999817 3H5.28986L6.14984 3.84998L6.49982 4H11.9998V6H8.49982L8.14984 6.15002L7.28986 7H2.49982L2.02985 7.33997L1.02985 10.42L0.999817 3ZM12.1298 13H1.18976L2.8598 8H7.49982L7.84979 7.84998L8.70978 7H14.4998L12.1298 13Z" fill="#424242"/>
<line y2="12" x2="8" y1="12" x1="16" stroke-width="1" stroke="green" fill="none"/>
<line y2="8" x2="12" y1="16" x1="12" stroke-width="1" stroke="green" fill="none"/>
</g>
<defs>
<clipPath id="clip0">
<path d="M-0.000183105 0H15.9998V16H-0.000183105V0Z" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 824 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M16.010 6.49c-3.885 0-7.167 0.906-9.328 2.813-0.063-0.12-0.109-0.219-0.188-0.339-0.224-0.365-0.438-0.776-1.104-1.188-0.411-0.26-0.87-0.438-1.349-0.516-0.208-0.021-0.422-0.021-0.63 0l0.135-0.016c-1.214 0-1.922 0.724-2.385 1.354-0.458 0.625-0.755 1.328-0.948 2.099-0.38 1.542-0.385 3.536 1.083 5.026 0.766 0.781 1.667 1.151 2.484 1.37 0.156 0.042 0.297 0.052 0.448 0.083 0.531 2.521 2.104 4.656 4.208 5.839v0.005c1.24 0.693 2.417 1.010 3.297 1.349 1.234 0.479 2.536 1 4.052 1.135l0.078 0.005h0.198c1.745 0 3.063-0.703 4.203-1.141 0.875-0.333 2.052-0.641 3.302-1.344 0.578-0.323 1.115-0.719 1.594-1.172 1.318-1.234 2.229-2.839 2.625-4.599 1.115-0.182 2.141-0.719 2.922-1.536 1.464-1.484 1.458-3.479 1.078-5.021-0.193-0.771-0.49-1.474-0.948-2.099-0.458-0.63-1.172-1.354-2.385-1.354l0.135 0.016c-0.208-0.021-0.422-0.021-0.63 0-0.479 0.078-0.938 0.255-1.344 0.516-0.667 0.411-0.88 0.823-1.104 1.182-0.073 0.12-0.12 0.219-0.188 0.333-2.156-1.901-5.432-2.802-9.313-2.802zM16.042 8.313c4.745 0 8.016 1.422 9.411 3.964 0.839-0.323 1.453-2.521 2.146-2.948 0.563-0.344 0.885-0.26 0.885-0.26 1.271 0 2.578 3.729 0.953 5.38-0.859 0.875-2.443 1.12-3.229 1.057-0.063 2.542-1.542 4.833-3.5 5.932-1 0.563-2.068 0.854-3.063 1.234-1.229 0.469-2.38 1.016-3.547 1.016h-0.125c-1.161-0.099-2.318-0.542-3.547-1.016-0.995-0.38-2.068-0.682-3.063-1.24-1.948-1.099-3.427-3.391-3.49-5.927-0.781 0.068-2.385-0.177-3.245-1.057-1.625-1.651-0.318-5.38 0.948-5.38 0 0 0.328-0.083 0.885 0.26 0.698 0.427 1.318 2.646 2.161 2.953 1.391-2.547 4.667-3.969 9.417-3.969zM10.875 11.422c-2.276-0.042-4.146 1.792-4.146 4.068 0 2.281 1.87 4.115 4.146 4.073 5.328-0.099 5.328-8.047 0-8.141zM21.208 11.422c-5.427 0-5.427 8.141 0 8.141s5.427-8.141 0-8.141zM11.453 13.708c2.349 0.063 2.349 3.552 0 3.615-1.182 0-2.042-1.115-1.75-2.255 0.318 0.771 1.469 0.547 1.464-0.292 0-0.406-0.318-0.745-0.729-0.76 0.302-0.203 0.656-0.313 1.016-0.307zM20.641 13.708c2.344 0.063 2.344 3.552 0 3.615-1.182 0-2.047-1.115-1.755-2.255 0.229 0.552 0.979 0.641 1.328 0.146 0.344-0.49 0.010-1.167-0.589-1.193 0.297-0.208 0.651-0.313 1.016-0.313zM15.359 19.906c-0.318 0.026-0.5 0.193-0.5 0.635 0 0.281 0.182 0.484 0.5 0.484 0.229 0 0.266-0.323 0.047-0.375-0.031-0.005-0.172-0.057-0.172-0.182 0-0.12 0-0.167 0.24-0.198 0.104-0.016 0.156-0.141 0.125-0.24s-0.125-0.135-0.24-0.125zM16.724 19.906c-0.115-0.005-0.208 0.026-0.24 0.125s0.021 0.224 0.125 0.24c0.24 0.031 0.24 0.078 0.24 0.198 0 0.125-0.141 0.177-0.172 0.182-0.219 0.052-0.182 0.375 0.042 0.375 0.323 0 0.51-0.203 0.51-0.484 0-0.443-0.188-0.609-0.505-0.635z" fill="#424242"/>
<line y2="24" x2="16" y1="26" x1="32" stroke-width="2" stroke="green" fill="none"/>
<line y2="16" x2="24" y1="32" x1="24" stroke-width="1" stroke="green" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.1.3",
"version": "1.2.2",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -27,8 +27,15 @@
"onView:codeQLQueryHistory",
"onView:test-explorer",
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.chooseDatabase",
"onCommand:codeQLDatabases.chooseDatabaseFolder",
"onCommand:codeQLDatabases.chooseDatabaseArchive",
"onCommand:codeQLDatabases.chooseDatabaseInternet",
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
"onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQL.chooseDatabaseFolder",
"onCommand:codeQL.chooseDatabaseArchive",
"onCommand:codeQL.chooseDatabaseInternet",
"onCommand:codeQL.chooseDatabaseLgtm",
"onCommand:codeQLDatabases.chooseDatabase",
"onCommand:codeQLDatabases.setCurrentDatabase",
"onCommand:codeQL.quickQuery",
@@ -172,11 +179,35 @@
"title": "CodeQL: Quick Query"
},
{
"command": "codeQL.chooseDatabase",
"title": "CodeQL: Choose Database",
"command": "codeQLDatabases.chooseDatabaseFolder",
"title": "Choose Database from Folder",
"icon": {
"light": "media/light/plus.svg",
"dark": "media/dark/plus.svg"
"light": "media/light/folder-opened-plus.svg",
"dark": "media/dark/folder-opened-plus.svg"
}
},
{
"command": "codeQLDatabases.chooseDatabaseArchive",
"title": "Choose Database from Archive",
"icon": {
"light": "media/light/archive-plus.svg",
"dark": "media/dark/archive-plus.svg"
}
},
{
"command": "codeQLDatabases.chooseDatabaseInternet",
"title": "Download Database",
"icon": {
"light": "media/light/cloud-download.svg",
"dark": "media/dark/cloud-download.svg"
}
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"title": "Download from LGTM",
"icon": {
"light": "media/light/lgtm-plus.svg",
"dark": "media/dark/lgtm-plus.svg"
}
},
{
@@ -211,6 +242,22 @@
"command": "codeQLDatabases.openDatabaseFolder",
"title": "Show Database Directory"
},
{
"command": "codeQL.chooseDatabaseFolder",
"title": "CodeQL: Choose Database from Folder"
},
{
"command": "codeQL.chooseDatabaseArchive",
"title": "CodeQL: Choose Database from Archive"
},
{
"command": "codeQL.chooseDatabaseInternet",
"title": "CodeQL: Download Database"
},
{
"command": "codeQL.chooseDatabaseLgtm",
"title": "CodeQL: Download Database from LGTM"
},
{
"command": "codeQLDatabases.sortByName",
"title": "Sort by Name",
@@ -251,6 +298,10 @@
"command": "codeQLQueryHistory.showQueryText",
"title": "Show Query Text"
},
{
"command": "codeQLQueryHistory.viewSarif",
"title": "View SARIF"
},
{
"command": "codeQLQueryResults.nextPathStep",
"title": "CodeQL: Show Next Step on Path"
@@ -289,7 +340,22 @@
"group": "navigation"
},
{
"command": "codeQL.chooseDatabase",
"command": "codeQLDatabases.chooseDatabaseFolder",
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLDatabases.chooseDatabaseArchive",
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLDatabases.chooseDatabaseInternet",
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "view == codeQLDatabases",
"group": "navigation"
}
@@ -345,6 +411,11 @@
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.viewSarif",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
},
{
"command": "codeQLTests.showOutputDifferences",
"group": "qltest@1",
@@ -360,7 +431,7 @@
{
"command": "codeQL.setCurrentDatabase",
"group": "9_qlCommands",
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder"
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zip"
},
{
"command": "codeQL.runQuery",
@@ -385,6 +456,14 @@
"command": "codeQLDatabases.setCurrentDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.renameDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.openDatabaseFolder",
"when": "false"
},
{
"command": "codeQLDatabases.sortByName",
"when": "false"
@@ -397,6 +476,26 @@
"command": "codeQLDatabases.removeDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseFolder",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseArchive",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseInternet",
"when": "false"
},
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "false"
},
{
"command": "codeQLDatabases.upgradeDatabase",
"when": "false"
},
{
"command": "codeQLQueryHistory.openQuery",
"when": "false"
@@ -417,6 +516,10 @@
"command": "codeQLQueryHistory.showQueryText",
"when": "false"
},
{
"command": "codeQLQueryHistory.viewSarif",
"when": "false"
},
{
"command": "codeQLQueryHistory.setLabel",
"when": "false"
@@ -463,9 +566,10 @@
"preintegration": "rm -rf ./out/vscode-tests && gulp",
"integration": "node ./out/vscode-tests/run-integration-tests.js",
"update-vscode": "node ./node_modules/vscode/bin/install",
"postinstall": "node ./node_modules/vscode/bin/install",
"postinstall": "npm rebuild && node ./node_modules/vscode/bin/install",
"format": "tsfmt -r",
"lint": "eslint src test --ext .ts,.tsx"
"lint": "eslint src test --ext .ts,.tsx",
"format-staged": "lint-staged"
},
"dependencies": {
"child-process-promise": "^2.2.1",
@@ -486,7 +590,9 @@
"vscode-languageclient": "^6.1.3",
"vscode-test-adapter-api": "~1.7.0",
"vscode-test-adapter-util": "~0.7.0",
"minimist": "~1.2.5"
"minimist": "~1.2.5",
"semver": "~7.3.2",
"@types/semver": "~7.2.0"
},
"devDependencies": {
"@types/chai": "^4.1.7",
@@ -529,7 +635,7 @@
"typescript-config": "^0.0.1",
"typescript-formatter": "^7.2.2",
"vsce": "^1.65.0",
"vscode-test": "^1.0.0",
"vscode-test": "^1.4.0",
"webpack": "^4.38.0",
"webpack-cli": "^3.3.2",
"eslint": "~6.8.0",
@@ -542,6 +648,23 @@
"@types/sinon-chai": "~3.2.3",
"proxyquire": "~2.1.3",
"@types/proxyquire": "~1.3.28",
"eslint-plugin-react": "~7.19.0"
"eslint-plugin-react": "~7.19.0",
"husky": "~4.2.5",
"lint-staged": "~10.2.2",
"prettier": "~2.0.5"
},
"husky": {
"hooks": {
"pre-commit": "npm run format-staged",
"pre-push": "npm run lint"
}
},
"lint-staged": {
"./**/*.{json,css,scss,md}": [
"prettier --write"
],
"./**/*.{ts,tsx}": [
"tsfmt -r"
]
}
}

View File

@@ -34,7 +34,7 @@ export interface RawResultSet {
function adaptKind(kind: ColumnKind): ColumnType {
// XXX what about 'u'?
if (kind === 'e') {
return { type: 'e', primitiveType: 's', locationStyle: LocationStyle.FivePart, hasLabel: true }
return { type: 'e', primitiveType: 's', locationStyle: LocationStyle.FivePart, hasLabel: true };
}
else {
return { type: kind };
@@ -51,7 +51,7 @@ export function adaptSchema(schema: ResultSetSchema): AdaptedSchema {
name: schema.name,
tupleCount: schema.rows,
version: 0,
}
};
}
export function adaptValue(val: ColumnValue): ResultValue {
@@ -87,7 +87,7 @@ export function adaptValue(val: ColumnValue): ResultValue {
// something like the code in sarif-utils.ts?
file: url.uri.replace(/file:/, ''),
}
}
};
}
@@ -99,5 +99,36 @@ export function adaptBqrs(schema: AdaptedSchema, page: DecodedBqrsChunk): RawRes
return {
schema,
rows: page.tuples.map(adaptRow),
}
};
}
/**
* This type has two branches; we are in the process of changing from
* one to the other. The old way is to parse them inside the webview,
* the new way is to parse them in the extension. The main motivation
* for this transition is to make pagination possible in such a way
* that only one page needs to be sent from the extension to the webview.
*/
export type ParsedResultSets = ExtensionParsedResultSets | WebviewParsedResultSets;
/**
* The old method doesn't require any nontrivial information to be included here,
* just a tag to indicate that it is being used.
*/
export interface WebviewParsedResultSets {
t: 'WebviewParsed';
selectedTable?: string; // when undefined, means 'show default table'
}
/**
* The new method includes which bqrs page is being sent, and the
* actual results parsed on the extension side.
*/
export interface ExtensionParsedResultSets {
t: 'ExtensionParsed';
pageNumber: number;
numPages: number;
selectedTable?: string; // when undefined, means 'show default table'
resultSetNames: string[];
resultSet: RawResultSet;
}

View File

@@ -173,7 +173,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
const ref = decodeSourceArchiveUri(uri);
const archive = await this.getArchive(ref.sourceArchiveZipPath);
const contents = archive.dirMap.get(ref.pathWithinSourceArchive);
const result = contents === undefined ? [] : Array.from(contents.entries());
const result = contents === undefined ? undefined : Array.from(contents.entries());
if (result === undefined) {
throw vscode.FileSystemError.FileNotFound(uri);
}
@@ -238,7 +238,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
if (archive.dirMap.has(reqPath)) {
return new Directory(reqPath);
}
throw vscode.FileSystemError.FileNotFound(uri);
throw vscode.FileSystemError.FileNotFound(`uri '${uri.toString()}', interpreted as '${reqPath}' in archive '${ref.sourceArchiveZipPath}'`);
}
private async _lookupAsFile(uri: vscode.Uri): Promise<File> {

View File

@@ -1,10 +1,11 @@
import * as semver from "semver";
import { runCodeQlCliCommand } from "./cli";
import { Logger } from "./logging";
/**
* Get the version of a CodeQL CLI.
*/
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<Version | undefined> {
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<semver.SemVer | undefined> {
const output: string = await runCodeQlCliCommand(
codeQlPath,
["version"],
@@ -12,85 +13,5 @@ export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): P
"Checking CodeQL version",
logger
);
return tryParseVersionString(output.trim());
}
/**
* Try to parse a version string, returning undefined if we can't parse it.
*
* Version strings must contain a major, minor, and patch version. They may optionally
* start with "v" and may optionally contain some "tail" string after the major, minor, and
* patch versions, for example as in `v2.1.0+baf5bff`.
*/
export function tryParseVersionString(versionString: string): Version | undefined {
const match = versionString.match(versionRegex);
if (match === null) {
return undefined;
}
return {
buildMetadata: match[5],
majorVersion: Number.parseInt(match[1], 10),
minorVersion: Number.parseInt(match[2], 10),
patchVersion: Number.parseInt(match[3], 10),
prereleaseVersion: match[4],
rawString: versionString,
}
}
/**
* Regex for parsing semantic versions
*
* From the semver spec https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
*/
const versionRegex = new RegExp(String.raw`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)` +
String.raw`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
String.raw`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`);
/**
* A version of the CodeQL CLI.
*/
export interface Version {
/**
* Build metadata
*
* For example, this will be `abcdef0` for version 2.1.0-alpha.1+abcdef0.
* Build metadata must be ignored when comparing versions.
*/
buildMetadata: string | undefined;
/**
* Major version number
*
* For example, this will be `2` for version 2.1.0-alpha.1+abcdef0.
*/
majorVersion: number;
/**
* Minor version number
*
* For example, this will be `1` for version 2.1.0-alpha.1+abcdef0.
*/
minorVersion: number;
/**
* Patch version number
*
* For example, this will be `0` for version 2.1.0-alpha.1+abcdef0.
*/
patchVersion: number;
/**
* Prerelease version
*
* For example, this will be `alpha.1` for version 2.1.0-alpha.1+abcdef0.
* The prerelease version must be considered when comparing versions.
*/
prereleaseVersion: string | undefined;
/**
* Raw version string
*
* For example, this will be `2.1.0-alpha.1+abcdef0` for version 2.1.0-alpha.1+abcdef0.
*/
rawString: string;
return semver.parse(output.trim()) || undefined;
}

View File

@@ -165,7 +165,7 @@ export class CodeQLCliServer implements Disposable {
// If the server is not running a command run this immediately
// otherwise add to the front of the queue (as we want to run this after the next command()).
if (this.commandInProcess) {
this.commandQueue.unshift(callback)
this.commandQueue.unshift(callback);
} else {
callback();
}
@@ -188,19 +188,19 @@ export class CodeQLCliServer implements Disposable {
*/
private async launchProcess(): Promise<child_process.ChildProcessWithoutNullStreams> {
const config = await this.getCodeQlPath();
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => { /**/ })
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => { /**/ });
}
private async runCodeQlCliInternal(command: string[], commandArgs: string[], description: string): Promise<string> {
const stderrBuffers: Buffer[] = [];
if (this.commandInProcess) {
throw new Error("runCodeQlCliInternal called while cli was running")
throw new Error("runCodeQlCliInternal called while cli was running");
}
this.commandInProcess = true;
try {
//Launch the process if it doesn't exist
if (!this.process) {
this.process = await this.launchProcess()
this.process = await this.launchProcess();
}
// Grab the process so that typescript know that it is always defined.
const process = this.process;
@@ -230,8 +230,8 @@ export class CodeQLCliServer implements Disposable {
// Listen for process exit.
process.addListener("close", (code) => reject(code));
// Write the command followed by a null terminator.
process.stdin.write(JSON.stringify(args), "utf8")
process.stdin.write(this.nullBuffer)
process.stdin.write(JSON.stringify(args), "utf8");
process.stdin.write(this.nullBuffer);
});
// Join all the data together
const fullBuffer = Buffer.concat(stdoutBuffers);
@@ -252,8 +252,8 @@ export class CodeQLCliServer implements Disposable {
} finally {
this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
// Remove the listeners we set up.
process.stdout.removeAllListeners('data')
process.stderr.removeAllListeners('data')
process.stdout.removeAllListeners('data');
process.stderr.removeAllListeners('data');
process.removeAllListeners("close");
}
} finally {
@@ -349,7 +349,7 @@ export class CodeQLCliServer implements Disposable {
try {
yield JSON.parse(event) as EventType;
} catch (err) {
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`)
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
}
}
}
@@ -375,11 +375,11 @@ export class CodeQLCliServer implements Disposable {
} catch (err) {
reject(err);
}
}
};
// If the server is not running a command, then run the given command immediately,
// otherwise add to the queue
if (this.commandInProcess) {
this.commandQueue.push(callback)
this.commandQueue.push(callback);
} else {
callback();
}
@@ -401,7 +401,7 @@ export class CodeQLCliServer implements Disposable {
try {
return JSON.parse(result) as OutputType;
} catch (err) {
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`)
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
}
}
@@ -535,12 +535,12 @@ export class CodeQLCliServer implements Disposable {
try {
output = await fs.readFile(interpretedResultsPath, 'utf8');
} catch (err) {
throw new Error(`Reading output of interpretation failed: ${err.stderr || err}`)
throw new Error(`Reading output of interpretation failed: ${err.stderr || err}`);
}
try {
return JSON.parse(output) as sarif.Log;
} catch (err) {
throw new Error(`Parsing output of interpretation failed: ${err.stderr || err}`)
throw new Error(`Parsing output of interpretation failed: ${err.stderr || err}`);
}
}
@@ -714,7 +714,7 @@ export async function runCodeQlCliCommand(codeQlPath: string, command: string[],
logger.log(`CLI command succeeded.`);
return result.stdout;
} catch (err) {
throw new Error(`${description} failed: ${err.stderr || err}`)
throw new Error(`${description} failed: ${err.stderr || err}`);
}
}

View File

@@ -42,13 +42,11 @@ const ROOT_SETTING = new Setting('codeQL');
// Enable experimental features
/**
* This setting is deliberately not in package.json so that it does
* not appear in the settings ui in vscode itself. If users want to
* enable experimental features, they can add
* "codeQl.experimentalFeatures" directly in their vscode settings
* json file.
* Any settings below are deliberately not in package.json so that
* they do not appear in the settings ui in vscode itself. If users
* want to enable experimental features, they can add them directly in
* their vscode settings json file.
*/
export const EXPERIMENTAL_FEATURES_SETTING = new Setting('experimentalFeatures', ROOT_SETTING);
/* Advanced setting: used to enable bqrs parsing in the cli instead of in the webview. */
export const EXPERIMENTAL_BQRS_SETTING = new Setting('experimentalBqrsParsing', ROOT_SETTING);

View File

@@ -0,0 +1,442 @@
import fetch, { Response } from "node-fetch";
import * as unzipper from "unzipper";
import {
Uri,
ProgressOptions,
ProgressLocation,
commands,
window,
} from "vscode";
import * as fs from "fs-extra";
import * as path from "path";
import { DatabaseManager, DatabaseItem } from "./databases";
import {
ProgressCallback,
showAndLogErrorMessage,
withProgress,
showAndLogInformationMessage,
} from "./helpers";
import { logger } from "./logging";
/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
*
* @param databasesManager the DatabaseManager
* @param storagePath where to store the unzipped database.
*/
export async function promptImportInternetDatabase(
databasesManager: DatabaseManager,
storagePath: string
): Promise<DatabaseItem | undefined> {
let item: DatabaseItem | undefined = undefined;
try {
const databaseUrl = await window.showInputBox({
prompt: "Enter URL of zipfile of database to download",
});
if (databaseUrl) {
validateHttpsUrl(databaseUrl);
const progressOptions: ProgressOptions = {
location: ProgressLocation.Notification,
title: "Adding database from URL",
cancellable: false,
};
await withProgress(
progressOptions,
async (progress) =>
(item = await databaseArchiveFetcher(
databaseUrl,
databasesManager,
storagePath,
progress
))
);
commands.executeCommand("codeQLDatabases.focus");
}
showAndLogInformationMessage(
"Database downloaded and imported successfully."
);
} catch (e) {
showAndLogErrorMessage(e.message);
}
return item;
}
/**
* Prompts a user to fetch a database from lgtm.
* User enters a project url and then the user is asked which language
* to download (if there is more than one)
*
* @param databasesManager the DatabaseManager
* @param storagePath where to store the unzipped database.
*/
export async function promptImportLgtmDatabase(
databasesManager: DatabaseManager,
storagePath: string
): Promise<DatabaseItem | undefined> {
let item: DatabaseItem | undefined = undefined;
try {
const lgtmUrl = await window.showInputBox({
prompt:
"Enter the project URL on LGTM (e.g., https://lgtm.com/projects/g/github/codeql)",
});
if (!lgtmUrl) {
return;
}
if (looksLikeLgtmUrl(lgtmUrl)) {
const databaseUrl = await convertToDatabaseUrl(lgtmUrl);
if (databaseUrl) {
const progressOptions: ProgressOptions = {
location: ProgressLocation.Notification,
title: "Adding database from LGTM",
cancellable: false,
};
await withProgress(
progressOptions,
async (progress) =>
(item = await databaseArchiveFetcher(
databaseUrl,
databasesManager,
storagePath,
progress
))
);
commands.executeCommand("codeQLDatabases.focus");
}
} else {
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
}
if (item) {
showAndLogInformationMessage(
"Database downloaded and imported successfully."
);
}
} catch (e) {
showAndLogErrorMessage(e.message);
}
return item;
}
/**
* Imports a database from a local archive.
*
* @param databaseUrl the file url of the archive to import
* @param databasesManager the DatabaseManager
* @param storagePath where to store the unzipped database.
*/
export async function importArchiveDatabase(
databaseUrl: string,
databasesManager: DatabaseManager,
storagePath: string
): Promise<DatabaseItem | undefined> {
let item: DatabaseItem | undefined = undefined;
try {
const progressOptions: ProgressOptions = {
location: ProgressLocation.Notification,
title: "Importing database from archive",
cancellable: false,
};
await withProgress(
progressOptions,
async (progress) =>
(item = await databaseArchiveFetcher(
databaseUrl,
databasesManager,
storagePath,
progress
))
);
commands.executeCommand("codeQLDatabases.focus");
if (item) {
showAndLogInformationMessage(
"Database unzipped and imported successfully."
);
}
} catch (e) {
showAndLogErrorMessage(e.message);
}
return item;
}
/**
* Fetches an archive database. The database might be on the internet
* or in the local filesystem.
*
* @param databaseUrl URL from which to grab the database
* @param databasesManager the DatabaseManager
* @param storagePath where to store the unzipped database.
* @param progressCallback optional callback to send progress messages to
*/
async function databaseArchiveFetcher(
databaseUrl: string,
databasesManager: DatabaseManager,
storagePath: string,
progressCallback?: ProgressCallback
): Promise<DatabaseItem> {
progressCallback?.({
maxStep: 3,
message: "Getting database",
step: 1,
});
if (!storagePath) {
throw new Error("No storage path specified.");
}
await fs.ensureDir(storagePath);
const unzipPath = await getStorageFolder(storagePath, databaseUrl);
if (isFile(databaseUrl)) {
await readAndUnzip(databaseUrl, unzipPath);
} else {
await fetchAndUnzip(databaseUrl, unzipPath, progressCallback);
}
progressCallback?.({
maxStep: 3,
message: "Opening database",
step: 3,
});
// find the path to the database. The actual database might be in a sub-folder
const dbPath = await findDirWithFile(
unzipPath,
".dbinfo",
"codeql-database.yml"
);
if (dbPath) {
const item = await databasesManager.openDatabase(Uri.file(dbPath));
databasesManager.setCurrentDatabaseItem(item);
return item;
} else {
throw new Error("Database not found in archive.");
}
}
async function getStorageFolder(storagePath: string, urlStr: string) {
// we need to generate a folder name for the unzipped archive,
// this needs to be human readable since we may use this name as the initial
// name for the database
const url = Uri.parse(urlStr);
// MacOS has a max filename length of 255
// and remove a few extra chars in case we need to add a counter at the end.
let lastName = path.basename(url.path).substring(0, 250);
if (lastName.endsWith(".zip")) {
lastName = lastName.substring(0, lastName.length - 4);
}
const realpath = await fs.realpath(storagePath);
let folderName = path.join(realpath, lastName);
// avoid overwriting existing folders
let counter = 0;
while (await fs.pathExists(folderName)) {
counter++;
folderName = path.join(realpath, `${lastName}-${counter}`);
if (counter > 100) {
throw new Error("Could not find a unique name for downloaded database.");
}
}
return folderName;
}
function validateHttpsUrl(databaseUrl: string) {
let uri;
try {
uri = Uri.parse(databaseUrl, true);
} catch (e) {
throw new Error(`Invalid url: ${databaseUrl}`);
}
if (uri.scheme !== "https") {
throw new Error("Must use https for downloading a database.");
}
}
async function readAndUnzip(databaseUrl: string, unzipPath: string) {
const unzipStream = unzipper.Extract({
path: unzipPath,
});
await new Promise((resolve, reject) => {
// we already know this is a file scheme
const databaseFile = Uri.parse(databaseUrl).fsPath;
const stream = fs.createReadStream(databaseFile);
stream.on("error", reject);
unzipStream.on("error", reject);
unzipStream.on("close", resolve);
stream.pipe(unzipStream);
});
}
async function fetchAndUnzip(
databaseUrl: string,
unzipPath: string,
progressCallback?: ProgressCallback
) {
const response = await fetch(databaseUrl);
await checkForFailingResponse(response);
const unzipStream = unzipper.Extract({
path: unzipPath,
});
progressCallback?.({
maxStep: 3,
message: "Unzipping database",
step: 2,
});
await new Promise((resolve, reject) => {
const handler = (err: Error) => {
if (err.message.startsWith('invalid signature')) {
reject(new Error('Not a valid archive.'));
} else {
reject(err);
}
};
response.body.on("error", handler);
unzipStream.on("error", handler);
unzipStream.on("close", resolve);
response.body.pipe(unzipStream);
});
}
async function checkForFailingResponse(response: Response): Promise<void | never> {
if (response.ok) {
return;
}
// An error downloading the database. Attempt to extract the resaon behind it.
const text = await response.text();
let msg: string;
try {
const obj = JSON.parse(text);
msg = obj.error || obj.message || obj.reason || JSON.stringify(obj, null, 2);
} catch (e) {
msg = text;
}
throw new Error(`Error downloading database.\n\nReason: ${msg}`);
}
function isFile(databaseUrl: string) {
return Uri.parse(databaseUrl).scheme === "file";
}
/**
* Recursively looks for a file in a directory. If the file exists, then returns the directory containing the file.
*
* @param dir The directory to search
* @param toFind The file to recursively look for in this directory
*
* @returns the directory containing the file, or undefined if not found.
*/
// exported for testing
export async function findDirWithFile(
dir: string,
...toFind: string[]
): Promise<string | undefined> {
if (!(await fs.stat(dir)).isDirectory()) {
return;
}
const files = await fs.readdir(dir);
if (toFind.some((file) => files.includes(file))) {
return dir;
}
for (const file of files) {
const newPath = path.join(dir, file);
const result = await findDirWithFile(newPath, ...toFind);
if (result) {
return result;
}
}
return;
}
/**
* The URL pattern is https://lgtm.com/projects/{provider}/{org}/{name}/{irrelevant-subpages}.
* There are several possibilities for the provider: in addition to GitHub.com(g),
* LGTM currently hosts projects from Bitbucket (b), GitLab (gl) and plain git (git).
*
* After the {provider}/{org}/{name} path components, there may be the components
* related to sub pages.
*
* This function accepts any url that matches the patter above
*
* @param lgtmUrl The URL to the lgtm project
*
* @return true if this looks like an LGTM project url
*/
// exported for testing
export function looksLikeLgtmUrl(lgtmUrl: string | undefined): lgtmUrl is string {
if (!lgtmUrl) {
return false;
}
try {
const uri = Uri.parse(lgtmUrl, true);
if (uri.scheme !== "https") {
return false;
}
if (uri.authority !== "lgtm.com" && uri.authority !== "www.lgtm.com") {
return false;
}
const paths = uri.path.split("/").filter((segment) => segment);
return paths.length >= 4 && paths[0] === "projects";
} catch (e) {
return false;
}
}
// exported for testing
export async function convertToDatabaseUrl(lgtmUrl: string) {
try {
const uri = Uri.parse(lgtmUrl, true);
const paths = ["api", "v1.0"].concat(
uri.path.split("/").filter((segment) => segment)
).slice(0, 6);
const projectUrl = `https://lgtm.com/${paths.join("/")}`;
const projectResponse = await fetch(projectUrl);
const projectJson = await projectResponse.json();
if (projectJson.code === 404) {
throw new Error();
}
const language = await promptForLanguage(projectJson);
if (!language) {
return;
}
return `https://lgtm.com/${[
"api",
"v1.0",
"snapshots",
projectJson.id,
language,
].join("/")}`;
} catch (e) {
logger.log(`Error: ${e.message}`);
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
}
}
async function promptForLanguage(
projectJson: any
): Promise<string | undefined> {
if (!projectJson?.languages?.length) {
return;
}
if (projectJson.languages.length === 1) {
return projectJson.languages[0].language;
}
return await window.showQuickPick(
projectJson.languages.map((lang: { language: string }) => lang.language), {
placeHolder: "Select the database language to download:"
}
);
}

View File

@@ -8,6 +8,8 @@ import { logger } from './logging';
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
import * as qsClient from './queryserver-client';
import { upgradeDatabase } from './upgrades';
import { importArchiveDatabase, promptImportInternetDatabase, promptImportLgtmDatabase } from './databaseFetcher';
import * as fs from 'fs-extra';
type ThemableIconPath = { light: string; dark: string } | string;
@@ -94,7 +96,7 @@ class DatabaseTreeDataProvider extends DisposableObject
public getChildren(element?: DatabaseItem): ProviderResult<DatabaseItem[]> {
if (element === undefined) {
return this.databaseManager.databaseItems.slice(0).sort((db1, db2) => {
switch(this.sortOrder) {
switch (this.sortOrder) {
case SortOrder.NameAsc:
return db1.name.localeCompare(db2.name);
case SortOrder.NameDesc:
@@ -147,12 +149,13 @@ function getFirst(list: Uri[] | undefined): Uri | undefined {
* XXX: no validation is done other than checking the directory name
* to make sure it really is a database directory.
*/
async function chooseDatabaseDir(): Promise<Uri | undefined> {
async function chooseDatabaseDir(byFolder: boolean): Promise<Uri | undefined> {
const chosen = await window.showOpenDialog({
openLabel: 'Choose Database',
canSelectFiles: true,
canSelectFolders: true,
canSelectMany: false
openLabel: byFolder ? 'Choose Database folder' : 'Choose Database archive',
canSelectFiles: !byFolder,
canSelectFolders: byFolder,
canSelectMany: false,
filters: byFolder ? {} : { Archives: ['zip'] }
});
return getFirst(chosen);
}
@@ -164,14 +167,19 @@ export class DatabaseUI extends DisposableObject {
ctx: ExtensionContext,
private cliserver: cli.CodeQLCliServer,
private databaseManager: DatabaseManager,
private readonly queryServer: qsClient.QueryServerClient | undefined
private readonly queryServer: qsClient.QueryServerClient | undefined,
private readonly storagePath: string
) {
super();
this.treeDataProvider = this.push(new DatabaseTreeDataProvider(ctx, databaseManager));
this.push(window.createTreeView('codeQLDatabases', { treeDataProvider: this.treeDataProvider }));
ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabase', this.handleChooseDatabase));
logger.log('Registering database panel commands.');
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.chooseDatabaseFolder', this.handleChooseDatabaseFolder));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.chooseDatabaseArchive', this.handleChooseDatabaseArchive));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.chooseDatabaseInternet', this.handleChooseDatabaseInternet));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.chooseDatabaseLgtm', this.handleChooseDatabaseLgtm));
ctx.subscriptions.push(commands.registerCommand('codeQL.setCurrentDatabase', this.handleSetCurrentDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQL.upgradeCurrentDatabase', this.handleUpgradeCurrentDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQL.clearCache', this.handleClearCache));
@@ -188,8 +196,30 @@ export class DatabaseUI extends DisposableObject {
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
}
private handleChooseDatabase = async (): Promise<DatabaseItem | undefined> => {
return await this.chooseAndSetDatabase();
handleChooseDatabaseFolder = async (): Promise<DatabaseItem | undefined> => {
try {
return await this.chooseAndSetDatabase(true);
} catch (e) {
showAndLogErrorMessage(e.message);
return undefined;
}
}
handleChooseDatabaseArchive = async (): Promise<DatabaseItem | undefined> => {
try {
return await this.chooseAndSetDatabase(false);
} catch (e) {
showAndLogErrorMessage(e.message);
return undefined;
}
}
handleChooseDatabaseInternet = async (): Promise<DatabaseItem | undefined> => {
return await promptImportInternetDatabase(this.databaseManager, this.storagePath);
}
handleChooseDatabaseLgtm = async (): Promise<DatabaseItem | undefined> => {
return await promptImportLgtmDatabase(this.databaseManager, this.storagePath);
}
private handleSortByName = async () => {
@@ -267,6 +297,11 @@ export class DatabaseUI extends DisposableObject {
}
private handleSetCurrentDatabase = async (uri: Uri): Promise<DatabaseItem | undefined> => {
// Assume user has selected an archive if the file has a .zip extension
if (uri.path.endsWith('.zip')) {
return await importArchiveDatabase(uri.toString(true), this.databaseManager, this.storagePath);
}
return await this.setCurrentDatabase(uri);
}
@@ -304,7 +339,7 @@ export class DatabaseUI extends DisposableObject {
*/
public async getDatabaseItem(): Promise<DatabaseItem | undefined> {
if (this.databaseManager.currentDatabaseItem === undefined) {
await this.chooseAndSetDatabase();
await this.chooseAndSetDatabase(false);
}
return this.databaseManager.currentDatabaseItem;
@@ -324,13 +359,44 @@ export class DatabaseUI extends DisposableObject {
* Ask the user for a database directory. Returns the chosen database, or `undefined` if the
* operation was canceled.
*/
private async chooseAndSetDatabase(): Promise<DatabaseItem | undefined> {
const uri = await chooseDatabaseDir();
if (uri !== undefined) {
return await this.setCurrentDatabase(uri);
}
else {
private async chooseAndSetDatabase(byFolder: boolean): Promise<DatabaseItem | undefined> {
const uri = await chooseDatabaseDir(byFolder);
if (!uri) {
return undefined;
}
if (byFolder) {
const fixedUri = await this.fixDbUri(uri);
// we are selecting a database folder
return await this.setCurrentDatabase(fixedUri);
}
else {
// we are selecting a database archive. Must unzip into a workspace-controlled area
// before importing.
return await importArchiveDatabase(uri.toString(true), this.databaseManager, this.storagePath);
}
}
/**
* Perform some heuristics to ensure a proper database location is chosen.
*
* 1. If the selected URI to add is a file, choose the containing directory
* 2. If the selected URI is a directory matching db-*, choose the containing directory
* 3. choose the current directory
*
* @param uri a URI that is a datbase folder or inside it
*
* @return the actual database folder found by using the heuristics above.
*/
private async fixDbUri(uri: Uri): Promise<Uri> {
let dbPath = uri.fsPath;
if ((await fs.stat(dbPath)).isFile()) {
dbPath = path.dirname(dbPath);
}
if (path.basename(dbPath).startsWith('db-')) {
dbPath = path.dirname(dbPath);
}
return Uri.file(dbPath);
}
}

View File

@@ -109,10 +109,11 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
return vscode.Uri.file(dbAbsolutePath);
}
async function findSourceArchive(databasePath: string, silent = false):
Promise<vscode.Uri | undefined> {
async function findSourceArchive(
databasePath: string, silent = false
): Promise<vscode.Uri | undefined> {
const relativePaths = ['src', 'output/src_archive']
const relativePaths = ['src', 'output/src_archive'];
for (const relativePath of relativePaths) {
const basePath = path.join(databasePath, relativePath);
@@ -463,13 +464,11 @@ function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | un
}
export class DatabaseManager extends DisposableObject {
private readonly _onDidChangeDatabaseItem =
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
private readonly _onDidChangeDatabaseItem = this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;
private readonly _onDidChangeCurrentDatabaseItem =
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
private readonly _onDidChangeCurrentDatabaseItem = this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
readonly onDidChangeCurrentDatabaseItem = this._onDidChangeCurrentDatabaseItem.event;
private readonly _databaseItems: DatabaseItemImpl[] = [];
@@ -570,7 +569,7 @@ export class DatabaseManager extends DisposableObject {
};
const item = new DatabaseItemImpl(vscode.Uri.parse(state.uri), undefined, fullOptions,
(item) => {
this._onDidChangeDatabaseItem.fire(item)
this._onDidChangeDatabaseItem.fire(item);
});
await this.addDatabaseItem(item);
@@ -669,6 +668,14 @@ export class DatabaseManager extends DisposableObject {
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
}
// Delete folder from file system only if it is controlled by the extension
if (this.isExtensionControlledLocation(item.databaseUri)) {
logger.log(`Deleting database from filesystem.`);
fs.remove(item.databaseUri.path).then(
() => logger.log(`Deleted '${item.databaseUri.path}'`),
e => logger.log(`Failed to delete '${item.databaseUri.path}'. Reason: ${e.message}`));
}
this._onDidChangeDatabaseItem.fire(undefined);
}
@@ -680,6 +687,11 @@ export class DatabaseManager extends DisposableObject {
private updatePersistedDatabaseList(): void {
this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
}
private isExtensionControlledLocation(uri: vscode.Uri) {
const storagePath = this.ctx.storagePath || this.ctx.globalStoragePath;
return uri.path.startsWith(storagePath);
}
}
/**

View File

@@ -34,6 +34,13 @@ function tagOfKeyType(keyType: KeyType): string {
}
}
function nameOfKeyType(keyType: KeyType): string {
switch (keyType) {
case KeyType.DefinitionQuery: return "definitions";
case KeyType.ReferenceQuery: return "references";
}
}
async function resolveQueries(cli: CodeQLCliServer, qlpack: string, keyType: KeyType): Promise<string[]> {
const suiteFile = tmp.fileSync({ postfix: '.qls' }).name;
const suiteYaml = { qlpack, include: { kind: 'definitions', 'tags contain': tagOfKeyType(keyType) } };
@@ -41,7 +48,10 @@ async function resolveQueries(cli: CodeQLCliServer, qlpack: string, keyType: Key
const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
if (queries.length === 0) {
throw new Error("Couldn't find any queries for qlpack");
vscode.window.showErrorMessage(
`No ${nameOfKeyType(keyType)} queries (tagged "${tagOfKeyType(keyType)}") could be found in the current library path. It might be necessary to upgrade the CodeQL libraries.`
);
throw new Error(`Couldn't find any queries tagged ${tagOfKeyType(keyType)} for qlpack ${qlpack}`);
}
return queries;
}
@@ -158,7 +168,7 @@ async function getLinksForUriString(
if (qlpack === undefined) {
throw new Error("Can't infer qlpack from database source archive");
}
const links: FullLocationLink[] = []
const links: FullLocationLink[] = [];
for (const query of await resolveQueries(cli, qlpack, keyType)) {
const templates: messages.TemplateDefinitions = {
[TEMPLATE_NAME]: {

View File

@@ -2,14 +2,15 @@ import * as fetch from "node-fetch";
import * as fs from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as semver from "semver";
import * as unzipper from "unzipper";
import * as url from "url";
import { ExtensionContext, Event } from "vscode";
import { DistributionConfig } from "./config";
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from "./helpers";
import { logger } from "./logging";
import * as helpers from "./helpers";
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
import { getCodeQlCliVersion } from "./cli-version";
/**
* distribution.ts
@@ -35,16 +36,11 @@ const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
/**
* Version constraint for the CLI.
* Range of versions of the CLI that are compatible with the extension.
*
* This applies to both extension-managed and CLI distributions.
*/
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
description: "2.*.*",
isVersionCompatible: (v: Version) => {
return v.majorVersion === 2 && v.minorVersion >= 0
}
}
export const DEFAULT_DISTRIBUTION_VERSION_RANGE: semver.Range = new semver.Range("2.x");
export interface DistributionProvider {
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
@@ -52,44 +48,63 @@ export interface DistributionProvider {
}
export class DistributionManager implements DistributionProvider {
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionConstraint: VersionConstraint) {
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionRange: semver.Range) {
this._config = config;
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionConstraint);
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionRange);
this._onDidChangeDistribution = config.onDidChangeDistributionConfiguration;
this._updateCheckRateLimiter = new InvocationRateLimiter(
extensionContext,
"extensionSpecificDistributionUpdateCheck",
() => this._extensionSpecificDistributionManager.checkForUpdatesToDistribution()
);
this._versionConstraint = versionConstraint;
this._versionRange = versionRange;
}
/**
* Look up a CodeQL launcher binary.
*/
public async getDistribution(): Promise<FindDistributionResult> {
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
if (codeQlPath === undefined) {
const distribution = await this.getDistributionWithoutVersionCheck();
if (distribution === undefined) {
return {
kind: FindDistributionResultKind.NoDistribution,
};
}
const version = await getCodeQlCliVersion(codeQlPath, logger);
if (version !== undefined && !this._versionConstraint.isVersionCompatible(version)) {
const version = await getCodeQlCliVersion(distribution.codeQlPath, logger);
if (version === undefined) {
return {
codeQlPath,
distribution,
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
};
}
/**
* Specifies whether prerelease versions of the CodeQL CLI should be accepted.
*
* Suppose a user sets the includePrerelease config option, obtains a prerelease, then decides
* they no longer want a prerelease, so unsets the includePrerelease config option.
* Unsetting the includePrerelease config option should trigger an update check, and this
* update check should present them an update that returns them back to a non-prerelease
* version.
*
* Therefore, we adopt the following:
*
* - If the user is managing their own CLI, they can use a prerelease without specifying the
* includePrerelease option.
* - If the user is using an extension-managed CLI, then prereleases are only accepted when the
* includePrerelease config option is set.
*/
const includePrerelease = distribution.kind !== DistributionKind.ExtensionManaged || this._config.includePrerelease;
if (!semver.satisfies(version, this._versionRange, { includePrerelease })) {
return {
distribution,
kind: FindDistributionResultKind.IncompatibleDistribution,
version,
};
}
if (version === undefined) {
return {
codeQlPath,
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
}
}
return {
codeQlPath,
distribution,
kind: FindDistributionResultKind.CompatibleDistribution,
version
};
@@ -100,10 +115,15 @@ export class DistributionManager implements DistributionProvider {
return result.kind !== FindDistributionResultKind.NoDistribution;
}
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
const distribution = await this.getDistributionWithoutVersionCheck();
return distribution?.codeQlPath;
}
/**
* Returns the path to a possibly-compatible CodeQL launcher binary, or undefined if a binary not be found.
*/
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
async getDistributionWithoutVersionCheck(): Promise<Distribution | undefined> {
// Check config setting, then extension specific distribution, then PATH.
if (this._config.customCodeQlPath) {
if (!await fs.pathExists(this._config.customCodeQlPath)) {
@@ -121,19 +141,28 @@ export class DistributionManager implements DistributionProvider {
) {
warnDeprecatedLauncher();
}
return this._config.customCodeQlPath;
return {
codeQlPath: this._config.customCodeQlPath,
kind: DistributionKind.CustomPathConfig
};
}
const extensionSpecificCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (extensionSpecificCodeQlPath !== undefined) {
return extensionSpecificCodeQlPath;
return {
codeQlPath: extensionSpecificCodeQlPath,
kind: DistributionKind.ExtensionManaged
};
}
if (process.env.PATH) {
for (const searchDirectory of process.env.PATH.split(path.delimiter)) {
const expectedLauncherPath = await getExecutableFromDirectory(searchDirectory);
if (expectedLauncherPath) {
return expectedLauncherPath;
return {
codeQlPath: expectedLauncherPath,
kind: DistributionKind.PathEnvironmentVariable
};
}
}
logger.log("INFO: Could not find CodeQL on path.");
@@ -150,9 +179,9 @@ export class DistributionManager implements DistributionProvider {
*/
public async checkForUpdatesToExtensionManagedDistribution(
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
const distribution = await this.getDistributionWithoutVersionCheck();
const extensionManagedCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (codeQlPath !== undefined && codeQlPath !== extensionManagedCodeQlPath) {
if (distribution?.codeQlPath !== extensionManagedCodeQlPath) {
// A distribution is present but it isn't managed by the extension.
return createInvalidLocationResult();
}
@@ -171,7 +200,7 @@ export class DistributionManager implements DistributionProvider {
* Returns a failed promise if an unexpected error occurs during installation.
*/
public installExtensionManagedDistributionRelease(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
progressCallback?: helpers.ProgressCallback): Promise<void> {
return this._extensionSpecificDistributionManager.installDistributionRelease(release, progressCallback);
}
@@ -198,14 +227,14 @@ export class DistributionManager implements DistributionProvider {
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
private readonly _updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
private readonly _onDidChangeDistribution: Event<void> | undefined;
private readonly _versionConstraint: VersionConstraint;
private readonly _versionRange: semver.Range;
}
class ExtensionSpecificDistributionManager {
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionConstraint: VersionConstraint) {
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionRange: semver.Range) {
this._extensionContext = extensionContext;
this._config = config;
this._versionConstraint = versionConstraint;
this._versionRange = versionRange;
}
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
@@ -253,14 +282,14 @@ class ExtensionSpecificDistributionManager {
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async installDistributionRelease(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
progressCallback?: helpers.ProgressCallback): Promise<void> {
await this.downloadDistribution(release, progressCallback);
// Store the installed release within the global extension state.
this.storeInstalledRelease(release);
}
private async downloadDistribution(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
progressCallback?: helpers.ProgressCallback): Promise<void> {
try {
await this.removeDistribution();
} catch (e) {
@@ -268,7 +297,18 @@ class ExtensionSpecificDistributionManager {
`but encountered an error: ${e}.`);
}
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(release.assets[0]);
// Filter assets to the unique one that we require.
const requiredAssetName = this.getRequiredAssetName();
const assets = release.assets.filter(asset => asset.name === requiredAssetName);
if (assets.length === 0) {
throw new Error(`Invariant violation: chose a release to install that didn't have ${requiredAssetName}`);
}
if (assets.length > 1) {
logger.log('WARNING: chose a release with more than one asset to install, found ' +
assets.map(asset => asset.name).join(', '));
}
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(assets[0]);
const tmpDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "vscode-codeql"));
try {
@@ -325,12 +365,36 @@ class ExtensionSpecificDistributionManager {
}
}
/**
* Get the name of the codeql cli installation we prefer to install, based on our current platform.
*/
private getRequiredAssetName(): string {
if (os.platform() === 'linux') return 'codeql-linux64.zip';
if (os.platform() === 'darwin') return 'codeql-osx64.zip';
if (os.platform() === 'win32') return 'codeql-win64.zip';
return 'codeql.zip';
}
private async getLatestRelease(): Promise<Release> {
const release = await this.createReleasesApiConsumer().getLatestRelease(this._versionConstraint, this._config.includePrerelease);
if (release.assets.length !== 1) {
throw new Error("Release had an unexpected number of assets");
}
return release;
const requiredAssetName = this.getRequiredAssetName();
logger.log(`Searching for latest release including ${requiredAssetName}.`);
return this.createReleasesApiConsumer().getLatestRelease(
this._versionRange,
this._config.includePrerelease,
release => {
const matchingAssets = release.assets.filter(asset => asset.name === requiredAssetName);
if (matchingAssets.length === 0) {
// For example, this could be a release with no platform-specific assets.
logger.log(`INFO: Ignoring a release with no assets named ${requiredAssetName}`);
return false;
}
if (matchingAssets.length > 1) {
logger.log(`WARNING: Ignoring a release with more than one asset named ${requiredAssetName}`);
return false;
}
return true;
}
);
}
private createReleasesApiConsumer(): ReleasesApiConsumer {
@@ -369,7 +433,7 @@ class ExtensionSpecificDistributionManager {
private readonly _config: DistributionConfig;
private readonly _extensionContext: ExtensionContext;
private readonly _versionConstraint: VersionConstraint;
private readonly _versionRange: semver.Range;
private static readonly _currentDistributionFolderBaseName = "distribution";
private static readonly _currentDistributionFolderIndexStateKey = "distributionFolderIndex";
@@ -390,7 +454,7 @@ export class ReleasesApiConsumer {
this._repoName = repoName;
}
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease = false): Promise<Release> {
public async getLatestRelease(versionRange: semver.Range, includePrerelease = false, additionalCompatibilityCheck?: (release: GithubRelease) => boolean): Promise<Release> {
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json();
const compatibleReleases = allReleases.filter(release => {
@@ -398,20 +462,20 @@ export class ReleasesApiConsumer {
return false;
}
const version = tryParseVersionString(release.tag_name);
if (version === undefined || !versionConstraint.isVersionCompatible(version)) {
const version = semver.parse(release.tag_name);
if (version === null || !semver.satisfies(version, versionRange, { includePrerelease })) {
return false;
}
return true;
return !additionalCompatibilityCheck || additionalCompatibilityCheck(release);
});
// tryParseVersionString must succeed due to the previous filtering step
// Tag names must all be parsable to semvers due to the previous filtering step.
const latestRelease = compatibleReleases.sort((a, b) => {
const versionComparison = versionCompare(tryParseVersionString(b.tag_name)!, tryParseVersionString(a.tag_name)!);
if (versionComparison === 0) {
return b.created_at.localeCompare(a.created_at);
const versionComparison = semver.compare(semver.parse(b.tag_name)!, semver.parse(a.tag_name)!);
if (versionComparison !== 0) {
return versionComparison;
}
return versionComparison;
return b.created_at.localeCompare(a.created_at, "en-US");
})[0];
if (latestRelease === undefined) {
throw new Error("No compatible CodeQL CLI releases were found. " +
@@ -480,7 +544,7 @@ export class ReleasesApiConsumer {
// mechanism is provided.
delete headers["authorization"];
}
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1)
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1);
}
return response;
@@ -511,29 +575,6 @@ export async function extractZipArchive(archivePath: string, outPath: string): P
}));
}
/**
* Comparison of semantic versions.
*
* Returns a positive number if a is greater than b.
* Returns 0 if a equals b.
* Returns a negative number if a is less than b.
*/
export function versionCompare(a: Version, b: Version): number {
if (a.majorVersion !== b.majorVersion) {
return a.majorVersion - b.majorVersion;
}
if (a.minorVersion !== b.minorVersion) {
return a.minorVersion - b.minorVersion;
}
if (a.patchVersion !== b.patchVersion) {
return a.patchVersion - b.patchVersion;
}
if (a.prereleaseVersion !== undefined && b.prereleaseVersion !== undefined) {
return a.prereleaseVersion.localeCompare(b.prereleaseVersion);
}
return 0;
}
function codeQlLauncherName(): string {
return (os.platform() === "win32") ? "codeql.exe" : "codeql";
}
@@ -550,6 +591,17 @@ function isRedirectStatusCode(statusCode: number): boolean {
* Types and helper functions relating to those types.
*/
export enum DistributionKind {
CustomPathConfig,
ExtensionManaged,
PathEnvironmentVariable
}
export interface Distribution {
codeQlPath: string;
kind: DistributionKind;
}
export enum FindDistributionResultKind {
CompatibleDistribution,
UnknownCompatibilityDistribution,
@@ -563,21 +615,27 @@ export type FindDistributionResult =
| IncompatibleDistributionResult
| NoDistributionResult;
interface CompatibleDistributionResult {
codeQlPath: string;
kind: FindDistributionResultKind.CompatibleDistribution;
version: Version;
/**
* A result representing a distribution of the CodeQL CLI that may or may not be compatible with
* the extension.
*/
interface DistributionResult {
distribution: Distribution;
kind: FindDistributionResultKind;
}
interface UnknownCompatibilityDistributionResult {
codeQlPath: string;
interface CompatibleDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.CompatibleDistribution;
version: semver.SemVer;
}
interface UnknownCompatibilityDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.UnknownCompatibilityDistribution;
}
interface IncompatibleDistributionResult {
codeQlPath: string;
interface IncompatibleDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.IncompatibleDistribution;
version: Version;
version: semver.SemVer;
}
interface NoDistributionResult {
@@ -717,7 +775,7 @@ export interface GithubRelease {
assets: GithubReleaseAsset[];
/**
* The creation date of the release on GitHub.
* The creation date of the release on GitHub, in ISO 8601 format.
*/
created_at: string;
@@ -762,11 +820,6 @@ export interface GithubReleaseAsset {
size: number;
}
interface VersionConstraint {
description: string;
isVersionCompatible(version: Version): boolean;
}
export class GithubApiError extends Error {
constructor(public status: number, public body: string) {
super(`API call failed with status code ${status}, body: ${body}`);

View File

@@ -3,11 +3,21 @@ import { LanguageClient } from 'vscode-languageclient';
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { CodeQLCliServer } from './cli';
import { DistributionConfigListener, QueryHistoryConfigListener, QueryServerConfigListener, EXPERIMENTAL_FEATURES_SETTING } from './config';
import { DistributionConfigListener, QueryHistoryConfigListener, QueryServerConfigListener } from './config';
import * as languageSupport from './languageSupport';
import { DatabaseManager } from './databases';
import { DatabaseUI } from './databases-ui';
import { TemplateQueryDefinitionProvider, TemplateQueryReferenceProvider } from './definitions';
import { DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, DistributionManager, DistributionUpdateCheckResultKind, FindDistributionResult, FindDistributionResultKind, GithubApiError, GithubRateLimitedError } from './distribution';
import {
DEFAULT_DISTRIBUTION_VERSION_RANGE,
DistributionKind,
DistributionManager,
DistributionUpdateCheckResultKind,
FindDistributionResult,
FindDistributionResultKind,
GithubApiError,
GithubRateLimitedError
} from './distribution';
import * as helpers from './helpers';
import { assertNever } from './helpers-pure';
import { spawnIdeServer } from './ide-server';
@@ -60,8 +70,9 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
const extension = extensions.getExtension(extensionId);
if (extension === undefined)
if (extension === undefined) {
throw new Error(`Can't find extension ${extensionId}`);
}
const stubbedCommands: string[]
= extension.packageJSON.contributes.commands.map((entry: { command: string }) => entry.command);
@@ -77,10 +88,12 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
logger.log('Starting CodeQL extension');
initializeLogging(ctx);
languageSupport.install();
const distributionConfigListener = new DistributionConfigListener();
ctx.subscriptions.push(distributionConfigListener);
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE;
const distributionManager = new DistributionManager(ctx, distributionConfigListener, codeQlVersionRange);
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
@@ -179,11 +192,25 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
const result = await distributionManager.getDistribution();
switch (result.kind) {
case FindDistributionResultKind.CompatibleDistribution:
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.rawString})`);
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.raw})`);
break;
case FindDistributionResultKind.IncompatibleDistribution:
helpers.showAndLogWarningMessage("The current version of the CodeQL CLI is incompatible with this extension.");
case FindDistributionResultKind.IncompatibleDistribution: {
const fixGuidanceMessage = (() => {
switch (result.distribution.kind) {
case DistributionKind.ExtensionManaged:
return "Please update the CodeQL CLI by running the \"CodeQL: Check for CLI Updates\" command.";
case DistributionKind.CustomPathConfig:
return `Please update the \"CodeQL CLI Executable Path\" setting to point to a CLI in the version range ${codeQlVersionRange}.`;
case DistributionKind.PathEnvironmentVariable:
return `Please update the CodeQL CLI on your PATH to a version compatible with ${codeQlVersionRange}, or ` +
`set the \"CodeQL CLI Executable Path\" setting to the path of a CLI version compatible with ${codeQlVersionRange}.`;
}
})();
helpers.showAndLogWarningMessage(`The current version of the CodeQL CLI (${result.version.raw}) ` +
"is incompatible with this extension. " + fixGuidanceMessage);
break;
}
case FindDistributionResultKind.UnknownCompatibilityDistribution:
helpers.showAndLogWarningMessage("Compatibility with the configured CodeQL CLI could not be determined. " +
"You may experience problems using the extension.");
@@ -249,31 +276,39 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
// of activation.
errorStubs.forEach(stub => stub.dispose());
logger.log('Initializing configuration listener...');
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(distributionManager);
ctx.subscriptions.push(qlConfigurationListener);
logger.log('Initializing CodeQL cli server...');
const cliServer = new CodeQLCliServer(distributionManager, logger);
ctx.subscriptions.push(cliServer);
logger.log('Initializing query server client.');
const qs = new qsClient.QueryServerClient(qlConfigurationListener, cliServer, {
logger: queryServerLogger,
}, task => Window.withProgress({ title: 'CodeQL query server', location: ProgressLocation.Window }, task));
ctx.subscriptions.push(qs);
await qs.startQueryServer();
logger.log('Initializing database manager.');
const dbm = new DatabaseManager(ctx, qlConfigurationListener, logger);
ctx.subscriptions.push(dbm);
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs);
logger.log('Initializing database panel.');
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs, getContextStoragePath(ctx));
ctx.subscriptions.push(databaseUI);
logger.log('Initializing query history manager.');
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
const qhm = new QueryHistoryManager(
ctx,
queryHistoryConfigurationListener,
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced)
);
logger.log('Initializing results panel interface.');
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
logger.log('Initializing source archive filesystem provider.');
archiveFilesystemProvider.activate(ctx);
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
@@ -304,6 +339,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
ctx.subscriptions.push(tmpDirDisposal);
logger.log('Initializing CodeQL language server.');
const client = new LanguageClient('CodeQL Language Server', () => spawnIdeServer(qlConfigurationListener), {
documentSelector: [
{ language: 'ql', scheme: 'file' },
@@ -316,6 +352,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
outputChannel: ideServerLogger.outputChannel
}, true);
logger.log('Initializing QLTest interface.');
const testExplorerExtension = extensions.getExtension<TestHub>(testExplorerExtensionId);
if (testExplorerExtension) {
const testHub = testExplorerExtension.exports;
@@ -326,6 +363,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
ctx.subscriptions.push(testUIService);
}
logger.log('Registering top-level command palette commands.');
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)));
@@ -333,25 +371,37 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
await qs.restartQueryServer();
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger });
}));
ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabaseFolder', () => databaseUI.handleChooseDatabaseFolder()));
ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabaseArchive', () => databaseUI.handleChooseDatabaseArchive()));
ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabaseLgtm', () => databaseUI.handleChooseDatabaseLgtm()));
ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabaseInternet', () => databaseUI.handleChooseDatabaseInternet()));
logger.log('Starting language server.');
ctx.subscriptions.push(client.start());
if (EXPERIMENTAL_FEATURES_SETTING.getValue()) {
languages.registerDefinitionProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryDefinitionProvider(cliServer, qs, dbm)
);
languages.registerReferenceProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryReferenceProvider(cliServer, qs, dbm)
);
}
// Jump-to-definition and find-references
logger.log('Registering jump-to-definition handlers.');
languages.registerDefinitionProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryDefinitionProvider(cliServer, qs, dbm)
);
languages.registerReferenceProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryReferenceProvider(cliServer, qs, dbm)
);
logger.log('Successfully finished extension initialization.');
}
function getContextStoragePath(ctx: ExtensionContext) {
return ctx.storagePath || ctx.globalStoragePath;
}
function initializeLogging(ctx: ExtensionContext): void {
logger.init(ctx);
queryServerLogger.init(ctx);
ideServerLogger.init(ctx);
const storagePath = getContextStoragePath(ctx);
logger.init(storagePath);
queryServerLogger.init(storagePath);
ideServerLogger.init(storagePath);
ctx.subscriptions.push(logger);
ctx.subscriptions.push(queryServerLogger);
ctx.subscriptions.push(ideServerLogger);

View File

@@ -22,6 +22,8 @@ export interface ProgressUpdate {
message: string;
}
export type ProgressCallback = (p: ProgressUpdate) => void;
/**
* This mediates between the kind of progress callbacks we want to
* write (where we *set* current progress position and give
@@ -114,7 +116,7 @@ async function internalShowAndLog(message: string, items: string[], outputLogger
*/
export async function showBinaryChoiceDialog(message: string): Promise<boolean> {
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
const noItem = { title: 'No', isCloseAffordance: true };
const chosenItem = await Window.showInformationMessage(message, { modal: true }, yesItem, noItem);
return chosenItem === yesItem;
}
@@ -138,7 +140,7 @@ export function getOnDiskWorkspaceFolders() {
const diskWorkspaceFolders: string[] = [];
for (const workspaceFolder of workspaceFolders) {
if (workspaceFolder.uri.scheme === "file")
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath)
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath);
}
return diskWorkspaceFolders;
}
@@ -269,7 +271,7 @@ export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemeP
return {
packName,
packDir: dirs[0]
}
};
});
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
@@ -283,7 +285,7 @@ export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemeP
}
export async function resolveDatasetFolder(cliServer: CodeQLCliServer, datasetFolder: string): Promise<DatasetFolderInfo> {
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'));
if (dbschemes.length < 1) {
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
@@ -320,7 +322,7 @@ export class CachedOperation<U> {
const fromCache = this.cached.get(t);
if (fromCache !== undefined) {
// Move to end of lru list
this.lru.push(this.lru.splice(this.lru.findIndex(v => v === t), 1)[0])
this.lru.push(this.lru.splice(this.lru.findIndex(v => v === t), 1)[0]);
return fromCache;
}
// Otherwise check if in progress

View File

@@ -1,6 +1,6 @@
import * as sarif from 'sarif';
import { ResolvableLocationValue } from 'semmle-bqrs';
import { RawResultSet } from './adapt';
import { ParsedResultSets } from './adapt';
/**
* Only ever show this many results per run in interpreted results.
@@ -12,6 +12,11 @@ export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
*/
export const RAW_RESULTS_LIMIT = 10000;
/**
* Show this many rows in a raw result table at a time.
*/
export const RAW_RESULTS_PAGE_SIZE = 100;
export interface DatabaseInfo {
name: string;
databaseUri: string;
@@ -81,9 +86,10 @@ export interface SetStateMsg {
/**
* An experimental way of providing results from the extension.
* Should be undefined unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
* Should be in the WebviewParsedResultSets branch of the type
* unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
*/
resultSets?: RawResultSet[];
parsedResultSets: ParsedResultSets;
}
/** Advance to the next or previous path no in the path viewer */
@@ -101,7 +107,8 @@ export type FromResultsViewMsg =
| ToggleDiagnostics
| ChangeRawResultsSortMsg
| ChangeInterpretedResultsSortMsg
| ResultViewLoaded;
| ResultViewLoaded
| ChangePage;
interface ViewSourceFileMsg {
t: 'viewSourceFile';
@@ -122,6 +129,12 @@ interface ResultViewLoaded {
t: 'resultViewLoaded';
}
interface ChangePage {
t: 'changePage';
pageNumber: number; // 0-indexed, displayed to the user as 1-indexed
selectedTable: string;
}
export enum SortDirection {
asc, desc
}

View File

@@ -0,0 +1,22 @@
import { RawResultSet } from "./adapt";
import { ResultSetSchema } from "semmle-bqrs";
import { Interpretation } from "./interface-types";
export const SELECT_TABLE_NAME = '#select';
export const ALERTS_TABLE_NAME = 'alerts';
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
export type ResultSet =
| RawTableResultSet
| PathTableResultSet;
export function getDefaultResultSet(resultSets: readonly ResultSet[]): string {
return getDefaultResultSetName(resultSets.map(resultSet => resultSet.schema.name));
}
export function getDefaultResultSetName(resultSetNames: readonly string[]): string {
// Choose first available result set from the array
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSetNames[0]].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
}

View File

@@ -10,14 +10,15 @@ import { CodeQLCliServer } from './cli';
import { DatabaseItem, DatabaseManager } from './databases';
import { showAndLogErrorMessage } from './helpers';
import { assertNever } from './helpers-pure';
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types';
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection, RAW_RESULTS_PAGE_SIZE } from './interface-types';
import { Logger } from './logging';
import * as messages from './messages';
import { CompletedQuery, interpretResults } from './query-results';
import { QueryInfo, tmpDir } from './run-queries';
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
import { adaptSchema, adaptBqrs, RawResultSet } from './adapt';
import { adaptSchema, adaptBqrs, RawResultSet, ParsedResultSets } from './adapt';
import { EXPERIMENTAL_BQRS_SETTING } from './config';
import { getDefaultResultSetName } from './interface-utils';
/**
* interface.ts
@@ -115,8 +116,13 @@ function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedR
}
}
function numPagesOfResultSet(resultSet: RawResultSet): number {
return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE);
}
export class InterfaceManager extends DisposableObject {
private _displayedQuery?: CompletedQuery;
private _interpretation?: Interpretation;
private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false;
private _panelLoadedCallBacks: (() => void)[] = [];
@@ -138,6 +144,7 @@ export class InterfaceManager extends DisposableObject {
this.handleSelectionChange.bind(this)
)
);
logger.log('Registering path-step navigation commands.');
this.push(
vscode.commands.registerCommand(
"codeQLQueryResults.nextPathStep",
@@ -287,6 +294,9 @@ export class InterfaceManager extends DisposableObject {
query.updateInterpretedSortState(this.cliServer, msg.sortState)
);
break;
case "changePage":
await this.showPageOfResults(msg.selectedTable, msg.pageNumber);
break;
default:
assertNever(msg);
}
@@ -338,6 +348,7 @@ export class InterfaceManager extends DisposableObject {
);
this._displayedQuery = results;
this._interpretation = interpretation;
const panel = this.getPanel();
await this.waitForPanelLoaded();
@@ -363,18 +374,37 @@ export class InterfaceManager extends DisposableObject {
});
}
let resultSets: RawResultSet[] | undefined;
const getParsedResultSets = async (): Promise<ParsedResultSets> => {
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
resultSets = [];
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath);
for (const schema of schemas["result-sets"]) {
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name);
const resultSetNames = schemas["result-sets"].map(resultSet => resultSet.name);
// This may not wind up being the page we actually show, if there are interpreted results,
// but speculatively send it anyway.
const selectedTable = getDefaultResultSetName(resultSetNames);
const schema = schemas["result-sets"].find(resultSet => resultSet.name == selectedTable)!;
if (schema === undefined) {
return { t: 'WebviewParsed' };
}
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[0]);
const adaptedSchema = adaptSchema(schema);
const resultSet = adaptBqrs(adaptedSchema, chunk);
resultSets.push(resultSet);
return {
t: 'ExtensionParsed',
pageNumber: 0,
numPages: numPagesOfResultSet(resultSet),
resultSet,
selectedTable: undefined,
resultSetNames
};
}
}
else {
return { t: 'WebviewParsed' };
}
};
await this.postMessage({
t: "setState",
@@ -383,7 +413,7 @@ export class InterfaceManager extends DisposableObject {
resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath
),
resultSets,
parsedResultSets: await getParsedResultSets(),
sortedResultsMap,
database: results.database,
shouldKeepOldResultsWhileRendering,
@@ -391,6 +421,59 @@ export class InterfaceManager extends DisposableObject {
});
}
/**
* Show a page of raw results from the chosen table.
*/
public async showPageOfResults(selectedTable: string, pageNumber: number): Promise<void> {
const results = this._displayedQuery;
if (results === undefined) {
throw new Error('trying to view a page of a query that is not loaded');
}
const sortedResultsMap: SortedResultsMap = {};
results.sortedResultsInfo.forEach(
(v, k) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
v
))
);
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
const resultSetNames = schemas["result-sets"].map(resultSet => resultSet.name);
const schema = schemas["result-sets"].find(resultSet => resultSet.name == selectedTable)!;
if (schema === undefined)
throw new Error(`Query result set '${selectedTable}' not found.`);
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[pageNumber]);
const adaptedSchema = adaptSchema(schema);
const resultSet = adaptBqrs(adaptedSchema, chunk);
const parsedResultSets: ParsedResultSets = {
t: 'ExtensionParsed',
pageNumber,
resultSet,
numPages: numPagesOfResultSet(resultSet),
selectedTable: selectedTable,
resultSetNames
};
await this.postMessage({
t: "setState",
interpretation: this._interpretation,
origResultsPaths: results.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath
),
parsedResultSets,
sortedResultsMap,
database: results.database,
shouldKeepOldResultsWhileRendering: false,
metadata: results.query.metadata
});
}
private async getTruncatedResults(
metadata: QueryMetadata | undefined,
resultsPaths: ResultsPaths,
@@ -401,7 +484,7 @@ export class InterfaceManager extends DisposableObject {
const sarif = await interpretResults(
this.cliServer,
metadata,
resultsPaths.resultsPath,
resultsPaths,
sourceInfo
);
// For performance reasons, limit the number of results we try
@@ -439,7 +522,7 @@ export class InterfaceManager extends DisposableObject {
): Promise<Interpretation | undefined> {
let interpretation: Interpretation | undefined = undefined;
if (
(await query.hasInterpretedResults()) &&
(await query.canHaveInterpretedResults()) &&
query.quickEvalPosition === undefined // never do results interpretation if quickEval
) {
try {

View File

@@ -0,0 +1,55 @@
import { IndentAction, languages } from "vscode";
/**
* OnEnterRules are available in language-configurations, but you cannot specify them in the language-configuration.json.
* They can only be specified programmatically.
*
* Also, we should keep the language-configuration.json as a json file and register it in the package.json because
* it is registered first, before the extension is activated, so language features are available quicker.
*
* See https://github.com/microsoft/vscode/issues/11514
* See https://github.com/microsoft/vscode/blob/master/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts
*/
export function install() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const langConfig = require('../language-configuration.json');
// setLanguageConfiguration requires a regexp for the wordpattern, not a string
langConfig.wordPattern = new RegExp(langConfig.wordPattern);
langConfig.onEnterRules = onEnterRules;
langConfig.indentationRules = {
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
};
languages.setLanguageConfiguration('ql', langConfig);
languages.setLanguageConfiguration('qll', langConfig);
languages.setLanguageConfiguration('dbscheme', langConfig);
}
const onEnterRules = [
{
// e.g. /** | */
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' }
}, {
// e.g. /** ...|
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
action: { indentAction: IndentAction.None, appendText: ' * ' }
}, {
// e.g. * ...|
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
oneLineAboveText: /^(\s*(\/\*\*|\*)).*/,
action: { indentAction: IndentAction.None, appendText: '* ' }
}, {
// e.g. */|
beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
action: { indentAction: IndentAction.None, removeText: 1 }
},
{
// e.g. *-----*/|
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
action: { indentAction: IndentAction.None, removeText: 1 }
}
];

View File

@@ -1,4 +1,4 @@
import { window as Window, OutputChannel, Progress, ExtensionContext, Disposable } from 'vscode';
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
import { DisposableObject } from 'semmle-vscode-utils';
import * as fs from 'fs-extra';
import * as path from 'path';
@@ -47,8 +47,8 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
this.push(this.outputChannel);
}
init(ctx: ExtensionContext): void {
this.additionalLogLocationPath = path.join(ctx.storagePath || ctx.globalStoragePath, this.title);
init(storagePath: string): void {
this.additionalLogLocationPath = path.join(storagePath, this.title);
// clear out any old state from previous runs
fs.remove(this.additionalLogLocationPath);

View File

@@ -74,7 +74,7 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
constructor(private ctx: ExtensionContext) {
}
getTreeItem(element: CompletedQuery): vscode.TreeItem {
async getTreeItem(element: CompletedQuery): Promise<vscode.TreeItem> {
const it = new vscode.TreeItem(element.toString());
it.command = {
@@ -83,6 +83,11 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
arguments: [element],
};
// Mark this query history item according to whether it has a
// SARIF file so that we can make context menu items conditionally
// available.
it.contextValue = await element.query.hasInterpretedResults() ? 'interpretedResultsItem' : 'rawResultsItem';
if (!element.didRunSuccessfully) {
it.iconPath = path.join(this.ctx.extensionPath, FAILED_QUERY_HISTORY_ITEM_ICON);
}
@@ -218,11 +223,16 @@ export class QueryHistoryManager {
if (queryHistoryItem.logFileLocation) {
const uri = vscode.Uri.file(queryHistoryItem.logFileLocation);
try {
await vscode.window.showTextDocument(uri, {
});
await vscode.window.showTextDocument(uri);
} catch (e) {
if (e.message.includes('Files above 50MB cannot be synchronized with extensions')) {
const res = await helpers.showBinaryChoiceDialog('File is too large to open in the editor, do you want to open it externally?');
const res = await helpers.showBinaryChoiceDialog(
`VS Code does not allow extensions to open files >50MB. This file
exceeds that limit. Do you want to open it outside of VS Code?
You can also try manually opening it inside VS Code by selecting
the file in the file explorer and dragging it into the workspace.`
);
if (res) {
try {
await vscode.commands.executeCommand('revealFileInOS', uri);
@@ -257,6 +267,22 @@ export class QueryHistoryManager {
}
}
async handleViewSarif(queryHistoryItem: CompletedQuery) {
try {
const hasInterpretedResults = await queryHistoryItem.query.canHaveInterpretedResults();
if (hasInterpretedResults) {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.query.resultsPaths.interpretedResultsPath));
await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
}
else {
const label = queryHistoryItem.getLabel();
helpers.showAndLogInformationMessage(`Query ${label} has no interpreted results.`);
}
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
async getQueryText(queryHistoryItem: CompletedQuery): Promise<string> {
if (queryHistoryItem.options.queryText) {
return queryHistoryItem.options.queryText;
@@ -290,11 +316,13 @@ export class QueryHistoryManager {
this.updateTreeViewSelectionIfVisible();
}
});
logger.log('Registering query history panel commands.');
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.openQuery', this.handleOpenQuery));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.removeHistoryItem', this.handleRemoveHistoryItem.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.setLabel', this.handleSetLabel.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.showQueryLog', this.handleShowQueryLog.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.showQueryText', this.handleShowQueryText.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.viewSarif', this.handleViewSarif.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.itemClicked', async (item) => {
return this.handleItemClicked(item);
}));
@@ -305,7 +333,7 @@ export class QueryHistoryManager {
// displays query text in a read-only document
vscode.workspace.registerTextDocumentContentProvider('codeql', {
provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
const params = new URLSearchParams(uri.query)
const params = new URLSearchParams(uri.query);
return (
JSON.parse(params.get('isQuickEval') || '') ? SHOW_QUERY_TEXT_QUICK_EVAL_MSG : SHOW_QUERY_TEXT_MSG

View File

@@ -5,7 +5,7 @@ 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 { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState, ResultsPaths } from "./interface-types";
import { QueryHistoryConfig } from "./config";
import { QueryHistoryItemOptions } from "./query-history";
@@ -54,13 +54,6 @@ export class CompletedQuery implements QueryWithResults {
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:
@@ -130,9 +123,8 @@ export class CompletedQuery implements QueryWithResults {
/**
* 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"
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
const { resultsPath, interpretedResultsPath } = resultsPaths;
if (await fs.pathExists(interpretedResultsPath)) {
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));
}

View File

@@ -82,7 +82,7 @@ export class QueryServerClient extends DisposableObject {
if (this.serverProcess !== undefined) {
this.disposeAndStopTracking(this.serverProcess);
} else {
this.logger.log('No server process to be stopped.')
this.logger.log('No server process to be stopped.');
}
}
@@ -136,13 +136,13 @@ export class QueryServerClient extends DisposableObject {
this.evaluationResultCallbacks[res.runId](res);
}
return {};
})
});
connection.onNotification(progress, res => {
const callback = this.progressCallbacks[res.id];
if (callback) {
callback(res);
}
})
});
this.serverProcess = new ServerProcess(child, connection, this.opts.logger);
// Ensure the server process is disposed together with this client.
this.track(this.serverProcess);

View File

@@ -87,7 +87,7 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL
return;
}
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME)
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME);
if (index === -1)
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
else

View File

@@ -76,7 +76,7 @@ export class QueryInfo {
): Promise<messages.EvaluationResult> {
let result: messages.EvaluationResult | null = null;
const callbackId = qs.registerCallback(res => { result = res });
const callbackId = qs.registerCallback(res => { result = res; });
const queryToRun: messages.QueryToRun = {
resultsPath: this.resultsPaths.resultsPath,
@@ -85,25 +85,25 @@ export class QueryInfo {
templateValues: this.templates,
id: callbackId,
timeoutSecs: qs.config.timeoutSecs,
}
};
const dataset: messages.Dataset = {
dbDir: this.dataset.fsPath,
workingSet: 'default'
}
};
const params: messages.EvaluateQueriesParams = {
db: dataset,
evaluateId: callbackId,
queries: [queryToRun],
stopOnError: false,
useSequenceHint: false
}
};
try {
await helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running Query",
cancellable: true,
}, (progress, token) => {
return qs.sendRequest(messages.runQueries, params, token, progress)
return qs.sendRequest(messages.runQueries, params, token, progress);
});
} finally {
qs.unRegisterCallback(callbackId);
@@ -157,15 +157,22 @@ export class QueryInfo {
}
/**
* Holds if this query should produce interpreted results.
* Holds if this query can in principle produce interpreted results.
*/
async hasInterpretedResults(): Promise<boolean> {
async canHaveInterpretedResults(): Promise<boolean> {
const hasMetadataFile = await this.dbItem.hasMetadataFile();
if (!hasMetadataFile) {
logger.log("Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.");
}
return hasMetadataFile;
}
/**
* Holds if this query actually has produced interpreted results.
*/
async hasInterpretedResults(): Promise<boolean> {
return fs.pathExists(this.resultsPaths.interpretedResultsPath);
}
}
export interface QueryWithResults {
@@ -263,7 +270,7 @@ async function checkDbschemeCompatibility(
const { scripts, finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
const hash = async function(filename: string): Promise<string> {
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
}
};
// At this point, we have learned about three dbschemes:
@@ -308,7 +315,7 @@ async function promptUserToSaveChanges(document: vscode.TextDocument): Promise<b
else {
const yesItem = { title: 'Yes', isCloseAffordance: false };
const alwaysItem = { title: 'Always Save', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
const noItem = { title: 'No', isCloseAffordance: true };
const message = 'Query file has unsaved changes. Save now?';
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, yesItem, alwaysItem, noItem);

View File

@@ -1,5 +1,5 @@
import * as Sarif from "sarif"
import * as path from "path"
import * as Sarif from "sarif";
import * as path from "path";
import { LocationStyle, ResolvableLocationValue } from "semmle-bqrs";
export interface SarifLink {

View File

@@ -87,8 +87,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
private readonly _tests = this.push(
new EventEmitter<TestLoadStartedEvent | TestLoadFinishedEvent>());
private readonly _testStates = this.push(
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
TestEvent>());
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent>());
private readonly _autorun = this.push(new EventEmitter<void>());
private runningTask?: vscode.CancellationTokenSource = undefined;
@@ -108,9 +107,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
return this._tests.event;
}
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
TestEvent> {
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent> {
return this._testStates.event;
}
@@ -118,9 +115,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
return this._autorun.event;
}
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]):
(TestSuiteInfo | TestInfo)[] {
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]): (TestSuiteInfo | TestInfo)[] {
return testNodes.map((childNode) => {
return QLTestAdapter.createTestOrSuiteInfo(childNode);
});
@@ -129,11 +124,9 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
private static createTestOrSuiteInfo(testNode: QLTestNode): TestSuiteInfo | TestInfo {
if (testNode instanceof QLTestFile) {
return QLTestAdapter.createTestInfo(testNode);
}
else if (testNode instanceof QLTestDirectory) {
} else if (testNode instanceof QLTestDirectory) {
return QLTestAdapter.createTestSuiteInfo(testNode, testNode.name);
}
else {
} else {
throw new Error('Unexpected test type.');
}
}
@@ -148,9 +141,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
};
}
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string):
TestSuiteInfo {
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string): TestSuiteInfo {
return {
type: 'suite',
id: testDirectory.path,

View File

@@ -5,6 +5,7 @@ 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';
import { logger } from './logging';
type VSCodeTestEvent = TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent;
@@ -32,6 +33,7 @@ export class TestUIService extends UIService implements TestController {
constructor(private readonly testHub: TestHub) {
super();
logger.log('Registering CodeQL test panel commands.');
this.registerCommand('codeQLTests.showOutputDifferences', this.showOutputDifferences);
this.registerCommand('codeQLTests.acceptOutput', this.acceptOutput);

View File

@@ -80,7 +80,7 @@ async function checkAndConfirmDatabaseUpgrade(
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
const noItem = { title: 'No', isCloseAffordance: true };
const dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
let messageLines = descriptionMessage.split('\n');
@@ -129,7 +129,7 @@ export async function upgradeDatabase(
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
qs.logger.log('Done compiling database upgrade.');
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
@@ -148,7 +148,7 @@ export async function upgradeDatabase(
return;
}
finally {
qs.logger.log('Done running database upgrade.')
qs.logger.log('Done running database upgrade.');
}
}
@@ -168,7 +168,7 @@ async function compileDatabaseUpgrade(
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,

View File

@@ -5,9 +5,10 @@ import * as Keys from '../result-keys';
import { LocationStyle } from 'semmle-bqrs';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results';
import { onNavigation, NavigationEvent, vscode } from './results';
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
import { PathTableResultSet } from '../interface-utils';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState {
@@ -124,7 +125,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
...previousState,
selectedPathNode: pathNodeKey
}));
}
};
};
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {

View File

@@ -1,12 +1,14 @@
import * as React from "react";
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
import { RawTableResultSet, vscode } from "./results";
import { vscode } from "./results";
import { ResultValue } from "../adapt";
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
import { RawTableResultSet } from "../interface-utils";
export type RawTableProps = ResultTableProps & {
resultSet: RawTableResultSet;
sortState?: RawResultsSortState;
offset: number;
};
export class RawTable extends React.Component<RawTableProps, {}> {
@@ -28,7 +30,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
<tr key={rowIndex} {...zebraStripe(rowIndex)}>
{
[
<td key={-1}>{rowIndex + 1}</td>,
<td key={-1}>{rowIndex + 1 + this.props.offset}</td>,
...row.map((value, columnIndex) =>
<td key={columnIndex}>
{
@@ -89,7 +91,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
*/
function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element {
if (typeof v === 'string') {
return <span>{v}</span>
return <span>{v}</span>;
}
else if ('uri' in v) {
return <a href={v.uri}>{v.uri}</a>;

View File

@@ -1,8 +1,9 @@
import * as React from 'react';
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
import { ResultSet, vscode } from './results';
import { vscode } from './results';
import { assertNever } from '../helpers-pure';
import { ResultSet } from '../interface-utils';
export interface ResultTableProps {
resultSet: ResultSet;
@@ -10,6 +11,7 @@ export interface ResultTableProps {
metadata?: QueryMetadata;
resultsPath: string | undefined;
sortState?: RawResultsSortState;
offset: number;
/**
* Holds if there are any raw results. When that is the case, we
@@ -78,7 +80,7 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
return <span title={title}>{displayLabel}</span>;
}
}
return <span />
return <span />;
}
/**
@@ -95,7 +97,7 @@ 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)
: zebraStripe(index, ...otherClasses);
}
/**

View File

@@ -1,14 +1,17 @@
import * as React from 'react';
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState } from '../interface-types';
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState, RAW_RESULTS_PAGE_SIZE } from '../interface-types';
import { PathTable } from './alert-table';
import { RawTable } from './raw-results-table';
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils';
import { ResultSet, vscode } from './results';
import { vscode } from './results';
import { ParsedResultSets, ExtensionParsedResultSets } from '../adapt';
import { ResultSet, ALERTS_TABLE_NAME, SELECT_TABLE_NAME, getDefaultResultSet } from '../interface-utils';
/**
* Properties for the `ResultTables` component.
*/
export interface ResultTablesProps {
parsedResultSets: ParsedResultSets;
rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined;
database: DatabaseInfo;
@@ -25,10 +28,9 @@ export interface ResultTablesProps {
*/
interface ResultTablesState {
selectedTable: string; // name of selected result set
selectedPage: string; // stringified selected page
}
const ALERTS_TABLE_NAME = 'alerts';
const SELECT_TABLE_NAME = '#select';
const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text";
function getResultCount(resultSet: ResultSet): number {
@@ -75,23 +77,66 @@ export class ResultTables
return resultSets;
}
private getResultSetNames(resultSets: ResultSet[]): string[] {
if (this.props.parsedResultSets.t === 'ExtensionParsed') {
return this.props.parsedResultSets.resultSetNames.concat([ALERTS_TABLE_NAME]);
}
else {
return resultSets.map(resultSet => resultSet.schema.name);
}
}
/**
* Holds if we have a result set obtained from the extension that came
* from the ExtensionParsed branch of ParsedResultSets. This is evidence
* that the user has the experimental flag turned on that allows extension-side
* bqrs parsing.
*/
paginationAllowed(): boolean {
return this.props.parsedResultSets.t === 'ExtensionParsed';
}
/**
* Holds if we actually should show pagination interface right now. This is
* still false for the time being when we're viewing alerts.
*/
paginationEnabled(): boolean {
return this.paginationAllowed() &&
this.props.parsedResultSets.selectedTable !== ALERTS_TABLE_NAME &&
this.state.selectedTable !== ALERTS_TABLE_NAME;
}
constructor(props: ResultTablesProps) {
super(props);
this.state = {
// Get the result set that should be displayed by default
selectedTable: ResultTables.getDefaultResultSet(this.getResultSets())
};
}
const selectedTable = props.parsedResultSets.selectedTable || getDefaultResultSet(this.getResultSets());
private static getDefaultResultSet(resultSets: readonly ResultSet[]): string {
const resultSetNames = resultSets.map(resultSet => resultSet.schema.name)
// Choose first available result set from the array
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSets[0].schema.name].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
let selectedPage: string;
switch (props.parsedResultSets.t) {
case 'ExtensionParsed':
selectedPage = (props.parsedResultSets.pageNumber + 1) + '';
break;
case 'WebviewParsed':
selectedPage = '';
break;
}
this.state = { selectedTable, selectedPage };
}
private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
this.setState({ selectedTable: event.target.value });
const selectedTable = event.target.value;
const fetchPageFromExtension = this.paginationAllowed() && selectedTable !== ALERTS_TABLE_NAME;
if (fetchPageFromExtension) {
vscode.postMessage({
t: 'changePage',
pageNumber: 0,
selectedTable
});
}
else
this.setState({ selectedTable });
}
private alertTableExtras(): JSX.Element | undefined {
@@ -115,27 +160,84 @@ export class ResultTables
return <div className={alertExtrasClassName}>
{displayProblemsAsAlertsToggle}
</div>
</div>;
}
getOffset(): number {
const { parsedResultSets } = this.props;
switch (parsedResultSets.t) {
case 'ExtensionParsed':
return parsedResultSets.pageNumber * RAW_RESULTS_PAGE_SIZE;
case 'WebviewParsed':
return 0;
}
}
renderPageButtons(resultSets: ExtensionParsedResultSets): JSX.Element {
const selectedTable = this.state.selectedTable;
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ selectedPage: e.target.value });
};
const choosePage = (input: string) => {
const pageNumber = parseInt(input);
if (pageNumber !== undefined && !isNaN(pageNumber)) {
const actualPageNumber = Math.max(0, Math.min(pageNumber - 1, resultSets.numPages - 1));
vscode.postMessage({
t: 'changePage',
pageNumber: actualPageNumber,
selectedTable,
});
}
};
const prevPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
vscode.postMessage({
t: 'changePage',
pageNumber: Math.max(resultSets.pageNumber - 1, 0),
selectedTable,
});
};
const nextPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
vscode.postMessage({
t: 'changePage',
pageNumber: Math.min(resultSets.pageNumber + 1, resultSets.numPages - 1),
selectedTable,
});
};
return <span>
<button onClick={prevPage} >&lt;</button>
<input value={this.state.selectedPage} onChange={onChange}
onBlur={e => choosePage(e.target.value)}
onKeyDown={e => { if (e.keyCode === 13) choosePage((e.target as HTMLInputElement).value); }}
/>
<button value=">" onClick={nextPage} >&gt;</button>
</span>;
}
renderButtons(): JSX.Element {
if (this.props.parsedResultSets.t === 'ExtensionParsed' && this.paginationEnabled())
return this.renderPageButtons(this.props.parsedResultSets);
else
return <span />;
}
render(): React.ReactNode {
const { selectedTable } = this.state;
const resultSets = this.getResultSets();
const resultSetNames = this.getResultSetNames(resultSets);
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
const nonemptyRawResults = resultSets.some(resultSet => resultSet.t == 'RawResultSet' && resultSet.rows.length > 0);
const numberOfResults = resultSet && renderResultCountString(resultSet);
const resultSetOptions =
resultSetNames.map(name => <option key={name} value={name}>{name}</option>);
return <div>
{this.renderButtons()}
<div className={tableSelectionHeaderClassName}>
<select value={selectedTable} onChange={this.onTableSelectionChange}>
{
resultSets.map(resultSet =>
<option key={resultSet.schema.name} value={resultSet.schema.name}>
{resultSet.schema.name}
</option>
)
}
{resultSetOptions}
</select>
{numberOfResults}
{selectedTable === ALERTS_TABLE_NAME ? this.alertTableExtras() : undefined}
@@ -152,7 +254,8 @@ export class ResultTables
resultsPath={this.props.resultsPath}
sortState={this.props.sortStates.get(resultSet.schema.name)}
nonemptyRawResults={nonemptyRawResults}
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }) }} />
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }); }}
offset={this.getOffset()} />
}
</div>;
}

View File

@@ -1,12 +1,13 @@
import * as React from 'react';
import * as Rdom from 'react-dom';
import * as bqrs from 'semmle-bqrs';
import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, tryGetResolvableLocation } from 'semmle-bqrs';
import { assertNever } from '../helpers-pure';
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';
import { RawResultSet, ResultValue, ResultRow } from '../adapt';
import { ResultValue, ResultRow, ParsedResultSets } from '../adapt';
import { ResultSet } from '../interface-utils';
/**
* results.tsx
@@ -24,13 +25,6 @@ interface VsCodeApi {
declare const acquireVsCodeApi: () => VsCodeApi;
export const vscode = acquireVsCodeApi();
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
export type ResultSet =
| RawTableResultSet
| PathTableResultSet;
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
if (!response.ok) {
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
@@ -107,8 +101,8 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
}
interface ResultsInfo {
parsedResultSets: ParsedResultSets;
resultsPath: string;
resultSets: ResultSet[] | undefined;
origResultsPaths: ResultsPaths;
database: DatabaseInfo;
interpretation: Interpretation | undefined;
@@ -169,7 +163,7 @@ class App extends React.Component<{}, ResultsViewState> {
case 'setState':
this.updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath,
resultSets: msg.resultSets?.map(x => ({ t: 'RawResultSet', ...x })),
parsedResultSets: msg.parsedResultSets,
origResultsPaths: msg.origResultsPaths,
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
database: msg.database,
@@ -221,6 +215,16 @@ class App extends React.Component<{}, ResultsViewState> {
});
}
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
const parsedResultSets = resultsInfo.parsedResultSets;
switch (parsedResultSets.t) {
case 'WebviewParsed': return await this.fetchResultSets(resultsInfo);
case 'ExtensionParsed': {
return [{ t: 'RawResultSet', ...parsedResultSets.resultSet }];
}
}
}
private async loadResults(): Promise<void> {
const resultsInfo = this.state.nextResultsInfo;
if (resultsInfo === null) {
@@ -230,7 +234,7 @@ class App extends React.Component<{}, ResultsViewState> {
let results: Results | null = null;
let statusText = '';
try {
const resultSets = resultsInfo.resultSets || await this.getResultSets(resultsInfo);
const resultSets = await this.getResultSets(resultsInfo);
results = {
resultSets,
database: resultsInfo.database,
@@ -261,11 +265,15 @@ class App extends React.Component<{}, ResultsViewState> {
},
nextResultsInfo: null,
isExpectingResultsUpdate: false
}
};
});
}
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
/**
* This is deprecated, because it calls `fetch`. We are moving
* towards doing all bqrs parsing in the extension.
*/
private async fetchResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
const unsortedResponse = await fetch(resultsInfo.resultsPath);
const unsortedResultSets = await parseResultSets(unsortedResponse);
return Promise.all(unsortedResultSets.map(async unsortedResultSet => {
@@ -291,7 +299,10 @@ class App extends React.Component<{}, ResultsViewState> {
render(): JSX.Element {
const displayedResults = this.state.displayedResults;
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
return <ResultTables rawResultSets={displayedResults.results.resultSets}
const parsedResultSets = displayedResults.resultsInfo.parsedResultSets;
return <ResultTables
parsedResultSets={parsedResultSets}
rawResultSets={displayedResults.results.resultSets}
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
database={displayedResults.results.database}
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
@@ -325,4 +336,4 @@ Rdom.render(
document.getElementById('root')
);
vscode.postMessage({ t: "resultViewLoaded" })
vscode.postMessage({ t: "resultViewLoaded" });

View File

@@ -75,7 +75,7 @@
}
.vscode-codeql__result-table-location-link {
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
text-decoration: none;
}
select {

View File

@@ -1,8 +1,10 @@
import { expect } from "chai";
import * as path from "path";
import { ArchiveFileSystemProvider, decodeSourceArchiveUri, encodeSourceArchiveUri, ZipFileReference } from "../../archive-filesystem-provider";
import { expect } from 'chai';
import * as path from 'path';
describe("archive filesystem provider", () => {
import { encodeSourceArchiveUri, ArchiveFileSystemProvider, decodeSourceArchiveUri, ZipFileReference } from '../../archive-filesystem-provider';
import { FileType, FileSystemError } from 'vscode';
describe('archive-filesystem-provider', () => {
it("reads empty file correctly", async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
@@ -12,6 +14,98 @@ describe("archive filesystem provider", () => {
const data = await archiveProvider.readFile(uri);
expect(data.length).to.equal(0);
});
it("read non-empty file correctly", async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/zip_with_folder.zip"),
pathWithinSourceArchive: "folder1/textFile.txt"
});
const data = await archiveProvider.readFile(uri);
expect(Buffer.from(data).toString('utf8')).to.be.equal('I am a text\n');
});
it("read a directory", async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/zip_with_folder.zip"),
pathWithinSourceArchive: "folder1"
});
const files = await archiveProvider.readDirectory(uri);
expect(files).to.be.deep.equal([
['folder2', FileType.Directory],
['textFile.txt', FileType.File],
['textFile2.txt', FileType.File],
]);
});
it('should handle a missing directory', async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/zip_with_folder.zip"),
pathWithinSourceArchive: "folder1/not-here"
});
try {
await archiveProvider.readDirectory(uri);
throw new Error('Failed');
} catch (e) {
expect(e).to.be.instanceOf(FileSystemError);
}
});
it('should handle a missing file', async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/zip_with_folder.zip"),
pathWithinSourceArchive: "folder1/not-here"
});
try {
await archiveProvider.readFile(uri);
throw new Error('Failed');
} catch (e) {
expect(e).to.be.instanceOf(FileSystemError);
}
});
it('should handle reading a file as a directory', async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/zip_with_folder.zip"),
pathWithinSourceArchive: "folder1/textFile.txt"
});
try {
await archiveProvider.readDirectory(uri);
throw new Error('Failed');
} catch (e) {
expect(e).to.be.instanceOf(FileSystemError);
}
});
it('should handle reading a directory as a file', async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/zip_with_folder.zip"),
pathWithinSourceArchive: "folder1/folder2"
});
try {
await archiveProvider.readFile(uri);
throw new Error('Failed');
} catch (e) {
expect(e).to.be.instanceOf(FileSystemError);
}
});
it("read a nested directory", async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/zip_with_folder.zip"),
pathWithinSourceArchive: "folder1/folder2"
});
const files = await archiveProvider.readDirectory(uri);
expect(files).to.be.deep.equal([
['textFile3.txt', FileType.File],
]);
});
});
describe('source archive uri encoding', function() {

View File

@@ -1,50 +0,0 @@
import { expect } from "chai";
import "mocha";
import { tryParseVersionString } from "../../cli-version";
describe("Version parsing", () => {
it("should accept version without prerelease and build metadata", () => {
const v = tryParseVersionString("3.2.4")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.be.undefined;
});
it("should accept v at the beginning of the version", () => {
const v = tryParseVersionString("v3.2.4")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.be.undefined;
});
it("should accept version with prerelease", () => {
const v = tryParseVersionString("v3.2.4-alpha.0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.equal("alpha.0");
expect(v.buildMetadata).to.be.undefined;
});
it("should accept version with prerelease and build metadata", () => {
const v = tryParseVersionString("v3.2.4-alpha.0+abcdef0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.equal("alpha.0");
expect(v.buildMetadata).to.equal("abcdef0");
});
it("should accept version with build metadata", () => {
const v = tryParseVersionString("v3.2.4+abcdef0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.equal("abcdef0");
});
});

View File

@@ -0,0 +1,140 @@
import "vscode-test";
import "mocha";
import * as chaiAsPromised from "chai-as-promised";
import * as sinon from "sinon";
// import * as sinonChai from 'sinon-chai';
import * as path from "path";
import * as fs from "fs-extra";
import * as tmp from "tmp";
import * as chai from "chai";
import { window } from "vscode";
import {
convertToDatabaseUrl,
looksLikeLgtmUrl,
findDirWithFile,
} from "../../databaseFetcher";
chai.use(chaiAsPromised);
const expect = chai.expect;
describe("databaseFetcher", () => {
describe("convertToDatabaseUrl", () => {
let quickPickSpy: sinon.SinonStub;
beforeEach(() => {
quickPickSpy = sinon.stub(window, "showQuickPick");
});
afterEach(() => {
(window.showQuickPick as sinon.SinonStub).restore();
});
it("should convert a project url to a database url", async () => {
quickPickSpy.returns("javascript" as any);
const lgtmUrl = "https://lgtm.com/projects/g/github/codeql";
const dbUrl = await convertToDatabaseUrl(lgtmUrl);
expect(dbUrl).to.equal(
"https://lgtm.com/api/v1.0/snapshots/1506465042581/javascript"
);
expect(quickPickSpy.firstCall.args[0]).to.contain("javascript");
expect(quickPickSpy.firstCall.args[0]).to.contain("python");
});
it("should convert a project url to a database url with extra path segments", async () => {
quickPickSpy.returns("python" as any);
const lgtmUrl =
"https://lgtm.com/projects/g/github/codeql/subpage/subpage2?query=xxx";
const dbUrl = await convertToDatabaseUrl(lgtmUrl);
expect(dbUrl).to.equal(
"https://lgtm.com/api/v1.0/snapshots/1506465042581/python"
);
});
it("should fail on a nonexistant prohect", async () => {
quickPickSpy.returns("javascript" as any);
const lgtmUrl = "https://lgtm.com/projects/g/github/hucairz";
expect(convertToDatabaseUrl(lgtmUrl)).to.rejectedWith(/Invalid LGTM URL/);
});
});
describe("looksLikeLgtmUrl", () => {
it("should handle invalid urls", () => {
expect(looksLikeLgtmUrl("")).to.be.false;
expect(looksLikeLgtmUrl("http://lgtm.com/projects/g/github/codeql")).to.be
.false;
expect(looksLikeLgtmUrl("https://ww.lgtm.com/projects/g/github/codeql"))
.to.be.false;
expect(looksLikeLgtmUrl("https://ww.lgtm.com/projects/g/github")).to.be
.false;
});
it("should handle valid urls", () => {
expect(looksLikeLgtmUrl("https://lgtm.com/projects/g/github/codeql")).to
.be.true;
expect(looksLikeLgtmUrl("https://www.lgtm.com/projects/g/github/codeql"))
.to.be.true;
expect(
looksLikeLgtmUrl("https://lgtm.com/projects/g/github/codeql/sub/pages")
).to.be.true;
expect(
looksLikeLgtmUrl(
"https://lgtm.com/projects/g/github/codeql/sub/pages?query=string"
)
).to.be.true;
});
});
describe("findDirWithFile", () => {
let dir: tmp.DirResult;
beforeEach(() => {
dir = tmp.dirSync({ unsafeCleanup: true });
createFile("a");
createFile("b");
createFile("c");
createDir("dir1");
createFile("dir1", "d");
createFile("dir1", "e");
createFile("dir1", "f");
createDir("dir2");
createFile("dir2", "g");
createFile("dir2", "h");
createFile("dir2", "i");
createDir("dir2", "dir3");
createFile("dir2", "dir3", "j");
createFile("dir2", "dir3", "k");
createFile("dir2", "dir3", "l");
});
it("should find files", async () => {
expect(await findDirWithFile(dir.name, "k")).to.equal(
path.join(dir.name, "dir2", "dir3")
);
expect(await findDirWithFile(dir.name, "h")).to.equal(
path.join(dir.name, "dir2")
);
expect(await findDirWithFile(dir.name, "z", "a")).to.equal(dir.name);
// there's some slight indeterminism when more than one name exists
// but in general, this will find files in the current directory before
// finding files in sub-dirs
expect(await findDirWithFile(dir.name, "k", "a")).to.equal(dir.name);
});
it("should not find files", async () => {
expect(await findDirWithFile(dir.name, "x", "y", "z")).to.be.undefined;
});
function createFile(...segments: string[]) {
fs.createFileSync(path.join(dir.name, ...segments));
}
function createDir(...segments: string[]) {
fs.mkdirSync(path.join(dir.name, ...segments));
}
});
});

View File

@@ -0,0 +1,47 @@
import 'vscode-test';
import 'mocha';
import * as tmp from 'tmp';
import * as path from 'path';
import * as fs from 'fs-extra';
import { expect } from 'chai';
import { Uri } from 'vscode';
import { DatabaseUI } from '../../databases-ui';
describe('databases-ui', () => {
describe('fixDbUri', () => {
const fixDbUri = (DatabaseUI.prototype as any).fixDbUri;
it('should choose current directory direcory normally', async () => {
const dir = tmp.dirSync().name;
const uri = await fixDbUri(Uri.file(dir));
expect(uri.toString()).to.eq(Uri.file(dir).toString());
});
it('should choose parent direcory when file is selected', async () => {
const file = tmp.fileSync().name;
const uri = await fixDbUri(Uri.file(file));
expect(uri.toString()).to.eq(Uri.file(path.dirname(file)).toString());
});
it('should choose parent direcory when db-* is selected', async () => {
const dir = tmp.dirSync().name;
const dbDir = path.join(dir, 'db-hucairz');
await fs.mkdirs(dbDir);
const uri = await fixDbUri(Uri.file(dbDir));
expect(uri.toString()).to.eq(Uri.file(dir).toString());
});
it('should choose parent\'s parent direcory when file selected is in db-*', async () => {
const dir = tmp.dirSync().name;
const dbDir = path.join(dir, 'db-hucairz');
const file = path.join(dbDir, 'nested');
await fs.mkdirs(dbDir);
await fs.createFile(file);
const uri = await fixDbUri(Uri.file(file));
expect(uri.toString()).to.eq(Uri.file(dir).toString());
});
});
});

View File

@@ -1,14 +1,14 @@
import * as chai from "chai";
import * as path from "path";
import * as fetch from "node-fetch";
import 'chai/register-should';
import * as sinonChai from 'sinon-chai';
import * as sinon from 'sinon';
import "chai/register-should";
import * as semver from "semver";
import * as sinonChai from "sinon-chai";
import * as sinon from "sinon";
import * as pq from "proxyquire";
import "mocha";
import { Version } from "../../cli-version";
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer, versionCompare } from "../../distribution";
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer } from "../../distribution";
const proxyquire = pq.noPreserveCache();
chai.use(sinonChai);
@@ -17,46 +17,58 @@ const expect = chai.expect;
describe("Releases API consumer", () => {
const owner = "someowner";
const repo = "somerepo";
const sampleReleaseResponse: GithubRelease[] = [
{
"assets": [],
"created_at": "2019-09-01T00:00:00Z",
"id": 1,
"name": "",
"prerelease": false,
"tag_name": "v2.1.0"
},
{
"assets": [],
"created_at": "2019-08-10T00:00:00Z",
"id": 2,
"name": "",
"prerelease": false,
"tag_name": "v3.1.1"
},
{
"assets": [],
"created_at": "2019-09-05T00:00:00Z",
"id": 3,
"name": "",
"prerelease": false,
"tag_name": "v2.0.0"
},
{
"assets": [],
"created_at": "2019-08-11T00:00:00Z",
"id": 4,
"name": "",
"prerelease": true,
"tag_name": "v3.1.2-pre"
},
];
const unconstrainedVersionConstraint = {
description: "*",
isVersionCompatible: () => true
};
const unconstrainedVersionRange = new semver.Range("*");
describe("picking the latest release", () => {
const sampleReleaseResponse: GithubRelease[] = [
{
"assets": [],
"created_at": "2019-09-01T00:00:00Z",
"id": 1,
"name": "",
"prerelease": false,
"tag_name": "v2.1.0"
},
{
"assets": [],
"created_at": "2019-08-10T00:00:00Z",
"id": 2,
"name": "",
"prerelease": false,
"tag_name": "v3.1.1"
},
{
"assets": [{
id: 1,
name: "exampleAsset.txt",
size: 1
}],
"created_at": "2019-09-05T00:00:00Z",
"id": 3,
"name": "",
"prerelease": false,
"tag_name": "v2.0.0"
},
{
"assets": [],
"created_at": "2019-08-11T00:00:00Z",
"id": 4,
"name": "",
"prerelease": true,
"tag_name": "v3.1.2-pre-1.1"
},
// Release ID 5 is older than release ID 4 but its version has a higher precedence, so release
// ID 5 should be picked over release ID 4.
{
"assets": [],
"created_at": "2019-08-09T00:00:00Z",
"id": 5,
"name": "",
"prerelease": true,
"tag_name": "v3.1.2-pre-2.0"
},
];
it("picking latest release: is based on version", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
@@ -66,45 +78,55 @@ describe("Releases API consumer", () => {
}
}
const consumer = new MockReleasesApiConsumer(owner, repo);
it("picked release has version with the highest precedence", async () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionConstraint);
expect(latestRelease.id).to.equal(2);
});
it("picking latest release: obeys version constraints", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
}
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
}
}
const consumer = new MockReleasesApiConsumer(owner, repo);
const latestRelease = await consumer.getLatestRelease({
description: "2.*.*",
isVersionCompatible: version => version.majorVersion === 2
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionRange);
expect(latestRelease.id).to.equal(2);
});
expect(latestRelease.id).to.equal(1);
});
it("picking latest release: includes prereleases when option set", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
}
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
}
}
it("version of picked release is within the version range", async () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
const consumer = new MockReleasesApiConsumer(owner, repo);
const latestRelease = await consumer.getLatestRelease(new semver.Range("2.*.*"));
expect(latestRelease.id).to.equal(1);
});
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionConstraint, true);
expect(latestRelease.id).to.equal(4);
it("fails if none of the releases are within the version range", async () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
await chai.expect(
consumer.getLatestRelease(new semver.Range("5.*.*"))
).to.be.rejectedWith(Error);
});
it("picked release passes additional compatibility test if an additional compatibility test is specified", async () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
const latestRelease = await consumer.getLatestRelease(
new semver.Range("2.*.*"),
true,
release => release.assets.some(asset => asset.name === "exampleAsset.txt")
);
expect(latestRelease.id).to.equal(3);
});
it("fails if none of the releases pass the additional compatibility test", async () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
await chai.expect(consumer.getLatestRelease(
new semver.Range("2.*.*"),
true,
release => release.assets.some(asset => asset.name === "otherExampleAsset.txt")
)).to.be.rejectedWith(Error);
});
it("picked release is the most recent prerelease when includePrereleases is set", async () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionRange, true);
expect(latestRelease.id).to.equal(5);
});
});
it("gets correct assets for a release", async () => {
@@ -141,7 +163,7 @@ describe("Releases API consumer", () => {
const consumer = new MockReleasesApiConsumer(owner, repo);
const assets = (await consumer.getLatestRelease(unconstrainedVersionConstraint)).assets;
const assets = (await consumer.getLatestRelease(unconstrainedVersionRange)).assets;
expect(assets.length).to.equal(expectedAssets.length);
expectedAssets.map((expectedAsset, index) => {
@@ -152,41 +174,6 @@ describe("Releases API consumer", () => {
});
});
describe("Release version ordering", () => {
function createVersion(majorVersion: number, minorVersion: number, patchVersion: number, prereleaseVersion?: string, buildMetadata?: string): Version {
return {
buildMetadata,
majorVersion,
minorVersion,
patchVersion,
prereleaseVersion,
rawString: `${majorVersion}.${minorVersion}.${patchVersion}` +
prereleaseVersion ? `-${prereleaseVersion}` : "" +
buildMetadata ? `+${buildMetadata}` : ""
};
}
it("major versions compare correctly", () => {
expect(versionCompare(createVersion(3, 0, 0), createVersion(2, 9, 9)) > 0).to.be.true;
});
it("minor versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 0), createVersion(2, 0, 9)) > 0).to.be.true;
});
it("patch versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 2), createVersion(2, 1, 1)) > 0).to.be.true;
});
it("prerelease versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 0, "alpha.2"), createVersion(2, 1, 0, "alpha.1")) > 0).to.true;
});
it("build metadata compares correctly", () => {
expect(versionCompare(createVersion(2, 1, 0, "alpha.1", "abcdef0"), createVersion(2, 1, 0, "alpha.1", "bcdef01"))).to.equal(0);
});
});
describe('Launcher path', () => {
const pathToCmd = `abc${path.sep}codeql.cmd`;
const pathToExe = `abc${path.sep}codeql.exe`;

View File

@@ -6,7 +6,7 @@ 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 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 ",
@@ -15,7 +15,7 @@ describe('parsing sarif', () => {
});
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 message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\]](1).";
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Prohibited term used in ",
@@ -23,14 +23,14 @@ describe('parsing sarif', () => {
]);
});
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 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 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

@@ -32,7 +32,7 @@ describe('webview uri conversion', function() {
return {
fileUriOnDisk,
panel
}
};
}
it('should correctly round trip from filesystem to webview and back', function() {

View File

@@ -1,4 +1,5 @@
import * as path from 'path';
import * as os from 'os';
import { runTests } from 'vscode-test';
// A subset of the fields in TestOptions from vscode-test, which we
@@ -11,25 +12,29 @@ type Suite = {
};
/**
* 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.
* Run an integration test suite `suite`, retrying if it segfaults, at
* most `tries` times.
*/
async function runTestsWithRetry(suite: Suite, tries: number): Promise<void> {
async function runTestsWithRetryOnSegfault(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...');
if (err === 'SIGSEGV') {
console.error('Test runner segfaulted.');
if (t < tries - 1)
console.error('Retrying...');
}
else if (os.platform() === 'win32') {
console.error(`Test runner caught exception (${err})`);
if (t < tries - 1)
console.error('Retrying...');
}
else {
throw err;
}
}
}
console.error(`Tried running suite ${tries} time(s), still failed, giving up.`);
@@ -67,7 +72,7 @@ async function main() {
];
for (const integrationTestSuite of integrationTestSuites) {
await runTestsWithRetry(integrationTestSuite, 3);
await runTestsWithRetryOnSegfault(integrationTestSuite, 3);
}
} catch (err) {
console.error(`Unexpected exception while running tests: ${err}`);

View File

@@ -1,3 +1,3 @@
predicate foo() {
1 == 1
}
1 = 1
}

View File

@@ -0,0 +1,101 @@
import { expect } from 'chai';
import * as path from 'path';
import * as fs from 'fs-extra';
type CmdDecl = {
command: string;
when?: string;
title?: string;
}
describe('commands declared in package.json', function() {
const manifest = fs.readJsonSync(path.join(__dirname, '../../package.json'));
const commands = manifest.contributes.commands;
const menus = manifest.contributes.menus;
const disabledInPalette: Set<string> = new Set<string>();
// These commands should appear in the command palette, and so
// should be prefixed with 'CodeQL: '.
const paletteCmds: Set<string> = new Set<string>();
// These commands arising on context menus in non-CodeQL controlled
// panels, (e.g. file browser) and so should be prefixed with 'CodeQL: '.
const contribContextMenuCmds: Set<string> = new Set<string>();
// These are commands used in CodeQL controlled panels, and so don't need any prefixing in their title.
const scopedCmds: Set<string> = new Set<string>();
const commandTitles: { [cmd: string]: string } = {};
commands.forEach((commandDecl: CmdDecl) => {
const { command, title } = commandDecl;
if (command.match(/^codeQL\./)
|| command.match(/^codeQLQueryResults\./)
|| command.match(/^codeQLTests\./)) {
paletteCmds.add(command);
expect(title).not.to.be.undefined;
commandTitles[command] = title!;
}
else if (command.match(/^codeQLDatabases\./)
|| command.match(/^codeQLQueryHistory\./)) {
scopedCmds.add(command);
expect(title).not.to.be.undefined;
commandTitles[command] = title!;
}
else {
expect.fail(`Unexpected command name ${command}`);
}
});
menus['explorer/context'].forEach((commandDecl: CmdDecl) => {
const { command } = commandDecl;
paletteCmds.delete(command);
contribContextMenuCmds.add(command);
});
menus['editor/context'].forEach((commandDecl: CmdDecl) => {
const { command } = commandDecl;
paletteCmds.delete(command);
contribContextMenuCmds.add(command);
});
menus.commandPalette.forEach((commandDecl: CmdDecl) => {
if (commandDecl.when === 'false')
disabledInPalette.add(commandDecl.command);
});
it('should have commands appropriately prefixed', function() {
paletteCmds.forEach(command => {
expect(commandTitles[command], `command ${command} should be prefixed with 'CodeQL: ', since it is accessible from the command palette`).to.match(/^CodeQL: /);
});
contribContextMenuCmds.forEach(command => {
expect(commandTitles[command], `command ${command} should be prefixed with 'CodeQL: ', since it is accessible from a context menu in a non-extension-controlled context`).to.match(/^CodeQL: /);
});
scopedCmds.forEach(command => {
expect(commandTitles[command], `command ${command} should not be prefixed with 'CodeQL: ', since it is accessible from an extension-controlled context`).not.to.match(/^CodeQL: /);
});
});
it('should have the right commands accessible from the command palette', function() {
paletteCmds.forEach(command => {
expect(disabledInPalette.has(command), `command ${command} should be enabled in the command palette`).to.be.false;
});
// Commands in contribContextMenuCmds may reasonbly be enabled or
// disabled in the command palette; for example, codeQL.runQuery
// is available there, since we heuristically figure out which
// query to run, but codeQL.setCurrentDatabase is not.
scopedCmds.forEach(command => {
expect(disabledInPalette.has(command), `command ${command} should be disabled in the command palette`).to.be.true;
});
});
});

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai';
import 'mocha';
import { LocationStyle, StringLocation, tryGetWholeFileLocation } from 'semmle-bqrs';
import { LocationStyle, StringLocation, tryGetResolvableLocation } from 'semmle-bqrs';
describe('processing string locations', function () {
it('should detect Windows whole-file locations', function () {
@@ -8,7 +8,7 @@ describe('processing string locations', function () {
t: LocationStyle.String,
loc: 'file://C:/path/to/file.ext:0:0:0:0'
};
const wholeFileLoc = tryGetWholeFileLocation(loc);
const wholeFileLoc = tryGetResolvableLocation(loc);
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: 'C:/path/to/file.ext'});
});
it('should detect Unix whole-file locations', function () {
@@ -16,12 +16,27 @@ describe('processing string locations', function () {
t: LocationStyle.String,
loc: 'file:///path/to/file.ext:0:0:0:0'
};
const wholeFileLoc = tryGetWholeFileLocation(loc);
const wholeFileLoc = tryGetResolvableLocation(loc);
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: '/path/to/file.ext'});
});
it('should detect Unix 5-part locations', function () {
const loc: StringLocation = {
t: LocationStyle.String,
loc: 'file:///path/to/file.ext:1:2:3:4'
};
const wholeFileLoc = tryGetResolvableLocation(loc);
expect(wholeFileLoc).to.eql({
t: LocationStyle.FivePart,
file: '/path/to/file.ext',
lineStart: 1,
colStart: 2,
lineEnd: 3,
colEnd: 4
});
});
it('should ignore other string locations', function () {
for (const loc of ['file:///path/to/file.ext', 'I am not a location']) {
const wholeFileLoc = tryGetWholeFileLocation({
const wholeFileLoc = tryGetResolvableLocation({
t: LocationStyle.String,
loc: loc
});

View File

@@ -48,18 +48,7 @@ describe('OutputChannelLogger tests', () => {
});
it('should create a side log in the workspace area', async () => {
await sideLogTest('storagePath', 'globalStoragePath');
});
it('should create a side log in the global area', async () => {
await sideLogTest('globalStoragePath', 'storagePath');
});
async function sideLogTest(expectedArea: string, otherArea: string): Promise<void> {
logger.init({
[expectedArea]: tempFolders[expectedArea].name,
[otherArea]: undefined
});
logger.init(tempFolders.storagePath.name);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -67,19 +56,16 @@ describe('OutputChannelLogger tests', () => {
await logger.log('aaa');
// expect 2 side logs
const testLoggerFolder = path.join(tempFolders[expectedArea].name, 'test-logger');
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
expect(fs.readdirSync(tempFolders[otherArea].name).length).to.equal(0);
// contents
expect(fs.readFileSync(path.join(testLoggerFolder, 'first'), 'utf8')).to.equal('xxx\nzzz');
expect(fs.readFileSync(path.join(testLoggerFolder, 'second'), 'utf8')).to.equal('yyy\n');
}
});
it('should delete side logs on dispose', async () => {
logger.init({
storagePath: tempFolders.storagePath.name
});
logger.init(tempFolders.storagePath.name);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -94,9 +80,7 @@ describe('OutputChannelLogger tests', () => {
});
it('should remove an additional log location', async () => {
logger.init({
storagePath: tempFolders.storagePath.name
});
logger.init(tempFolders.storagePath.name);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -112,9 +96,7 @@ describe('OutputChannelLogger tests', () => {
it('should delete an existing folder on init', async () => {
fs.createFileSync(path.join(tempFolders.storagePath.name, 'test-logger', 'xxx'));
logger.init({
storagePath: tempFolders.storagePath.name
});
logger.init(tempFolders.storagePath.name);
// should be empty dir
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');

View File

@@ -33,7 +33,7 @@ class Checkpoint<T> {
constructor() {
this.res = () => { /**/ };
this.rej = () => { /**/ };
this.promise = new Promise((res, rej) => { this.res = res; this.rej = rej; })
this.promise = new Promise((res, rej) => { this.res = res; this.rej = rej; });
}
async done(): Promise<T> {
@@ -114,7 +114,7 @@ describe('using the query server', function() {
async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
return codeQlPath;
},
}, logger)
}, logger);
qs = new qsClient.QueryServerClient(
{
codeQlPath,
@@ -187,7 +187,7 @@ describe('using the query server', function() {
const db: messages.Dataset = {
dbDir: path.join(__dirname, '../test-db'),
workingSet: 'default',
}
};
const params: messages.EvaluateQueriesParams = {
db,
evaluateId: callbackId,

View File

@@ -1,4 +1,4 @@
import { LocationStyle } from './bqrs-schema';
import { LocationStyle } from "./bqrs-schema";
// See https://help.semmle.com/QL/learn-ql/ql/locations.html for how these are used.
export interface FivePartLocation {
@@ -31,54 +31,69 @@ export type LocationValue = RawLocationValue | WholeFileLocation;
/** A location that may be resolved to a source code element. */
export type ResolvableLocationValue = FivePartLocation | WholeFileLocation;
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
* to describe the location of an entire filesystem resource.
* Such locations appear as `StringLocation`s instead of `FivePartLocation`s.
*
*
* Folder resources also get similar URLs, but with the `folder` scheme.
* They are deliberately ignored here, since there is no suitable location to show the user.
*/
const WHOLE_FILE_LOCATION_REGEX = /file:\/\/(.+):0:0:0:0/;
const FILE_LOCATION_REGEX = /file:\/\/(.+):([0-9]+):([0-9]+):([0-9]+):([0-9]+)/;
/**
* Gets a resolvable source file location for the specified `LocationValue`, if possible.
* @param loc The location to test.
*/
export function tryGetResolvableLocation(loc: LocationValue | undefined): ResolvableLocationValue | undefined {
export function tryGetResolvableLocation(
loc: LocationValue | undefined
): ResolvableLocationValue | undefined {
if (loc === undefined) {
return undefined;
}
else if ((loc.t === LocationStyle.FivePart) && loc.file) {
} else if (loc.t === LocationStyle.FivePart && loc.file) {
return loc;
}
else if ((loc.t === LocationStyle.WholeFile) && loc.file) {
} else if (loc.t === LocationStyle.WholeFile && loc.file) {
return loc;
}
else if ((loc.t === LocationStyle.String) && loc.loc) {
return tryGetWholeFileLocation(loc);
}
else {
} else if (loc.t === LocationStyle.String && loc.loc) {
return tryGetLocationFromString(loc);
} else {
return undefined;
}
}
export function tryGetWholeFileLocation(loc: StringLocation): WholeFileLocation | undefined {
const matches = WHOLE_FILE_LOCATION_REGEX.exec(loc.loc);
export function tryGetLocationFromString(
loc: StringLocation
): ResolvableLocationValue | undefined {
const matches = FILE_LOCATION_REGEX.exec(loc.loc);
if (matches && matches.length > 1 && matches[1]) {
// Whole-file location.
// We could represent this as a FivePartLocation with all numeric fields set to zero,
// but that would be a deliberate misuse as those fields are intended to be 1-based.
return {
t: LocationStyle.WholeFile,
file: matches[1]
};
if (isWholeFileMatch(matches)) {
return {
t: LocationStyle.WholeFile,
file: matches[1],
};
} else {
return {
t: LocationStyle.FivePart,
file: matches[1],
lineStart: Number(matches[2]),
colStart: Number(matches[3]),
lineEnd: Number(matches[4]),
colEnd: Number(matches[5]),
}
}
} else {
return undefined;
}
}
function isWholeFileMatch(matches: RegExpExecArray): boolean {
return (
matches[2] === "0" &&
matches[3] === "0" &&
matches[4] === "0" &&
matches[5] === "0"
);
}
export interface ElementBase {
id: PrimitiveColumnValue;
label?: string;
@@ -93,8 +108,7 @@ export interface ElementWithLocation extends ElementBase {
location: LocationValue;
}
export interface Element extends Required<ElementBase> {
}
export interface Element extends Required<ElementBase> {}
export type PrimitiveColumnValue = string | boolean | number | Date;
export type ColumnValue = PrimitiveColumnValue | ElementBase;

View File

@@ -9,7 +9,8 @@
"pnpmOptions": {
"strictPeerDependencies": true
},
"nodeSupportedVersionRange": ">=10.13.0 <13.0.0",
"nodeSupportedVersionRange": ">=10.13.0 <15.0.0",
"suppressNodeLtsWarning": true,
"ensureConsistentVersions": true,
"projectFolderMinDepth": 2,
"projectFolderMaxDepth": 2,

View File

@@ -1,5 +1,6 @@
{
"newLineCharacter": "\n",
"convertTabsToSpaces": true,
"indentStyle": 2,
"insertSpaceAfterCommaDelimiter": true,
"insertSpaceAfterSemicolonInForStatements": true,