Compare commits

...

428 Commits

Author SHA1 Message Date
jcreedcmu
379b69a0e9 Merge pull request #489 from jcreedcmu/jcreed/v1.3.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-07-07 14:34:28 -04:00
Jason Reed
c4353981fa update CHANGELOG for release 2020-07-07 14:28:53 -04:00
jcreedcmu
cc7fb39be7 Merge pull request #488 from jcreedcmu/jcreed/query-text
Fix display of quick-query query text
2020-07-07 11:57:07 -04:00
Jason Reed
d8266b7bc1 Update CHANGELOG. 2020-07-07 08:40:07 -04:00
Jason Reed
d50277380b Fix display of quick-query query text 2020-07-07 08:39:03 -04:00
Andrew Eisenberg
3e149e7bb3 Update changelog 2020-07-06 07:32:35 -07:00
Andrew Eisenberg
00e252d48a Change styling on pagination section 2020-07-06 07:32:35 -07:00
Andrew Eisenberg
6a2832fcc7 Update changelog 2020-07-06 07:21:12 -07:00
Andrew Eisenberg
a7d99cc7e2 Fix nested problem adding database starting with db-* 2020-07-06 07:21:12 -07:00
jcreedcmu
454e8471a4 Merge pull request #481 from jcreedcmu/jcreed/interpreted-pagination
Allow pagination for interpreted results
2020-07-06 07:53:06 -04:00
Jason Reed
e2d125a558 Fix broken raw queries. 2020-07-02 10:32:38 -04:00
Jason Reed
e345425051 Fix wrong number of pages shown on initial display. 2020-07-02 10:27:15 -04:00
Jason Reed
0b32961f6d Unbreak typing page number into field 2020-07-02 09:39:47 -04:00
Jason Reed
e0a58a86fc Inline now-trivial function paginationEnabled 2020-07-01 09:20:31 -04:00
Jason Reed
ec45db3bc3 Appease linter. 2020-06-30 11:13:10 -04:00
Jason Reed
94d230308c Allow switching to alerts table 2020-06-30 10:17:46 -04:00
Jason Reed
96688e3379 Fix listing of tables when in alerts view 2020-06-30 10:14:14 -04:00
Jason Reed
88c27618b1 Show number of results correctly 2020-06-30 09:54:13 -04:00
Jason Reed
11c538a99d Teach webview to do pagination in interpreted results 2020-06-30 09:39:04 -04:00
Jason Reed
0e3b7a8eb5 Also show pagination interface for interpreted results 2020-06-29 12:52:19 -04:00
Jason Reed
65aa6928e4 Show number of pages in pagination interface 2020-06-29 12:38:48 -04:00
Jason Reed
fe02a58e45 Teach extension to accept ShowInterpretedPageMsg 2020-06-29 12:36:09 -04:00
Jason Reed
4030ddbdc2 Teach extension how to request display of alerts page 2020-06-29 11:57:44 -04:00
Jason Reed
b3642bd62e Compute truncation functionally rather than imperative update 2020-06-29 11:44:17 -04:00
Jason Reed
addddb0095 Factor out truncation of results from interpretation itself 2020-06-29 11:10:32 -04:00
Jason Reed
d7732c4ed6 Add interpreted results page size 2020-06-29 11:02:30 -04:00
Andrew Eisenberg
6e34c03b05 Update changelog 2020-06-26 11:40:40 -07:00
Andrew Eisenberg
75518a5d01 Ensure source folders are zipped
Zips source folders of databases when they are added. Only if
the databases are fully controlled by VS Code.

Fixes #479
2020-06-26 11:40:40 -07:00
Andrew Eisenberg
4beead54be Fix file extension of generated query suite
See https://github.com/github/codeql-coreql-team/issues/452
2020-06-26 11:40:40 -07:00
Andrew Eisenberg
7379f4996a Add the zip-a-folder package 2020-06-26 11:40:40 -07:00
Andrew Eisenberg
c40b8fe1a5 Remove unused code path
`resolveRawDataset` can not be called.
2020-06-26 07:56:59 -07:00
Andrew Eisenberg
210bbcd2e9 Avoid using a synchronous file system command
Add the tmp-promise package to allow for async tmp file
operations.
2020-06-25 13:25:10 -07:00
jcreedcmu
461892759b Merge pull request #474 from shati-patel/edits
Small editorials tweaks
2020-06-24 07:41:57 -04:00
Shati Patel
6277e5cecb Small editorials tweaks 2020-06-24 11:33:48 +01:00
Andrew Eisenberg
42ebc3fbe6 Update changelog 2020-06-23 13:27:20 -07:00
Andrew Eisenberg
77b13bd8e3 Ensure query compare order matches expectation
A user typically expects that the first selection would be
the query that they are comparing _from_ and the second query
is being compared _to_.

This commit ensures that something like this expectation will
always hold.

So, when there are two queries selected, the first one selected
will always be _from_ and appear on the left side of the compare
view. The one selected later will be _to_ and appear on the right.

There is a corner case when there are 3 or more selected queries
and a user *unselects* a query. We do not track the selection
order of the remaining two queries.
2020-06-23 13:27:20 -07:00
Andrew Eisenberg
f4e983e214 Change command name 2020-06-23 09:58:07 -07:00
Andrew Eisenberg
60620a5618 Update changelog 2020-06-23 08:14:05 -07:00
Andrew Eisenberg
25bac72ac5 Use Open instead of Extract to open zip files
This allows opening zip files whose local headers are not correct.
2020-06-23 08:14:05 -07:00
github-actions[bot]
e7ee1f86a8 Bump version to v1.3.1 2020-06-22 16:07:21 -07:00
jcreedcmu
1f3decc83a Merge pull request #466 from jcreedcmu/jcreed/1.3.0
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
Bump version and CHANGELOG for release
2020-06-22 14:23:51 -04:00
Jason Reed
c2ebaa2422 Bump version and CHANGELOG for release 2020-06-22 14:20:09 -04:00
Andrew Eisenberg
6f46bcc459 Add better error message when comparing queries
Also, fix type error.
2020-06-22 11:19:39 -07:00
Andrew Eisenberg
6ae6e91195 Add maximum number of queries to run
Throw error if user tries to run more than that.
2020-06-22 09:08:12 -07:00
Andrew Eisenberg
fabef96f08 Update changelog 2020-06-22 09:08:12 -07:00
Andrew Eisenberg
3a23f05a0a Add command to run multiple queries at once from file explorer
New command called `codeQL.runQueries`.

When invoked, gather all selected files and folders, and recursively
search for ql files to run. Warn the user if a directory is selected.
See comment inline for reason.
2020-06-22 09:08:12 -07:00
Andrew Eisenberg
52c6ee4477 Update vscode settings
Recommend formatting and linting extension. Also, use 100 columns
for formatting.
2020-06-22 09:08:12 -07:00
Andrew Eisenberg
727d0db387 Update changelog 2020-06-18 09:55:43 -07:00
Andrew Eisenberg
86f10fa41f Add multi-select support for db view
These commands now work on multiple databases:
* remove databases
* open external folder
* upgrade databases
2020-06-18 09:55:43 -07:00
Andrew Eisenberg
3d44b987d7 Update changelog 2020-06-17 10:54:08 -07:00
Andrew Eisenberg
bd6a6ff40d Add Multi-select support to query history view
This is not quite ideal due to
https://github.com/microsoft/vscode/issues/99767

Allow multiselection in the query-history view. For commands
that shouldn't accept multiple options, show a user message
to that effect.

For remove query, allow multiple removals at once.

For compare query, allow selecting of exactly two queries.
Otherwise, throw an error. Also, verify that the selected queries
are compatible to compare.
2020-06-17 10:54:08 -07:00
jcreedcmu
dd44bf74e3 Merge pull request #452 from jcreedcmu/jcreed/master-to-main
Change references to master branch to main branch
2020-06-17 08:37:16 -04:00
Andrew Eisenberg
95988f0960 Update changelog 2020-06-16 12:08:33 -07:00
Andrew Eisenberg
ab41be243b Vertically align the two compare tables
Avoids an issue when one table has many more rows than the other and
the tables are off-centered.
2020-06-16 12:08:33 -07:00
Andrew Eisenberg
75fe8fb040 Display error message when can't compare queries
* The error message will be displayed instead of the empty results
  tables.
* Also, uncomment onEnterRules. That should never have been committed.
* Also, extract CompareTable to its own component.
2020-06-16 12:08:33 -07:00
Andrew Eisenberg
15d65b308c Re-apply formatting to all files 2020-06-16 12:08:33 -07:00
Andrew Eisenberg
9be355aa9d Add link to open query results from compare view 2020-06-16 12:08:33 -07:00
Andrew Eisenberg
b803a80d39 Add unit tests for interface-utils.ts
Also, some moving around of functions and whitespace changes.
2020-06-16 12:08:33 -07:00
Andrew Eisenberg
fceea64a08 More work on diffs 2020-06-16 12:08:33 -07:00
Andrew Eisenberg
e9fbd6d430 Add the compare-interface
This module will behave like the interface.ts module and handle
interactions between the compare webview and the extension.
2020-06-16 12:08:33 -07:00
Andrew Eisenberg
2ab4c1ac14 Extract shared components out of interface.ts
This is in preparation for creating a new webview, extract shared
functionality to the webview-utils file
2020-06-16 12:08:33 -07:00
Andrew Eisenberg
e38a34edce Introduce the skeleton compare view
Adds the tsx file and updates the webpack config so that this new tsx
file is properly compiled.
2020-06-16 12:08:33 -07:00
jcreedcmu
ed04ae9364 Merge pull request #451 from jcreedcmu/jcreed/tiny-promisify-fix
Remove unnecessary promisify.
2020-06-16 14:03:28 -04:00
Jason Reed
963ff9f458 Change references to master branch to main branch
I have already pushed `main` upstream pointing at the same commit as
master, so this PR should be safe to merge. To the best of my
knowledge all that's necessary after that is to change the
default branch
in https://github.com/github/vscode-codeql/settings/branches.
2020-06-16 14:00:02 -04:00
Jason Reed
dfb7a8fd54 Remove unnecessary promisify.
fs-extra already has an async overload for `readdir`.
2020-06-16 13:18:52 -04:00
Andrew Eisenberg
ff8e72a318 Run eslint --fix on the extension 2020-06-16 08:44:37 -07:00
Andrew Eisenberg
45dc2a29cf Auto-format on commit and use single quotes 2020-06-16 08:44:37 -07:00
Andrew Eisenberg
c7ee9fa8c7 Fail linting if there are any warnings 2020-06-16 08:44:37 -07:00
jcreedcmu
1f3707f74e Merge pull request #450 from jcreedcmu/jcreed/gitattributes
Use union merge strategy for CHANGELOG.md
2020-06-15 15:35:07 -04:00
Jason Reed
249ab78249 Use union merge strategy for CHANGELOG.md 2020-06-15 15:31:19 -04:00
Andrew Eisenberg
a6ed674816 Compare against title property
Co-authored-by: jcreedcmu <jcreed@gmail.com>
2020-06-15 12:22:01 -07:00
Andrew Eisenberg
3c6169fe23 Add unit tests for tryOpenExternalFile 2020-06-15 12:22:01 -07:00
Andrew Eisenberg
4bc17ed333 Update changelog 2020-06-15 12:22:01 -07:00
Andrew Eisenberg
39a1524ad1 Ask user to open sarif file externally when too large
Use the same mechanism that we are using for log files to open
large sarif files. This is because the extension is not
capable of opening large (>50MB) files due to vscode restrictions.
2020-06-15 12:22:01 -07:00
Andrew Eisenberg
081aab7acb Clarify comment on locale compare 2020-06-15 12:18:40 -07:00
Andrew Eisenberg
7440e0d779 Update changelog 2020-06-15 12:18:40 -07:00
Andrew Eisenberg
7fae9ee175 Explicitly use the workspace-configured locale
When the `env.language` value is not passed as the locale, OS-default
locale is used. This change ensures that we use the workspace locale
where we want and explicitly calls out where we should continue to use
the OS-default.
2020-06-15 12:18:40 -07:00
Andrew Eisenberg
058c89114a Update changelog 2020-06-15 12:11:42 -07:00
Andrew Eisenberg
4680614455 Add descriptive message for archive import failure 2020-06-15 12:11:42 -07:00
Andrew Eisenberg
d360153d69 Update changelog 2020-06-15 12:04:25 -07:00
Andrew Eisenberg
2baae8481a Catch and report error when selecting invalid database 2020-06-15 12:04:25 -07:00
Andrew Eisenberg
bba2f0217b Force undefined version if version command fails
The assumption is that the cli is old or corrupted.
In either case we want to upgrade.
2020-06-15 08:06:49 -07:00
Dave Bartolomeo
7898463a27 Merge pull request #432 from dbartol/dbartol/utilities
Make a few internal packages publishable
2020-06-10 10:35:20 -04:00
Dave Bartolomeo
07d9bdb5fa Fix file paths in launch.json 2020-06-09 10:18:10 -04:00
Dave Bartolomeo
7c38af29ff Publish utility packages 2020-06-09 10:17:50 -04:00
Dave Bartolomeo
e9397bbba2 Change notes 2020-06-09 09:58:25 -04:00
Dave Bartolomeo
aa232849fd Support packages from multiple registries 2020-06-09 09:57:18 -04:00
Dave Bartolomeo
69dd8f5d89 Merge remote-tracking branch 'upstream/master' into dbartol/utilities 2020-06-09 08:36:30 -04:00
Dave Bartolomeo
c68c9e6b57 Rename @github/codeql-build-tasks to @github/codeql-gulp-tasks 2020-06-09 08:36:01 -04:00
Andrew Eisenberg
6b7cc9659f Update changelog with new section 2020-06-08 12:54:56 -07:00
github-actions[bot]
8e28c432bd Bump version to v1.2.3 2020-06-08 11:39:21 -07:00
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
Dave Bartolomeo
4c5361b611 Publish utility packages 2020-06-06 02:55:53 -04:00
Dave Bartolomeo
31ee9af939 Change notes 2020-06-06 02:54:23 -04:00
Dave Bartolomeo
8f49386a4a Remove unnecessary dependency on typescript 2020-06-06 02:53:02 -04:00
Dave Bartolomeo
69abf60581 Add missing peer dependency on glob 2020-06-06 02:52:47 -04:00
Dave Bartolomeo
9a7fdf8dda Publish utility packages 2020-06-06 02:46:43 -04:00
Dave Bartolomeo
d3caf77f90 Change notes 2020-06-05 17:45:58 -04:00
Dave Bartolomeo
4d90751638 Rename build-tasks to @github/codeql-build-tasks 2020-06-05 17:38:13 -04:00
Dave Bartolomeo
b436468ca9 Rename semmle-vscode-utils -> @github/codeql-vscode-utils
Also adds a version policy to support publishing as an npm package.
2020-06-05 17:25:52 -04: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
jcreedcmu
e0383b3f9a Merge pull request #355 from jcreedcmu/jcreed/1.1.3
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-08 12:41:15 -04:00
Jason Reed
0d972d7916 Update CHANGELOG for release. 2020-05-08 12:34:37 -04:00
jcreedcmu
ab020f24ae Merge pull request #354 from aeisenberg/aesienberg/database-commands
Rename database and open database directory
2020-05-08 12:30:57 -04:00
jcreedcmu
81cbf26910 Merge branch 'master' into aesienberg/database-commands 2020-05-08 12:08:59 -04:00
Andrew Eisenberg
2e2f101131 Update changelog 2020-05-07 21:58:16 -07:00
Andrew Eisenberg
610d40c99c Add a command to open a database directory externally 2020-05-07 15:51:00 -07:00
Andrew Eisenberg
adf6f66517 Add ability to rename database in database tree 2020-05-07 15:50:59 -07:00
Dave Bartolomeo
8f84989d98 Merge pull request #352 from jcreedcmu/jcreed/update-lsp
Update versions of json-rpc dependencies.
2020-05-07 12:02:30 -04:00
Jason Reed
22c9386123 Use ^versions not ~versions. 2020-05-07 11:32:17 -04:00
jcreedcmu
53e1794b50 Merge pull request #351 from jcreedcmu/jcreed/no-paginate
Don't paginate at all in experimental bqrs parsing codepath
2020-05-07 09:46:31 -04:00
Jason Reed
307d6d7c7f Update versions of json-rpc dependencies. 2020-05-07 09:45:23 -04:00
Jason Reed
a0e60fb154 Don't paginate at all in experimental bqrs parsing codepath 2020-05-06 12:07:47 -04:00
jcreedcmu
8b5bdbb6ef Merge pull request #350 from jcreedcmu/jcreed/cli-bqrs-parsing
Experimental: Enable parsing bqrs with the cli instead of in the webview
2020-05-06 11:34:35 -04:00
Jason Reed
0ad9cdd5ac Address review comments and fix formatting. 2020-05-06 10:39:27 -04:00
Jason Reed
c3b2e9d478 Add experimental use of cli bqrs parsing.
When `codeQL.experimentalBqrsParsing` is set to true, parse raw
results from the bqrs file using the cli, rather than doing it in the
webview.
2020-05-05 17:00:20 -04:00
Jason Reed
c20bbd9606 Fix formatting.
This is simultaneously compatible with eslint and tsfmt.
2020-05-05 16:21:58 -04:00
jcreedcmu
6080a0d585 Merge pull request #347 from jcreedcmu/jcreed/launch-config
internal: Revert specifying workspace in launch config
2020-05-05 13:08:28 -04:00
jcreedcmu
9fda320589 Merge pull request #340 from jcreedcmu/jcreed/no-qhelp-alias
Remove 'qhelp' as global alias for 'xml' filetype
2020-05-05 11:01:12 -04:00
Jason Reed
143b51ef82 Revert specifying workspace in launch config
The behavior without this line is to use whichever workspace was
opened last when testing. I find this more convenient, since I have
several (non-vscode-codeql-starter-workspace) local workspaces I use
for manual testing, and it's nice to have them persist from one run to
the next.
2020-05-05 10:58:54 -04:00
Alexander Eyers-Taylor
51d4c87af4 Merge pull request #346 from jcreedcmu/jcreed/fix-jump-to-def-bug
Jump-to-definition: Fix mistakenly always using the references query
2020-05-01 18:41:02 +01:00
Jason Reed
be5efc01ee Jump-to-definition: Fix mistakenly always using the references query 2020-05-01 13:17:17 -04:00
jcreedcmu
08a30c454a Merge pull request #345 from jcreedcmu/jcreed/better-empty-message
Add suggestive message to alerts view when 0 alerts, >0 raw results.
2020-05-01 09:01:25 -04:00
Jason Reed
1377969213 Add suggestive message to alerts view when 0 alerts, >0 raw results.
Fixes https://github.com/github/codeql-coreql-team/issues/383.
2020-05-01 08:17:51 -04:00
jcreedcmu
41f1aae71d Merge pull request #344 from github/shati/changelog-date
Changelog: Add release date
2020-04-28 13:49:41 -04:00
Shati Patel
62cae6ead1 Changelog: Add release date 2020-04-28 18:30:45 +01:00
jcreedcmu
39e3627e06 Merge pull request #343 from github/version/bump-to-v1.1.3
Bump version to v1.1.3
2020-04-28 11:40:48 -04:00
github-actions[bot]
43586c91d9 Bump version to v1.1.3 2020-04-28 15:34:08 +00:00
jcreedcmu
8efb060031 Merge pull request #337 from jcreedcmu/jcreed/jump-to-def
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
Add experimental support for Jump-to-def and Find-references
2020-04-28 11:24:55 -04:00
Jason Reed
31414b7506 Remove 'qhelp' as global alias for 'xml' filetype 2020-04-23 10:06:27 -04:00
Jason Reed
e242a8fbeb Review comments 2020-04-22 14:09:20 -04:00
Jason Reed
ee591e802f Fix lint violations 2020-04-21 13:19:07 -04:00
Jason Reed
7df8905aa0 Scope tags for ide queries 2020-04-21 13:14:19 -04:00
Shati Patel
23b1c00179 Update changelog 2020-04-21 08:27:29 -07:00
Jason Reed
701804b6a4 Guard find-references with experimental setting 2020-04-21 11:15:12 -04:00
Jason Reed
66665bf25e Refactor definitions/references finding 2020-04-21 11:15:12 -04:00
Jason Reed
1c6b4a6d1e Put CachedOperation in helpers. 2020-04-21 11:15:12 -04:00
Jason Reed
28be98411d Add constants for bqrs column kinds 2020-04-21 11:15:12 -04:00
Jason Reed
5592a77963 Can autodetect language 2020-04-21 11:15:12 -04:00
Jason Reed
a6cd08fb0b Add wrapper for 'resolve queries' cli command 2020-04-21 11:15:12 -04:00
Jason Reed
881c909540 Make rudimentary jump-to-definition work 2020-04-21 11:15:12 -04:00
Henry Mercer
f5e3af02e4 Merge pull request #336 from github/pr-template
Update link in PR template
2020-04-21 12:11:30 +01:00
Shati Patel
3eca4f6734 Update link in PR template 2020-04-20 23:22:25 +01:00
Andrew Eisenberg
596ccdb722 Update change log 2020-04-20 08:50:54 -07:00
Andrew Eisenberg
2aeda002fa Add sort by date added for databases
This uses the dateAdded field on databases. It will only work for 
databases added after that field was added. Otherwise, the dateAdded
property will be undefined.
2020-04-20 08:50:54 -07:00
Andrew Eisenberg
27623f3325 Add dateAdded field for databases
Existing databases will have this value as `undefined`. Newly added
databases will have this value as the Unix timestamp when added.

Stuffing this value into the `options` field of the database. The
name is not quite right, but it seems like this is the right place.
Should I rename?
2020-04-20 08:50:54 -07:00
Andrew Eisenberg
f3df3b9f3e Rename svg for sorting dates add svg for sorting names 2020-04-20 08:50:54 -07:00
Andrew Eisenberg
5850ed3288 refactor: Move icons into light/dark folders
This follows the vscode styles where all icons for dark mode are in
the `dark` folder and all for light mode in the `light` folder. They
always have the same name.
2020-04-20 08:50:54 -07:00
Andrew Eisenberg
a2f8c85359 Add command to sort databases by name
This assumes that we will be adding other sorting mechanisms in the
future, like sort by data added.

Not happy with the icon I chose, but I didn't want to spend too much
time creating a new one.
2020-04-20 08:50:54 -07:00
Andrew Eisenberg
62d9efc4ee Update changelog 2020-04-20 07:55:59 -07:00
Andrew Eisenberg
00026a7727 feat: User can see query text at time of execution
Add new command to view the query text in a synthetic, read-only
document.

Quick eval queries will show the text selected when initially running
the query. Quick eval queries where the user has a single caret
selection will show the entire line of text.
2020-04-20 07:55:59 -07:00
Jason Reed
c292f58e20 Factor out database qlpack inference. 2020-04-20 09:59:41 -04:00
jcreedcmu
6f935ae6e4 Merge pull request #331 from aeisenberg/aeisenberg/changelog
Update changelog
2020-04-16 12:58:57 -04:00
Andrew Eisenberg
1fb65cd7e9 Update changelog 2020-04-16 09:51:01 -07:00
jcreedcmu
21500f0a5b Merge pull request #330 from dbartol/dbartol/unique-highlighting
Highlight `unique` as a keyword
2020-04-15 09:49:24 -04:00
Dave Bartolomeo
efcf9815f0 Update CHANGELOG.md for unique highlighting 2020-04-14 12:53:33 -04:00
Dave Bartolomeo
f8635f41a5 Highlight unique as a keyword
`unique` is really a context-sensitive keyword, but that's even more of a hassle in a TextMate grammars than it is in the compiler itself. We'll just highlight it as a real keyword. The worst that will happen is that existing variables and predicates named "unique" will be highlighted like keywords, which will hopefully just encourage QL developers to rename those anyway.
2020-04-14 12:48:18 -04:00
jcreedcmu
e4df717d2b Merge pull request #324 from jcreedcmu/jcreed/always-save
Add 'Always Save' to running unsaved query dialog
2020-04-07 12:49:07 -04:00
jcreedcmu
9ea4b3936a Merge pull request #326 from p-/qhelp-syntax-highlighting
feat: XML syntax highlighting for qhelp files
2020-04-07 12:10:09 -04:00
Jason Reed
e5305ab4b5 Fix lint compatibly with tsfmt 2020-04-07 12:02:50 -04:00
Jason Reed
c2c86aed0a Add 'Always Save' to running unsaved query dialog 2020-04-07 12:02:50 -04:00
Dave Bartolomeo
2df512f018 Merge pull request #327 from jcreedcmu/jcreed/fix-windows-ci
chore: Fix codeql.cmd overwriting codeql.exe in CI
2020-04-07 11:48:04 -04:00
Jason Reed
ba3381fbf9 Fix codeql.cmd overwriting codeql.exe in CI 2020-04-07 10:06:29 -04:00
Peter Stöckli
869029b856 feat: XML syntax highlighting for .qhelp files 2020-04-04 16:59:02 +02:00
Andrew Eisenberg
b3ad1d6814 fix: Avoid warning user if no new launcher exists
Allow extension to remain on the deprecated launcher.
2020-03-27 08:46:45 -07:00
Andrew Eisenberg
130d3c09e3 build: Enable linting in the build 2020-03-25 12:02:12 -07:00
Andrew Eisenberg
bb28dafc43 lint: Add proper linting for react 2020-03-25 12:02:12 -07:00
Andrew Eisenberg
db6aadbf93 fix: Use a proper 'file' schema for uris 2020-03-25 11:16:32 -07:00
Henry Mercer
d97c8e864d Merge pull request #313 from shati-patel/error-typo
Fix typo in error message
2020-03-25 10:25:54 +00:00
Shati Patel
d8a6368e60 Fix typo in error message
Spotted a small typo ("exterally") and updated the error message.
2020-03-25 08:59:04 +00:00
jcreedcmu
76d6ab4e81 Merge pull request #305 from github/version/bump-to-v1.1.2
Bump version to v1.1.2
2020-03-23 13:01:36 -04:00
github-actions[bot]
bdcabae60e Bump version to v1.1.2 2020-03-23 16:56:34 +00:00
jcreedcmu
aa0fb498a0 Merge pull request #304 from jcreedcmu/jcreed/v1.1.1
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-03-23 12:53:20 -04:00
Jason Reed
176dc1fc71 Better message. 2020-03-23 12:43:03 -04:00
Jason Reed
a0eebb1e5f Fix typo 2020-03-23 12:40:22 -04:00
Jason Reed
2af917284b Capitalize the Brand 2020-03-23 12:30:08 -04:00
Jason Reed
4adb8b6301 Update CHANGELOG for release. 2020-03-23 12:25:24 -04:00
jcreedcmu
8f5ddbd87c Merge pull request #303 from aeisenberg/aeisenberg/deprecate
feat: Display warning when codeql.cmd is used
2020-03-23 12:24:03 -04:00
Andrew Eisenberg
b689e55f61 Merge pull request #302 from aeisenberg/aeisenberg/large-log
feat: Allow large log files to be opened externally
2020-03-23 09:13:42 -07:00
Andrew Eisenberg
7ce3dc2c43 feat: Display warning when codeql.cmd is used
The old launcher has been deprecated and codeql.exe is
recommended.

Fixes #287.
2020-03-23 09:06:53 -07:00
Andrew Eisenberg
eed85e9e28 feat: Allow large log files to be opened externally
If the user tries to open a log file that is too large for vscode's
extension mechanism to handle, reveal the file in the finder/explorer
and let the user open in an external program.
2020-03-23 09:03:15 -07:00
Andrew Eisenberg
0b56092466 Merge pull request #297 from aeisenberg/aeisenberg/updates
fix: Avoid auto-updating the codeql binaries
2020-03-21 21:52:36 -07:00
Andrew Eisenberg
4fce213ca8 fix: Allow autoupdating if no distribution installed 2020-03-20 11:09:29 -07:00
Andrew Eisenberg
8ed7b991be fix: Avoid auto-updating the codeql binaries
On startup, if a new binary is available, request user acceptance
before starting the update.

Fixes #283
2020-03-19 14:16:57 -07:00
Andrew Eisenberg
deb544ab93 Merge pull request #295 from aeisenberg/aeisenberg/lint
lint: Ran the auto-fix command for the linter
2020-03-19 10:40:09 -07:00
jcreedcmu
9ec017a30d Merge pull request #299 from aeisenberg/aeisenberg/package-lock
chore: Remove unnecessary package lock
2020-03-19 13:33:35 -04:00
Andrew Eisenberg
ebdf576196 lint: Run the auto linter on all files 2020-03-19 09:46:20 -07:00
Andrew Eisenberg
13f725acfe chore: Update package lock 2020-03-19 09:44:14 -07:00
jcreedcmu
1401115c08 Merge pull request #298 from aeisenberg/aeisenberg/log-files
chore: Update changelog
2020-03-19 12:01:26 -04:00
Andrew Eisenberg
85c04fc63a chore: Update changelog 2020-03-19 08:38:22 -07:00
Andrew Eisenberg
54ad3649b1 Merge pull request #284 from aeisenberg/aeisenberg/log-files
feat: Save log files per query
2020-03-19 08:29:10 -07:00
Andrew Eisenberg
66e9272525 feat: Remove side log location when query removed
When removing query history item from view, also remove the side log.

Log files can be large, so ensure they don't stick around.

Last piece of #236 and #234.
2020-03-19 08:11:20 -07:00
Andrew Eisenberg
6793f8e92d feat: Adds command to show query log in editor
Right clicking on the history page will now have a new option to show
the associated log.

Closes #236
Closes #234
2020-03-19 08:11:20 -07:00
Andrew Eisenberg
da28beb82e fix: Should join paths using comma 2020-03-19 08:11:20 -07:00
Andrew Eisenberg
b04ff3c8b9 lint: Formatting 2020-03-19 08:11:20 -07:00
Andrew Eisenberg
fd4d6b7f30 fix: Avoid accidentally treating '' as valid 2020-03-19 08:11:20 -07:00
Andrew Eisenberg
5facab1f9e lint: Fix linting and update linting rules 2020-03-19 08:11:20 -07:00
Nick Rolfe
f25c9fd6fd Use codeql.exe instead of codeql.cmd on Windows 2020-03-19 08:11:20 -07:00
Andrew Eisenberg
a6043f2518 feat: Save log files per query
This feature adds logging per-query. Each query will be logged in its
own location in either workspace or globally shared location in
vscode.

There are limitations here. We are only guessing when one query ends
and another begins. We assume that queries don't occur in parallel.
If they do, the previous query will have its results intermingled
with the current query's results.

To fix that, we will need to update how the query-server emits log
messages so that each query message is attached to a tag that
specifies the query that emitted it.
2020-03-19 08:11:20 -07:00
Andrew Eisenberg
6a746ae5bd deps: Add new dependencies for testing
sinon-chai, and proxyquire.
2020-03-19 08:11:20 -07:00
Dave Bartolomeo
a9eb0a40fd Merge pull request #296 from aeisenberg/aeisenberg/fix-minimist
chore: Fix security warning in minimist
2020-03-19 09:32:18 -04:00
Andrew Eisenberg
d6be401d46 chore: Fix security warning in minimist
There is a security warning for minimist. The extension only depends
on it transitively. Not all of its direct dependencies have updated it
yet. I don't like having to add a dependency like this, but if it
avoids github screaming at us, then I think we should.
2020-03-18 11:53:48 -07:00
jcreedcmu
158a07cd89 Merge pull request #294 from jcreedcmu/jcreed/fix-quick-eval
Fix quick-eval in .qll error
2020-03-18 13:44:17 -04:00
Jason Reed
7ac5a8f777 Fix windows broken test 2020-03-18 13:29:41 -04:00
Jason Reed
dc09925149 Add test for quick-query in .qll fix. 2020-03-18 10:42:49 -04:00
Jason Reed
5fd2596537 Fix quick-query error in .qll files
Fixes https://github.com/github/vscode-codeql/issues/293
2020-03-18 10:41:57 -04:00
Jason Reed
22003e1375 Add dependencies for testing 2020-03-18 10:40:34 -04:00
jcreedcmu
2fee4cc368 Merge pull request #292 from github/jcreedcmu-patch-1
Expand release documentation
2020-03-17 16:55:29 -04:00
jcreedcmu
9d2504959b Update CONTRIBUTING.md 2020-03-17 16:52:18 -04:00
jcreedcmu
77b3f0a025 Merge pull request #291 from github/version/bump-to-v1.1.1
Bump version to v1.1.1
2020-03-17 16:50:16 -04:00
jcreedcmu
a096e79bd4 Expand release documentation 2020-03-17 16:45:24 -04:00
github-actions[bot]
dedc9c46ab Bump version to v1.1.1 2020-03-17 20:25:10 +00:00
jcreedcmu
a472786d93 Merge pull request #290 from jcreedcmu/jcreed/bump-1.1.0
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
Fix package.json for minor version bump.
2020-03-17 16:21:58 -04:00
Jason Reed
bb6faaedbe Fix package.json for minor version bump. 2020-03-17 16:13:52 -04:00
jcreedcmu
91fcd4e26c Merge pull request #288 from jcreedcmu/jcreed/v1.1.0
Update CHANGELOG.md for release.
2020-03-17 16:05:54 -04:00
jcreedcmu
61f182342f Update extensions/ql-vscode/CHANGELOG.md
Co-Authored-By: Shati Patel <42641846+shati-patel@users.noreply.github.com>
2020-03-17 15:59:16 -04:00
Jason Reed
416a87fe1d Update CHANGELOG.md for release. 2020-03-17 15:50:49 -04:00
Andrew Eisenberg
0bd835958b Merge pull request #281 from aeisenberg/aeisenberg/eslint
chore: Introduce eslint
2020-03-17 11:58:04 -07:00
Andrew Eisenberg
8e73c64e63 Merge pull request #282 from aeisenberg/aeisenberg/error-message
fix: Readable error message for invalid file
2020-03-17 09:24:21 -07:00
Andrew Eisenberg
443abea7d7 chore: Introduce eslint
Adds eslint support and fixes linting problems in a few files.

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

The idea is to slowly fix linting problems over time.

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

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

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

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

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

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

Also includes some light refactoring.

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

Closes #251
2020-03-06 13:03:51 -08:00
Andrew Eisenberg
afe0a65fc5 Change vscode settings to hide generated files 2020-03-06 13:00:24 -08:00
Dave Bartolomeo
7fc501f795 Revert "Remove unnecessary? import hack"
This reverts commit 39805bc4a1.
2020-03-06 12:50:29 -05:00
Dave Bartolomeo
39805bc4a1 Remove unnecessary? import hack 2020-03-06 12:36:01 -05:00
Henry Mercer
35f619e97a Merge pull request #262 from jcreedcmu/jcreed/retry-tests
Retry integration tests when they fail
2020-03-05 17:57:01 +00:00
Jason Reed
87e563e24e Review comments. 2020-03-05 12:47:02 -05:00
Jason Reed
3a1219bb64 Retry integration tests when they fail 2020-03-05 09:26:26 -05:00
Henry Mercer
222cafb73c Merge pull request #260 from alexet/tm-test
Add compiled grammar to top level for use in linguist
2020-03-04 18:44:03 +00:00
Alexander Eyers-Taylor
2435a0b2f7 Update syntaxes/README.md
Co-Authored-By: jcreedcmu <jcreed@gmail.com>
2020-03-04 18:28:20 +00:00
alexet
dd9f0e811b Add compiled grammar to top level for use in linguist 2020-03-04 18:28:20 +00:00
Henry Mercer
ed076afde7 Merge pull request #256 from github/version/bump-to-v1.0.7
Bump version to v1.0.7
2020-03-04 15:17:16 +00:00
Dave Bartolomeo
370444c364 Sort tests by name 2020-03-03 15:33:42 -05:00
Dave Bartolomeo
984ba73080 Merge from master 2020-03-03 15:13:15 -05:00
jcreedcmu
c4aa9d9396 Merge pull request #255 from jcreedcmu/jcreed/1.0.6-release
Update CHANGELOG
2020-02-28 12:40:51 -05:00
github-actions[bot]
735f70276a Bump version to v1.0.7 2020-02-28 17:06:29 +00:00
Dave Bartolomeo
f2c525b56d Fix references to renamed type 2020-02-18 11:51:55 -07:00
Dave Bartolomeo
afcc05fb03 Remove unused interface 2020-02-14 18:07:22 -07:00
Dave Bartolomeo
1b7d0da277 Fix typo 2020-02-14 17:54:18 -07:00
Dave Bartolomeo
90a975321f Merge from master 2020-02-14 17:45:27 -07:00
Jason Reed
9aaffb9a89 Fix weird behavior of Quick Query in non-multi-root workspaces 2020-02-11 11:40:14 -05:00
Dave Bartolomeo
edc1f1c2ab Merge remote-tracking branch 'upstream/master' into dbartol/QLTest 2020-01-13 11:19:24 -07:00
Dave Bartolomeo
0947a35332 Implement cancellation of test run 2020-01-06 16:09:34 -07:00
Dave Bartolomeo
207743e7b7 Merge remote-tracking branch 'upstream/master' into dbartol/QLTest 2020-01-03 11:49:15 -07:00
Dave Bartolomeo
de2a6cc0b7 Make QLTest use codeql test run instead of odasa qltest 2020-01-03 11:48:48 -07:00
Dave Bartolomeo
55d1a4aa1c Merge from master 2019-12-16 15:41:16 -07:00
Dave Bartolomeo
2f9a31484c Merge from master 2019-12-04 09:53:06 -07:00
Dave Bartolomeo
9e6100f383 Merge remote-tracking branch 'upstream/master' into dbartol/QLTest 2019-11-22 10:35:04 -07:00
Dave Bartolomeo
7d325e3832 Fix file-watching bug in QLTest discovery 2019-11-21 14:13:55 -07:00
Dave Bartolomeo
cbe3c055b6 odasa -> Semmle Core 2019-11-20 17:19:29 -07:00
Dave Bartolomeo
e37807c45e Better error message when odasa is not configured properly 2019-11-20 17:12:29 -07:00
Dave Bartolomeo
be72e9b67a Activate when Test Explorer view is opened 2019-11-20 17:11:43 -07:00
Dave Bartolomeo
85f7ff1d11 Attempt to fix pure test failure 2019-11-20 16:28:03 -07:00
Dave Bartolomeo
ddf42d81d1 Fix LGTM alerts 2019-11-20 14:55:08 -07:00
Dave Bartolomeo
444aca3bae Implement QL Test support (using odasa for now)
The PR contains the initial implementing of QL Test support in CodeQL for Visual Studio Code. Because QL Test support isn't quite ready in the CLI yet, this PR uses `odasa` to run the tests for now. As CLI support comes online, it should be straightforward to swap out the implementation to use the CLI.

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

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

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

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

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

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

View File

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

7
.gitattributes vendored
View File

@@ -12,3 +12,10 @@ yarn.lock merge=binary
# For more information, see this issue: https://github.com/Microsoft/web-build-tools/issues/1088
#
*.json linguist-language=JSON-with-Comments
# Reduce incidence of needless merge conflicts on CHANGELOG.md
# The man page at
# https://mirrors.edge.kernel.org/pub/software/scm/git/docs/gitattributes.html
# suggests that this might interleave lines arbitrarily, but empirically
# it keeps added chunks contiguous
CHANGELOG.md merge=union

View File

@@ -9,7 +9,7 @@ assignees: ''
---
- [ ] Update this issue title to refer to the version of the release
- [ ] Trigger a release build on Actions by adding a new tag on master of the format `vxx.xx.xx`
- [ ] Trigger a release build on Actions by adding a new tag on branch `main` of the format `vxx.xx.xx`
- [ ] Monitor the status of the release build in the `Release` workflow in the Actions tab.
- [ ] Download the VSIX from the draft GitHub release that is created when the release build finishes.
- [ ] Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).

View File

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

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,18 +56,25 @@ jobs:
node-version: '10.18.1'
# We have to build the dependencies in `lib` before running any tests.
- name: Build
run: |
cd build
npm install
npm run build-ci
- name: Install dependencies
run: node common/scripts/install-run-rush.js install
shell: bash
- name: Build
run: node common/scripts/install-run-rush.js build
shell: bash
- name: Lint
run: |
cd extensions/ql-vscode
npm run lint
- name: Install CodeQL
run: |
mkdir codeql-home
curl -L --silent https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip -o codeql-home/codeql.zip
unzip -q -o codeql-home/codeql.zip -d codeql-home
unzip -q -o codeql-home/codeql.zip codeql/codeql.exe -d codeql-home
rm codeql-home/codeql.zip
shell: bash
@@ -80,7 +88,7 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
cd extensions/ql-vscode
$env:CODEQL_PATH=$(Join-Path $env:GITHUB_WORKSPACE -ChildPath 'codeql-home/codeql/codeql.cmd')
$env:CODEQL_PATH=$(Join-Path $env:GITHUB_WORKSPACE -ChildPath 'codeql-home/codeql/codeql.exe')
npm run test
- name: Run integration tests (Linux)

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
@@ -93,12 +93,12 @@ jobs:
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
asset_content_type: application/zip
# The checkout action does not fetch the master branch.
# Fetch the master branch so that we can base the version bump PR against master.
- name: Fetch master branch
# The checkout action does not fetch the main branch.
# Fetch the main branch so that we can base the version bump PR against main.
- name: Fetch main branch
run: |
git fetch --depth=1 origin master:master
git checkout master
git fetch --depth=1 origin main:main
git checkout main
- name: Bump patch version
id: bump-patch-version
@@ -119,4 +119,4 @@ jobs:
title: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
body: This PR was automatically generated by the GitHub Actions release workflow in this repository.
branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
base: master
base: main

1
.gitignore vendored
View File

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

View File

@@ -3,8 +3,10 @@
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"eamodio.tsl-problem-matcher"
"eamodio.tsl-problem-matcher",
"dbaeumer.vscode-eslint",
"eternalphane.tsfmt-vscode"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}
}

47
.vscode/launch.json vendored
View File

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

36
.vscode/settings.json vendored
View File

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

11
.vscode/tasks.json vendored
View File

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

View File

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

View File

@@ -4,7 +4,7 @@ This project is an extension for Visual Studio Code that adds rich language supp
The extension is released. You can download it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql).
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).
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md).
[![CI status badge](https://github.com/github/vscode-codeql/workflows/Build%20Extension/badge.svg)](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amaster)
[![VS Marketplace badge](https://vsmarketplacebadge.apphb.com/version/github.vscode-codeql.svg)](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql)
@@ -13,7 +13,7 @@ To see what has changed in the last few versions of the extension, see the [Chan
* Enables you to use CodeQL to query databases and discover problems in codebases.
* 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).
* 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.

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.

1294
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

@@ -2,5 +2,9 @@
* This is configuration file is used for advanced publishing configurations with Rush.
* For full documentation, please see https://rushjs.io/pages/configs/version_policies_json/
*/
[]
[
{
"definitionName": "individualVersion",
"policyName": "utilities"
}
]

View File

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

View File

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

View File

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

View File

@@ -27,4 +27,4 @@
"../../test",
"../../**/view"
]
}
}

View File

@@ -0,0 +1,35 @@
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2018,
sourceType: "module",
project: ["tsconfig.json", "./src/**/tsconfig.json"],
},
plugins: ["@typescript-eslint"],
env: {
node: true,
es6: true,
},
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
rules: {
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/no-unused-vars": [
"warn",
{
vars: "all",
args: "none",
ignoreRestSiblings: false,
},
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"prefer-const": ["warn", { destructuring: "all" }],
indent: "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/no-throw-literal": "error",
"no-useless-escape": 0,
semi: 2,
quotes: ["warn", "single"]
},
};

View File

@@ -1,5 +1,96 @@
# CodeQL for Visual Studio Code: Changelog
## 1.3.1 - 7 July 2020
- Fix unzipping of large files.
- Ensure compare order is consistent when selecting two queries to compare. The first query selected is always the _from_ query and the query selected later is always the _to_ query.
- Ensure added databases have zipped source locations for databases added as archives or downloaded from the internet.
- Fix bug where it is not possible to add databases starting with `db-*`.
- Change styling of pagination section of the results page.
- Fix display of query text for stored quick queries.
## 1.3.0 - 22 June 2020
- Report error when selecting invalid database.
- Add descriptive message for database archive import failure.
- Respect VS Code's i18n locale setting when formatting dates and sorting strings.
- Allow the opening of large SARIF files externally from VS Code.
- Add new 'CodeQL: Compare Query' command that shows the differences between two queries.
- Allow multiple items in the query history view to be removed in one operation.
- Allow multiple items in the databases view to be removed in one operation.
- Allow multiple items in the databases view to be upgraded in one operation.
- Allow multiple items in the databases view to have their external folders opened.
- Allow all selected queries to be run in one command from the file explorer.
## 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
raw results but no alerts.
- Add the ability to rename databases in the database view.
- Add the ability to open the directory in the filesystem
of a database.
## 1.1.2 - 28 April 2020
- Implement syntax highlighting for the new `unique` aggregate.
- Implement XML syntax highlighting for `.qhelp` files.
- Add option to auto save queries before running them.
- Add new command in query history to view the query text of the
selected query (note that this may be different from the current
contents of the query file if the file has been edited).
- Add ability to sort CodeQL databases by name or by date added.
## 1.1.1 - 23 March 2020
- Fix quick evaluation in `.qll` files.
- Add new command in query history view to view the log file of a
query.
- Request user acknowledgment before updating the CodeQL binaries.
- Warn when using the deprecated `codeql.cmd` launcher on Windows.
## 1.1.0 - 17 March 2020
- Add functionality for testing custom CodeQL queries by using the VS
Code Test Explorer extension and `codeql test`. See the documentation for
more details.
- Add a "Show log" button to all information, error, and warning
popups that will display the CodeQL extension log.
- Display a message when a query times out.
- Show canceled queries in query history.
- Improve error messages when attempting to run non-query files.
## 1.0.6 - 28 February 2020
- Add command to restart query server.
@@ -20,7 +111,7 @@
## 1.0.3 - 13 January 2020
- Reduce the frequency of CodeQL CLI update checks to help avoid hitting GitHub API limits of 60 requests per
hour for unauthenticated IPs.
hour for unauthenticated IPs.
- Fix sorting of result sets with names containing special characters.
## 1.0.2 - 13 December 2019
@@ -29,8 +120,7 @@ hour for unauthenticated IPs.
- Allow customization of query history labels from settings and from
query history view context menu.
- Show number of results in results view.
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show
Previous Step on Path` for navigating the steps on the currently
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show Previous Step on Path` for navigating the steps on the currently
shown path result.
## 1.0.1 - 21 November 2019
@@ -41,7 +131,6 @@ hour for unauthenticated IPs.
- Fix the automatic upgrading of CodeQL databases when using upgrade scripts from the workspace.
- Allow removal of items from the CodeQL Query History view.
## 1.0.0 - 14 November 2019
Initial release of CodeQL for Visual Studio Code.

View File

@@ -2,30 +2,30 @@
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).
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md).
## Quick start overview
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

@@ -9,7 +9,7 @@ const {
compileTextMateGrammar,
copyTestData,
copyViewCss
} = require('build-tasks');
} = require('@github/codeql-gulp-tasks');
const { compileView } = require('./webpack');
exports.buildWithoutPackage = gulp.parallel(compileTypeScript, compileTextMateGrammar, compileView, copyTestData, copyViewCss);

View File

@@ -4,13 +4,14 @@ import * as webpack from 'webpack';
export const config: webpack.Configuration = {
mode: 'development',
entry: {
resultsView: './src/view/results.tsx'
resultsView: './src/view/results.tsx',
compareView: './src/compare/view/Compare.tsx',
},
output: {
path: path.resolve(__dirname, '..', 'out'),
filename: "[name].js"
},
devtool: 'source-map',
devtool: "inline-source-map",
resolve: {
extensions: ['.js', '.ts', '.tsx', '.json']
},

View File

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

View File

@@ -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

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" fill="none"
viewBox="0 0 432 432" style="enable-background:new 0 0 432 432;" xml:space="preserve">
<g>
<g>
<g>
<polygon points="234.24,9.067 183.893,59.413 284.587,59.413" fill="#C5C5C5"/>
<polygon points="301.44,304.32 427.947,120.853 427.947,93.973 250.88,93.973 250.88,128.107 376.32,128.107 250.027,310.72
250.027,338.24 432,338.24 432,304.32" fill="#C5C5C5"/>
<polygon points="234.24,422.933 283.947,373.227 184.533,373.227" fill="#C5C5C5"/>
<path d="M226.773,338.24L130.987,93.76H96L0,338.24h39.253l19.627-52.267h109.013l19.627,52.267H226.773z M71.893,250.987
L113.28,140.48l41.387,110.507H71.893z" fill="#C5C5C5"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 953 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="M7 2L6 3V6H7V3H14V5.45306L14.2071 5.29286L15 6.08576V3L14 2H7ZM8 4H10V6H8V4ZM5 9H3V11H5V9ZM2 7L1 8V13L2 14H9L10 13V8L9 7H2ZM2 13V8H9V13H2ZM8 10H6V12H8V10ZM13 4H12V7.86388L10.818 6.68192L10.1109 7.38903L12.1465 9.42454L12.8536 9.42454L14.889 7.38908L14.1819 6.68197L13 7.86388V4Z" fill="#C5C5C5"/>
</svg>

After

Width:  |  Height:  |  Size: 449 B

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

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 432 432" style="enable-background:new 0 0 432 432;" xml:space="preserve">
<g>
<g>
<g>
<polygon points="234.24,9.067 183.893,59.413 284.587,59.413 "/>
<polygon points="301.44,304.32 427.947,120.853 427.947,93.973 250.88,93.973 250.88,128.107 376.32,128.107 250.027,310.72
250.027,338.24 432,338.24 432,304.32 "/>
<polygon points="234.24,422.933 283.947,373.227 184.533,373.227 "/>
<path d="M226.773,338.24L130.987,93.76H96L0,338.24h39.253l19.627-52.267h109.013l19.627,52.267H226.773z M71.893,250.987
L113.28,140.48l41.387,110.507H71.893z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 894 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="M7 2L6 3V6H7V3H14V5.45306L14.2071 5.29286L15 6.08576V3L14 2H7ZM8 4H10V6H8V4ZM5 9H3V11H5V9ZM2 7L1 8V13L2 14H9L10 13V8L9 7H2ZM2 13V8H9V13H2ZM8 10H6V12H8V10ZM13 4H12V7.86388L10.818 6.68192L10.1109 7.38903L12.1465 9.42454L12.8536 9.42454L14.889 7.38908L14.1819 6.68197L13 7.86388V4Z" fill="#424242"/>
</svg>

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.0.6",
"version": "1.3.1",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -18,13 +18,24 @@
"categories": [
"Programming Languages"
],
"extensionDependencies": [
"hbenl.vscode-test-explorer"
],
"activationEvents": [
"onLanguage:ql",
"onView:codeQLDatabases",
"onView:codeQLQueryHistory",
"onView:test-explorer",
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.chooseDatabase",
"onCommand: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",
@@ -73,6 +84,12 @@
".dbscheme"
],
"configuration": "./language-configuration.json"
},
{
"id": "xml",
"extensions": [
".qhelp"
]
}
],
"grammars": [
@@ -95,7 +112,7 @@
"scope": "machine",
"type": "string",
"default": "",
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.cmd` on Windows. This overrides all other CodeQL CLI settings."
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. This overrides all other CodeQL CLI settings."
},
"codeQL.runningQueries.numberOfThreads": {
"type": "integer",
@@ -128,10 +145,23 @@
"default": false,
"description": "Enable debug logging and tuple counting when running CodeQL queries. This information is useful for debugging query performance."
},
"codeQL.runningQueries.autoSave": {
"type": "boolean",
"default": false,
"description": "Enable automatically saving a modified query file when running a query."
},
"codeQL.queryHistory.format": {
"type": "string",
"default": "[%t] %q on %d - %s",
"description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, and %s is a status string."
},
"codeQL.runningTests.numberOfThreads": {
"scope": "window",
"type": "integer",
"default": 1,
"minimum": 1,
"maximum": 1024,
"description": "Number of threads for running CodeQL tests."
}
}
},
@@ -140,6 +170,10 @@
"command": "codeQL.runQuery",
"title": "CodeQL: Run Query"
},
{
"command": "codeQL.runQueries",
"title": "CodeQL: Run Queries in Selected Files"
},
{
"command": "codeQL.quickEval",
"title": "CodeQL: Quick Evaluation"
@@ -149,11 +183,35 @@
"title": "CodeQL: Quick Query"
},
{
"command": "codeQL.chooseDatabase",
"title": "CodeQL: Choose Database",
"command": "codeQLDatabases.chooseDatabaseFolder",
"title": "Choose Database from Folder",
"icon": {
"light": "media/black-plus.svg",
"dark": "media/white-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"
}
},
{
@@ -180,6 +238,46 @@
"command": "codeQLDatabases.upgradeDatabase",
"title": "Upgrade Database"
},
{
"command": "codeQLDatabases.renameDatabase",
"title": "Rename Database"
},
{
"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",
"icon": {
"light": "media/light/sort-alpha.svg",
"dark": "media/dark/sort-alpha.svg"
}
},
{
"command": "codeQLDatabases.sortByDateAdded",
"title": "Sort by Date Added",
"icon": {
"light": "media/light/sort-date.svg",
"dark": "media/dark/sort-date.svg"
}
},
{
"command": "codeQL.checkForUpdatesToCLI",
"title": "CodeQL: Check for CLI Updates"
@@ -196,6 +294,26 @@
"command": "codeQLQueryHistory.itemClicked",
"title": "Query History Item"
},
{
"command": "codeQLQueryHistory.showQueryLog",
"title": "Show Query Log"
},
{
"command": "codeQLQueryHistory.showQueryText",
"title": "Show Query Text"
},
{
"command": "codeQLQueryHistory.viewSarif",
"title": "View SARIF"
},
{
"command": "codeQLQueryHistory.setLabel",
"title": "Set Label"
},
{
"command": "codeQLQueryHistory.compareWith",
"title": "Compare Results"
},
{
"command": "codeQLQueryResults.nextPathStep",
"title": "CodeQL: Show Next Step on Path"
@@ -204,19 +322,48 @@
"command": "codeQLQueryResults.previousPathStep",
"title": "CodeQL: Show Previous Step on Path"
},
{
"command": "codeQLQueryHistory.setLabel",
"title": "Set Label"
},
{
"command": "codeQL.restartQueryServer",
"title": "CodeQL: Restart Query Server"
},
{
"command": "codeQLTests.showOutputDifferences",
"title": "CodeQL: Show Test Output Differences"
},
{
"command": "codeQLTests.acceptOutput",
"title": "CodeQL: Accept Test Output"
}
],
"menus": {
"view/title": [
{
"command": "codeQL.chooseDatabase",
"command": "codeQLDatabases.sortByName",
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLDatabases.sortByDateAdded",
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
"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"
}
@@ -237,6 +384,16 @@
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLDatabases.renameDatabase",
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLDatabases.openDatabaseFolder",
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLQueryHistory.openQuery",
"group": "9_qlCommands",
@@ -251,18 +408,47 @@
"command": "codeQLQueryHistory.setLabel",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.compareWith",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.showQueryLog",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.showQueryText",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.viewSarif",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
},
{
"command": "codeQLTests.showOutputDifferences",
"group": "qltest@1",
"when": "view == test-explorer && viewItem == testWithSource"
},
{
"command": "codeQLTests.acceptOutput",
"group": "qltest@2",
"when": "view == test-explorer && viewItem == testWithSource"
}
],
"explorer/context": [
{
"command": "codeQL.setCurrentDatabase",
"group": "9_qlCommands",
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder"
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zip"
},
{
"command": "codeQL.runQuery",
"group": "9_qlCommands",
"when": "resourceLangId == ql && resourceExtname == .ql"
"command": "codeQL.runQueries",
"group": "9_qlCommands"
}
],
"commandPalette": [
@@ -270,6 +456,10 @@
"command": "codeQL.runQuery",
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runQueries",
"when": "false"
},
{
"command": "codeQL.quickEval",
"when": "editorLangId == ql"
@@ -282,10 +472,46 @@
"command": "codeQLDatabases.setCurrentDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.renameDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.openDatabaseFolder",
"when": "false"
},
{
"command": "codeQLDatabases.sortByName",
"when": "false"
},
{
"command": "codeQLDatabases.sortByDateAdded",
"when": "false"
},
{
"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"
@@ -298,9 +524,25 @@
"command": "codeQLQueryHistory.itemClicked",
"when": "false"
},
{
"command": "codeQLQueryHistory.showQueryLog",
"when": "false"
},
{
"command": "codeQLQueryHistory.showQueryText",
"when": "false"
},
{
"command": "codeQLQueryHistory.viewSarif",
"when": "false"
},
{
"command": "codeQLQueryHistory.setLabel",
"when": "false"
},
{
"command": "codeQLQueryHistory.compareWith",
"when": "false"
}
],
"editor/context": [
@@ -344,10 +586,13 @@
"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",
"format": "tsfmt -r"
"postinstall": "npm rebuild && node ./node_modules/vscode/bin/install",
"format": "tsfmt -r && eslint src test --ext .ts,.tsx --fix",
"lint": "eslint src test --ext .ts,.tsx --max-warnings=0",
"format-staged": "lint-staged"
},
"dependencies": {
"child-process-promise": "^2.2.1",
"classnames": "~2.2.6",
"fs-extra": "^8.1.0",
"glob-promise": "^3.4.0",
@@ -357,14 +602,23 @@
"react-dom": "^16.8.6",
"semmle-bqrs": "^0.0.1",
"semmle-io-node": "^0.0.1",
"semmle-vscode-utils": "^0.0.1",
"@github/codeql-vscode-utils": "^0.0.4",
"tmp": "^0.1.0",
"tree-kill": "~1.2.2",
"unzipper": "~0.10.5",
"vscode-jsonrpc": "^4.0.0",
"vscode-languageclient": "^5.2.1"
"vscode-jsonrpc": "^5.0.1",
"vscode-languageclient": "^6.1.3",
"vscode-test-adapter-api": "~1.7.0",
"vscode-test-adapter-util": "~0.7.0",
"minimist": "~1.2.5",
"semver": "~7.3.2",
"@types/semver": "~7.2.0",
"tmp-promise": "~3.0.2",
"zip-a-folder": "~0.0.12"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/child-process-promise": "^2.2.1",
"@types/classnames": "~2.2.9",
"@types/fs-extra": "^8.0.0",
"@types/glob": "^7.1.1",
@@ -383,16 +637,17 @@
"@types/vscode": "^1.39.0",
"@types/webpack": "^4.32.1",
"@types/xml2js": "~0.4.4",
"build-tasks": "^0.0.1",
"@github/codeql-gulp-tasks": "^0.0.4",
"chai": "^4.2.0",
"child-process-promise": "^2.2.1",
"css-loader": "~3.1.0",
"glob": "^7.1.4",
"gulp": "^4.0.2",
"gulp-sourcemaps": "^2.6.5",
"gulp-typescript": "^5.0.1",
"mocha": "~6.2.1",
"mocha-sinon": "~2.1.0",
"npm-run-all": "^4.1.5",
"sinon": "~9.0.0",
"style-loader": "~0.23.1",
"through2": "^3.0.1",
"ts-loader": "^5.4.5",
@@ -402,8 +657,37 @@
"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"
"webpack-cli": "^3.3.2",
"eslint": "~6.8.0",
"@typescript-eslint/eslint-plugin": "~2.23.0",
"@typescript-eslint/parser": "~2.23.0",
"chai-as-promised": "~7.1.1",
"@types/chai-as-promised": "~7.1.2",
"@types/sinon": "~7.5.2",
"sinon-chai": "~3.5.0",
"@types/sinon-chai": "~3.2.3",
"proxyquire": "~2.1.3",
"@types/proxyquire": "~1.3.28",
"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",
"eslint --fix"
]
}
}

View File

@@ -0,0 +1,136 @@
import { DecodedBqrsChunk, ResultSetSchema, ColumnKind, Column, ColumnValue } from './bqrs-cli-types';
import { LocationValue, ResultSetSchema as AdaptedSchema, ColumnSchema, ColumnType, LocationStyle } from 'semmle-bqrs';
import { ResultSet } from './interface-types';
// FIXME: This is a temporary bit of impedance matching to convert
// from the types provided by ./bqrs-cli-types, to the types used by
// the view layer.
//
// The reason that it is benign for now is that it is only used by
// feature-flag-guarded codepaths that won't be encountered by normal
// users. It is not yet guaranteed to produce correct output for raw
// results.
//
// Eventually, the view layer should be refactored to directly accept data
// of types coming from bqrs-cli-types, and this file can be deleted.
export type ResultRow = ResultValue[];
export interface ResultElement {
label: string;
location?: LocationValue;
}
export interface ResultUri {
uri: string;
}
export type ResultValue = ResultElement | ResultUri | string;
export interface RawResultSet {
readonly schema: AdaptedSchema;
readonly rows: readonly ResultRow[];
}
function adaptKind(kind: ColumnKind): ColumnType {
// XXX what about 'u'?
if (kind === 'e') {
return { type: 'e', primitiveType: 's', locationStyle: LocationStyle.FivePart, hasLabel: true };
}
else {
return { type: kind };
}
}
function adaptColumn(col: Column): ColumnSchema {
return { name: col.name!, type: adaptKind(col.kind) };
}
export function adaptSchema(schema: ResultSetSchema): AdaptedSchema {
return {
columns: schema.columns.map(adaptColumn),
name: schema.name,
tupleCount: schema.rows,
version: 0,
};
}
export function adaptValue(val: ColumnValue): ResultValue {
// XXX taking a lot of incorrect shortcuts here
if (typeof val === 'string') {
return val;
}
if (typeof val === 'number' || typeof val === 'boolean') {
return val + '';
}
const url = val.url;
if (typeof url === 'string') {
return url;
}
if (url === undefined) {
return 'none';
}
return {
label: val.label || '',
location: {
t: LocationStyle.FivePart,
lineStart: url.startLine,
lineEnd: url.endLine,
colStart: url.startColumn,
colEnd: url.endColumn,
// FIXME: This seems definitely wrong. Should we be using
// something like the code in sarif-utils.ts?
file: url.uri.replace(/file:/, ''),
}
};
}
export function adaptRow(row: ColumnValue[]): ResultRow {
return row.map(adaptValue);
}
export function adaptBqrs(schema: AdaptedSchema, page: DecodedBqrsChunk): RawResultSet {
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;
numInterpretedPages: number;
selectedTable?: string; // when undefined, means 'show default table'
resultSetNames: string[];
resultSet: ResultSet;
}

View File

@@ -51,7 +51,10 @@ export type Entry = File | Directory;
*/
export type DirectoryHierarchyMap = Map<string, Map<string, vscode.FileType>>;
export type ZipFileReference = { sourceArchiveZipPath: string, pathWithinSourceArchive: string };
export type ZipFileReference = {
sourceArchiveZipPath: string;
pathWithinSourceArchive: string;
};
/** Encodes a reference to a source file within a zipped source archive into a single URI. */
export function encodeSourceArchiveUri(ref: ZipFileReference): vscode.Uri {
@@ -87,7 +90,7 @@ export function encodeSourceArchiveUri(ref: ZipFileReference): vscode.Uri {
});
}
const sourceArchiveUriAuthorityPattern = /^(\d+)\-(\d+)$/;
const sourceArchiveUriAuthorityPattern = /^(\d+)-(\d+)$/;
class InvalidSourceArchiveUriError extends Error {
constructor(uri: vscode.Uri) {
@@ -139,8 +142,8 @@ function ensureDir(map: DirectoryHierarchyMap, dir: string) {
}
type Archive = {
unzipped: unzipper.CentralDirectory,
dirMap: DirectoryHierarchyMap,
unzipped: unzipper.CentralDirectory;
dirMap: DirectoryHierarchyMap;
};
export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
@@ -169,8 +172,8 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
const ref = decodeSourceArchiveUri(uri);
const archive = await this.getArchive(ref.sourceArchiveZipPath);
let contents = archive.dirMap.get(ref.pathWithinSourceArchive);
const result = contents === undefined ? [] : Array.from(contents.entries());
const contents = archive.dirMap.get(ref.pathWithinSourceArchive);
const result = contents === undefined ? undefined : Array.from(contents.entries());
if (result === undefined) {
throw vscode.FileSystemError.FileNotFound(uri);
}
@@ -189,7 +192,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// write operations, all disabled
writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): void {
writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { create: boolean; overwrite: boolean }): void {
throw this.readOnlyError;
}
@@ -235,11 +238,11 @@ 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> {
let entry = await this._lookup(uri);
const entry = await this._lookup(uri);
if (entry instanceof File) {
return entry;
}
@@ -254,7 +257,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
watch(_resource: vscode.Uri): vscode.Disposable {
// ignore, fires for all changes...
return new vscode.Disposable(() => { });
return new vscode.Disposable(() => { /**/ });
}
}

View File

@@ -0,0 +1,99 @@
export const PAGE_SIZE = 1000;
/**
* The single-character codes used in the bqrs format for the the kind
* of a result column. This namespace is intentionally not an enum, see
* the "for the sake of extensibility" comment in messages.ts.
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ColumnKindCode {
export const FLOAT = 'f';
export const INTEGER = 'i';
export const STRING = 's';
export const BOOLEAN = 'b';
export const DATE = 'd';
export const ENTITY = 'e';
}
export type ColumnKind =
| typeof ColumnKindCode.FLOAT
| typeof ColumnKindCode.INTEGER
| typeof ColumnKindCode.STRING
| typeof ColumnKindCode.BOOLEAN
| typeof ColumnKindCode.DATE
| typeof ColumnKindCode.ENTITY;
export interface Column {
name?: string;
kind: ColumnKind;
}
export interface ResultSetSchema {
name: string;
rows: number;
columns: Column[];
pagination?: PaginationInfo;
}
export function getResultSetSchema(resultSetName: string, resultSets: BQRSInfo): ResultSetSchema | undefined {
for (const schema of resultSets['result-sets']) {
if (schema.name === resultSetName) {
return schema;
}
}
return undefined;
}
export interface PaginationInfo {
'step-size': number;
offsets: number[];
}
export interface BQRSInfo {
'result-sets': ResultSetSchema[];
}
export interface EntityValue {
url?: UrlValue;
label?: string;
}
export interface LineColumnLocation {
uri: string;
startLine: number;
startColumn: number;
endLine: number;
endColumn: number;
charOffset: never;
charLength: never;
}
export interface OffsetLengthLocation {
uri: string;
startLine: never;
startColumn: never;
endLine: never;
endColumn: never;
charOffset: number;
charLength: number;
}
export interface WholeFileLocation {
uri: string;
startLine: never;
startColumn: never;
endLine: never;
endColumn: never;
charOffset: never;
charLength: never;
}
export type UrlValue = LineColumnLocation | OffsetLengthLocation | WholeFileLocation | string;
export type ColumnValue = EntityValue | number | string | boolean;
export interface DecodedBqrsChunk {
tuples: ColumnValue[][];
next?: number;
}

View File

@@ -1,96 +1,24 @@
import { runCodeQlCliCommand } from "./cli";
import { Logger } from "./logging";
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> {
const output: string = await runCodeQlCliCommand(
codeQlPath,
["version"],
["--format=terse"],
"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) {
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<semver.SemVer | undefined> {
try {
const output: string = await runCodeQlCliCommand(
codeQlPath,
['version'],
['--format=terse'],
'Checking CodeQL version',
logger
);
return semver.parse(output.trim()) || undefined;
} catch (e) {
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted.
// Either way, we can't determine compatibility.
logger.log(`Failed to run 'codeql version'. Reason: ${e.message}`);
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;
}

View File

@@ -1,18 +1,24 @@
import * as child_process from "child_process";
/* eslint-disable @typescript-eslint/camelcase */
import * as cpp from 'child-process-promise';
import * as child_process from 'child_process';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as sarif from 'sarif';
import { Readable } from 'stream';
import { StringDecoder } from 'string_decoder';
import * as tk from 'tree-kill';
import * as util from 'util';
import { Logger, ProgressReporter } from "./logging";
import { Disposable } from "vscode";
import { DistributionProvider } from "./distribution";
import { SortDirection, QueryMetadata } from "./interface-types";
import { assertNever } from "./helpers-pure";
import { CancellationToken, Disposable } from 'vscode';
import { BQRSInfo, DecodedBqrsChunk } from './bqrs-cli-types';
import { DistributionProvider } from './distribution';
import { assertNever } from './helpers-pure';
import { QueryMetadata, SortDirection } from './interface-types';
import { Logger, ProgressReporter } from './logging';
/**
* The version of the SARIF format that we are using.
*/
const SARIF_FORMAT = "sarifv2.1.0";
const SARIF_FORMAT = 'sarifv2.1.0';
/**
* Flags to pass to all cli commands.
@@ -23,10 +29,10 @@ const LOGGING_FLAGS = ['-v', '--log-to-stderr'];
* The expected output of `codeql resolve library-path`.
*/
export interface QuerySetup {
libraryPath: string[],
dbscheme: string,
relativeName?: string,
compilationCache?: string
libraryPath: string[];
dbscheme: string;
relativeName?: string;
compilationCache?: string;
}
/**
@@ -62,6 +68,31 @@ export interface SourceInfo {
sourceLocationPrefix: string;
}
/**
* The expected output of `codeql resolve tests`.
*/
export type ResolvedTests = string[];
/**
* Options for `codeql test run`.
*/
export interface TestRunOptions {
cancellationToken?: CancellationToken;
logger?: Logger;
}
/**
* Event fired by `codeql test run`.
*/
export interface TestCompleted {
test: string;
pass: boolean;
messages: string[];
compilationMs: number;
evaluationMs: number;
expected: string;
}
/**
* This class manages a cli server started by `codeql execute cli-server` to
* run commands without the overhead of starting a new java
@@ -91,16 +122,16 @@ export class CodeQLCliServer implements Disposable {
}
dispose() {
dispose(): void {
this.killProcessIfRunning();
}
killProcessIfRunning() {
killProcessIfRunning(): void {
if (this.process) {
// Tell the Java CLI server process to shut down.
this.logger.log('Sending shutdown request');
try {
this.process.stdin.write(JSON.stringify(["shutdown"]), "utf8");
this.process.stdin.write(JSON.stringify(['shutdown']), 'utf8');
this.process.stdin.write(this.nullBuffer);
this.logger.log('Sent shutdown request');
} catch (e) {
@@ -122,8 +153,8 @@ export class CodeQLCliServer implements Disposable {
/**
* Restart the server when the current command terminates
*/
private restartCliServer() {
let callback = () => {
private restartCliServer(): void {
const callback = (): void => {
try {
this.killProcessIfRunning();
} finally {
@@ -134,43 +165,51 @@ 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();
}
}
/**
* Get the path to the CodeQL CLI distribution, or throw an exception if not found.
*/
private async getCodeQlPath(): Promise<string> {
const codeqlPath = await this.config.getCodeQlPathWithoutVersionCheck();
if (!codeqlPath) {
throw new Error('Failed to find CodeQL distribution.');
}
return codeqlPath;
}
/**
* Launch the cli server
*/
private async launchProcess(): Promise<child_process.ChildProcessWithoutNullStreams> {
const config = await this.config.getCodeQlPathWithoutVersionCheck();
if (!config) {
throw new Error("Failed to find codeql distribution")
}
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => { })
const config = await this.getCodeQlPath();
return spawnServer(config, 'CodeQL CLI Server', ['execute', 'cli-server'], [], this.logger, _data => { /**/ });
}
private async runCodeQlCliInternal(command: string[], commandArgs: string[], description: string): Promise<string> {
let stderrBuffers: Buffer[] = [];
const stderrBuffers: Buffer[] = [];
if (this.commandInProcess) {
throw new Error("runCodeQlCliInternal called while cli was running")
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;
// The array of fragments of stdout
let stdoutBuffers: Buffer[] = [];
const stdoutBuffers: Buffer[] = [];
// Compute the full args array
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(" ");
const argsString = args.join(' ');
this.logger.log(`${description} using CodeQL CLI: ${argsString}...`);
try {
await new Promise((resolve, reject) => {
@@ -189,32 +228,33 @@ export class CodeQLCliServer implements Disposable {
stderrBuffers.push(newData);
});
// Listen for process exit.
process.addListener("close", (code) => reject(code));
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
let fullBuffer = Buffer.concat(stdoutBuffers);
const fullBuffer = Buffer.concat(stdoutBuffers);
// Make sure we remove the terminator;
let data = fullBuffer.toString("utf8", 0, fullBuffer.length - 1);
this.logger.log(`CLI command succeeded.`);
const data = fullBuffer.toString('utf8', 0, fullBuffer.length - 1);
this.logger.log('CLI command succeeded.');
return data;
} catch (err) {
// Kill the process if it isn't already dead.
this.killProcessIfRunning();
// Report the error (if there is a stderr then use that otherwise just report the error cod or nodejs error)
if (stderrBuffers.length == 0) {
throw new Error(`${description} failed: ${err}`)
} else {
throw new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
}
const newError =
stderrBuffers.length == 0
? new Error(`${description} failed: ${err}`)
: new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString('utf8')}`);
newError.stack += (err.stack || '');
throw newError;
} finally {
this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
this.logger.log(Buffer.concat(stderrBuffers).toString('utf8'));
// Remove the listeners we set up.
process.stdout.removeAllListeners('data')
process.stderr.removeAllListeners('data')
process.removeAllListeners("close");
process.stdout.removeAllListeners('data');
process.stderr.removeAllListeners('data');
process.removeAllListeners('close');
}
} finally {
this.commandInProcess = false;
@@ -226,13 +266,94 @@ export class CodeQLCliServer implements Disposable {
/**
* Run the next command in the queue
*/
private runNext() {
private runNext(): void {
const callback = this.commandQueue.shift();
if (callback) {
callback();
}
}
/**
* Runs an asynchronous CodeQL CLI command without invoking the CLI server, returning any events
* fired by the command as an asynchronous generator.
*
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param cancellationToken CancellationToken to terminate the test process.
* @param logger Logger to write text output from the command.
* @returns The sequence of async events produced by the command.
*/
private async* runAsyncCodeQlCliCommandInternal(
command: string[],
commandArgs: string[],
cancellationToken?: CancellationToken,
logger?: Logger
): AsyncGenerator<string, void, unknown> {
// Add format argument first, in case commandArgs contains positional parameters.
const args = [
...command,
'--format', 'jsonz',
...commandArgs
];
// Spawn the CodeQL process
const codeqlPath = await this.getCodeQlPath();
const childPromise = cpp.spawn(codeqlPath, args);
const child = childPromise.childProcess;
let cancellationRegistration: Disposable | undefined = undefined;
try {
if (cancellationToken !== undefined) {
cancellationRegistration = cancellationToken.onCancellationRequested(_e => {
tk(child.pid);
});
}
if (logger !== undefined) {
// The human-readable output goes to stderr.
logStream(child.stderr!, logger);
}
for await (const event of await splitStreamAtSeparators(child.stdout!, ['\0'])) {
yield event;
}
await childPromise;
}
finally {
if (cancellationRegistration !== undefined) {
cancellationRegistration.dispose();
}
}
}
/**
* Runs an asynchronous CodeQL CLI command without invoking the CLI server, returning any events
* fired by the command as an asynchronous generator.
*
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param description Description of the action being run, to be shown in log and error messages.
* @param cancellationToken CancellationToken to terminate the test process.
* @param logger Logger to write text output from the command.
* @returns The sequence of async events produced by the command.
*/
public async* runAsyncCodeQlCliCommand<EventType>(
command: string[],
commandArgs: string[],
description: string,
cancellationToken?: CancellationToken,
logger?: Logger
): AsyncGenerator<EventType, void, unknown> {
for await (const event of await this.runAsyncCodeQlCliCommandInternal(command, commandArgs,
cancellationToken, logger)) {
try {
yield JSON.parse(event) as EventType;
} catch (err) {
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
}
}
}
/**
* Runs a CodeQL CLI command on the server, returning the output as a string.
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
@@ -248,17 +369,17 @@ export class CodeQLCliServer implements Disposable {
return new Promise((resolve, reject) => {
// Construct the command that actually does the work
const callback = () => {
const callback = (): void => {
try {
this.runCodeQlCliInternal(command, commandArgs, description).then(resolve, reject);
} catch (err) {
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();
}
@@ -280,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}`);
}
}
@@ -292,10 +413,44 @@ export class CodeQLCliServer implements Disposable {
async resolveLibraryPath(workspaces: string[], queryPath: string): Promise<QuerySetup> {
const subcommandArgs = [
'--query', queryPath,
"--additional-packs",
'--additional-packs',
workspaces.join(path.delimiter)
];
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, "Resolving library paths");
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, 'Resolving library paths');
}
/**
* Finds all available QL tests in a given directory.
* @param testPath Root of directory tree to search for tests.
* @returns The list of tests that were found.
*/
public async resolveTests(testPath: string): Promise<ResolvedTests> {
const subcommandArgs = [
testPath
];
return await this.runJsonCodeQlCliCommand<ResolvedTests>(['resolve', 'tests'], subcommandArgs, 'Resolving tests');
}
/**
* Runs QL tests.
* @param testPaths Full paths of the tests to run.
* @param workspaces Workspace paths to use as search paths for QL packs.
* @param options Additional options.
*/
public async* runTests(
testPaths: string[], workspaces: string[], options: TestRunOptions
): AsyncGenerator<TestCompleted, void, unknown> {
const subcommandArgs = [
'--additional-packs', workspaces.join(path.delimiter),
'--threads', '8',
...testPaths
];
for await (const event of await this.runAsyncCodeQlCliCommand<TestCompleted>(['test', 'run'],
subcommandArgs, 'Run CodeQL Tests', options.cancellationToken, options.logger)) {
yield event;
}
}
/**
@@ -303,13 +458,14 @@ export class CodeQLCliServer implements Disposable {
* @param queryPath The path to the query.
*/
async resolveMetadata(queryPath: string): Promise<QueryMetadata> {
return await this.runJsonCodeQlCliCommand<QueryMetadata>(['resolve', 'metadata'], [queryPath], "Resolving query metadata");
return await this.runJsonCodeQlCliCommand<QueryMetadata>(['resolve', 'metadata'], [queryPath], 'Resolving query metadata');
}
/**
* Gets the RAM setting for the query server.
* @param queryMemoryMb The maximum amount of RAM to use, in MB.
* Leave `undefined` for CodeQL to choose a limit based on the available system memory.
* @param progressReporter The progress reporter to send progress information to.
* @returns String arguments that can be passed to the CodeQL query server,
* indicating how to split the given RAM limit between heap and off-heap memory.
*/
@@ -318,41 +474,73 @@ export class CodeQLCliServer implements Disposable {
if (queryMemoryMb !== undefined) {
args.push('--ram', queryMemoryMb.toString());
}
return await this.runJsonCodeQlCliCommand<string[]>(['resolve', 'ram'], args, "Resolving RAM settings", progressReporter);
return await this.runJsonCodeQlCliCommand<string[]>(['resolve', 'ram'], args, 'Resolving RAM settings', progressReporter);
}
/**
* Gets the headers (and optionally pagination info) of a bqrs.
* @param bqrsPath The path to the bqrs.
* @param pageSize The page size to precompute offsets into the binary file for.
*/
async bqrsInfo(bqrsPath: string, pageSize?: number): Promise<BQRSInfo> {
const subcommandArgs = (
pageSize ? ['--paginate-rows', pageSize.toString()] : []
).concat(
bqrsPath
);
return await this.runJsonCodeQlCliCommand<BQRSInfo>(['bqrs', 'info'], subcommandArgs, 'Reading bqrs header');
}
/**
* Gets the results from a bqrs.
* @param bqrsPath The path to the bqrs.
* @param resultSet The result set to get.
* @param pageSize How many results to get.
* @param offset The 0-based index of the first result to get.
*/
async bqrsDecode(bqrsPath: string, resultSet: string, pageSize?: number, offset?: number): Promise<DecodedBqrsChunk> {
const subcommandArgs = [
'--entities=url,string',
'--result-set', resultSet,
].concat(
pageSize ? ['--rows', pageSize.toString()] : []
).concat(
offset ? ['--start-at', offset.toString()] : []
).concat([bqrsPath]);
return await this.runJsonCodeQlCliCommand<DecodedBqrsChunk>(['bqrs', 'decode'], subcommandArgs, 'Reading bqrs data');
}
async interpretBqrs(metadata: { kind: string, id: string }, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
async interpretBqrs(metadata: { kind: string; id: string }, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
const args = [
`-t=kind=${metadata.kind}`,
`-t=id=${metadata.id}`,
"--output", interpretedResultsPath,
"--format", SARIF_FORMAT,
'--output', interpretedResultsPath,
'--format', SARIF_FORMAT,
// TODO: This flag means that we don't group interpreted results
// by primary location. We may want to revisit whether we call
// interpretation with and without this flag, or do some
// grouping client-side.
"--no-group-results",
'--no-group-results',
];
if (sourceInfo !== undefined) {
args.push(
"--source-archive", sourceInfo.sourceArchive,
"--source-location-prefix", sourceInfo.sourceLocationPrefix
'--source-archive', sourceInfo.sourceArchive,
'--source-location-prefix', sourceInfo.sourceLocationPrefix
);
}
args.push(resultsPath);
await this.runCodeQlCliCommand(['bqrs', 'interpret'], args, "Interpreting query results");
await this.runCodeQlCliCommand(['bqrs', 'interpret'], args, 'Interpreting query results');
let output: string;
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}`);
}
}
@@ -361,9 +549,9 @@ export class CodeQLCliServer implements Disposable {
const sortDirectionStrings = sortDirections.map(direction => {
switch (direction) {
case SortDirection.asc:
return "asc";
return 'asc';
case SortDirection.desc:
return "desc";
return 'desc';
default:
return assertNever(direction);
}
@@ -371,14 +559,14 @@ export class CodeQLCliServer implements Disposable {
await this.runCodeQlCliCommand(['bqrs', 'decode'],
[
"--format=bqrs",
'--format=bqrs',
`--result-set=${resultSet}`,
`--output=${sortedResultsPath}`,
`--sort-key=${sortKeys.join(",")}`,
`--sort-direction=${sortDirectionStrings.join(",")}`,
`--sort-key=${sortKeys.join(',')}`,
`--sort-direction=${sortDirectionStrings.join(',')}`,
resultsPath
],
"Sorting query results");
'Sorting query results');
}
@@ -388,7 +576,7 @@ export class CodeQLCliServer implements Disposable {
*/
resolveDatabase(databasePath: string): Promise<DbInfo> {
return this.runJsonCodeQlCliCommand(['resolve', 'database'], [databasePath],
"Resolving database");
'Resolving database');
}
/**
@@ -403,22 +591,48 @@ export class CodeQLCliServer implements Disposable {
return this.runJsonCodeQlCliCommand<UpgradesInfo>(
['resolve', 'upgrades'],
args,
"Resolving database upgrade scripts",
'Resolving database upgrade scripts',
);
}
/**
* Gets information about available qlpacks
* @param searchPath A list of directories to search for qlpacks
* @param additionalPacks A list of directories to search for qlpacks before searching in `searchPath`.
* @param searchPath A list of directories to search for packs not found in `additionalPacks`. If undefined,
* the default CLI search path is used.
* @returns A dictionary mapping qlpack name to the directory it comes from
*/
resolveQlpacks(searchPath: string[]): Promise<QlpacksInfo> {
const args = ['--additional-packs', searchPath.join(path.delimiter)];
resolveQlpacks(additionalPacks: string[], searchPath?: string[]): Promise<QlpacksInfo> {
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
if (searchPath !== undefined) {
args.push('--search-path', path.join(...searchPath));
}
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
['resolve', 'qlpacks'],
args,
"Resolving qlpack information",
'Resolving qlpack information',
);
}
/**
* Gets information about queries in a query suite.
* @param suite The suite to resolve.
* @param additionalPacks A list of directories to search for qlpacks before searching in `searchPath`.
* @param searchPath A list of directories to search for packs not found in `additionalPacks`. If undefined,
* the default CLI search path is used.
* @returns A list of query files found.
*/
resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
if (searchPath !== undefined) {
args.push('--search-path', path.join(...searchPath));
}
args.push(suite);
return this.runJsonCodeQlCliCommand<string[]>(
['resolve', 'queries'],
args,
'Resolving queries',
);
}
}
@@ -452,7 +666,7 @@ export function spawnServer(
// Start the server process.
const base = codeqlPath;
const argsString = args.join(" ");
const argsString = args.join(' ');
if (progressReporter !== undefined) {
progressReporter.report({ message: `Starting ${name}` });
}
@@ -489,7 +703,7 @@ export function spawnServer(
export async function runCodeQlCliCommand(codeQlPath: string, command: string[], commandArgs: string[], description: string, logger: Logger, progressReporter?: ProgressReporter): Promise<string> {
// Add logging arguments first, in case commandArgs contains positional parameters.
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(" ");
const argsString = args.join(' ');
try {
if (progressReporter !== undefined) {
progressReporter.report({ message: description });
@@ -497,9 +711,107 @@ export async function runCodeQlCliCommand(codeQlPath: string, command: string[],
logger.log(`${description} using CodeQL CLI: ${codeQlPath} ${argsString}...`);
const result = await util.promisify(child_process.execFile)(codeQlPath, args);
logger.log(result.stderr);
logger.log(`CLI command succeeded.`);
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}`);
}
}
/**
* Buffer to hold state used when splitting a text stream into lines.
*/
class SplitBuffer {
private readonly decoder = new StringDecoder('utf8');
private readonly maxSeparatorLength: number;
private buffer = '';
private searchIndex = 0;
constructor(private readonly separators: readonly string[]) {
this.maxSeparatorLength = separators.map(s => s.length).reduce((a, b) => Math.max(a, b), 0);
}
/**
* Append new text data to the buffer.
* @param chunk The chunk of data to append.
*/
public addChunk(chunk: Buffer): void {
this.buffer += this.decoder.write(chunk);
}
/**
* Signal that the end of the input stream has been reached.
*/
public end(): void {
this.buffer += this.decoder.end();
this.buffer += this.separators[0]; // Append a separator to the end to ensure the last line is returned.
}
/**
* Extract the next full line from the buffer, if one is available.
* @returns The text of the next available full line (without the separator), or `undefined` if no
* line is available.
*/
public getNextLine(): string | undefined {
while (this.searchIndex <= (this.buffer.length - this.maxSeparatorLength)) {
for (const separator of this.separators) {
if (this.buffer.startsWith(separator, this.searchIndex)) {
const line = this.buffer.substr(0, this.searchIndex);
this.buffer = this.buffer.substr(this.searchIndex + separator.length);
this.searchIndex = 0;
return line;
}
}
this.searchIndex++;
}
return undefined;
}
}
/**
* Splits a text stream into lines based on a list of valid line separators.
* @param stream The text stream to split. This stream will be fully consumed.
* @param separators The list of strings that act as line separators.
* @returns A sequence of lines (not including separators).
*/
async function* splitStreamAtSeparators(
stream: Readable, separators: string[]
): AsyncGenerator<string, void, unknown> {
const buffer = new SplitBuffer(separators);
for await (const chunk of stream) {
buffer.addChunk(chunk);
let line: string | undefined;
do {
line = buffer.getNextLine();
if (line !== undefined) {
yield line;
}
} while (line !== undefined);
}
buffer.end();
let line: string | undefined;
do {
line = buffer.getNextLine();
if (line !== undefined) {
yield line;
}
} while (line !== undefined);
}
/**
* Standard line endings for splitting human-readable text.
*/
const lineEndings = ['\r\n', '\r', '\n'];
/**
* Log a text stream to a `Logger` interface.
* @param stream The stream to log.
* @param logger The logger that will consume the stream output.
*/
async function logStream(stream: Readable, logger: Logger): Promise<void> {
for await (const line of await splitStreamAtSeparators(stream, lineEndings)) {
logger.log(line);
}
}

View File

@@ -0,0 +1,279 @@
import { DisposableObject } from '@github/codeql-vscode-utils';
import {
WebviewPanel,
ExtensionContext,
window as Window,
ViewColumn,
Uri,
} from 'vscode';
import * as path from 'path';
import { tmpDir } from '../run-queries';
import { CompletedQuery } from '../query-results';
import {
FromCompareViewMessage,
ToCompareViewMessage,
QueryCompareResult,
} from '../interface-types';
import { Logger } from '../logging';
import { CodeQLCliServer } from '../cli';
import { DatabaseManager } from '../databases';
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
import { adaptSchema, adaptBqrs, RawResultSet } from '../adapt';
import { BQRSInfo } from '../bqrs-cli-types';
import resultsDiff from './resultsDiff';
interface ComparePair {
from: CompletedQuery;
to: CompletedQuery;
}
export class CompareInterfaceManager extends DisposableObject {
private comparePair: ComparePair | undefined;
private panel: WebviewPanel | undefined;
private panelLoaded = false;
private panelLoadedCallBacks: (() => void)[] = [];
constructor(
private ctx: ExtensionContext,
private databaseManager: DatabaseManager,
private cliServer: CodeQLCliServer,
private logger: Logger,
private showQueryResultsCallback: (
item: CompletedQuery
) => Promise<void>
) {
super();
}
async showResults(
from: CompletedQuery,
to: CompletedQuery,
selectedResultSetName?: string
) {
this.comparePair = { from, to };
this.getPanel().reveal(undefined, true);
await this.waitForPanelLoaded();
const [
commonResultSetNames,
currentResultSetName,
fromResultSet,
toResultSet,
] = await this.findCommonResultSetNames(
from,
to,
selectedResultSetName
);
if (currentResultSetName) {
let rows: QueryCompareResult | undefined;
let message: string | undefined;
try {
rows = this.compareResults(fromResultSet, toResultSet);
} catch (e) {
message = e.message;
}
await this.postMessage({
t: 'setComparisons',
stats: {
fromQuery: {
// since we split the description into several rows
// only run interpolation if the label is user-defined
// otherwise we will wind up with duplicated rows
name: from.options.label
? from.interpolate(from.getLabel())
: from.queryName,
status: from.statusString,
time: from.time,
},
toQuery: {
name: to.options.label
? to.interpolate(to.getLabel())
: to.queryName,
status: to.statusString,
time: to.time,
},
},
columns: fromResultSet.schema.columns,
commonResultSetNames,
currentResultSetName: currentResultSetName,
rows,
message,
datebaseUri: to.database.databaseUri,
});
}
}
getPanel(): WebviewPanel {
if (this.panel == undefined) {
const { ctx } = this;
const panel = (this.panel = Window.createWebviewPanel(
'compareView',
'Compare CodeQL Query Results',
{ viewColumn: ViewColumn.Active, preserveFocus: true },
{
enableScripts: true,
enableFindWidget: true,
retainContextWhenHidden: true,
localResourceRoots: [
Uri.file(tmpDir.name),
Uri.file(path.join(this.ctx.extensionPath, 'out')),
],
}
));
this.panel.onDidDispose(
() => {
this.panel = undefined;
this.comparePair = undefined;
},
null,
ctx.subscriptions
);
const scriptPathOnDisk = Uri.file(
ctx.asAbsolutePath('out/compareView.js')
);
const stylesheetPathOnDisk = Uri.file(
ctx.asAbsolutePath('out/resultsView.css')
);
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
stylesheetPathOnDisk
);
panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
);
}
return this.panel;
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve) => {
if (this.panelLoaded) {
resolve();
} else {
this.panelLoadedCallBacks.push(resolve);
}
});
}
private async handleMsgFromView(
msg: FromCompareViewMessage
): Promise<void> {
switch (msg.t) {
case 'compareViewLoaded':
this.panelLoaded = true;
this.panelLoadedCallBacks.forEach((cb) => cb());
this.panelLoadedCallBacks = [];
break;
case 'changeCompare':
this.changeTable(msg.newResultSetName);
break;
case 'viewSourceFile':
await jumpToLocation(msg, this.databaseManager, this.logger);
break;
case 'openQuery':
await this.openQuery(msg.kind);
break;
}
}
private postMessage(msg: ToCompareViewMessage): Thenable<boolean> {
return this.getPanel().webview.postMessage(msg);
}
private async findCommonResultSetNames(
from: CompletedQuery,
to: CompletedQuery,
selectedResultSetName: string | undefined
): Promise<[string[], string, RawResultSet, RawResultSet]> {
const fromSchemas = await this.cliServer.bqrsInfo(
from.query.resultsPaths.resultsPath
);
const toSchemas = await this.cliServer.bqrsInfo(
to.query.resultsPaths.resultsPath
);
const fromSchemaNames = fromSchemas['result-sets'].map(
(schema) => schema.name
);
const toSchemaNames = toSchemas['result-sets'].map(
(schema) => schema.name
);
const commonResultSetNames = fromSchemaNames.filter((name) =>
toSchemaNames.includes(name)
);
const currentResultSetName =
selectedResultSetName || commonResultSetNames[0];
const fromResultSet = await this.getResultSet(
fromSchemas,
currentResultSetName,
from.query.resultsPaths.resultsPath
);
const toResultSet = await this.getResultSet(
toSchemas,
currentResultSetName,
to.query.resultsPaths.resultsPath
);
return [
commonResultSetNames,
currentResultSetName,
fromResultSet,
toResultSet,
];
}
private async changeTable(newResultSetName: string) {
if (!this.comparePair?.from || !this.comparePair.to) {
return;
}
await this.showResults(
this.comparePair.from,
this.comparePair.to,
newResultSetName
);
}
private async getResultSet(
bqrsInfo: BQRSInfo,
resultSetName: string,
resultsPath: string
): Promise<RawResultSet> {
const schema = bqrsInfo['result-sets'].find(
(schema) => schema.name === resultSetName
);
if (!schema) {
throw new Error(`Schema ${resultSetName} not found.`);
}
const chunk = await this.cliServer.bqrsDecode(
resultsPath,
resultSetName
);
const adaptedSchema = adaptSchema(schema);
return adaptBqrs(adaptedSchema, chunk);
}
private compareResults(
fromResults: RawResultSet,
toResults: RawResultSet
): QueryCompareResult {
// Only compare columns that have the same name
return resultsDiff(fromResults, toResults);
}
private openQuery(kind: 'from' | 'to') {
const toOpen =
kind === 'from' ? this.comparePair?.from : this.comparePair?.to;
if (toOpen) {
this.showQueryResultsCallback(toOpen);
}
}
}

View File

@@ -0,0 +1,58 @@
import { RawResultSet } from '../adapt';
import { QueryCompareResult } from '../interface-types';
/**
* Compare the rows of two queries. Use deep equality to determine if
* rows have been added or removed across two invocations of a query.
*
* Assumptions:
*
* 1. Queries have the same sort order
* 2. Queries have same number and order of columns
* 3. Rows are not changed or re-ordered, they are only added or removed
*
* @param fromResults the source query
* @param toResults the target query
*
* @throws Error when:
* 1. number of columns do not match
* 2. If either query is empty
* 3. If the queries are 100% disjoint
*/
export default function resultsDiff(
fromResults: RawResultSet,
toResults: RawResultSet
): QueryCompareResult {
if (fromResults.schema.columns.length !== toResults.schema.columns.length) {
throw new Error('CodeQL Compare: Columns do not match.');
}
if (!fromResults.rows.length) {
throw new Error('CodeQL Compare: Source query has no results.');
}
if (!toResults.rows.length) {
throw new Error('CodeQL Compare: Target query has no results.');
}
const results = {
from: arrayDiff(fromResults.rows, toResults.rows),
to: arrayDiff(toResults.rows, fromResults.rows),
};
if (
fromResults.rows.length === results.from.length &&
toResults.rows.length === results.to.length
) {
throw new Error('CodeQL Compare: No overlap between the selected queries.');
}
return results;
}
function arrayDiff<T>(source: readonly T[], toRemove: readonly T[]): T[] {
// Stringify the object so that we can compare hashes in the set
const rest = new Set(toRemove.map((item) => JSON.stringify(item)));
return source.filter((element) => !rest.has(JSON.stringify(element)));
}

View File

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

View File

@@ -0,0 +1,78 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import * as Rdom from 'react-dom';
import {
ToCompareViewMessage,
SetComparisonsMessage,
} from '../../interface-types';
import CompareSelector from './CompareSelector';
import { vscode } from '../../view/vscode-api';
import CompareTable from './CompareTable';
const emptyComparison: SetComparisonsMessage = {
t: 'setComparisons',
stats: {},
rows: undefined,
columns: [],
commonResultSetNames: [],
currentResultSetName: '',
datebaseUri: '',
message: 'Empty comparison'
};
export function Compare(_: {}): JSX.Element {
const [comparison, setComparison] = useState<SetComparisonsMessage>(
emptyComparison
);
const message = comparison.message || 'Empty comparison';
const hasRows = comparison.rows && (comparison.rows.to.length || comparison.rows.from.length);
useEffect(() => {
window.addEventListener('message', (evt: MessageEvent) => {
const msg: ToCompareViewMessage = evt.data;
switch (msg.t) {
case 'setComparisons':
setComparison(msg);
}
});
});
if (!comparison) {
return <div>Waiting for results to load.</div>;
}
try {
return (
<>
<div className="vscode-codeql__compare-header">
<div className="vscode-codeql__compare-header-item">
Table to compare:
</div>
<CompareSelector
availableResultSets={comparison.commonResultSetNames}
currentResultSetName={comparison.currentResultSetName}
updateResultSet={(newResultSetName: string) =>
vscode.postMessage({ t: 'changeCompare', newResultSetName })
}
/>
</div>
{hasRows ? (
<CompareTable comparison={comparison}></CompareTable>
) : (
<div className="vscode-codeql__compare-message">{message}</div>
)}
</>
);
} catch (err) {
console.error(err);
return <div>Error!</div>;
}
}
Rdom.render(
<Compare />,
document.getElementById('root'),
// Post a message to the extension when fully loaded.
() => vscode.postMessage({ t: 'compareViewLoaded' })
);

View File

@@ -0,0 +1,22 @@
import * as React from 'react';
interface Props {
availableResultSets: string[];
currentResultSetName: string;
updateResultSet: (newResultSet: string) => void;
}
export default function CompareSelector(props: Props) {
return (
<select
value={props.currentResultSetName}
onChange={(e) => props.updateResultSet(e.target.value)}
>
{props.availableResultSets.map((resultSet) => (
<option key={resultSet} value={resultSet}>
{resultSet}
</option>
))}
</select>
);
}

View File

@@ -0,0 +1,96 @@
import * as React from 'react';
import { SetComparisonsMessage } from '../../interface-types';
import RawTableHeader from '../../view/RawTableHeader';
import { className } from '../../view/result-table-utils';
import { ResultRow } from '../../adapt';
import RawTableRow from '../../view/RawTableRow';
import { vscode } from '../../view/vscode-api';
interface Props {
comparison: SetComparisonsMessage;
}
export default function CompareTable(props: Props) {
const comparison = props.comparison;
const rows = props.comparison.rows!;
async function openQuery(kind: 'from' | 'to') {
vscode.postMessage({
t: 'openQuery',
kind,
});
}
function createRows(rows: ResultRow[], databaseUri: string) {
return (
<tbody>
{rows.map((row, rowIndex) => (
<RawTableRow
key={rowIndex}
rowIndex={rowIndex}
row={row}
databaseUri={databaseUri}
/>
))}
</tbody>
);
}
return (
<table className='vscode-codeql__compare-body'>
<thead>
<tr>
<td>
<a
onClick={() => openQuery('from')}
className='vscode-codeql__compare-open'
>
{comparison.stats.fromQuery?.name}
</a>
</td>
<td>
<a
onClick={() => openQuery('to')}
className='vscode-codeql__compare-open'
>
{comparison.stats.toQuery?.name}
</a>
</td>
</tr>
<tr>
<td>{comparison.stats.fromQuery?.time}</td>
<td>{comparison.stats.toQuery?.time}</td>
</tr>
<tr>
<th>{rows.from.length} rows removed</th>
<th>{rows.to.length} rows added</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<table className={className}>
<RawTableHeader
columns={comparison.columns}
schemaName={comparison.currentResultSetName}
preventSort={true}
/>
{createRows(rows.from, comparison.datebaseUri)}
</table>
</td>
<td>
<table className={className}>
<RawTableHeader
columns={comparison.columns}
schemaName={comparison.currentResultSetName}
preventSort={true}
/>
{createRows(rows.to, comparison.datebaseUri)}
</table>
</td>
</tr>
</tbody>
</table>
);
}

View File

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

View File

@@ -1,5 +1,5 @@
import { DisposableObject } from 'semmle-vscode-utils';
import { workspace, Event, EventEmitter, ConfigurationChangeEvent } from 'vscode';
import { DisposableObject } from '@github/codeql-vscode-utils';
import { workspace, Event, EventEmitter, ConfigurationChangeEvent, ConfigurationTarget } from 'vscode';
import { DistributionManager } from './distribution';
import { logger } from './logging';
@@ -27,10 +27,30 @@ class Setting {
}
return workspace.getConfiguration(this.parent.qualifiedName).get<T>(this.name)!;
}
updateValue<T>(value: T, target: ConfigurationTarget): Thenable<void> {
if (this.parent === undefined) {
throw new Error('Cannot update the value of a root setting.');
}
return workspace.getConfiguration(this.parent.qualifiedName).update(this.name, value, target);
}
}
const ROOT_SETTING = new Setting('codeQL');
// Enable experimental features
/**
* 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.
*/
/* 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);
// Distribution configuration
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
@@ -59,16 +79,17 @@ const NUMBER_OF_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_QUERIES
const TIMEOUT_SETTING = new Setting('timeout', RUNNING_QUERIES_SETTING);
const MEMORY_SETTING = new Setting('memory', RUNNING_QUERIES_SETTING);
const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
export const AUTOSAVE_SETTING = new Setting('autoSave', RUNNING_QUERIES_SETTING);
/** When these settings change, the running query server should be restarted. */
const QUERY_SERVER_RESTARTING_SETTINGS = [NUMBER_OF_THREADS_SETTING, MEMORY_SETTING, DEBUG_SETTING];
export interface QueryServerConfig {
codeQlPath: string,
debug: boolean,
numThreads: number,
queryMemoryMb?: number,
timeoutSecs: number,
codeQlPath: string;
debug: boolean;
numThreads: number;
queryMemoryMb?: number;
timeoutSecs: number;
onDidChangeQueryServerConfiguration?: Event<void>;
}
@@ -76,7 +97,7 @@ export interface QueryServerConfig {
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING];
export interface QueryHistoryConfig {
format: string,
format: string;
onDidChangeQueryHistoryConfiguration: Event<void>;
}
@@ -111,7 +132,7 @@ abstract class ConfigListener extends DisposableObject {
export class DistributionConfigListener extends ConfigListener implements DistributionConfig {
public get customCodeQlPath(): string | undefined {
return CUSTOM_CODEQL_PATH_SETTING.getValue() ? CUSTOM_CODEQL_PATH_SETTING.getValue() : undefined;
return CUSTOM_CODEQL_PATH_SETTING.getValue() || undefined;
}
public get includePrerelease(): boolean {
@@ -119,7 +140,7 @@ export class DistributionConfigListener extends ConfigListener implements Distri
}
public get personalAccessToken(): string | undefined {
return PERSONAL_ACCESS_TOKEN_SETTING.getValue() ? PERSONAL_ACCESS_TOKEN_SETTING.getValue() : undefined;
return PERSONAL_ACCESS_TOKEN_SETTING.getValue() || undefined;
}
public get onDidChangeDistributionConfiguration(): Event<void> {

View File

@@ -0,0 +1,467 @@
import fetch, { Response } from 'node-fetch';
import * as unzipper from 'unzipper';
import { zip } from 'zip-a-folder';
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) {
if (e.message.includes('unexpected end of file')) {
showAndLogErrorMessage('Database is corrupt or too large. Try unzipping outside of VS Code and importing the unzipped folder instead.');
} else {
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?.({
message: 'Getting database',
step: 1,
maxStep: 4,
});
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?.({
message: 'Opening database',
step: 3,
maxStep: 4,
});
// 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) {
progressCallback?.({
message: 'Validating and fixing source location',
step: 4,
maxStep: 4,
});
await ensureZippedSourceLocation(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 databaseFile = Uri.parse(databaseUrl).fsPath;
const directory = await unzipper.Open.file(databaseFile);
await directory.extract({ path: unzipPath });
}
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:'
}
);
}
/**
* Databases created by the old odasa tool will not have a zipped
* source location. However, this extension works better if sources
* are zipped.
*
* This function ensures that the source location is zipped. If the
* `src` folder exists and the `src.zip` file does not, the `src`
* folder will be zipped and then deleted.
*
* @param databasePath The full path to the unzipped database
*/
async function ensureZippedSourceLocation(databasePath: string): Promise<void> {
const srcFolderPath = path.join(databasePath, 'src');
const srcZipPath = srcFolderPath + '.zip';
if ((await fs.pathExists(srcFolderPath)) && !(await fs.pathExists(srcZipPath))) {
await zip(srcFolderPath, srcZipPath);
await fs.remove(srcFolderPath);
}
}

View File

@@ -1,22 +1,44 @@
import * as path from 'path';
import { DisposableObject } from 'semmle-vscode-utils';
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
import { DisposableObject } from '@github/codeql-vscode-utils';
import {
commands,
Event,
EventEmitter,
ExtensionContext,
ProviderResult,
TreeDataProvider,
TreeItem,
Uri,
window,
env,
} from 'vscode';
import * as fs from 'fs-extra';
import * as cli from './cli';
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
import { getOnDiskWorkspaceFolders } from './helpers';
import {
DatabaseItem,
DatabaseManager,
getUpgradesDirectories,
} from './databases';
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers';
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';
type ThemableIconPath = { light: string, dark: string } | string;
type ThemableIconPath = { light: string; dark: string } | string;
/**
* Path to icons to display next to currently selected database.
*/
const SELECTED_DATABASE_ICON: ThemableIconPath = {
light: 'media/check-light-mode.svg',
dark: 'media/check-dark-mode.svg',
light: 'media/light/check.svg',
dark: 'media/dark/check.svg',
};
/**
@@ -24,14 +46,23 @@ const SELECTED_DATABASE_ICON: ThemableIconPath = {
*/
const INVALID_DATABASE_ICON: ThemableIconPath = 'media/red-x.svg';
function joinThemableIconPath(base: string, iconPath: ThemableIconPath): ThemableIconPath {
function joinThemableIconPath(
base: string,
iconPath: ThemableIconPath
): ThemableIconPath {
if (typeof iconPath == 'object')
return {
light: path.join(base, iconPath.light),
dark: path.join(base, iconPath.dark)
dark: path.join(base, iconPath.dark),
};
else
return path.join(base, iconPath);
else return path.join(base, iconPath);
}
enum SortOrder {
NameAsc = 'NameAsc',
NameDesc = 'NameDesc',
DateAddedAsc = 'DateAddedAsc',
DateAddedDesc = 'DateAddedDesc',
}
/**
@@ -39,29 +70,46 @@ function joinThemableIconPath(base: string, iconPath: ThemableIconPath): Themabl
*/
class DatabaseTreeDataProvider extends DisposableObject
implements TreeDataProvider<DatabaseItem> {
private _sortOrder = SortOrder.NameAsc;
private readonly _onDidChangeTreeData = new EventEmitter<DatabaseItem | undefined>();
private readonly _onDidChangeTreeData = new EventEmitter<
DatabaseItem | undefined
>();
private currentDatabaseItem: DatabaseItem | undefined;
constructor(private ctx: ExtensionContext, private databaseManager: DatabaseManager) {
constructor(
private ctx: ExtensionContext,
private databaseManager: DatabaseManager
) {
super();
this.currentDatabaseItem = databaseManager.currentDatabaseItem;
this.push(this.databaseManager.onDidChangeDatabaseItem(this.handleDidChangeDatabaseItem));
this.push(this.databaseManager.onDidChangeCurrentDatabaseItem(
this.handleDidChangeCurrentDatabaseItem));
this.push(
this.databaseManager.onDidChangeDatabaseItem(
this.handleDidChangeDatabaseItem
)
);
this.push(
this.databaseManager.onDidChangeCurrentDatabaseItem(
this.handleDidChangeCurrentDatabaseItem
)
);
}
public get onDidChangeTreeData(): Event<DatabaseItem | undefined> {
return this._onDidChangeTreeData.event;
}
private handleDidChangeDatabaseItem = (databaseItem: DatabaseItem | undefined): void => {
private handleDidChangeDatabaseItem = (
databaseItem: DatabaseItem | undefined
): void => {
this._onDidChangeTreeData.fire(databaseItem);
}
};
private handleDidChangeCurrentDatabaseItem = (databaseItem: DatabaseItem | undefined): void => {
private handleDidChangeCurrentDatabaseItem = (
databaseItem: DatabaseItem | undefined
): void => {
if (this.currentDatabaseItem) {
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
}
@@ -69,14 +117,20 @@ class DatabaseTreeDataProvider extends DisposableObject
if (this.currentDatabaseItem) {
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
}
}
};
public getTreeItem(element: DatabaseItem): TreeItem {
const item = new TreeItem(element.name);
if (element === this.currentDatabaseItem) {
item.iconPath = joinThemableIconPath(this.ctx.extensionPath, SELECTED_DATABASE_ICON);
item.iconPath = joinThemableIconPath(
this.ctx.extensionPath,
SELECTED_DATABASE_ICON
);
} else if (element.error !== undefined) {
item.iconPath = joinThemableIconPath(this.ctx.extensionPath, INVALID_DATABASE_ICON);
item.iconPath = joinThemableIconPath(
this.ctx.extensionPath,
INVALID_DATABASE_ICON
);
}
item.tooltip = element.databaseUri.fsPath;
return item;
@@ -84,9 +138,19 @@ class DatabaseTreeDataProvider extends DisposableObject
public getChildren(element?: DatabaseItem): ProviderResult<DatabaseItem[]> {
if (element === undefined) {
return this.databaseManager.databaseItems.slice(0);
}
else {
return this.databaseManager.databaseItems.slice(0).sort((db1, db2) => {
switch (this.sortOrder) {
case SortOrder.NameAsc:
return db1.name.localeCompare(db2.name, env.language);
case SortOrder.NameDesc:
return db2.name.localeCompare(db1.name, env.language);
case SortOrder.DateAddedAsc:
return (db1.dateAdded || 0) - (db2.dateAdded || 0);
case SortOrder.DateAddedDesc:
return (db2.dateAdded || 0) - (db1.dateAdded || 0);
}
});
} else {
return [];
}
}
@@ -98,14 +162,22 @@ class DatabaseTreeDataProvider extends DisposableObject
public getCurrent(): DatabaseItem | undefined {
return this.currentDatabaseItem;
}
public get sortOrder() {
return this._sortOrder;
}
public set sortOrder(newSortOrder: SortOrder) {
this._sortOrder = newSortOrder;
this._onDidChangeTreeData.fire();
}
}
/** Gets the first element in the given list, if any, or undefined if the list is empty or undefined. */
function getFirst(list: Uri[] | undefined): Uri | undefined {
if (list === undefined || list.length === 0) {
return undefined;
}
else {
} else {
return list[0];
}
}
@@ -118,107 +190,340 @@ 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);
}
export class DatabaseUI extends DisposableObject {
public constructor(ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
private readonly queryServer: qsClient.QueryServerClient | undefined) {
private treeDataProvider: DatabaseTreeDataProvider;
public constructor(
ctx: ExtensionContext,
private cliserver: cli.CodeQLCliServer,
private databaseManager: DatabaseManager,
private readonly queryServer: qsClient.QueryServerClient | undefined,
private readonly storagePath: string
) {
super();
const treeDataProvider = this.push(new DatabaseTreeDataProvider(ctx, databaseManager));
this.push(window.createTreeView('codeQLDatabases', { treeDataProvider }));
ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabase', this.handleChooseDatabase));
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));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.setCurrentDatabase', this.handleMakeCurrentDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.removeDatabase', this.handleRemoveDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.upgradeDatabase', this.handleUpgradeDatabase));
}
private handleMakeCurrentDatabase = async (databaseItem: DatabaseItem): Promise<void> => {
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
}
private handleChooseDatabase = async (): Promise<DatabaseItem | undefined> => {
return await this.chooseAndSetDatabase();
}
private handleUpgradeCurrentDatabase = async (): Promise<void> => {
await this.handleUpgradeDatabase(this.databaseManager.currentDatabaseItem);
}
private handleUpgradeDatabase = async (databaseItem: DatabaseItem | undefined): Promise<void> => {
if (this.queryServer === undefined) {
logger.log('Received request to upgrade database, but there is no running query server.');
return;
}
if (databaseItem === undefined) {
logger.log('Received request to upgrade database, but no database was provided.');
return;
}
if (databaseItem.contents === undefined) {
logger.log('Received request to upgrade database, but database contents could not be found.');
return;
}
if (databaseItem.contents.dbSchemeUri === undefined) {
logger.log('Received request to upgrade database, but database has no schema.');
return;
}
// Search for upgrade scripts in any workspace folders available
const searchPath: string[] = getOnDiskWorkspaceFolders();
const upgradeInfo = await this.cliserver.resolveUpgrades(
databaseItem.contents.dbSchemeUri.fsPath,
searchPath,
this.treeDataProvider = this.push(
new DatabaseTreeDataProvider(ctx, databaseManager)
);
this.push(
window.createTreeView('codeQLDatabases', {
treeDataProvider: this.treeDataProvider,
canSelectMany: true,
})
);
const { scripts, finalDbscheme } = upgradeInfo;
logger.log('Registering database panel commands.');
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)
);
if (finalDbscheme === undefined) {
logger.log('Could not determine target dbscheme to upgrade to.');
return;
}
const targetDbSchemeUri = Uri.file(finalDbscheme);
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(
'codeQLDatabases.setCurrentDatabase',
this.handleMakeCurrentDatabase
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQLDatabases.sortByName',
this.handleSortByName
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQLDatabases.sortByDateAdded',
this.handleSortByDateAdded
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQLDatabases.removeDatabase',
this.handleRemoveDatabase
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQLDatabases.upgradeDatabase',
this.handleUpgradeDatabase
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQLDatabases.renameDatabase',
this.handleRenameDatabase
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQLDatabases.openDatabaseFolder',
this.handleOpenFolder
)
);
}
private handleMakeCurrentDatabase = async (
databaseItem: DatabaseItem
): Promise<void> => {
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
};
handleChooseDatabaseFolder = async (): Promise<DatabaseItem | undefined> => {
try {
await upgradeDatabase(this.queryServer, databaseItem, targetDbSchemeUri, getUpgradesDirectories(scripts));
return await this.chooseAndSetDatabase(true);
} catch (e) {
showAndLogErrorMessage(e.message);
return undefined;
}
catch (e) {
};
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 () => {
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc) {
this.treeDataProvider.sortOrder = SortOrder.NameDesc;
} else {
this.treeDataProvider.sortOrder = SortOrder.NameAsc;
}
};
private handleSortByDateAdded = async () => {
if (this.treeDataProvider.sortOrder === SortOrder.DateAddedAsc) {
this.treeDataProvider.sortOrder = SortOrder.DateAddedDesc;
} else {
this.treeDataProvider.sortOrder = SortOrder.DateAddedAsc;
}
};
private handleUpgradeCurrentDatabase = async (): Promise<void> => {
await this.handleUpgradeDatabase(
this.databaseManager.currentDatabaseItem,
[]
);
};
private handleUpgradeDatabase = async (
databaseItem: DatabaseItem | undefined,
multiSelect: DatabaseItem[] | undefined
): Promise<void> => {
try {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) => this.handleUpgradeDatabase(dbItem, []))
);
}
if (this.queryServer === undefined) {
logger.log(
'Received request to upgrade database, but there is no running query server.'
);
return;
}
if (databaseItem === undefined) {
logger.log(
'Received request to upgrade database, but no database was provided.'
);
return;
}
if (databaseItem.contents === undefined) {
logger.log(
'Received request to upgrade database, but database contents could not be found.'
);
return;
}
if (databaseItem.contents.dbSchemeUri === undefined) {
logger.log(
'Received request to upgrade database, but database has no schema.'
);
return;
}
// Search for upgrade scripts in any workspace folders available
const searchPath: string[] = getOnDiskWorkspaceFolders();
const upgradeInfo = await this.cliserver.resolveUpgrades(
databaseItem.contents.dbSchemeUri.fsPath,
searchPath
);
const { scripts, finalDbscheme } = upgradeInfo;
if (finalDbscheme === undefined) {
logger.log('Could not determine target dbscheme to upgrade to.');
return;
}
const targetDbSchemeUri = Uri.file(finalDbscheme);
await upgradeDatabase(
this.queryServer,
databaseItem,
targetDbSchemeUri,
getUpgradesDirectories(scripts)
);
} catch (e) {
if (e instanceof UserCancellationException) {
logger.log(e.message);
}
else
throw e;
} else throw e;
}
}
};
private handleClearCache = async (): Promise<void> => {
if ((this.queryServer !== undefined) &&
(this.databaseManager.currentDatabaseItem !== undefined)) {
await clearCacheInDatabase(this.queryServer, this.databaseManager.currentDatabaseItem);
if (
this.queryServer !== undefined &&
this.databaseManager.currentDatabaseItem !== undefined
) {
await clearCacheInDatabase(
this.queryServer,
this.databaseManager.currentDatabaseItem
);
}
}
};
private handleSetCurrentDatabase = async (uri: Uri): Promise<DatabaseItem | undefined> => {
return await this.setCurrentDatabase(uri);
}
private handleSetCurrentDatabase = async (
uri: Uri
): Promise<DatabaseItem | undefined> => {
try {
// 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
);
}
private handleRemoveDatabase = (databaseItem: DatabaseItem): void => {
this.databaseManager.removeDatabaseItem(databaseItem);
}
return await this.setCurrentDatabase(uri);
} catch (e) {
showAndLogErrorMessage(
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${
e.message
}`
);
return undefined;
}
};
private handleRemoveDatabase = (
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined
): void => {
try {
if (multiSelect?.length) {
multiSelect.forEach((dbItem) =>
this.databaseManager.removeDatabaseItem(dbItem)
);
} else {
this.databaseManager.removeDatabaseItem(databaseItem);
}
} catch (e) {
showAndLogErrorMessage(e.message);
}
};
private handleRenameDatabase = async (
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined
): Promise<void> => {
try {
this.assertSingleDatabase(multiSelect);
const newName = await window.showInputBox({
prompt: 'Choose new database name',
value: databaseItem.name,
});
if (newName) {
this.databaseManager.renameDatabaseItem(databaseItem, newName);
}
} catch (e) {
showAndLogErrorMessage(e.message);
}
};
private handleOpenFolder = async (
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined
): Promise<void> => {
try {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri))
);
} else {
await env.openExternal(databaseItem.databaseUri);
}
} catch (e) {
showAndLogErrorMessage(e.message);
}
};
/**
* Return the current database directory. If we don't already have a
@@ -227,13 +532,15 @@ 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;
}
private async setCurrentDatabase(uri: Uri): Promise<DatabaseItem | undefined> {
private async setCurrentDatabase(
uri: Uri
): Promise<DatabaseItem | undefined> {
let databaseItem = this.databaseManager.findDatabaseItem(uri);
if (databaseItem === undefined) {
databaseItem = await this.databaseManager.openDatabase(uri);
@@ -247,13 +554,64 @@ 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 (isLikelyDbFolder(dbPath)) {
dbPath = path.dirname(dbPath);
}
return Uri.file(dbPath);
}
private assertSingleDatabase(
multiSelect: DatabaseItem[] = [],
message = 'Please select a single database.'
) {
if (multiSelect.length > 1) {
throw new Error(message);
}
}
}
const dbRegeEx = /^db-(javascript|go|cpp|java|python)$/;
function isLikelyDbFolder(dbPath: string) {
return path.basename(dbPath).match(dbRegeEx);
}

View File

@@ -6,7 +6,7 @@ import * as cli from './cli';
import { ExtensionContext } from 'vscode';
import { showAndLogErrorMessage, showAndLogWarningMessage, showAndLogInformationMessage } from './helpers';
import { zipArchiveScheme, encodeSourceArchiveUri, decodeSourceArchiveUri } from './archive-filesystem-provider';
import { DisposableObject } from 'semmle-vscode-utils';
import { DisposableObject } from '@github/codeql-vscode-utils';
import { QueryServerConfig } from './config';
import { Logger, logger } from './logging';
@@ -24,21 +24,23 @@ import { Logger, logger } from './logging';
* The name of the key in the workspaceState dictionary in which we
* persist the current database across sessions.
*/
const CURRENT_DB: string = 'currentDatabase';
const CURRENT_DB = 'currentDatabase';
/**
* The name of the key in the workspaceState dictionary in which we
* persist the list of databases across sessions.
*/
const DB_LIST: string = 'databaseList';
const DB_LIST = 'databaseList';
export interface DatabaseOptions {
displayName?: string;
ignoreSourceArchive?: boolean;
dateAdded?: number | undefined;
}
interface FullDatabaseOptions extends DatabaseOptions {
ignoreSourceArchive: boolean;
dateAdded: number | undefined;
}
interface PersistedDatabaseItem {
@@ -107,10 +109,11 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
return vscode.Uri.file(dbAbsolutePath);
}
async function findSourceArchive(databasePath: string, silent: boolean = 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);
@@ -128,8 +131,9 @@ async function findSourceArchive(databasePath: string, silent: boolean = false):
return undefined;
}
async function resolveDatabase(databasePath: string):
Promise<DatabaseContents | undefined> {
async function resolveDatabase(
databasePath: string
): Promise<DatabaseContents> {
const name = path.basename(databasePath);
@@ -151,20 +155,6 @@ async function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {
return await glob('*.dbscheme', { cwd: dbDirectory });
}
async function resolveRawDataset(datasetPath: string): Promise<DatabaseContents | undefined> {
if ((await getDbSchemeFiles(datasetPath)).length > 0) {
return {
kind: DatabaseKind.RawDataset,
name: path.basename(datasetPath),
datasetUri: vscode.Uri.file(datasetPath),
sourceArchiveUri: undefined
};
}
else {
return undefined;
}
}
async function resolveDatabaseContents(uri: vscode.Uri): Promise<DatabaseContents> {
if (uri.scheme !== 'file') {
throw new Error(`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`);
@@ -174,7 +164,7 @@ async function resolveDatabaseContents(uri: vscode.Uri): Promise<DatabaseContent
throw new InvalidDatabaseError(`Database '${databasePath}' does not exist.`);
}
const contents = await resolveDatabase(databasePath) || await resolveRawDataset(databasePath);
const contents = await resolveDatabase(databasePath);
if (contents === undefined) {
throw new InvalidDatabaseError(`'${databasePath}' is not a valid database.`);
@@ -200,7 +190,7 @@ export interface DatabaseItem {
/** The URI of the database */
readonly databaseUri: vscode.Uri;
/** The name of the database to be displayed in the UI */
readonly name: string;
name: string;
/** The URI of the database's source archive, or `undefined` if no source archive is to be used. */
readonly sourceArchive: vscode.Uri | undefined;
/**
@@ -208,6 +198,12 @@ export interface DatabaseItem {
* Will be `undefined` if the database is invalid. Can be updated by calling `refresh()`.
*/
readonly contents: DatabaseContents | undefined;
/**
* The date this database was added as a unix timestamp. Or undefined if we don't know.
*/
readonly dateAdded: number | undefined;
/** If the database is invalid, describes why. */
readonly error: Error | undefined;
/**
@@ -278,6 +274,10 @@ class DatabaseItemImpl implements DatabaseItem {
}
}
public set name(newName: string) {
this.options.displayName = newName;
}
public get sourceArchive(): vscode.Uri | undefined {
if (this.options.ignoreSourceArchive || (this._contents === undefined)) {
return undefined;
@@ -291,6 +291,10 @@ class DatabaseItemImpl implements DatabaseItem {
return this._contents;
}
public get dateAdded(): number | undefined {
return this.options.dateAdded;
}
public get error(): Error | undefined {
return this._error;
}
@@ -427,31 +431,30 @@ class DatabaseItemImpl implements DatabaseItem {
* `event` fires. If waiting for the event takes too long (by default
* >1000ms) log a warning, and resolve to undefined.
*/
function eventFired<T>(event: vscode.Event<T>, timeoutMs: number = 1000): Promise<T | undefined> {
function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | undefined> {
return new Promise((res, _rej) => {
let timeout: NodeJS.Timeout | undefined;
let disposable: vscode.Disposable | undefined;
function dispose() {
if (timeout !== undefined) clearTimeout(timeout);
if (disposable !== undefined) disposable.dispose();
}
disposable = event(e => {
res(e); dispose();
});
timeout = setTimeout(() => {
const timeout = setTimeout(() => {
logger.log(`Waiting for event ${event} timed out after ${timeoutMs}ms`);
res(undefined); dispose();
res(undefined);
dispose();
}, timeoutMs);
const disposable = event(e => {
res(e);
dispose();
});
function dispose() {
clearTimeout(timeout);
disposable.dispose();
}
});
}
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[] = [];
@@ -465,8 +468,9 @@ export class DatabaseManager extends DisposableObject {
this.loadPersistedState(); // Let this run async.
}
public async openDatabase(uri: vscode.Uri, options?: DatabaseOptions):
Promise<DatabaseItem> {
public async openDatabase(
uri: vscode.Uri, options?: DatabaseOptions
): Promise<DatabaseItem> {
const contents = await resolveDatabaseContents(uri);
const realOptions = options || {};
@@ -475,7 +479,8 @@ export class DatabaseManager extends DisposableObject {
const fullOptions: FullDatabaseOptions = {
ignoreSourceArchive: (realOptions.ignoreSourceArchive !== undefined) ?
realOptions.ignoreSourceArchive : isQLTestDatabase,
displayName: realOptions.displayName
displayName: realOptions.displayName,
dateAdded: realOptions.dateAdded || Date.now()
};
const databaseItem = new DatabaseItemImpl(uri, contents, fullOptions, (item) => {
this._onDidChangeDatabaseItem.fire(item);
@@ -525,11 +530,13 @@ export class DatabaseManager extends DisposableObject {
}
}
private async createDatabaseItemFromPersistedState(state: PersistedDatabaseItem):
Promise<DatabaseItem> {
private async createDatabaseItemFromPersistedState(
state: PersistedDatabaseItem
): Promise<DatabaseItem> {
let displayName: string | undefined = undefined;
let ignoreSourceArchive = false;
let dateAdded = undefined;
if (state.options) {
if (typeof state.options.displayName === 'string') {
displayName = state.options.displayName;
@@ -537,14 +544,18 @@ export class DatabaseManager extends DisposableObject {
if (typeof state.options.ignoreSourceArchive === 'boolean') {
ignoreSourceArchive = state.options.ignoreSourceArchive;
}
if (typeof state.options.dateAdded === 'number') {
dateAdded = state.options.dateAdded;
}
}
const fullOptions: FullDatabaseOptions = {
ignoreSourceArchive: ignoreSourceArchive,
displayName: displayName
ignoreSourceArchive,
displayName,
dateAdded
};
const item = new DatabaseItemImpl(vscode.Uri.parse(state.uri), undefined, fullOptions,
(item) => {
this._onDidChangeDatabaseItem.fire(item)
this._onDidChangeDatabaseItem.fire(item);
});
await this.addDatabaseItem(item);
@@ -571,7 +582,7 @@ export class DatabaseManager extends DisposableObject {
}
} catch (e) {
// database list had an unexpected type - nothing to be done?
showAndLogErrorMessage('Database list loading failed: ${}', e.message);
showAndLogErrorMessage(`Database list loading failed: ${e.message}`);
}
}
@@ -584,7 +595,7 @@ export class DatabaseManager extends DisposableObject {
}
public async setCurrentDatabaseItem(item: DatabaseItem | undefined,
skipRefresh: boolean = false): Promise<void> {
skipRefresh = false): Promise<void> {
if (!skipRefresh && (item !== undefined)) {
await item.refresh(); // Will throw on invalid database.
@@ -610,12 +621,23 @@ export class DatabaseManager extends DisposableObject {
return this._databaseItems.find(item => item.databaseUri.toString(true) === uriString);
}
public findDatabaseItemBySourceArchive(uri: vscode.Uri): DatabaseItem | undefined {
const uriString = uri.toString(true);
return this._databaseItems.find(item => item.sourceArchive && item.sourceArchive.toString(true) === uriString);
}
private async addDatabaseItem(item: DatabaseItemImpl) {
this._databaseItems.push(item);
this.updatePersistedDatabaseList();
this._onDidChangeDatabaseItem.fire(undefined);
}
public async renameDatabaseItem(item: DatabaseItem, newName: string) {
item.name = newName;
this.updatePersistedDatabaseList();
this._onDidChangeDatabaseItem.fire(item);
}
public removeDatabaseItem(item: DatabaseItem) {
if (this._currentDatabaseItem == item)
this._currentDatabaseItem = undefined;
@@ -632,6 +654,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);
}
@@ -643,6 +673,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

@@ -0,0 +1,216 @@
import * as fs from 'fs-extra';
import * as yaml from 'js-yaml';
import * as tmp from 'tmp-promise';
import * as vscode from 'vscode';
import { decodeSourceArchiveUri, zipArchiveScheme } from './archive-filesystem-provider';
import { ColumnKindCode, EntityValue, getResultSetSchema, LineColumnLocation, UrlValue } from './bqrs-cli-types';
import { CodeQLCliServer } from './cli';
import { DatabaseItem, DatabaseManager } from './databases';
import * as helpers from './helpers';
import { CachedOperation } from './helpers';
import * as messages from './messages';
import { QueryServerClient } from './queryserver-client';
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from './run-queries';
/**
* Run templated CodeQL queries to find definitions and references in
* source-language files. We may eventually want to find a way to
* generalize this to other custom queries, e.g. showing dataflow to
* or from a selected identifier.
*/
const TEMPLATE_NAME = 'selectedSourceFile';
const SELECT_QUERY_NAME = '#select';
enum KeyType {
DefinitionQuery = 'DefinitionQuery',
ReferenceQuery = 'ReferenceQuery',
}
function tagOfKeyType(keyType: KeyType): string {
switch (keyType) {
case KeyType.DefinitionQuery: return 'ide-contextual-queries/local-definitions';
case KeyType.ReferenceQuery: return 'ide-contextual-queries/local-references';
}
}
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 = (await tmp.file({
postfix: '.qls'
})).path;
const suiteYaml = { qlpack, include: { kind: 'definitions', 'tags contain': tagOfKeyType(keyType) } };
await fs.writeFile(suiteFile, yaml.safeDump(suiteYaml), 'utf8');
const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
if (queries.length === 0) {
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;
}
async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<string | undefined> {
if (db.contents === undefined)
return undefined;
const datasetPath = db.contents.datasetUri.fsPath;
const { qlpack } = await helpers.resolveDatasetFolder(cli, datasetPath);
return qlpack;
}
interface FullLocationLink extends vscode.LocationLink {
originUri: vscode.Uri;
}
export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvider {
private cache: CachedOperation<vscode.LocationLink[]>;
constructor(
private cli: CodeQLCliServer,
private qs: QueryServerClient,
private dbm: DatabaseManager,
) {
this.cache = new CachedOperation<vscode.LocationLink[]>(this.getDefinitions.bind(this));
}
async getDefinitions(uriString: string): Promise<vscode.LocationLink[]> {
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, KeyType.DefinitionQuery, (src, _dest) => src === uriString);
}
async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.LocationLink[]> {
const fileLinks = await this.cache.get(document.uri.toString());
const locLinks: vscode.LocationLink[] = [];
for (const link of fileLinks) {
if (link.originSelectionRange!.contains(position)) {
locLinks.push(link);
}
}
return locLinks;
}
}
export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider {
private cache: CachedOperation<FullLocationLink[]>;
constructor(
private cli: CodeQLCliServer,
private qs: QueryServerClient,
private dbm: DatabaseManager,
) {
this.cache = new CachedOperation<FullLocationLink[]>(this.getReferences.bind(this));
}
async getReferences(uriString: string): Promise<FullLocationLink[]> {
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, KeyType.ReferenceQuery, (_src, dest) => dest === uriString);
}
async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, _token: vscode.CancellationToken): Promise<vscode.Location[]> {
const fileLinks = await this.cache.get(document.uri.toString());
const locLinks: vscode.Location[] = [];
for (const link of fileLinks) {
if (link.targetRange!.contains(position)) {
locLinks.push({ range: link.originSelectionRange!, uri: link.originUri });
}
}
return locLinks;
}
}
interface FileRange {
file: vscode.Uri;
range: vscode.Range;
}
async function getLinksFromResults(results: QueryWithResults, cli: CodeQLCliServer, db: DatabaseItem, filter: (srcFile: string, destFile: string) => boolean): Promise<FullLocationLink[]> {
const localLinks: FullLocationLink[] = [];
const bqrsPath = results.query.resultsPaths.resultsPath;
const info = await cli.bqrsInfo(bqrsPath);
const selectInfo = getResultSetSchema(SELECT_QUERY_NAME, info);
if (selectInfo && selectInfo.columns.length == 3
&& selectInfo.columns[0].kind == ColumnKindCode.ENTITY
&& selectInfo.columns[1].kind == ColumnKindCode.ENTITY
&& selectInfo.columns[2].kind == ColumnKindCode.STRING) {
// TODO: Page this
const allTuples = await cli.bqrsDecode(bqrsPath, SELECT_QUERY_NAME);
for (const tuple of allTuples.tuples) {
const src = tuple[0] as EntityValue;
const dest = tuple[1] as EntityValue;
const srcFile = src.url && fileRangeFromURI(src.url, db);
const destFile = dest.url && fileRangeFromURI(dest.url, db);
if (srcFile && destFile && filter(srcFile.file.toString(), destFile.file.toString())) {
localLinks.push({ targetRange: destFile.range, targetUri: destFile.file, originSelectionRange: srcFile.range, originUri: srcFile.file });
}
}
}
return localLinks;
}
async function getLinksForUriString(
cli: CodeQLCliServer,
qs: QueryServerClient,
dbm: DatabaseManager,
uriString: string,
keyType: KeyType,
filter: (src: string, dest: string) => boolean
) {
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
const sourceArchiveUri = vscode.Uri.file(uri.sourceArchiveZipPath).with({ scheme: zipArchiveScheme });
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
if (db) {
const qlpack = await qlpackOfDatabase(cli, db);
if (qlpack === undefined) {
throw new Error('Can\'t infer qlpack from database source archive');
}
const links: FullLocationLink[] = [];
for (const query of await resolveQueries(cli, qlpack, keyType)) {
const templates: messages.TemplateDefinitions = {
[TEMPLATE_NAME]: {
values: {
tuples: [[{
stringValue: uri.pathWithinSourceArchive
}]]
}
}
};
const results = await compileAndRunQueryAgainstDatabase(cli, qs, db, false, vscode.Uri.file(query), templates);
if (results.result.resultType == messages.QueryResultType.SUCCESS) {
links.push(...await getLinksFromResults(results, cli, db, filter));
}
}
return links;
} else {
return [];
}
}
function fileRangeFromURI(uri: UrlValue, db: DatabaseItem): FileRange | undefined {
if (typeof uri === 'string') {
return undefined;
} else if ('startOffset' in uri) {
return undefined;
} else {
const loc = uri as LineColumnLocation;
const range = new vscode.Range(Math.max(0, loc.startLine - 1),
Math.max(0, loc.startColumn - 1),
Math.max(0, loc.endLine - 1),
Math.max(0, loc.endColumn));
try {
const parsed = vscode.Uri.parse(uri.uri, true);
if (parsed.scheme === 'file') {
return { file: db.resolveSourceFile(parsed.fsPath), range };
}
return undefined;
} catch (e) {
return undefined;
}
}
}

View File

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

View File

@@ -1,14 +1,16 @@
import * as fetch from "node-fetch";
import * as fs from "fs-extra";
import * as os from "os";
import * as path from "path";
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 { logger } from "./logging";
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
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, showAndLogErrorMessage } from './helpers';
import { logger } from './logging';
import * as helpers from './helpers';
import { getCodeQlCliVersion } from './cli-version';
/**
* distribution.ts
@@ -19,109 +21,151 @@ import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-versi
/**
* Default value for the owner name of the extension-managed distribution on GitHub.
*
*
* We set the default here rather than as a default config value so that this default is invoked
* upon blanking the setting.
*/
const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
const DEFAULT_DISTRIBUTION_OWNER_NAME = 'github';
/**
* Default value for the repository name of the extension-managed distribution on GitHub.
*
*
* We set the default here rather than as a default config value so that this default is invoked
* upon blanking the setting.
*/
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
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>,
onDidChangeDistribution?: Event<void>
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
onDidChangeDistribution?: Event<void>;
}
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",
'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
};
}
public async hasDistribution(): Promise<boolean> {
const result = await this.getDistribution();
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 !== undefined) {
if (this._config.customCodeQlPath) {
if (!await fs.pathExists(this._config.customCodeQlPath)) {
showAndLogErrorMessage(`The CodeQL executable path is specified as "${this._config.customCodeQlPath}" ` +
"by a configuration setting, but a CodeQL executable could not be found at that path. Please check " +
"that a CodeQL executable exists at the specified path or remove the setting.");
'by a configuration setting, but a CodeQL executable could not be found at that path. Please check ' +
'that a CodeQL executable exists at the specified path or remove the setting.');
return undefined;
}
return this._config.customCodeQlPath;
// emit a warning if using a deprecated launcher and a non-deprecated launcher exists
if (
deprecatedCodeQlLauncherName() &&
this._config.customCodeQlPath.endsWith(deprecatedCodeQlLauncherName()!) &&
await this.hasNewLauncherName()
) {
warnDeprecatedLauncher();
}
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 = path.join(searchDirectory, codeQlLauncherName());
if (await fs.pathExists(expectedLauncherPath)) {
return expectedLauncherPath;
const expectedLauncherPath = await getExecutableFromDirectory(searchDirectory);
if (expectedLauncherPath) {
return {
codeQlPath: expectedLauncherPath,
kind: DistributionKind.PathEnvironmentVariable
};
}
}
logger.log("INFO: Could not find CodeQL on path.");
logger.log('INFO: Could not find CodeQL on path.');
}
return undefined;
@@ -130,14 +174,14 @@ export class DistributionManager implements DistributionProvider {
/**
* Check for updates to the extension-managed distribution. If one has not already been installed,
* this will return an update available result with the latest available release.
*
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
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();
}
@@ -152,11 +196,11 @@ export class DistributionManager implements DistributionProvider {
/**
* Installs a release of the extension-managed distribution.
*
*
* 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);
}
@@ -164,33 +208,47 @@ export class DistributionManager implements DistributionProvider {
return this._onDidChangeDistribution;
}
/**
* @return true if the non-deprecated launcher name exists on the file system
* in the same directory as the specified launcher only if using an external
* installation. False otherwise.
*/
private async hasNewLauncherName(): Promise<boolean> {
if (!this._config.customCodeQlPath) {
// not managed externally
return false;
}
const dir = path.dirname(this._config.customCodeQlPath);
const newLaunderPath = path.join(dir, codeQlLauncherName());
return await fs.pathExists(newLaunderPath);
}
private readonly _config: DistributionConfig;
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> {
if (this.getInstalledRelease() !== undefined) {
// An extension specific distribution has been installed.
const expectedLauncherPath = path.join(this.getDistributionRootPath(), codeQlLauncherName());
if (await fs.pathExists(expectedLauncherPath)) {
const expectedLauncherPath = await getExecutableFromDirectory(this.getDistributionRootPath(), true);
if (expectedLauncherPath) {
return expectedLauncherPath;
}
logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
"Will try PATH.");
try {
await this.removeDistribution();
} catch (e) {
logger.log("WARNING: Tried to remove corrupted CodeQL CLI at " +
logger.log('WARNING: Tried to remove corrupted CodeQL CLI at ' +
`${this.getDistributionStoragePath()} but encountered an error: ${e}.`);
}
}
@@ -200,7 +258,7 @@ class ExtensionSpecificDistributionManager {
/**
* Check for updates to the extension-managed distribution. If one has not already been installed,
* this will return an update available result with the latest available release.
*
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToDistribution(): Promise<DistributionUpdateCheckResult> {
@@ -208,7 +266,11 @@ class ExtensionSpecificDistributionManager {
const extensionSpecificRelease = this.getInstalledRelease();
const latestRelease = await this.getLatestRelease();
if (extensionSpecificRelease !== undefined && codeQlPath !== undefined && latestRelease.id === extensionSpecificRelease.id) {
if (
extensionSpecificRelease !== undefined &&
codeQlPath !== undefined &&
latestRelease.id === extensionSpecificRelease.id
) {
return createAlreadyUpToDateResult();
}
return createUpdateAvailableResult(latestRelease);
@@ -216,18 +278,18 @@ class ExtensionSpecificDistributionManager {
/**
* Installs a release of the extension-managed distribution.
*
*
* 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) {
@@ -235,20 +297,31 @@ class ExtensionSpecificDistributionManager {
`but encountered an error: ${e}.`);
}
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(release.assets[0]);
const tmpDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "vscode-codeql"));
// 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 {
const archivePath = path.join(tmpDirectory, "distributionDownload.zip");
const archivePath = path.join(tmpDirectory, 'distributionDownload.zip');
const archiveFile = fs.createWriteStream(archivePath);
const contentLength = assetStream.headers.get("content-length");
const contentLength = assetStream.headers.get('content-length');
let numBytesDownloaded = 0;
if (progressCallback && contentLength !== null) {
const totalNumBytes = parseInt(contentLength, 10);
const bytesToDisplayMB = (numBytes: number) => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
const updateProgress = () => {
const bytesToDisplayMB = (numBytes: number): string => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
const updateProgress = (): void => {
progressCallback({
step: numBytesDownloaded,
maxStep: totalNumBytes,
@@ -259,7 +332,7 @@ class ExtensionSpecificDistributionManager {
// Display the progress straight away rather than waiting for the first chunk.
updateProgress();
assetStream.body.on("data", data => {
assetStream.body.on('data', data => {
numBytesDownloaded += data.length;
updateProgress();
});
@@ -267,8 +340,8 @@ class ExtensionSpecificDistributionManager {
await new Promise((resolve, reject) =>
assetStream.body.pipe(archiveFile)
.on("finish", resolve)
.on("error", reject)
.on('finish', resolve)
.on('error', reject)
);
await this.bumpDistributionFolderIndex();
@@ -282,7 +355,7 @@ class ExtensionSpecificDistributionManager {
/**
* Remove the extension-managed distribution.
*
*
* This should not be called for a distribution that is currently in use, as remove may fail.
*/
private async removeDistribution(): Promise<void> {
@@ -292,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 {
@@ -316,7 +413,7 @@ class ExtensionSpecificDistributionManager {
private getDistributionStoragePath(): string {
// Use an empty string for the initial distribution for backwards compatibility.
const distributionFolderIndex = this._extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0) || "";
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0) || '';
return path.join(this._extensionContext.globalStoragePath,
ExtensionSpecificDistributionManager._currentDistributionFolderBaseName + distributionFolderIndex);
}
@@ -336,28 +433,28 @@ 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";
private static readonly _installedReleaseStateKey = "distributionRelease";
private static readonly _codeQlExtractedFolderName = "codeql";
private static readonly _currentDistributionFolderBaseName = 'distribution';
private static readonly _currentDistributionFolderIndexStateKey = 'distributionFolderIndex';
private static readonly _installedReleaseStateKey = 'distributionRelease';
private static readonly _codeQlExtractedFolderName = 'codeql';
}
export class ReleasesApiConsumer {
constructor(ownerName: string, repoName: string, personalAccessToken?: string) {
// Specify version of the GitHub API
this._defaultHeaders["accept"] = "application/vnd.github.v3+json";
this._defaultHeaders['accept'] = 'application/vnd.github.v3+json';
if (personalAccessToken) {
this._defaultHeaders["authorization"] = `token ${personalAccessToken}`;
this._defaultHeaders['authorization'] = `token ${personalAccessToken}`;
}
this._ownerName = ownerName;
this._repoName = repoName;
}
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease: boolean = 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 => {
@@ -365,24 +462,24 @@ 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. " +
"Please check that the CodeQL extension is up to date.");
throw new Error('No compatible CodeQL CLI releases were found. ' +
'Please check that the CodeQL extension is up to date.');
}
const assets: ReleaseAsset[] = latestRelease.assets.map(asset => {
return {
@@ -404,7 +501,7 @@ export class ReleasesApiConsumer {
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases/assets/${asset.id}`;
return await this.makeApiCall(apiPath, {
"accept": "application/octet-stream"
'accept': 'application/octet-stream'
});
}
@@ -414,7 +511,7 @@ export class ReleasesApiConsumer {
if (!response.ok) {
// Check for rate limiting
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
const rateLimitResetValue = response.headers.get('X-RateLimit-Reset');
if (response.status === 403 && rateLimitResetValue) {
const secondsToMillisecondsFactor = 1000;
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
@@ -428,26 +525,26 @@ export class ReleasesApiConsumer {
private async makeRawRequest(
requestUrl: string,
headers: { [key: string]: string },
redirectCount: number = 0): Promise<fetch.Response> {
redirectCount = 0): Promise<fetch.Response> {
const response = await fetch.default(requestUrl, {
headers,
redirect: "manual"
redirect: 'manual'
});
const redirectUrl = response.headers.get("location");
const redirectUrl = response.headers.get('location');
if (isRedirectStatusCode(response.status) && redirectUrl && redirectCount < ReleasesApiConsumer._maxRedirects) {
const parsedRedirectUrl = url.parse(redirectUrl);
if (parsedRedirectUrl.protocol != "https:") {
throw new Error("Encountered a non-https redirect, rejecting");
if (parsedRedirectUrl.protocol != 'https:') {
throw new Error('Encountered a non-https redirect, rejecting');
}
if (parsedRedirectUrl.host != "api.github.com") {
if (parsedRedirectUrl.host != 'api.github.com') {
// Remove authorization header if we are redirected outside of the GitHub API.
//
// This is necessary to stream release assets since AWS fails if more than one auth
// mechanism is provided.
delete headers["authorization"];
delete headers['authorization'];
}
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1)
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1);
}
return response;
@@ -457,7 +554,7 @@ export class ReleasesApiConsumer {
private readonly _ownerName: string;
private readonly _repoName: string;
private static readonly _apiBase = "https://api.github.com";
private static readonly _apiBase = 'https://api.github.com';
private static readonly _maxRedirects = 20;
}
@@ -478,31 +575,12 @@ 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';
}
function codeQlLauncherName(): string {
return (os.platform() === "win32") ? "codeql.cmd" : "codeql";
function deprecatedCodeQlLauncherName(): string | undefined {
return (os.platform() === 'win32') ? 'codeql.cmd' : undefined;
}
function isRedirectStatusCode(statusCode: number): boolean {
@@ -513,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,
@@ -520,24 +609,33 @@ export enum FindDistributionResultKind {
NoDistribution
}
export type FindDistributionResult = CompatibleDistributionResult | UnknownCompatibilityDistributionResult |
IncompatibleDistributionResult | NoDistributionResult;
export type FindDistributionResult =
| CompatibleDistributionResult
| UnknownCompatibilityDistributionResult
| 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 {
@@ -551,11 +649,14 @@ export enum DistributionUpdateCheckResultKind {
UpdateAvailable
}
type DistributionUpdateCheckResult = AlreadyCheckedRecentlyResult | AlreadyUpToDateResult | InvalidLocationResult |
UpdateAvailableResult;
type DistributionUpdateCheckResult =
| AlreadyCheckedRecentlyResult
| AlreadyUpToDateResult
| InvalidLocationResult
| UpdateAvailableResult;
export interface AlreadyCheckedRecentlyResult {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult;
}
export interface AlreadyUpToDateResult {
@@ -599,6 +700,31 @@ function createUpdateAvailableResult(updatedRelease: Release): UpdateAvailableRe
};
}
// Exported for testing
export async function getExecutableFromDirectory(directory: string, warnWhenNotFound = false): Promise<string | undefined> {
const expectedLauncherPath = path.join(directory, codeQlLauncherName());
const deprecatedLauncherName = deprecatedCodeQlLauncherName();
const alternateExpectedLauncherPath = deprecatedLauncherName ? path.join(directory, deprecatedLauncherName) : undefined;
if (await fs.pathExists(expectedLauncherPath)) {
return expectedLauncherPath;
} else if (alternateExpectedLauncherPath && (await fs.pathExists(alternateExpectedLauncherPath))) {
warnDeprecatedLauncher();
return alternateExpectedLauncherPath;
}
if (warnWhenNotFound) {
logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
'Will try PATH.');
}
return undefined;
}
function warnDeprecatedLauncher() {
helpers.showAndLogWarningMessage(
`The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` +
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`
);
}
/**
* A release on GitHub.
*/
@@ -649,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;
@@ -694,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

@@ -1,24 +1,39 @@
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
import { ErrorCodes, LanguageClient, ResponseError } from 'vscode-languageclient';
import { commands, Disposable, ExtensionContext, extensions, languages, ProgressLocation, ProgressOptions, Uri, window as Window, env } from 'vscode';
import { LanguageClient } from 'vscode-languageclient';
import * as path from 'path';
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
import { CodeQLCliServer } from './cli';
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 {
DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError
DEFAULT_DISTRIBUTION_VERSION_RANGE,
DistributionKind,
DistributionManager,
DistributionUpdateCheckResultKind,
FindDistributionResult,
FindDistributionResultKind,
GithubApiError,
GithubRateLimitedError
} from './distribution';
import * as helpers from './helpers';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager, WebviewReveal } from './interface';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
import { CompletedQuery } from './query-results';
import { QueryHistoryManager } from './query-history';
import * as qsClient from './queryserver-client';
import { CodeQLCliServer } from './cli';
import { assertNever } from './helpers-pure';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager } from './interface';
import { WebviewReveal } from './interface-utils';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { QueryHistoryManager } from './query-history';
import { CompletedQuery } from './query-results';
import * as qsClient from './queryserver-client';
import { displayQuickQuery } from './quick-query';
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
import { QLTestAdapterFactory } from './test-adapter';
import { TestUIService } from './test-ui';
import { CompareInterfaceManager } from './compare/compare-interface';
import { gatherQlFiles } from './files';
/**
* extension.ts
@@ -53,14 +68,15 @@ let isInstallingOrUpdatingDistribution = false;
*
* @param excludedCommands List of commands for which we should not register error stubs.
*/
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => void) {
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => void): void {
// Remove existing stubs
errorStubs.forEach(stub => stub.dispose());
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);
@@ -73,15 +89,17 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
}
export async function activate(ctx: ExtensionContext): Promise<void> {
// Initialise logging, and ensure all loggers are disposed upon exit.
ctx.subscriptions.push(logger);
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";
const shouldUpdateOnNextActivationKey = 'shouldUpdateOnNextActivation';
registerErrorStubs([checkForUpdatesCommand], command => () => {
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
@@ -90,6 +108,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
interface DistributionUpdateConfig {
isUserInitiated: boolean;
shouldDisplayMessageWhenNoUpdates: boolean;
allowAutoUpdating: boolean;
}
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
@@ -97,24 +116,28 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
helpers.showAndLogInformationMessage : async (message: string) => logger.log(message);
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
// We do want to auto update if there is no distribution at all
const allowAutoUpdating = config.allowAutoUpdating || !await distributionManager.hasDistribution();
switch (result.kind) {
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
logger.log("Didn't perform CodeQL CLI update check since a check was already performed within the previous " +
logger.log('Didn\'t perform CodeQL CLI update check since a check was already performed within the previous ' +
`${minSecondsSinceLastUpdateCheck} seconds.`);
break;
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
await noUpdatesLoggingFunc("CodeQL CLI already up to date.");
await noUpdatesLoggingFunc('CodeQL CLI already up to date.');
break;
case DistributionUpdateCheckResultKind.InvalidLocation:
await noUpdatesLoggingFunc("CodeQL CLI is installed externally so could not be updated.");
await noUpdatesLoggingFunc('CodeQL CLI is installed externally so could not be updated.');
break;
case DistributionUpdateCheckResultKind.UpdateAvailable:
if (beganMainExtensionActivation) {
if (beganMainExtensionActivation || !allowAutoUpdating) {
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
"The update will be installed after Visual Studio Code restarts. Restart now to upgrade?";
'Do you wish to upgrade?';
await ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, "Restart and Upgrade")) {
await commands.executeCommand("workbench.action.reloadWindow");
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, 'Restart and Upgrade')) {
await commands.executeCommand('workbench.action.reloadWindow');
}
} else {
const progressOptions: ProgressOptions = {
@@ -136,13 +159,17 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
async function installOrUpdateDistribution(config: DistributionUpdateConfig): Promise<void> {
if (isInstallingOrUpdatingDistribution) {
throw new Error("Already installing or updating CodeQL CLI");
throw new Error('Already installing or updating CodeQL CLI');
}
isInstallingOrUpdatingDistribution = true;
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
const willUpdateCodeQl = ctx.globalState.get(shouldUpdateOnNextActivationKey);
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
const messageText = willUpdateCodeQl
? 'Updating CodeQL CLI'
: codeQlInstalled
? 'Checking for updates to CodeQL CLI'
: 'Installing CodeQL CLI';
try {
await installOrUpdateDistributionWithProgressTitle(messageText, config);
} catch (e) {
@@ -150,12 +177,12 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
// or updating the distribution.
const alertFunction = (codeQlInstalled && !config.isUserInitiated) ?
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
const taskDescription = (willUpdateCodeQl ? "update" :
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
const taskDescription = (willUpdateCodeQl ? 'update' :
codeQlInstalled ? 'check for updates to' : 'install') + ' CodeQL CLI';
if (e instanceof GithubRateLimitedError) {
alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString()}.`);
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString(env.language)}.`);
} else if (e instanceof GithubApiError) {
alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
}
@@ -169,17 +196,31 @@ 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.");
helpers.showAndLogWarningMessage('Compatibility with the configured CodeQL CLI could not be determined. ' +
'You may experience problems using the extension.');
break;
case FindDistributionResultKind.NoDistribution:
helpers.showAndLogErrorMessage("The CodeQL CLI could not be found.");
helpers.showAndLogErrorMessage('The CodeQL CLI could not be found.');
break;
default:
assertNever(result);
@@ -197,12 +238,15 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
await activateWithInstalledDistribution(ctx, distributionManager);
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
registerErrorStubs([checkForUpdatesCommand], command => async () => {
const installActionName = "Install CodeQL CLI";
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
const installActionName = 'Install CodeQL CLI';
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, {
items: [installActionName]
});
if (chosenAction === installActionName) {
installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: false
shouldDisplayMessageWhenNoUpdates: false,
allowAutoUpdating: true
});
}
});
@@ -211,112 +255,289 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: false
shouldDisplayMessageWhenNoUpdates: false,
allowAutoUpdating: true
})));
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: true
shouldDisplayMessageWhenNoUpdates: true,
allowAutoUpdating: true
})));
await installOrUpdateThenTryActivate({
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
shouldDisplayMessageWhenNoUpdates: false
shouldDisplayMessageWhenNoUpdates: false,
// only auto update on startup if the user has previously requested an update
// otherwise, ask user to accept the update
allowAutoUpdating: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
});
}
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager) {
async function activateWithInstalledDistribution(
ctx: ExtensionContext,
distributionManager: DistributionManager
): Promise<void> {
beganMainExtensionActivation = true;
// Remove any error stubs command handlers left over from first part
// of activation.
errorStubs.forEach(stub => stub.dispose());
errorStubs.forEach((stub) => stub.dispose());
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(distributionManager);
logger.log('Initializing configuration listener...');
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(
distributionManager
);
ctx.subscriptions.push(qlConfigurationListener);
ctx.subscriptions.push(queryServerLogger);
ctx.subscriptions.push(ideServerLogger);
logger.log('Initializing CodeQL cli server...');
const cliServer = new CodeQLCliServer(distributionManager, logger);
ctx.subscriptions.push(cliServer);
const qs = new qsClient.QueryServerClient(qlConfigurationListener, cliServer, {
logger: queryServerLogger,
}, task => Window.withProgress({ title: 'CodeQL query server', location: ProgressLocation.Window }, task));
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 showResults = async (item: CompletedQuery) =>
showResultsForCompletedQuery(item, WebviewReveal.Forced);
const qhm = new QueryHistoryManager(
ctx,
queryHistoryConfigurationListener,
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced)
showResults,
async (from: CompletedQuery, to: CompletedQuery) =>
showResultsForComparison(from, to),
);
logger.log('Initializing results panel interface.');
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
logger.log('Initializing compare panel interface.');
const cmpm = new CompareInterfaceManager(
ctx,
dbm,
cliServer,
queryServerLogger,
showResults
);
ctx.subscriptions.push(cmpm);
logger.log('Initializing source archive filesystem provider.');
archiveFilesystemProvider.activate(ctx);
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
async function showResultsForComparison(
from: CompletedQuery,
to: CompletedQuery
): Promise<void> {
try {
await cmpm.showResults(from, to);
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
async function showResultsForCompletedQuery(
query: CompletedQuery,
forceReveal: WebviewReveal
): Promise<void> {
await intm.showResults(query, forceReveal, false);
}
async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined) {
async function compileAndRunQuery(
quickEval: boolean,
selectedQuery: Uri | undefined
): Promise<void> {
if (qs !== undefined) {
try {
const dbItem = await databaseUI.getDatabaseItem();
if (dbItem === undefined) {
throw new Error('Can\'t run query without a selected database');
}
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
const info = await compileAndRunQueryAgainstDatabase(
cliServer,
qs,
dbItem,
quickEval,
selectedQuery
);
const item = qhm.addQuery(info);
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
}
catch (e) {
} catch (e) {
if (e instanceof UserCancellationException) {
logger.log(e.message);
}
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
logger.log(e.message);
}
else if (e instanceof Error)
helpers.showAndLogWarningMessage(e.message);
} else if (e instanceof Error) {
helpers.showAndLogErrorMessage(e.message);
else
} else {
throw e;
}
}
}
}
ctx.subscriptions.push(tmpDirDisposal);
let client = new LanguageClient('CodeQL Language Server', () => spawnIdeServer(qlConfigurationListener), {
documentSelector: [
{ language: 'ql', scheme: 'file' },
{ language: 'yaml', scheme: 'file', pattern: '**/qlpack.yml' }
],
synchronize: {
configurationSection: 'codeQL'
logger.log('Initializing CodeQL language server.');
const client = new LanguageClient(
'CodeQL Language Server',
() => spawnIdeServer(qlConfigurationListener),
{
documentSelector: [
{ language: 'ql', scheme: 'file' },
{ language: 'yaml', scheme: 'file', pattern: '**/qlpack.yml' },
],
synchronize: {
configurationSection: 'codeQL',
},
// Ensure that language server exceptions are logged to the same channel as its output.
outputChannel: ideServerLogger.outputChannel,
},
// Ensure that language server exceptions are logged to the same channel as its output.
outputChannel: ideServerLogger.outputChannel
}, true);
true
);
ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)));
ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)));
ctx.subscriptions.push(commands.registerCommand('codeQL.quickQuery', async () => displayQuickQuery(ctx, cliServer, databaseUI)));
ctx.subscriptions.push(commands.registerCommand('codeQL.restartQueryServer', async () => {
await qs.restartQueryServer();
const response = await Window.showInformationMessage('CodeQL Query Server restarted.', 'Show Log');
if (response === 'Show Log') {
qs.showLog();
}
}));
logger.log('Initializing QLTest interface.');
const testExplorerExtension = extensions.getExtension<TestHub>(
testExplorerExtensionId
);
if (testExplorerExtension) {
const testHub = testExplorerExtension.exports;
const testAdapterFactory = new QLTestAdapterFactory(testHub, cliServer);
ctx.subscriptions.push(testAdapterFactory);
const testUIService = new TestUIService(testHub);
ctx.subscriptions.push(testUIService);
}
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.runQueries',
async (_: Uri | undefined, multi: Uri[]) => {
const maxQueryCount = 20;
try {
const [files, dirFound] = await gatherQlFiles(multi.map(uri => uri.fsPath));
if (files.length > maxQueryCount) {
throw new Error(`You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries.`);
}
// warn user and display selected files when a directory is selected because some ql
// files may be hidden from the user.
if (dirFound) {
const fileString = files.map(file => path.basename(file)).join(', ');
const res = await helpers.showBinaryChoiceDialog(
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`
);
if (!res) {
return;
}
}
const queryUris = files.map(path => Uri.parse(`file:${path}`, true));
await Promise.all(queryUris.map(uri => compileAndRunQuery(false, uri)));
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQL.quickEval',
async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)
)
);
ctx.subscriptions.push(
commands.registerCommand('codeQL.quickQuery', async () =>
displayQuickQuery(ctx, cliServer, databaseUI)
)
);
ctx.subscriptions.push(
commands.registerCommand('codeQL.restartQueryServer', async () => {
await qs.restartQueryServer();
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', {
outputLogger: queryServerLogger,
});
})
);
ctx.subscriptions.push(
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());
// 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 {
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);
}
const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';

View File

@@ -0,0 +1,30 @@
import * as fs from 'fs-extra';
import * as path from 'path';
/**
* Recursively finds all .ql files in this set of Uris.
*
* @param paths The list of Uris to search through
*
* @returns list of ql files and a boolean describing whether or not a directory was found/
*/
export async function gatherQlFiles(paths: string[]): Promise<[string[], boolean]> {
const gatheredUris: Set<string> = new Set();
let dirFound = false;
for (const nextPath of paths) {
if (
(await fs.pathExists(nextPath)) &&
(await fs.stat(nextPath)).isDirectory()
) {
dirFound = true;
const subPaths = await fs.readdir(nextPath);
const fullPaths = subPaths.map(p => path.join(nextPath, p));
const nestedFiles = (await gatherQlFiles(fullPaths))[0];
nestedFiles.forEach(nested => gatheredUris.add(nested));
} else if (nextPath.endsWith('.ql')) {
gatheredUris.add(nextPath);
}
}
return [Array.from(gatheredUris), dirFound];
}

View File

@@ -10,7 +10,7 @@
*/
class ExhaustivityCheckingError extends Error {
constructor(public expectedExhaustiveValue: never) {
super("Internal error: exhaustivity checking failure");
super('Internal error: exhaustivity checking failure');
}
}

View File

@@ -1,5 +1,9 @@
import * as fs from 'fs-extra';
import * as glob from 'glob-promise';
import * as yaml from 'js-yaml';
import * as path from 'path';
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
import { CodeQLCliServer } from './cli';
import { logger } from './logging';
import { QueryInfo } from './run-queries';
@@ -18,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
@@ -47,37 +53,59 @@ export function withProgress<R>(
* Show an error message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param options.outputLogger The output logger that will receive the message
* @param options.items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showErrorMessage(message, ...items);
export async function showAndLogErrorMessage(message: string, {
outputLogger = logger,
items = [] as string[]
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showErrorMessage);
}
/**
* Show a warning message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param options.outputLogger The output logger that will receive the message
* @param options.items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showWarningMessage(message, ...items);
export async function showAndLogWarningMessage(message: string, {
outputLogger = logger,
items = [] as string[]
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showWarningMessage);
}
/**
* Show an information message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param options.outputLogger The output logger that will receive the message
* @param options.items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showInformationMessage(message, ...items);
export async function showAndLogInformationMessage(message: string, {
outputLogger = logger,
items = [] as string[]
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showInformationMessage);
}
type ShowMessageFn = (message: string, ...items: string[]) => Thenable<string | undefined>;
async function internalShowAndLog(message: string, items: string[], outputLogger = logger,
fn: ShowMessageFn): Promise<string | undefined> {
const label = 'Show Log';
outputLogger.log(message);
const result = await fn(message, label, ...items);
if (result === label) {
outputLogger.show();
}
return result;
}
/**
@@ -88,9 +116,9 @@ export function showAndLogInformationMessage(message: string, ...items: string[]
*/
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;
return chosenItem?.title === yesItem.title;
}
/**
@@ -109,10 +137,10 @@ export async function showInformationMessageWithAction(message: string, actionMe
/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFolders() {
const workspaceFolders = workspace.workspaceFolders || [];
let diskWorkspaceFolders: string[] = [];
const diskWorkspaceFolders: string[] = [];
for (const workspaceFolder of workspaceFolders) {
if (workspaceFolder.uri.scheme === "file")
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath)
if (workspaceFolder.uri.scheme === 'file')
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath);
}
return diskWorkspaceFolders;
}
@@ -157,8 +185,12 @@ export class InvocationRateLimiter<T> {
public async invokeFunctionIfIntervalElapsed(minSecondsSinceLastInvocation: number): Promise<InvocationRateLimiterResult<T>> {
const updateCheckStartDate = this._createDate();
const lastInvocationDate = this.getLastInvocationDate();
if (minSecondsSinceLastInvocation && lastInvocationDate && lastInvocationDate <= updateCheckStartDate &&
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()) {
if (
minSecondsSinceLastInvocation &&
lastInvocationDate &&
lastInvocationDate <= updateCheckStartDate &&
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()
) {
return createRateLimitedResult();
}
const result = await this._func();
@@ -181,7 +213,7 @@ export class InvocationRateLimiter<T> {
private readonly _func: () => Promise<T>;
private readonly _funcIdentifier: string;
private static readonly _invocationRateLimiterPrefix = "invocationRateLimiter_lastInvocationDate_";
private static readonly _invocationRateLimiterPrefix = 'invocationRateLimiter_lastInvocationDate_';
}
export enum InvocationRateLimiterResultKind {
@@ -193,15 +225,15 @@ export enum InvocationRateLimiterResultKind {
* The function was invoked and returned the value `result`.
*/
interface InvokedResult<T> {
kind: InvocationRateLimiterResultKind.Invoked,
result: T
kind: InvocationRateLimiterResultKind.Invoked;
result: T;
}
/**
* The function was not invoked as the minimum interval since the last invocation had not elapsed.
*/
interface RateLimitedResult {
kind: InvocationRateLimiterResultKind.RateLimited
kind: InvocationRateLimiterResultKind.RateLimited;
}
type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
@@ -218,3 +250,110 @@ function createRateLimitedResult(): RateLimitedResult {
kind: InvocationRateLimiterResultKind.RateLimited
};
}
export type DatasetFolderInfo = {
dbscheme: string;
qlpack: string;
}
export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
const packs: { packDir: string | undefined; packName: string }[] =
Object.entries(qlpacks).map(([packName, dirs]) => {
if (dirs.length < 1) {
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
return { packName, packDir: undefined };
}
if (dirs.length > 1) {
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
}
return {
packName,
packDir: dirs[0]
};
});
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
return packName;
}
}
}
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
}
export async function resolveDatasetFolder(cliServer: CodeQLCliServer, datasetFolder: string): Promise<DatasetFolderInfo> {
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'));
if (dbschemes.length < 1) {
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
}
dbschemes.sort();
const dbscheme = dbschemes[0];
if (dbschemes.length > 1) {
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
}
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);
return { dbscheme, qlpack };
}
/**
* A cached mapping from strings to value of type U.
*/
export class CachedOperation<U> {
private readonly operation: (t: string) => Promise<U>;
private readonly cached: Map<string, U>;
private readonly lru: string[];
private readonly inProgressCallbacks: Map<string, [(u: U) => void, (reason?: any) => void][]>;
constructor(operation: (t: string) => Promise<U>, private cacheSize = 100) {
this.operation = operation;
this.lru = [];
this.inProgressCallbacks = new Map<string, [(u: U) => void, (reason?: any) => void][]>();
this.cached = new Map<string, U>();
}
async get(t: string): Promise<U> {
// Try and retrieve from the cache
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]);
return fromCache;
}
// Otherwise check if in progress
const inProgressCallback = this.inProgressCallbacks.get(t);
if (inProgressCallback !== undefined) {
// If so wait for it to resolve
return await new Promise((resolve, reject) => {
inProgressCallback.push([resolve, reject]);
});
}
// Otherwise compute the new value, but leave a callback to allow sharing work
const callbacks: [(u: U) => void, (reason?: any) => void][] = [];
this.inProgressCallbacks.set(t, callbacks);
try {
const result = await this.operation(t);
callbacks.forEach(f => f[0](result));
this.inProgressCallbacks.delete(t);
if (this.lru.length > this.cacheSize) {
const toRemove = this.lru.shift()!;
this.cached.delete(toRemove);
}
this.lru.push(t);
this.cached.set(t, result);
return result;
} catch (e) {
// Rethrow error on all callbacks
callbacks.forEach(f => f[1](e));
throw e;
} finally {
this.inProgressCallbacks.delete(t);
}
}
}

View File

@@ -17,8 +17,8 @@ export async function spawnIdeServer(config: QueryServerConfig): Promise<StreamI
['execute', 'language-server'],
['--check-errors', 'ON_CHANGE'],
ideServerLogger,
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
progressReporter
);
return { writer: child.stdin!, reader: child.stdout! };

View File

@@ -1,16 +1,43 @@
import * as sarif from 'sarif';
import { ResolvableLocationValue } from 'semmle-bqrs';
import {
ResolvableLocationValue,
ColumnSchema,
ResultSetSchema,
} from 'semmle-bqrs';
import { ResultRow, ParsedResultSets, RawResultSet } from './adapt';
/**
* Only ever show this many results per run in interpreted results.
* This module contains types and code that are shared between
* the webview and the extension.
*/
export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
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;
/**
* Only ever show this many rows in a raw result table.
*/
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;
/**
* Show this many rows in an interpreted results table at a time.
*/
export const INTERPRETED_RESULTS_PAGE_SIZE = 100;
export interface DatabaseInfo {
name: string;
databaseUri: string;
@@ -18,10 +45,10 @@ export interface DatabaseInfo {
/** Arbitrary query metadata */
export interface QueryMetadata {
name?: string,
description?: string,
id?: string,
kind?: string
name?: string;
description?: string;
id?: string;
kind?: string;
}
export interface PreviousExecution {
@@ -34,6 +61,7 @@ export interface PreviousExecution {
export interface Interpretation {
sourceLocationPrefix: string;
numTruncatedResults: number;
numTotalResults: number;
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
@@ -70,53 +98,82 @@ export interface SetStateMsg {
sortedResultsMap: SortedResultsMap;
interpretation: undefined | Interpretation;
database: DatabaseInfo;
metadata?: QueryMetadata
metadata?: QueryMetadata;
/**
* Whether to keep displaying the old results while rendering the new results.
*
* This is useful to prevent properties like scroll state being lost when rendering the sorted results after sorting a column.
*/
shouldKeepOldResultsWhileRendering: boolean;
};
/**
* An experimental way of providing results from the extension.
* Should be in the WebviewParsedResultSets branch of the type
* unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
*/
parsedResultSets: ParsedResultSets;
}
export interface ShowInterpretedPageMsg {
t: 'showInterpretedPage';
interpretation: Interpretation;
database: DatabaseInfo;
metadata?: QueryMetadata;
pageNumber: number;
numPages: number;
resultSetNames: string[];
}
/** Advance to the next or previous path no in the path viewer */
export interface NavigatePathMsg {
t: 'navigatePath',
t: 'navigatePath';
/** 1 for next, -1 for previous */
direction: number;
}
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
export type IntoResultsViewMsg =
| ResultsUpdatingMsg
| SetStateMsg
| ShowInterpretedPageMsg
| NavigatePathMsg;
export type FromResultsViewMsg =
| ViewSourceFileMsg
| ToggleDiagnostics
| ChangeRawResultsSortMsg
| ChangeInterpretedResultsSortMsg
| ResultViewLoaded;
| ResultViewLoaded
| ChangePage;
interface ViewSourceFileMsg {
export interface ViewSourceFileMsg {
t: 'viewSourceFile';
loc: ResolvableLocationValue;
databaseUri: string;
};
}
interface ToggleDiagnostics {
t: 'toggleDiagnostics';
databaseUri: string;
metadata?: QueryMetadata
metadata?: QueryMetadata;
origResultsPaths: ResultsPaths;
visible: boolean;
kind?: string;
};
}
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
asc,
desc,
}
export interface RawResultsSortState {
@@ -124,8 +181,7 @@ export interface RawResultsSortState {
sortDirection: SortDirection;
}
export type InterpretedResultsSortColumn =
'alert-message';
export type InterpretedResultsSortColumn = 'alert-message';
export interface InterpretedResultsSortState {
sortBy: InterpretedResultsSortColumn;
@@ -150,3 +206,90 @@ interface ChangeInterpretedResultsSortMsg {
*/
sortState?: InterpretedResultsSortState;
}
export type FromCompareViewMessage =
| CompareViewLoadedMessage
| ChangeCompareMessage
| ViewSourceFileMsg
| OpenQueryMessage;
interface CompareViewLoadedMessage {
t: 'compareViewLoaded';
}
export interface OpenQueryMessage {
readonly t: 'openQuery';
readonly kind: 'from' | 'to';
}
interface ChangeCompareMessage {
t: 'changeCompare';
newResultSetName: string;
}
export type ToCompareViewMessage = SetComparisonsMessage;
export interface SetComparisonsMessage {
readonly t: 'setComparisons';
readonly stats: {
fromQuery?: {
name: string;
status: string;
time: string;
};
toQuery?: {
name: string;
status: string;
time: string;
};
};
readonly columns: readonly ColumnSchema[];
readonly commonResultSetNames: string[];
readonly currentResultSetName: string;
readonly rows: QueryCompareResult | undefined;
readonly message: string | undefined;
readonly datebaseUri: string;
}
export enum DiffKind {
Add = 'Add',
Remove = 'Remove',
Change = 'Change',
}
/**
* from is the set of rows that have changes in the "from" query.
* to is the set of rows that have changes in the "to" query.
* They are in the same order, so element 1 in "from" corresponds to
* element 1 in "to".
*
* If an array element is null, that means that the element was removed
* (or added) in the comparison.
*/
export type QueryCompareResult = {
from: ResultRow[];
to: ResultRow[];
};
/**
* Extract the name of the default result. Prefer returning
* 'alerts', or '#select'. Otherwise return the first in the list.
*
* Note that this is the only function in this module. It must be
* placed here since it is shared across the webview boundary.
*
* We should consider moving to a separate module to ensure this
* one is types only.
*
* @param resultSetNames
*/
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

@@ -0,0 +1,233 @@
import * as crypto from 'crypto';
import {
Uri,
Location,
Range,
WebviewPanel,
Webview,
workspace,
window as Window,
ViewColumn,
Selection,
TextEditorRevealType,
ThemeColor,
} from 'vscode';
import {
FivePartLocation,
LocationStyle,
LocationValue,
tryGetResolvableLocation,
WholeFileLocation,
ResolvableLocationValue,
} from 'semmle-bqrs';
import { DatabaseItem, DatabaseManager } from './databases';
import { ViewSourceFileMsg } from './interface-types';
import { Logger } from './logging';
/**
* This module contains functions and types that are sharedd between
* interface.ts and compare-interface.ts.
*/
/** Gets a nonce string created with 128 bits of entropy. */
export function getNonce(): string {
return crypto.randomBytes(16).toString('base64');
}
/**
* Whether to force webview to reveal
*/
export enum WebviewReveal {
Forced,
NotForced,
}
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
export function fileUriToWebviewUri(
panel: WebviewPanel,
fileUriOnDisk: Uri
): string {
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
}
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
export function webviewUriToFileUri(webviewUri: string): Uri {
// Webview URIs used the vscode-resource scheme. The filesystem path of the resource can be obtained from the path component of the webview URI.
const path = Uri.parse(webviewUri).path;
// For this path to be interpreted on the filesystem, we need to parse it as a filesystem URI for the current platform.
return Uri.file(path);
}
/**
* Resolves the specified CodeQL location to a URI into the source archive.
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
* @param databaseItem Database in which to resolve the file location.
*/
function resolveFivePartLocation(
loc: FivePartLocation,
databaseItem: DatabaseItem
): Location {
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
// are one-based. Adjust accordingly.
const range = new Range(
Math.max(0, loc.lineStart - 1),
Math.max(0, loc.colStart - 1),
Math.max(0, loc.lineEnd - 1),
Math.max(0, loc.colEnd)
);
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Resolves the specified CodeQL filesystem resource location to a URI into the source archive.
* @param loc CodeQL location to resolve, corresponding to an entire filesystem resource. Must have a non-empty value for `loc.file`.
* @param databaseItem Database in which to resolve the filesystem resource location.
*/
function resolveWholeFileLocation(
loc: WholeFileLocation,
databaseItem: DatabaseItem
): Location {
// A location corresponding to the start of the file.
const range = new Range(0, 0, 0, 0);
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`.
* @param loc CodeQL location to resolve
* @param databaseItem Database in which to resolve the file location.
*/
export function tryResolveLocation(
loc: LocationValue | undefined,
databaseItem: DatabaseItem
): Location | undefined {
const resolvableLoc = tryGetResolvableLocation(loc);
if (resolvableLoc === undefined) {
return undefined;
}
switch (resolvableLoc.t) {
case LocationStyle.FivePart:
return resolveFivePartLocation(resolvableLoc, databaseItem);
case LocationStyle.WholeFile:
return resolveWholeFileLocation(resolvableLoc, databaseItem);
default:
return undefined;
}
}
/**
* Returns HTML to populate the given webview.
* Uses a content security policy that only loads the given script.
*/
export function getHtmlForWebview(
webview: Webview,
scriptUriOnDisk: Uri,
stylesheetUriOnDisk: Uri
): string {
// Convert the on-disk URIs into webview URIs.
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk);
// Use a nonce in the content security policy to uniquely identify the above resources.
const nonce = getNonce();
/*
* Content security policy:
* default-src: allow nothing by default.
* script-src: allow only the given script, using the nonce.
* style-src: allow only the given stylesheet, using the nonce.
* connect-src: only allow fetch calls to webview resource URIs
* (this is used to load BQRS result files).
*/
return `
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">
</head>
<body>
<div id=root>
</div>
<script nonce="${nonce}" src="${scriptWebviewUri}">
</script>
</body>
</html>`;
}
export async function showLocation(
loc: ResolvableLocationValue,
databaseItem: DatabaseItem
): Promise<void> {
const resolvedLocation = tryResolveLocation(loc, databaseItem);
if (resolvedLocation) {
const doc = await workspace.openTextDocument(resolvedLocation.uri);
const editorsWithDoc = Window.visibleTextEditors.filter(
(e) => e.document === doc
);
const editor =
editorsWithDoc.length > 0
? editorsWithDoc[0]
: await Window.showTextDocument(doc, ViewColumn.One);
const range = resolvedLocation.range;
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
// trigger based on where we place the cursor/selection, and will compete for the user's attention.
// For reference:
// - Occurences are highlighted when the cursor is next to or inside a word or a whole word is selected.
// - Brackets are highlighted when the cursor is next to a bracket and there is an empty selection.
// - Multi-line selections explicitly highlight line-break characters, but multi-line decorators do not.
//
// For single-line ranges, select the whole range, mainly to disable bracket highlighting.
// For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks.
// Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting.
const selectionEnd =
range.start.line === range.end.line ? range.end : range.start;
editor.selection = new Selection(range.start, selectionEnd);
editor.revealRange(range, TextEditorRevealType.InCenter);
editor.setDecorations(shownLocationDecoration, [range]);
editor.setDecorations(shownLocationLineDecoration, [range]);
}
}
const findMatchBackground = new ThemeColor('editor.findMatchBackground');
const findRangeHighlightBackground = new ThemeColor(
'editor.findRangeHighlightBackground'
);
export const shownLocationDecoration = Window.createTextEditorDecorationType({
backgroundColor: findMatchBackground,
});
export const shownLocationLineDecoration = Window.createTextEditorDecorationType(
{
backgroundColor: findRangeHighlightBackground,
isWholeLine: true,
}
);
export async function jumpToLocation(
msg: ViewSourceFileMsg,
databaseManager: DatabaseManager,
logger: Logger
) {
const databaseItem = databaseManager.findDatabaseItem(
Uri.parse(msg.databaseUri)
);
if (databaseItem !== undefined) {
try {
await showLocation(msg.loc, databaseItem);
} catch (e) {
if (e instanceof Error) {
if (e.message.match(/File not found/)) {
Window.showErrorMessage(
'Original file of this result is not in the database\'s source archive.'
);
} else {
logger.log(`Unable to handleMsgFromView: ${e.message}`);
}
} else {
logger.log(`Unable to handleMsgFromView: ${e}`);
}
}
}
}

View File

@@ -1,21 +1,58 @@
import * as crypto from 'crypto';
import * as path from 'path';
import * as Sarif from 'sarif';
import { FivePartLocation, LocationStyle, LocationValue, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
import { DisposableObject } from 'semmle-vscode-utils';
import { DisposableObject } from '@github/codeql-vscode-utils';
import * as vscode from 'vscode';
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Range, Uri, window as Window, workspace } from 'vscode';
import {
Diagnostic,
DiagnosticRelatedInformation,
DiagnosticSeverity,
languages,
Uri,
window as Window,
env
} from 'vscode';
import * as cli from './cli';
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,
IntoResultsViewMsg,
QueryMetadata,
ResultsPaths,
SortedResultSetInfo,
SortedResultsMap,
InterpretedResultsSortState,
SortDirection,
RAW_RESULTS_PAGE_SIZE,
INTERPRETED_RESULTS_PAGE_SIZE,
ALERTS_TABLE_NAME,
} 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,
ParsedResultSets,
RawResultSet,
} from './adapt';
import { EXPERIMENTAL_BQRS_SETTING } from './config';
import {
WebviewReveal,
fileUriToWebviewUri,
tryResolveLocation,
getHtmlForWebview,
shownLocationDecoration,
shownLocationLineDecoration,
jumpToLocation,
} from './interface-utils';
import { getDefaultResultSetName } from './interface-types';
import { ResultSetSchema } from './bqrs-cli-types';
/**
* interface.ts
@@ -25,83 +62,30 @@ import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
* webview asks us to.
*/
/** Gets a nonce string created with 128 bits of entropy. */
function getNonce(): string {
return crypto.randomBytes(16).toString('base64');
}
/**
* Whether to force webview to reveal
*/
export enum WebviewReveal {
Forced,
NotForced,
}
/**
* Returns HTML to populate the given webview.
* Uses a content security policy that only loads the given script.
*/
function getHtmlForWebview(webview: vscode.Webview, scriptUriOnDisk: vscode.Uri, stylesheetUriOnDisk: vscode.Uri) {
// Convert the on-disk URIs into webview URIs.
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk);
// Use a nonce in the content security policy to uniquely identify the above resources.
const nonce = getNonce();
/*
* Content security policy:
* default-src: allow nothing by default.
* script-src: allow only the given script, using the nonce.
* style-src: allow only the given stylesheet, using the nonce.
* connect-src: only allow fetch calls to webview resource URIs
* (this is used to load BQRS result files).
*/
const html = `
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">
</head>
<body>
<div id=root>
</div>
<script nonce="${nonce}" src="${scriptWebviewUri}">
</script>
</body>
</html>`;
webview.html = html;
}
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
export function fileUriToWebviewUri(panel: vscode.WebviewPanel, fileUriOnDisk: Uri): string {
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
}
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
export function webviewUriToFileUri(webviewUri: string): Uri {
// Webview URIs used the vscode-resource scheme. The filesystem path of the resource can be obtained from the path component of the webview URI.
const path = Uri.parse(webviewUri).path;
// For this path to be interpreted on the filesystem, we need to parse it as a filesystem URI for the current platform.
return Uri.file(path);
}
function sortMultiplier(sortDirection: SortDirection): number {
switch (sortDirection) {
case SortDirection.asc: return 1;
case SortDirection.desc: return -1;
case SortDirection.asc:
return 1;
case SortDirection.desc:
return -1;
}
}
function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedResultsSortState | undefined): void {
function sortInterpretedResults(
results: Sarif.Result[],
sortState: InterpretedResultsSortState | undefined
): void {
if (sortState !== undefined) {
const multiplier = sortMultiplier(sortState.sortDirection);
switch (sortState.sortBy) {
case 'alert-message':
results.sort((a, b) =>
a.message.text === undefined ? 0 :
b.message.text === undefined ? 0 :
multiplier * (a.message.text?.localeCompare(b.message.text)));
a.message.text === undefined
? 0
: b.message.text === undefined
? 0
: multiplier * a.message.text?.localeCompare(b.message.text, env.language)
);
break;
default:
assertNever(sortState.sortBy);
@@ -109,26 +93,55 @@ function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedR
}
}
function numPagesOfResultSet(resultSet: RawResultSet): number {
return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE);
}
function numInterpretedPages(interpretation: Interpretation | undefined): number {
return Math.ceil((interpretation?.sarif.runs[0].results?.length || 0) / INTERPRETED_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)[] = [];
private readonly _diagnosticCollection = languages.createDiagnosticCollection(`codeql-query-results`);
constructor(public ctx: vscode.ExtensionContext, private databaseManager: DatabaseManager,
public cliServer: CodeQLCliServer, public logger: Logger) {
private readonly _diagnosticCollection = languages.createDiagnosticCollection(
'codeql-query-results'
);
constructor(
public ctx: vscode.ExtensionContext,
private databaseManager: DatabaseManager,
public cliServer: CodeQLCliServer,
public logger: Logger
) {
super();
this.push(this._diagnosticCollection);
this.push(vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange.bind(this)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.nextPathStep', this.navigatePathStep.bind(this, 1)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.previousPathStep', this.navigatePathStep.bind(this, -1)));
this.push(
vscode.window.onDidChangeTextEditorSelection(
this.handleSelectionChange.bind(this)
)
);
logger.log('Registering path-step navigation commands.');
this.push(
vscode.commands.registerCommand(
'codeQLQueryResults.nextPathStep',
this.navigatePathStep.bind(this, 1)
)
);
this.push(
vscode.commands.registerCommand(
'codeQLQueryResults.previousPathStep',
this.navigatePathStep.bind(this, -1)
)
);
}
navigatePathStep(direction: number) {
this.postMessage({ t: "navigatePath", direction });
navigatePathStep(direction: number): void {
this.postMessage({ t: 'navigatePath', direction });
}
// Returns the webview panel, creating it if it doesn't already
@@ -136,7 +149,7 @@ export class InterfaceManager extends DisposableObject {
getPanel(): vscode.WebviewPanel {
if (this._panel == undefined) {
const { ctx } = this;
const panel = this._panel = Window.createWebviewPanel(
const panel = (this._panel = Window.createWebviewPanel(
'resultsView', // internal name
'CodeQL Query Results', // user-visible name
{ viewColumn: vscode.ViewColumn.Beside, preserveFocus: true },
@@ -149,21 +162,42 @@ export class InterfaceManager extends DisposableObject {
vscode.Uri.file(path.join(this.ctx.extensionPath, 'out'))
]
}
));
this._panel.onDidDispose(
() => {
this._panel = undefined;
this._displayedQuery = undefined;
},
null,
ctx.subscriptions
);
const scriptPathOnDisk = vscode.Uri.file(
ctx.asAbsolutePath('out/resultsView.js')
);
const stylesheetPathOnDisk = vscode.Uri.file(
ctx.asAbsolutePath('out/resultsView.css')
);
panel.webview.html = getHtmlForWebview(
panel.webview,
scriptPathOnDisk,
stylesheetPathOnDisk
);
panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
);
this._panel.onDidDispose(() => { this._panel = undefined; }, null, ctx.subscriptions);
const scriptPathOnDisk = vscode.Uri
.file(ctx.asAbsolutePath('out/resultsView.js'));
const stylesheetPathOnDisk = vscode.Uri
.file(ctx.asAbsolutePath('out/resultsView.css'));
getHtmlForWebview(panel.webview, scriptPathOnDisk, stylesheetPathOnDisk);
panel.webview.onDidReceiveMessage(async (e) => this.handleMsgFromView(e), undefined, ctx.subscriptions);
}
return this._panel;
}
private async changeSortState(update: (query: CompletedQuery) => Promise<void>): Promise<void> {
private async changeSortState(
update: (query: CompletedQuery) => Promise<void>
): Promise<void> {
if (this._displayedQuery === undefined) {
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
showAndLogErrorMessage(
'Failed to sort results since evaluation info was unknown.'
);
return;
}
// Notify the webview that it should expect new results.
@@ -175,32 +209,20 @@ export class InterfaceManager extends DisposableObject {
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
switch (msg.t) {
case 'viewSourceFile': {
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
if (databaseItem !== undefined) {
try {
await showLocation(msg.loc, databaseItem);
}
catch (e) {
if (e instanceof Error) {
if (e.message.match(/File not found/)) {
vscode.window.showErrorMessage(`Original file of this result is not in the database's source archive.`);
}
else {
this.logger.log(`Unable to handleMsgFromView: ${e.message}`);
}
}
else {
this.logger.log(`Unable to handleMsgFromView: ${e}`);
}
}
}
await jumpToLocation(msg, this.databaseManager, this.logger);
break;
}
case 'toggleDiagnostics': {
if (msg.visible) {
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
const databaseItem = this.databaseManager.findDatabaseItem(
Uri.parse(msg.databaseUri)
);
if (databaseItem !== undefined) {
await this.showResultsAsDiagnostics(msg.origResultsPaths, msg.metadata, databaseItem);
await this.showResultsAsDiagnostics(
msg.origResultsPaths,
msg.metadata,
databaseItem
);
}
} else {
// TODO: Only clear diagnostics on the same database.
@@ -208,16 +230,32 @@ export class InterfaceManager extends DisposableObject {
}
break;
}
case "resultViewLoaded":
case 'resultViewLoaded':
this._panelLoaded = true;
this._panelLoadedCallBacks.forEach(cb => cb());
this._panelLoadedCallBacks.forEach((cb) => cb());
this._panelLoadedCallBacks = [];
break;
case 'changeSort':
await this.changeSortState((query) => query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState));
await this.changeSortState(query =>
query.updateSortState(
this.cliServer,
msg.resultSetName,
msg.sortState
)
);
break;
case 'changeInterpretedSort':
await this.changeSortState((query) => query.updateInterpretedSortState(this.cliServer, msg.sortState));
await this.changeSortState(query =>
query.updateInterpretedSortState(this.cliServer, msg.sortState)
);
break;
case 'changePage':
if (msg.selectedTable === ALERTS_TABLE_NAME) {
await this.showPageOfInterpretedResults(msg.pageNumber);
}
else {
await this.showPageOfRawResults(msg.selectedTable, msg.pageNumber);
}
break;
default:
assertNever(msg);
@@ -229,13 +267,13 @@ export class InterfaceManager extends DisposableObject {
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve, _reject) => {
return new Promise((resolve) => {
if (this._panelLoaded) {
resolve();
} else {
this._panelLoadedCallBacks.push(resolve)
this._panelLoadedCallBacks.push(resolve);
}
})
});
}
/**
@@ -247,16 +285,26 @@ export class InterfaceManager extends DisposableObject {
* UI interaction requesting results, e.g. clicking on a query
* history entry.
*/
public async showResults(results: CompletedQuery, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
public async showResults(
results: CompletedQuery,
forceReveal: WebviewReveal,
shouldKeepOldResultsWhileRendering = false
): Promise<void> {
if (results.result.resultType !== messages.QueryResultType.SUCCESS) {
return;
}
const interpretation = await this.interpretResultsInfo(results.query, results.interpretedResultsSortState);
this._interpretation = undefined;
const interpretationPage = await this.interpretResultsInfo(
results.query,
results.interpretedResultsSortState
);
const sortedResultsMap: SortedResultsMap = {};
results.sortedResultsInfo.forEach((v, k) =>
sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v));
results.sortedResultsInfo.forEach(
(v, k) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
);
this._displayedQuery = results;
@@ -264,8 +312,7 @@ export class InterfaceManager extends DisposableObject {
await this.waitForPanelLoaded();
if (forceReveal === WebviewReveal.Forced) {
panel.reveal(undefined, true);
}
else if (!panel.visible) {
} else if (!panel.visible) {
// The results panel exists, (`.getPanel()` guarantees it) but
// is not visible; it's in a not-currently-viewed tab. Show a
// more asynchronous message to not so abruptly interrupt
@@ -273,106 +320,316 @@ export class InterfaceManager extends DisposableObject {
const showButton = 'View Results';
const queryName = results.queryName;
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${(queryName.length > 0) ? `${queryName}` : ''}.`,
`Finished running query ${
queryName.length > 0 ? ` "${queryName}"` : ''
}.`,
showButton
);
// Address this click asynchronously so we still update the
// query history immediately.
resultPromise.then(result => {
resultPromise.then((result) => {
if (result === showButton) {
panel.reveal();
}
});
}
const getParsedResultSets = async (): Promise<ParsedResultSets> => {
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
const resultSetSchemas = await this.getResultSetSchemas(results);
const resultSetNames = resultSetSchemas.map(schema => schema.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 = resultSetSchemas.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);
return {
t: 'ExtensionParsed',
pageNumber: 0,
numPages: numPagesOfResultSet(resultSet),
numInterpretedPages: numInterpretedPages(this._interpretation),
resultSet: { t: 'RawResultSet', ...resultSet },
selectedTable: undefined,
resultSetNames,
};
} else {
return { t: 'WebviewParsed' };
}
};
await this.postMessage({
t: 'setState',
interpretation,
interpretation: interpretationPage,
origResultsPaths: results.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(results.query.resultsPaths.resultsPath),
resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath
),
parsedResultSets: await getParsedResultSets(),
sortedResultsMap,
database: results.database,
shouldKeepOldResultsWhileRendering,
metadata: results.query.metadata
metadata: results.query.metadata,
});
}
private async getTruncatedResults(metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo: cli.SourceInfo | undefined, sourceLocationPrefix: string, sortState: InterpretedResultsSortState | undefined): Promise<Interpretation> {
const sarif = await interpretResults(this.cliServer, metadata, resultsPaths.resultsPath, sourceInfo);
// For performance reasons, limit the number of results we try
// to serialize and send to the webview. TODO: possibly also
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach(run => {
if (run.results !== undefined) {
sortInterpretedResults(run.results, sortState);
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
}
}
});
return { sarif, sourceLocationPrefix, numTruncatedResults, sortState };
}
private async interpretResultsInfo(query: QueryInfo, sortState: InterpretedResultsSortState | undefined): Promise<Interpretation | undefined> {
let interpretation: Interpretation | undefined = undefined;
if (await query.hasInterpretedResults()
&& query.quickEvalPosition === undefined // never do results interpretation if quickEval
) {
try {
const sourceLocationPrefix = await query.dbItem.getSourceLocationPrefix(this.cliServer);
const sourceArchiveUri = query.dbItem.sourceArchive;
const sourceInfo = sourceArchiveUri === undefined ?
undefined :
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
interpretation = await this.getTruncatedResults(query.metadata, query.resultsPaths, sourceInfo, sourceLocationPrefix, sortState);
}
catch (e) {
// If interpretation fails, accept the error and continue
// trying to render uninterpreted results anyway.
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
}
/**
* Show a page of interpreted results
*/
public async showPageOfInterpretedResults(
pageNumber: number
): Promise<void> {
if (this._displayedQuery === undefined) {
throw new Error('Trying to show interpreted results but displayed query was undefined');
}
if (this._interpretation === undefined) {
throw new Error('Trying to show interpreted results but interpretation was undefined');
}
if (this._interpretation.sarif.runs[0].results === undefined) {
throw new Error('Trying to show interpreted results but results were undefined');
}
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery);
const resultSetNames = resultSetSchemas.map(schema => schema.name);
await this.postMessage({
t: 'showInterpretedPage',
interpretation: this.getPageOfInterpretedResults(pageNumber),
database: this._displayedQuery.database,
metadata: this._displayedQuery.query.metadata,
pageNumber,
resultSetNames,
numPages: numInterpretedPages(this._interpretation),
});
}
private async getResultSetSchemas(results: CompletedQuery): Promise<ResultSetSchema[]> {
const schemas = await this.cliServer.bqrsInfo(
results.query.resultsPaths.resultsPath,
RAW_RESULTS_PAGE_SIZE
);
return schemas['result-sets'];
}
/**
* Show a page of raw results from the chosen table.
*/
public async showPageOfRawResults(
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 resultSetSchemas = await this.getResultSetSchemas(results);
const resultSetNames = resultSetSchemas.map(schema => schema.name);
const schema = resultSetSchemas.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: { t: 'RawResultSet', ...resultSet },
numPages: numPagesOfResultSet(resultSet),
numInterpretedPages: numInterpretedPages(this._interpretation),
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 _getInterpretedResults(
metadata: QueryMetadata | undefined,
resultsPaths: ResultsPaths,
sourceInfo: cli.SourceInfo | undefined,
sourceLocationPrefix: string,
sortState: InterpretedResultsSortState | undefined
): Promise<Interpretation> {
const sarif = await interpretResults(
this.cliServer,
metadata,
resultsPaths,
sourceInfo
);
sarif.runs.forEach(run => {
if (run.results !== undefined)
sortInterpretedResults(run.results, sortState);
});
const numTotalResults = (() => {
if (sarif.runs.length === 0) return 0;
if (sarif.runs[0].results === undefined) return 0;
return sarif.runs[0].results.length;
})();
const interpretation: Interpretation = {
sarif,
sourceLocationPrefix,
numTruncatedResults: 0,
numTotalResults,
sortState,
};
this._interpretation = interpretation;
return interpretation;
}
private getPageOfInterpretedResults(
pageNumber: number
): Interpretation {
private async showResultsAsDiagnostics(resultsInfo: ResultsPaths, metadata: QueryMetadata | undefined, database: DatabaseItem) {
const sourceLocationPrefix = await database.getSourceLocationPrefix(this.cliServer);
function getPageOfRun(run: Sarif.Run): Sarif.Run {
return {
...run, results: run.results?.slice(
INTERPRETED_RESULTS_PAGE_SIZE * pageNumber,
INTERPRETED_RESULTS_PAGE_SIZE * (pageNumber + 1)
)
};
}
if (this._interpretation === undefined) {
throw new Error('Tried to get interpreted results before interpretation finished');
}
if (this._interpretation.sarif.runs.length !== 1) {
this.logger.log(`Warning: SARIF file had ${this._interpretation.sarif.runs.length} runs, expected 1`);
}
const interp = this._interpretation;
return {
...interp,
sarif: { ...interp.sarif, runs: [getPageOfRun(interp.sarif.runs[0])] },
};
}
private async interpretResultsInfo(
query: QueryInfo,
sortState: InterpretedResultsSortState | undefined
): Promise<Interpretation | undefined> {
if (
(await query.canHaveInterpretedResults()) &&
query.quickEvalPosition === undefined // never do results interpretation if quickEval
) {
try {
const sourceLocationPrefix = await query.dbItem.getSourceLocationPrefix(
this.cliServer
);
const sourceArchiveUri = query.dbItem.sourceArchive;
const sourceInfo =
sourceArchiveUri === undefined
? undefined
: {
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix,
};
await this._getInterpretedResults(
query.metadata,
query.resultsPaths,
sourceInfo,
sourceLocationPrefix,
sortState
);
} catch (e) {
// If interpretation fails, accept the error and continue
// trying to render uninterpreted results anyway.
this.logger.log(
`Exception during results interpretation: ${e.message}. Will show raw results instead.`
);
}
}
return this._interpretation && this.getPageOfInterpretedResults(0);
}
private async showResultsAsDiagnostics(
resultsInfo: ResultsPaths,
metadata: QueryMetadata | undefined,
database: DatabaseItem
): Promise<void> {
const sourceLocationPrefix = await database.getSourceLocationPrefix(
this.cliServer
);
const sourceArchiveUri = database.sourceArchive;
const sourceInfo = sourceArchiveUri === undefined ?
undefined :
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
const interpretation = await this.getTruncatedResults(
const sourceInfo =
sourceArchiveUri === undefined
? undefined
: {
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix,
};
// TODO: Performance-testing to determine whether this truncation is necessary.
const interpretation = await this._getInterpretedResults(
metadata,
resultsInfo,
sourceInfo,
sourceLocationPrefix,
undefined,
undefined
);
try {
await this.showProblemResultsAsDiagnostics(interpretation, database);
}
catch (e) {
} catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
this.logger.log(
`Exception while computing problem results as diagnostics: ${msg}`
);
this._diagnosticCollection.clear();
}
}
private async showProblemResultsAsDiagnostics(interpretation: Interpretation, databaseItem: DatabaseItem): Promise<void> {
private async showProblemResultsAsDiagnostics(
interpretation: Interpretation,
databaseItem: DatabaseItem
): Promise<void> {
const { sarif, sourceLocationPrefix } = interpretation;
if (!sarif.runs || !sarif.runs[0].results) {
this.logger.log("Didn't find a run in the sarif results. Error processing sarif?")
this.logger.log(
'Didn\'t find a run in the sarif results. Error processing sarif?'
);
return;
}
@@ -381,59 +638,68 @@ export class InterfaceManager extends DisposableObject {
for (const result of sarif.runs[0].results) {
const message = result.message.text;
if (message === undefined) {
this.logger.log("Sarif had result without plaintext message")
this.logger.log('Sarif had result without plaintext message');
continue;
}
if (!result.locations) {
this.logger.log("Sarif had result without location")
this.logger.log('Sarif had result without location');
continue;
}
const sarifLoc = parseSarifLocation(result.locations[0], sourceLocationPrefix);
if (sarifLoc.t == "NoLocation") {
const sarifLoc = parseSarifLocation(
result.locations[0],
sourceLocationPrefix
);
if (sarifLoc.t == 'NoLocation') {
continue;
}
const resultLocation = tryResolveLocation(sarifLoc, databaseItem)
const resultLocation = tryResolveLocation(sarifLoc, databaseItem);
if (!resultLocation) {
this.logger.log("Sarif location was not resolvable " + sarifLoc)
this.logger.log('Sarif location was not resolvable ' + sarifLoc);
continue;
}
const parsedMessage = parseSarifPlainTextMessage(message);
const relatedInformation: DiagnosticRelatedInformation[] = [];
const relatedLocationsById: { [k: number]: Sarif.Location } = {};
for (let loc of result.relatedLocations || []) {
for (const loc of result.relatedLocations || []) {
relatedLocationsById[loc.id!] = loc;
}
let resultMessageChunks: string[] = [];
const resultMessageChunks: string[] = [];
for (const section of parsedMessage) {
if (typeof section === "string") {
if (typeof section === 'string') {
resultMessageChunks.push(section);
} else {
resultMessageChunks.push(section.text);
const sarifChunkLoc = parseSarifLocation(relatedLocationsById[section.dest], sourceLocationPrefix);
if (sarifChunkLoc.t == "NoLocation") {
const sarifChunkLoc = parseSarifLocation(
relatedLocationsById[section.dest],
sourceLocationPrefix
);
if (sarifChunkLoc.t == 'NoLocation') {
continue;
}
const referenceLocation = tryResolveLocation(sarifChunkLoc, databaseItem);
const referenceLocation = tryResolveLocation(
sarifChunkLoc,
databaseItem
);
if (referenceLocation) {
const related = new DiagnosticRelatedInformation(referenceLocation,
section.text);
const related = new DiagnosticRelatedInformation(
referenceLocation,
section.text
);
relatedInformation.push(related);
}
}
}
const diagnostic = new Diagnostic(resultLocation.range, resultMessageChunks.join(""), DiagnosticSeverity.Warning);
const diagnostic = new Diagnostic(
resultLocation.range,
resultMessageChunks.join(''),
DiagnosticSeverity.Warning
);
diagnostic.relatedInformation = relatedInformation;
diagnostics.push([
resultLocation.uri,
[diagnostic]
]);
diagnostics.push([resultLocation.uri, [diagnostic]]);
}
this._diagnosticCollection.set(diagnostics);
}
@@ -442,111 +708,25 @@ export class InterfaceManager extends DisposableObject {
return fileUriToWebviewUri(this.getPanel(), Uri.file(path));
}
private convertPathPropertiesToWebviewUris(info: SortedResultSetInfo): SortedResultSetInfo {
private convertPathPropertiesToWebviewUris(
info: SortedResultSetInfo
): SortedResultSetInfo {
return {
resultsPath: this.convertPathToWebviewUri(info.resultsPath),
sortState: info.sortState
sortState: info.sortState,
};
}
private handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent) {
private handleSelectionChange(
event: vscode.TextEditorSelectionChangeEvent
): void {
if (event.kind === vscode.TextEditorSelectionChangeKind.Command) {
return; // Ignore selection events we caused ourselves.
}
let editor = vscode.window.activeTextEditor;
const editor = vscode.window.activeTextEditor;
if (editor !== undefined) {
editor.setDecorations(shownLocationDecoration, []);
editor.setDecorations(shownLocationLineDecoration, []);
}
}
}
const findMatchBackground = new vscode.ThemeColor('editor.findMatchBackground');
const findRangeHighlightBackground = new vscode.ThemeColor('editor.findRangeHighlightBackground');
const shownLocationDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: findMatchBackground,
});
const shownLocationLineDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: findRangeHighlightBackground,
isWholeLine: true
});
async function showLocation(loc: ResolvableLocationValue, databaseItem: DatabaseItem): Promise<void> {
const resolvedLocation = tryResolveLocation(loc, databaseItem);
if (resolvedLocation) {
const doc = await workspace.openTextDocument(resolvedLocation.uri);
const editorsWithDoc = Window.visibleTextEditors.filter(e => e.document === doc);
const editor = editorsWithDoc.length > 0
? editorsWithDoc[0]
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
let range = resolvedLocation.range;
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
// trigger based on where we place the cursor/selection, and will compete for the user's attention.
// For reference:
// - Occurences are highlighted when the cursor is next to or inside a word or a whole word is selected.
// - Brackets are highlighted when the cursor is next to a bracket and there is an empty selection.
// - Multi-line selections explicitly highlight line-break characters, but multi-line decorators do not.
//
// For single-line ranges, select the whole range, mainly to disable bracket highlighting.
// For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks.
// Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting.
let selectionEnd = (range.start.line === range.end.line)
? range.end
: range.start;
editor.selection = new vscode.Selection(range.start, selectionEnd);
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
editor.setDecorations(shownLocationDecoration, [range]);
editor.setDecorations(shownLocationLineDecoration, [range]);
}
}
/**
* Resolves the specified CodeQL location to a URI into the source archive.
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
* @param databaseItem Database in which to resolve the file location.
*/
function resolveFivePartLocation(loc: FivePartLocation, databaseItem: DatabaseItem): Location {
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
// are one-based. Adjust accordingly.
const range = new Range(Math.max(0, loc.lineStart - 1),
Math.max(0, loc.colStart - 1),
Math.max(0, loc.lineEnd - 1),
Math.max(0, loc.colEnd));
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Resolves the specified CodeQL filesystem resource location to a URI into the source archive.
* @param loc CodeQL location to resolve, corresponding to an entire filesystem resource. Must have a non-empty value for `loc.file`.
* @param databaseItem Database in which to resolve the filesystem resource location.
*/
function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: DatabaseItem): Location {
// A location corresponding to the start of the file.
const range = new Range(0, 0, 0, 0);
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`.
* @param loc CodeQL location to resolve
* @param databaseItem Database in which to resolve the file location.
*/
function tryResolveLocation(loc: LocationValue | undefined,
databaseItem: DatabaseItem): Location | undefined {
const resolvableLoc = tryGetResolvableLocation(loc);
if (resolvableLoc === undefined) {
return undefined;
}
switch (resolvableLoc.t) {
case LocationStyle.FivePart:
return resolveFivePartLocation(resolvableLoc, databaseItem);
case LocationStyle.WholeFile:
return resolveWholeFileLocation(resolvableLoc, databaseItem);
default:
return undefined;
}
}

View File

@@ -0,0 +1,57 @@
import { languages, IndentAction, OnEnterRule } 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: OnEnterRule[] = [
{
// 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,42 +1,135 @@
import { window as Window, OutputChannel, Progress } from 'vscode';
import { DisposableObject } from 'semmle-vscode-utils';
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
import { DisposableObject } from '@github/codeql-vscode-utils';
import * as fs from 'fs-extra';
import * as path from 'path';
interface LogOptions {
/** If false, don't output a trailing newline for the log entry. Default true. */
trailingNewline?: boolean;
/** If specified, add this log entry to the log file at the specified location. */
additionalLogLocation?: string;
}
export interface Logger {
/** Writes the given log message, followed by a newline. */
log(message: string): void;
/** Writes the given log message, not followed by a newline. */
logWithoutTrailingNewline(message: string): void;
/** Writes the given log message, optionally followed by a newline. */
log(message: string, options?: LogOptions): Promise<void>;
/**
* Reveal this channel in the UI.
*
* @param preserveFocus When `true` the channel will not take focus.
*/
show(preserveFocus?: boolean): void;
/**
* Remove the log at the specified location
* @param location log to remove
*/
removeAdditionalLogLocation(location: string | undefined): void;
/**
* The base location location where all side log files are stored.
*/
getBaseLocation(): string | undefined;
}
export type ProgressReporter = Progress<{ message: string }>;
/** A logger that writes messages to an output channel in the Output tab. */
export class OutputChannelLogger extends DisposableObject implements Logger {
outputChannel: OutputChannel;
public readonly outputChannel: OutputChannel;
private readonly additionalLocations = new Map<string, AdditionalLogLocation>();
private additionalLogLocationPath: string | undefined;
constructor(title: string) {
constructor(private title: string) {
super();
this.outputChannel = Window.createOutputChannel(title);
this.push(this.outputChannel);
}
log(message: string) {
this.outputChannel.appendLine(message);
init(storagePath: string): void {
this.additionalLogLocationPath = path.join(storagePath, this.title);
// clear out any old state from previous runs
fs.remove(this.additionalLogLocationPath);
}
logWithoutTrailingNewline(message: string) {
this.outputChannel.append(message);
/**
* This function is asynchronous and will only resolve once the message is written
* to the side log (if required). It is not necessary to await the results of this
* function if you don't need to guarantee that the log writing is complete before
* continuing.
*/
async log(message: string, options = {} as LogOptions): Promise<void> {
if (options.trailingNewline === undefined) {
options.trailingNewline = true;
}
if (options.trailingNewline) {
this.outputChannel.appendLine(message);
} else {
this.outputChannel.append(message);
}
if (this.additionalLogLocationPath && options.additionalLogLocation) {
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
let additional = this.additionalLocations.get(logPath);
if (!additional) {
const msg = `| Log being saved to ${logPath} |`;
const separator = new Array(msg.length).fill('-').join('');
this.outputChannel.appendLine(separator);
this.outputChannel.appendLine(msg);
this.outputChannel.appendLine(separator);
additional = new AdditionalLogLocation(logPath);
this.additionalLocations.set(logPath, additional);
this.track(additional);
}
await additional.log(message, options);
}
}
show(preserveFocus?: boolean) {
show(preserveFocus?: boolean): void {
this.outputChannel.show(preserveFocus);
}
removeAdditionalLogLocation(location: string | undefined): void {
if (this.additionalLogLocationPath && location) {
const logPath = location.startsWith(this.additionalLogLocationPath)
? location
: path.join(this.additionalLogLocationPath, location);
const additional = this.additionalLocations.get(logPath);
if (additional) {
this.disposeAndStopTracking(additional);
this.additionalLocations.delete(logPath);
}
}
}
getBaseLocation() {
return this.additionalLogLocationPath;
}
}
class AdditionalLogLocation extends Disposable {
constructor(private location: string) {
super(() => { /**/ });
}
async log(message: string, options = {} as LogOptions): Promise<void> {
if (options.trailingNewline === undefined) {
options.trailingNewline = true;
}
await fs.ensureFile(this.location);
await fs.appendFile(this.location, message + (options.trailingNewline ? '\n' : ''), {
encoding: 'utf8'
});
}
async dispose(): Promise<void> {
await fs.remove(this.location);
}
}
/** The global logger for the extension. */
@@ -46,4 +139,9 @@ export const logger = new OutputChannelLogger('CodeQL Extension Log');
export const queryServerLogger = new OutputChannelLogger('CodeQL Query Server');
/** The logger for messages from the language server. */
export const ideServerLogger = new OutputChannelLogger('CodeQL Language Server');
export const ideServerLogger = new OutputChannelLogger(
'CodeQL Language Server'
);
/** The logger for messages from tests. */
export const testLogger = new OutputChannelLogger('CodeQL Tests');

View File

@@ -1,6 +1,17 @@
/**
* Types for messages exchanged during jsonrpc communication with the
* the CodeQL query server.
*
* This file exists in the queryserver and in the vscode extension, and
* should be kept in sync between them.
*
* A note about the namespaces below, which look like they are
* essentially enums, namely Severity, ResultColumnKind, and
* QueryResultType. By design, for the sake of extensibility, clients
* receiving messages of this protocol are supposed to accept any
* number for any of these types. We commit to the given meaning of
* the numbers listed in constants in the namespaces, and we commit to
* the fact that any unknown QueryResultType value counts as an error.
*/
import * as rpc from 'vscode-jsonrpc';
@@ -205,19 +216,19 @@ export interface QlFileSet {
/**
* The files imported by the given file
*/
imports: { [key: string]: string[]; };
imports: { [key: string]: string[] };
/**
* An id of each file
*/
nodeNumbering: { [key: string]: number; };
nodeNumbering: { [key: string]: number };
/**
* The code for each file
*/
qlCode: { [key: string]: string; };
qlCode: { [key: string]: string };
/**
* The resolution of an import in each directory.
*/
resolvedDirImports: { [key: string]: { [key: string]: string; }; };
resolvedDirImports: { [key: string]: { [key: string]: string } };
}
/**
@@ -294,11 +305,15 @@ export interface CompilationMessage {
/**
* The severity of the message
*/
severity: number;
severity: Severity;
}
export type Severity = number;
/**
* Severity of different messages
* Severity of different messages. This namespace is intentionally not
* an enum, see "for the sake of extensibility" comment above.
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Severity {
/**
* The message is a compilation error.
@@ -333,7 +348,7 @@ export interface ResultColumn {
* The kind of the column. See `ResultColumnKind`
* for the current possible meanings
*/
kind: number;
kind: ResultColumnKind;
/**
* The name of the column.
* This may be compiler generated for complex select expressions.
@@ -341,9 +356,12 @@ export interface ResultColumn {
name: string;
}
export type ResultColumnKind = number;
/**
* The kind of a result column.
* The kind of a result column. This namespace is intentionally not an enum, see "for the sake of
* extensibility" comment above.
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ResultColumnKind {
/**
* A column of type `float`
@@ -619,6 +637,8 @@ export interface EvaluateQueriesParams {
useSequenceHint: boolean;
}
export type TemplateDefinitions = { [key: string]: TemplateSource }
/**
* A single query that should be run
*/
@@ -642,7 +662,7 @@ export interface QueryToRun {
/**
* Values to set for each template
*/
templateValues?: { [key: string]: TemplateSource; };
templateValues?: TemplateDefinitions;
/**
* Whether templates without values in the templateValues
* map should be set to the empty set or give an error.
@@ -730,7 +750,7 @@ export interface ResultSet {
/**
* The type returned when the evaluation is complete
*/
export interface EvaluationComplete { }
export type EvaluationComplete = {};
/**
* The result of a single query
@@ -748,7 +768,7 @@ export interface EvaluationResult {
* The type of the result. See QueryResultType for
* possible meanings. Any other result should be interpreted as an error.
*/
resultType: number;
resultType: QueryResultType;
/**
* The wall clock time it took to evaluate the query.
* The time is from when we initially tried to evaluate the query
@@ -760,11 +780,19 @@ export interface EvaluationResult {
* An error message if an error happened
*/
message?: string;
/**
* Full path to file with all log messages emitted while this query was active, if one exists
*/
logFileLocation?: string;
}
export type QueryResultType = number;
/**
* The result of running a query,
* The result of running a query. This namespace is intentionally not
* an enum, see "for the sake of extensibility" comment above.
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace QueryResultType {
/**
* The query ran successfully
@@ -818,7 +846,7 @@ export interface RunUpgradeResult {
* The type of the result. See QueryResultType for
* possible meanings. Any other result should be interpreted as an error.
*/
resultType: number;
resultType: QueryResultType;
/**
* The error message if an error occurred
*/
@@ -837,11 +865,11 @@ export interface WithProgressId<T> {
/**
* The main body
*/
body: T,
body: T;
/**
* The id used to report progress updates
*/
progressId: number
progressId: number;
}
export interface ProgressMessage {
@@ -910,7 +938,7 @@ export const runUpgrade = new rpc.RequestType<WithProgressId<RunUpgradeParams>,
* Request returned to the client to notify completion of a query.
* The full runQueries job is completed when all queries are acknowledged.
*/
export const completeQuery = new rpc.RequestType<EvaluationResult, Object, void, void>('evaluation/queryCompleted');
export const completeQuery = new rpc.RequestType<EvaluationResult, Record<string, any>, void, void>('evaluation/queryCompleted');
/**
* A notification that the progress has been changed.

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ import { ExtensionContext, window as Window } from 'vscode';
import { CompletedQuery } from './query-results';
import { QueryHistoryConfig } from './config';
import { QueryWithResults } from './run-queries';
import * as helpers from './helpers';
import { logger } from './logging';
import { URLSearchParams } from 'url';
/**
* query-history.ts
@@ -15,28 +18,54 @@ import { QueryWithResults } from './run-queries';
*/
export type QueryHistoryItemOptions = {
label?: string, // user-settable label
queryText?: string, // stored query for quick query
}
label?: string; // user-settable label
queryText?: string; // text of the selected file
isQuickQuery?: boolean;
};
const SHOW_QUERY_TEXT_MSG = `\
////////////////////////////////////////////////////////////////////////////////////
// This is the text of the entire query file when it was executed for this query //
// run. The text or dependent libraries may have changed since then. //
// //
// This buffer is readonly. To re-execute this query, you must open the original //
// query file. //
////////////////////////////////////////////////////////////////////////////////////
`;
const SHOW_QUERY_TEXT_QUICK_EVAL_MSG = `\
////////////////////////////////////////////////////////////////////////////////////
// This is the Quick Eval selection of the query file when it was executed for //
// this query run. The text or dependent libraries may have changed since then. //
// //
// This buffer is readonly. To re-execute this query, you must open the original //
// query file. //
////////////////////////////////////////////////////////////////////////////////////
`;
/**
* Path to icon to display next to a failed query history item.
*/
const FAILED_QUERY_HISTORY_ITEM_ICON: string = 'media/red-x.svg';
const FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';
/**
* Tree data provider for the query history view.
*/
class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery> {
class HistoryTreeDataProvider
implements vscode.TreeDataProvider<CompletedQuery> {
/**
* XXX: This idiom for how to get a `.fire()`-able event emitter was
* cargo culted from another vscode extension. It seems rather
* involved and I hope there's something better that can be done
* instead.
*/
private _onDidChangeTreeData: vscode.EventEmitter<CompletedQuery | undefined> = new vscode.EventEmitter<CompletedQuery | undefined>();
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this._onDidChangeTreeData.event;
private _onDidChangeTreeData: vscode.EventEmitter<
CompletedQuery | undefined
> = new vscode.EventEmitter<CompletedQuery | undefined>();
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this
._onDidChangeTreeData.event;
private history: CompletedQuery[] = [];
@@ -45,10 +74,9 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
*/
private current: CompletedQuery | undefined;
constructor(private ctx: ExtensionContext) {
}
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 = {
@@ -57,18 +85,29 @@ 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);
it.iconPath = path.join(
this.ctx.extensionPath,
FAILED_QUERY_HISTORY_ITEM_ICON
);
}
return it;
}
getChildren(element?: CompletedQuery): vscode.ProviderResult<CompletedQuery[]> {
getChildren(
element?: CompletedQuery
): vscode.ProviderResult<CompletedQuery[]> {
if (element == undefined) {
return this.history;
}
else {
} else {
return [];
}
}
@@ -92,9 +131,8 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
}
remove(item: CompletedQuery) {
if (this.current === item)
this.current = undefined;
const index = this.history.findIndex(i => i === item);
if (this.current === item) this.current = undefined;
const index = this.history.findIndex((i) => i === item);
if (index >= 0) {
this.history.splice(index, 1);
if (this.current === undefined && this.history.length > 0) {
@@ -106,8 +144,16 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
}
}
get allHistory(): CompletedQuery[] {
return this.history;
}
refresh() {
this._onDidChangeTreeData.fire();
this._onDidChangeTreeData.fire(undefined);
}
find(queryId: number): CompletedQuery | undefined {
return this.allHistory.find((query) => query.query.queryID === queryId);
}
}
@@ -119,10 +165,109 @@ const DOUBLE_CLICK_TIME = 500;
export class QueryHistoryManager {
treeDataProvider: HistoryTreeDataProvider;
ctx: ExtensionContext;
treeView: vscode.TreeView<CompletedQuery>;
selectedCallback: ((item: CompletedQuery) => void) | undefined;
lastItemClick: { time: Date, item: CompletedQuery } | undefined;
lastItemClick: { time: Date; item: CompletedQuery } | undefined;
compareWithItem: CompletedQuery | undefined;
constructor(
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
private selectedCallback: (item: CompletedQuery) => Promise<void>,
private doCompareCallback: (
from: CompletedQuery,
to: CompletedQuery
) => Promise<void>
) {
const treeDataProvider = (this.treeDataProvider = new HistoryTreeDataProvider(
ctx
));
this.treeView = Window.createTreeView('codeQLQueryHistory', {
treeDataProvider,
canSelectMany: true,
});
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
this.treeView.onDidChangeVisibility(async (_ev) =>
this.updateTreeViewSelectionIfVisible()
);
// Don't allow the selection to become empty
this.treeView.onDidChangeSelection(async (ev) => {
if (ev.selection.length == 0) {
this.updateTreeViewSelectionIfVisible();
}
this.updateCompareWith(ev.selection);
});
logger.log('Registering query history panel commands.');
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.openQuery',
this.handleOpenQuery.bind(this)
)
);
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.compareWith',
this.handleCompareWith.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, [item]);
}
)
);
queryHistoryConfigListener.onDidChangeQueryHistoryConfiguration(() => {
this.treeDataProvider.refresh();
});
// 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);
return (
(JSON.parse(params.get('isQuickEval') || '')
? SHOW_QUERY_TEXT_QUICK_EVAL_MSG
: SHOW_QUERY_TEXT_MSG) + params.get('queryText')
);
},
});
}
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
if (this.selectedCallback !== undefined) {
@@ -131,19 +276,42 @@ export class QueryHistoryManager {
}
}
async handleOpenQuery(queryHistoryItem: CompletedQuery): Promise<void> {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.query.program.queryPath));
const editor = await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
const queryText = queryHistoryItem.options.queryText;
if (queryText !== undefined) {
await editor.edit(edit => edit.replace(textDocument.validateRange(
new vscode.Range(0, 0, textDocument.lineCount, 0)), queryText)
async handleOpenQuery(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
): Promise<void> {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
const textDocument = await vscode.workspace.openTextDocument(
vscode.Uri.file(singleItem.query.program.queryPath)
);
const editor = await vscode.window.showTextDocument(
textDocument,
vscode.ViewColumn.One
);
const queryText = singleItem.options.queryText;
if (queryText !== undefined && singleItem.options.isQuickQuery) {
await editor.edit((edit) =>
edit.replace(
textDocument.validateRange(
new vscode.Range(0, 0, textDocument.lineCount, 0)
),
queryText
)
);
}
}
async handleRemoveHistoryItem(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.remove(queryHistoryItem);
async handleRemoveHistoryItem(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
(multiSelect || [singleItem]).forEach((item) => {
this.treeDataProvider.remove(item);
item.dispose();
});
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
this.treeView.reveal(current);
@@ -151,69 +319,157 @@ export class QueryHistoryManager {
}
}
async handleSetLabel(queryHistoryItem: CompletedQuery) {
async handleSetLabel(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
): Promise<void> {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
const response = await vscode.window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
value: queryHistoryItem.getLabel(),
value: singleItem.getLabel(),
});
// undefined response means the user cancelled the dialog; don't change anything
if (response !== undefined) {
if (response === '')
// Interpret empty string response as "go back to using default"
queryHistoryItem.options.label = undefined;
else
queryHistoryItem.options.label = response;
// Interpret empty string response as 'go back to using default'
singleItem.options.label = undefined;
else singleItem.options.label = response;
this.treeDataProvider.refresh();
}
}
async handleItemClicked(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.setCurrentItem(queryHistoryItem);
async handleCompareWith(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
try {
if (!singleItem.didRunSuccessfully) {
throw new Error('Please select a successful query.');
}
const from = this.compareWithItem || singleItem;
const to = await this.findOtherQueryToCompare(from, multiSelect);
if (from && to) {
this.doCompareCallback(from, to);
}
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
async handleItemClicked(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
this.treeDataProvider.setCurrentItem(singleItem);
const now = new Date();
const prevItemClick = this.lastItemClick;
this.lastItemClick = { time: now, item: queryHistoryItem };
this.lastItemClick = { time: now, item: singleItem };
if (prevItemClick !== undefined
&& (now.valueOf() - prevItemClick.time.valueOf()) < DOUBLE_CLICK_TIME
&& queryHistoryItem == prevItemClick.item) {
if (
prevItemClick !== undefined &&
now.valueOf() - prevItemClick.time.valueOf() < DOUBLE_CLICK_TIME &&
singleItem == prevItemClick.item
) {
// show original query file on double click
await this.handleOpenQuery(queryHistoryItem);
}
else {
await this.handleOpenQuery(singleItem, [singleItem]);
} else {
// show results on single click
await this.invokeCallbackOn(queryHistoryItem);
await this.invokeCallbackOn(singleItem);
}
}
constructor(
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
selectedCallback?: (item: CompletedQuery) => Promise<void>
async handleShowQueryLog(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
this.ctx = ctx;
this.selectedCallback = selectedCallback;
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider(ctx);
this.treeView = Window.createTreeView('codeQLQueryHistory', { treeDataProvider });
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
this.treeView.onDidChangeVisibility(async _ev => this.updateTreeViewSelectionIfVisible());
// Don't allow the selection to become empty
this.treeView.onDidChangeSelection(async ev => {
if (ev.selection.length == 0) {
this.updateTreeViewSelectionIfVisible();
if (!this.assertSingleQuery(multiSelect)) {
return;
}
if (singleItem.logFileLocation) {
await this.tryOpenExternalFile(singleItem.logFileLocation);
} else {
helpers.showAndLogWarningMessage('No log file available');
}
}
async handleShowQueryText(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
try {
const queryName = singleItem.queryName.endsWith('.ql')
? singleItem.queryName
: singleItem.queryName + '.ql';
const params = new URLSearchParams({
isQuickEval: String(!!singleItem.query.quickEvalPosition),
queryText: encodeURIComponent(await this.getQueryText(singleItem)),
});
const uri = vscode.Uri.parse(
`codeql:${singleItem.query.queryID}-${queryName}?${params.toString()}`
);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
async handleViewSarif(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
try {
const hasInterpretedResults = await singleItem.query.canHaveInterpretedResults();
if (hasInterpretedResults) {
await this.tryOpenExternalFile(
singleItem.query.resultsPaths.interpretedResultsPath
);
} else {
const label = singleItem.getLabel();
helpers.showAndLogInformationMessage(
`Query ${label} has no interpreted results.`
);
}
});
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.itemClicked', async (item) => {
return this.handleItemClicked(item);
}));
queryHistoryConfigListener.onDidChangeQueryHistoryConfiguration(() => {
this.treeDataProvider.refresh();
});
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
async getQueryText(queryHistoryItem: CompletedQuery): Promise<string> {
if (queryHistoryItem.options.queryText) {
return queryHistoryItem.options.queryText;
} else if (queryHistoryItem.query.quickEvalPosition) {
// capture all selected lines
const startLine = queryHistoryItem.query.quickEvalPosition.line;
const endLine = queryHistoryItem.query.quickEvalPosition.endLine;
const textDocument = await vscode.workspace.openTextDocument(
queryHistoryItem.query.quickEvalPosition.fileName
);
return textDocument.getText(
new vscode.Range(startLine - 1, 0, endLine, 0)
);
} else {
return '';
}
}
addQuery(info: QueryWithResults): CompletedQuery {
@@ -223,6 +479,10 @@ export class QueryHistoryManager {
return item;
}
find(queryId: number): CompletedQuery | undefined {
return this.treeDataProvider.find(queryId);
}
/**
* Update the tree view selection if the tree view is visible.
*
@@ -242,4 +502,121 @@ export class QueryHistoryManager {
}
}
}
private async tryOpenExternalFile(fileLocation: string) {
const uri = vscode.Uri.file(fileLocation);
try {
await vscode.window.showTextDocument(uri);
} catch (e) {
if (
e.message.includes(
'Files above 50MB cannot be synchronized with extensions'
) ||
e.message.includes('too large to open')
) {
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);
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
} else {
helpers.showAndLogErrorMessage(`Could not open file ${fileLocation}`);
logger.log(e.message);
logger.log(e.stack);
}
}
}
private async findOtherQueryToCompare(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
): Promise<CompletedQuery | undefined> {
const dbName = singleItem.database.name;
// if exactly 2 queries are selected, use those
if (multiSelect?.length === 2) {
// return the query that is not the first selected one
const otherQuery =
singleItem === multiSelect[0] ? multiSelect[1] : multiSelect[0];
if (!otherQuery.didRunSuccessfully) {
throw new Error('Please select a successful query.');
}
if (otherQuery.database.name !== dbName) {
throw new Error('Query databases must be the same.');
}
return otherQuery;
}
if (multiSelect?.length > 1) {
throw new Error('Please select no more than 2 queries.');
}
// otherwise, let the user choose
const comparableQueryLabels = this.treeDataProvider.allHistory
.filter(
(otherQuery) =>
otherQuery !== singleItem &&
otherQuery.didRunSuccessfully &&
otherQuery.database.name === dbName
)
.map((otherQuery) => ({
label: otherQuery.toString(),
description: otherQuery.databaseName,
detail: otherQuery.statusString,
query: otherQuery,
}));
if (comparableQueryLabels.length < 1) {
throw new Error('No other queries available to compare with.');
}
const choice = await vscode.window.showQuickPick(comparableQueryLabels);
return choice?.query;
}
private assertSingleQuery(multiSelect: CompletedQuery[] = [], message = 'Please select a single query.') {
if (multiSelect.length > 1) {
helpers.showAndLogErrorMessage(
message
);
return false;
}
return true;
}
/**
* Updates the compare with source query. This ensures that all compare command invocations
* when exactly 2 queries are selected always have the proper _from_ query. Always use
* compareWithItem as the _from_ query.
*
* The heuristic is this:
*
* 1. If selection is empty or has length > 2 delete compareWithItem.
* 2. If selection is length 1, then set that item to compareWithItem.
* 3. If selection is length 2, then make sure compareWithItem is one of the selected items
* if not, then delete compareWithItem. If it is then, do nothing.
*
* This ensures that compareWithItem is always the first item selected if there are only
* two selected items.
*
* @param newSelection the new selection after the most recent selection change
*/
private updateCompareWith(newSelection: CompletedQuery[]) {
if (newSelection.length === 1) {
this.compareWithItem = newSelection[0];
} else if (
newSelection.length !== 2 ||
!this.compareWithItem ||
!newSelection.includes(this.compareWithItem)
) {
this.compareWithItem = undefined;
}
}
}

View File

@@ -1,20 +1,24 @@
import { QueryWithResults, tmpDir, QueryInfo } from "./run-queries";
import { env } from 'vscode';
import { QueryWithResults, tmpDir, QueryInfo } from './run-queries';
import * as messages from './messages';
import * as helpers from './helpers';
import * as cli from './cli';
import * as sarif from 'sarif';
import * as fs from 'fs-extra';
import * as path from 'path';
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState } from "./interface-types";
import { QueryHistoryConfig } from "./config";
import { QueryHistoryItemOptions } from "./query-history";
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState, ResultsPaths } from './interface-types';
import { QueryHistoryConfig } from './config';
import { QueryHistoryItemOptions } from './query-history';
export class CompletedQuery implements QueryWithResults {
readonly time: string;
readonly query: QueryInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
readonly logFileLocation?: string;
options: QueryHistoryItemOptions;
dispose: () => void;
/**
* Map from result set name to SortedResultSetInfo.
@@ -31,15 +35,18 @@ export class CompletedQuery implements QueryWithResults {
interpretedResultsSortState: InterpretedResultsSortState | undefined;
constructor(
evalaution: QueryWithResults,
evaluation: QueryWithResults,
public config: QueryHistoryConfig,
) {
this.query = evalaution.query;
this.result = evalaution.result;
this.database = evalaution.database;
this.time = new Date().toLocaleString();
this.query = evaluation.query;
this.result = evaluation.result;
this.database = evaluation.database;
this.logFileLocation = evaluation.logFileLocation;
this.options = evaluation.options;
this.dispose = evaluation.dispose;
this.time = new Date().toLocaleString(env.language);
this.sortedResultsInfo = new Map();
this.options = evalaution.options;
}
get databaseName(): string {
@@ -49,26 +56,19 @@ 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:
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OOM:
return `out of memory`;
return 'out of memory';
case messages.QueryResultType.SUCCESS:
return `finished in ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return `failed`;
return this.result.message ? `failed: ${this.result.message}` : 'failed';
}
}
@@ -125,9 +125,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'));
}
@@ -141,7 +140,7 @@ export async function interpretResults(server: cli.CodeQLCliServer, metadata: Qu
if (id === undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we use a dummy id.
id = "dummy-id";
id = 'dummy-id';
}
return await server.interpretBqrs({ kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
}

View File

@@ -1,14 +1,18 @@
import * as cp from 'child_process';
import { DisposableObject } from 'semmle-vscode-utils';
import * as path from 'path';
// Import from the specific module within `semmle-vscode-utils`, rather than via `index.ts`, because
// we avoid taking an accidental runtime dependency on `vscode` this way.
import { DisposableObject } from '@github/codeql-vscode-utils/out/disposable-object';
import { Disposable } from 'vscode';
import { CancellationToken, createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
import * as cli from './cli';
import { QueryServerConfig } from './config';
import { Logger, ProgressReporter } from './logging';
import { completeQuery, EvaluationResult, progress, ProgressMessage, WithProgressId } from './messages';
import * as messages from './messages';
type ServerOpts = {
logger: Logger
logger: Logger;
}
/** A running query server process and its associated message connection. */
@@ -23,7 +27,7 @@ class ServerProcess implements Disposable {
this.logger = logger;
}
dispose() {
dispose(): void {
this.logger.log('Stopping query server...');
this.connection.dispose();
this.child.stdin!.end();
@@ -51,6 +55,7 @@ export class QueryServerClient extends DisposableObject {
nextCallback: number;
nextProgress: number;
withProgressReporting: WithProgressReporting;
public activeQueryName: string | undefined;
constructor(readonly config: QueryServerConfig, readonly cliServer: cli.CodeQLCliServer, readonly opts: ServerOpts, withProgressReporting: WithProgressReporting) {
super();
@@ -68,35 +73,37 @@ export class QueryServerClient extends DisposableObject {
this.evaluationResultCallbacks = {};
}
get logger() { return this.opts.logger; }
get logger(): Logger {
return this.opts.logger;
}
/** Stops the query server by disposing of the current server process. */
private stopQueryServer() {
private stopQueryServer(): void {
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.');
}
}
/** Restarts the query server by disposing of the current server process and then starting a new one. */
async restartQueryServer() {
async restartQueryServer(): Promise<void> {
this.stopQueryServer();
await this.startQueryServer();
}
async showLog() {
showLog(): void {
this.logger.show();
}
/** Starts a new query server process, sending progress messages to the status bar. */
async startQueryServer() {
async startQueryServer(): Promise<void> {
// Use an arrow function to preserve the value of `this`.
return this.withProgressReporting((progress, _) => this.startQueryServerImpl(progress));
}
/** Starts a new query server process, sending progress messages to the given reporter. */
private async startQueryServerImpl(progressReporter: ProgressReporter) {
private async startQueryServerImpl(progressReporter: ProgressReporter): Promise<void> {
const ramArgs = await this.cliServer.resolveRam(this.config.queryMemoryMb, progressReporter);
const args = ['--threads', this.config.numThreads.toString()].concat(ramArgs);
if (this.config.debug) {
@@ -108,7 +115,10 @@ export class QueryServerClient extends DisposableObject {
['execute', 'query-server'],
args,
this.logger,
data => this.logger.logWithoutTrailingNewline(data.toString()),
data => this.logger.log(data.toString(), {
trailingNewline: false,
additionalLogLocation: this.activeQueryName
}),
undefined, // no listener for stdout
progressReporter
);
@@ -119,16 +129,20 @@ export class QueryServerClient extends DisposableObject {
this.logger.log(`No callback associated with run id ${res.runId}, continuing without executing any callback`);
}
else {
const baseLocation = this.logger.getBaseLocation();
if (baseLocation && this.activeQueryName) {
res.logFileLocation = path.join(baseLocation, this.activeQueryName);
}
this.evaluationResultCallbacks[res.runId](res);
}
return {};
})
});
connection.onNotification(progress, res => {
let callback = this.progressCallbacks[res.id];
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);
@@ -146,7 +160,7 @@ export class QueryServerClient extends DisposableObject {
return id;
}
unRegisterCallback(id: number) {
unRegisterCallback(id: number): void {
delete this.evaluationResultCallbacks[id];
}
@@ -155,8 +169,10 @@ export class QueryServerClient extends DisposableObject {
}
async sendRequest<P, R, E, RO>(type: RequestType<WithProgressId<P>, R, E, RO>, parameter: P, token?: CancellationToken, progress?: (res: ProgressMessage) => void): Promise<R> {
let id = this.nextProgress++;
const id = this.nextProgress++;
this.progressCallbacks[id] = progress;
this.updateActiveQuery(type.method, parameter);
try {
if (this.serverProcess === undefined) {
throw new Error('No query server process found.');
@@ -166,4 +182,19 @@ export class QueryServerClient extends DisposableObject {
delete this.progressCallbacks[id];
}
}
/**
* Updates the active query every time there is a new request to compile.
* The active query is used to specify the side log.
*
* This isn't ideal because in situations where there are queries running
* in parallel, each query's log messages are interleaved. Fixing this
* properly will require a change in the query server.
*/
private updateActiveQuery(method: string, parameter: any): void {
if (method === messages.compileQuery.method) {
const queryPath = parameter?.queryToCheck?.queryPath || 'unknown';
this.activeQueryName = `query-${path.basename(queryPath)}-${this.nextProgress}.log`;
}
}
}

View File

@@ -1,5 +1,4 @@
import * as fs from 'fs-extra';
import * as glob from 'glob-promise';
import * as yaml from 'js-yaml';
import * as path from 'path';
import { ExtensionContext, window as Window, workspace, Uri } from 'vscode';
@@ -12,38 +11,12 @@ import { UserCancellationException } from './run-queries';
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
const QUICK_QUERY_WORKSPACE_FOLDER_NAME = 'Quick Queries';
export function isQuickQueryPath(queryPath: string): boolean {
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
}
async function getQlPackFor(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
const qlpacks = await cliServer.resolveQlpacks(helpers.getOnDiskWorkspaceFolders());
const packs: { packDir: string | undefined, packName: string }[] =
Object.entries(qlpacks).map(([packName, dirs]) => {
if (dirs.length < 1) {
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
return { packName, packDir: undefined };
}
if (dirs.length > 1) {
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
}
return {
packName,
packDir: dirs[0]
}
});
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
return packName;
}
}
}
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
}
/**
* `getBaseText` heuristically returns an appropriate import statement
* prelude based on the filename of the dbscheme file given. TODO: add
@@ -59,7 +32,7 @@ function getBaseText(dbschemeBase: string) {
return 'select ""';
}
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
function getQuickQueriesDir(ctx: ExtensionContext): string {
const storagePath = ctx.storagePath;
if (storagePath === undefined) {
throw new Error('Workspace storage path is undefined');
@@ -69,11 +42,25 @@ async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
return queriesPath;
}
/**
* Show a buffer the user can enter a simple query into.
*/
export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQLCliServer, databaseUI: DatabaseUI) {
function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
workspace.updateWorkspaceFolders(
index,
len,
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
);
}
try {
const workspaceFolders = workspace.workspaceFolders || [];
const queriesDir = await getQuickQueriesDir(ctx);
// If there is already a quick query open, don't clobber it, just
// show it.
@@ -83,19 +70,29 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL
return;
}
const queriesDir = await getQuickQueriesDir(ctx);
// We need this folder in workspace folders so the language server
// knows how to find its qlpack.yml
if (workspace.workspaceFolders === undefined
|| !workspace.workspaceFolders.some(folder => folder.uri.fsPath === queriesDir)) {
workspace.updateWorkspaceFolders(
(workspace.workspaceFolders || []).length,
0,
{ uri: Uri.file(queriesDir), name: "Quick Queries" }
);
// We need to have a multi-root workspace to make quick query work
// at all. Changing the workspace from single-root to multi-root
// causes a restart of the whole extension host environment, so we
// basically can't do anything that survives that restart.
//
// So if we are currently in a single-root workspace (of which the
// only reliable signal appears to be `workspace.workspaceFile`
// being undefined) just let the user know that they're in for a
// restart.
if (workspace.workspaceFile === undefined) {
const makeMultiRoot = await helpers.showBinaryChoiceDialog('Quick query requires multiple folders in the workspace. Reload workspace as multi-folder workspace?');
if (makeMultiRoot) {
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
}
return;
}
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME);
if (index === -1)
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
else
updateQuickQueryDir(queriesDir, index, 1);
// We're going to infer which qlpack to use from the current database
const dbItem = await databaseUI.getDatabaseItem();
if (dbItem === undefined) {
@@ -103,22 +100,10 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL
}
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
if (dbschemes.length < 1) {
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
}
dbschemes.sort();
const dbscheme = dbschemes[0];
if (dbschemes.length > 1) {
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
}
const qlpack = await getQlPackFor(cliServer, dbscheme);
const { qlpack, dbscheme } = await helpers.resolveDatasetFolder(cliServer, datasetFolder);
const quickQueryQlpackYaml: any = {
name: "quick-query",
version: "1.0.0",
name: 'quick-query',
version: '1.0.0',
libraryPathDependencies: [qlpack]
};

View File

@@ -38,12 +38,12 @@ export function getResult(sarif: sarif.Log, key: Result): sarif.Result | undefin
* Looks up a specific path in a result set.
*/
export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefined {
let result = getResult(sarif, key);
const result = getResult(sarif, key);
if (result === undefined) return undefined;
let index = -1;
if (result.codeFlows === undefined) return undefined;
for (let codeFlows of result.codeFlows) {
for (let threadFlow of codeFlows.threadFlows) {
for (const codeFlows of result.codeFlows) {
for (const threadFlow of codeFlows.threadFlows) {
++index;
if (index == key.pathIndex)
return threadFlow;
@@ -56,7 +56,7 @@ export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefin
* Looks up a specific path node in a result set.
*/
export function getPathNode(sarif: sarif.Log, key: PathNode): sarif.Location | undefined {
let path = getPath(sarif, key);
const path = getPath(sarif, key);
if (path === undefined) return undefined;
return path.locations[key.pathNodeIndex];
}
@@ -85,7 +85,7 @@ export function equalsNotUndefined(key1: PathNode | undefined, key2: PathNode |
*/
export function getAllPaths(result: sarif.Result): sarif.ThreadFlow[] {
if (result.codeFlows === undefined) return [];
let paths = [];
const paths = [];
for (const codeFlow of result.codeFlows) {
for (const threadFlow of codeFlow.threadFlows) {
paths.push(threadFlow);

View File

@@ -2,9 +2,11 @@ import * as crypto from 'crypto';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as tmp from 'tmp';
import { promisify } from 'util';
import * as vscode from 'vscode';
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
import * as cli from './cli';
import * as config from './config';
import { DatabaseItem, getUpgradesDirectories } from './databases';
import * as helpers from './helpers';
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './interface-types';
@@ -54,6 +56,7 @@ export class QueryInfo {
public readonly queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
public readonly quickEvalPosition?: messages.Position,
public readonly metadata?: QueryMetadata,
public readonly templates?: messages.TemplateDefinitions,
) {
this.queryID = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
@@ -72,45 +75,55 @@ 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,
qlo: vscode.Uri.file(this.compiledQueryPath).toString(),
allowUnknownTemplates: true,
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",
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);
}
return result || { evaluationTime: 0, message: "No result from server", queryId: -1, runId: callbackId, resultType: messages.QueryResultType.OTHER_ERROR };
return result || {
evaluationTime: 0,
message: 'No result from server',
queryId: -1,
runId: callbackId,
resultType: messages.QueryResultType.OTHER_ERROR
};
}
async compile(
qs: qsClient.QueryServerClient,
): Promise<messages.CompilationMessage[]> {
let compiled: messages.CheckQueryResult;
let compiled: messages.CheckQueryResult | undefined;
try {
const target = this.quickEvalPosition ? {
quickEval: { quickEvalPos: this.quickEvalPosition }
} : { query: {} };
const params: messages.CompileQueryParams = {
compilationOptions: {
computeNoLocationUrls: true,
@@ -126,34 +139,39 @@ export class QueryInfo {
},
queryToCheck: this.program,
resultPath: this.compiledQueryPath,
target: !!this.quickEvalPosition ? { quickEval: { quickEvalPos: this.quickEvalPosition } } : { query: {} }
target,
};
compiled = await helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling Query",
title: 'Compiling Query',
cancellable: true,
}, (progress, token) => {
return qs.sendRequest(messages.compileQuery, params, token, progress);
});
} finally {
qs.logger.log(" - - - COMPILATION DONE - - - ");
qs.logger.log(' - - - COMPILATION DONE - - - ');
}
return (compiled.messages || []).filter(msg => msg.severity == 0);
return (compiled?.messages || []).filter(msg => msg.severity === messages.Severity.ERROR);
}
/**
* 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.");
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 {
@@ -161,10 +179,13 @@ export interface QueryWithResults {
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
readonly options: QueryHistoryItemOptions;
readonly logFileLocation?: string;
readonly dispose: () => void;
}
export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbItem: DatabaseItem):
Promise<messages.ClearCacheResult> {
export async function clearCacheInDatabase(
qs: qsClient.QueryServerClient, dbItem: DatabaseItem
): Promise<messages.ClearCacheResult> {
if (dbItem.contents === undefined) {
throw new Error('Can\'t clear the cache in an invalid database.');
}
@@ -181,7 +202,7 @@ export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbIte
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Clearing Cache",
title: 'Clearing Cache',
cancellable: false,
}, (progress, token) =>
qs.sendRequest(messages.clearCache, params, token, progress)
@@ -194,7 +215,7 @@ export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbIte
*
*/
async function convertToQlPath(filePath: string): Promise<string> {
if (process.platform === "win32") {
if (process.platform === 'win32') {
if (path.parse(filePath).root === filePath) {
// Java assumes uppercase drive letters are canonical.
@@ -202,14 +223,17 @@ async function convertToQlPath(filePath: string): Promise<string> {
} else {
const dir = await convertToQlPath(path.dirname(filePath));
const fileName = path.basename(filePath);
const fileNames = await promisify<string, string[]>(fs.readdir)(dir);
const fileNames = await fs.readdir(dir);
for (const name of fileNames) {
// Leave the locale argument empty so that the default OS locale is used.
// We do this because this operation works on filesystem entities, which
// use the os locale, regardless of the locale of the running VS Code instance.
if (fileName.localeCompare(name, undefined, { sensitivity: 'accent' }) === 0) {
return path.join(dir, name);
}
}
}
throw new Error("Can't convert path to form suitable for QL:" + filePath);
throw new Error('Can\'t convert path to form suitable for QL:' + filePath);
} else {
return filePath;
}
@@ -224,8 +248,10 @@ async function getSelectedPosition(editor: vscode.TextEditor): Promise<messages.
// Convert from 0-based to 1-based line and column numbers.
return {
fileName: await convertToQlPath(editor.document.fileName),
line: pos.line + 1, column: pos.character + 1,
endLine: posEnd.line + 1, endColumn: posEnd.character + 1
line: pos.line + 1,
column: pos.character + 1,
endLine: posEnd.line + 1,
endColumn: posEnd.character + 1
};
}
@@ -244,9 +270,9 @@ async function checkDbschemeCompatibility(
if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) {
const { scripts, finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
async function hash(filename: string): Promise<string> {
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:
@@ -279,19 +305,39 @@ async function checkDbschemeCompatibility(
}
}
/** Prompts the user to save `document` if it has unsaved changes. */
async function promptUserToSaveChanges(document: vscode.TextDocument) {
/**
* Prompts the user to save `document` if it has unsaved changes.
* Returns true if we should save changes.
*/
async function promptUserToSaveChanges(document: vscode.TextDocument): Promise<boolean> {
if (document.isDirty) {
// TODO: add 'always save' button which records preference in configuration
if (await helpers.showBinaryChoiceDialog('Query file has unsaved changes. Save now?')) {
await document.save();
if (config.AUTOSAVE_SETTING.getValue()) {
return true;
}
else {
const yesItem = { title: 'Yes', isCloseAffordance: false };
const alwaysItem = { title: 'Always Save', isCloseAffordance: false };
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);
if (chosenItem === alwaysItem) {
await config.AUTOSAVE_SETTING.updateValue(true, vscode.ConfigurationTarget.Workspace);
return true;
}
if (chosenItem === yesItem) {
return true;
}
}
}
return false;
}
type SelectedQuery = {
queryPath: string,
quickEvalPosition?: messages.Position
queryPath: string;
quickEvalPosition?: messages.Position;
quickEvalText?: string;
};
/**
@@ -304,7 +350,7 @@ type SelectedQuery = {
* @param selectedResourceUri The selected resource when the command was run.
* @param quickEval Whether the command being run is `Quick Evaluation`.
*/
async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefined, quickEval: boolean): Promise<SelectedQuery> {
export async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefined, quickEval: boolean): Promise<SelectedQuery> {
const editor = vscode.window.activeTextEditor;
// Choose which QL file to use.
@@ -325,16 +371,30 @@ async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefine
if (queryUri.scheme !== 'file') {
throw new Error('Can only run queries that are on disk.');
}
const queryPath = queryUri.fsPath;
const queryPath = queryUri.fsPath || '';
if (quickEval) {
if (!(queryPath.endsWith('.ql') || queryPath.endsWith('.qll'))) {
throw new Error('The selected resource is not a CodeQL file; It should have the extension ".ql" or ".qll".');
}
}
else {
if (!(queryPath.endsWith('.ql'))) {
throw new Error('The selected resource is not a CodeQL query file; It should have the extension ".ql".');
}
}
// Whether we chose the file from the active editor or from a context menu,
// if the same file is open with unsaved changes in the active editor,
// then prompt the user to save it first.
if (editor !== undefined && editor.document.uri.fsPath === queryPath) {
await promptUserToSaveChanges(editor.document);
if (await promptUserToSaveChanges(editor.document)) {
editor.document.save();
}
}
let quickEvalPosition: messages.Position | undefined = undefined;
let quickEvalText: string | undefined = undefined;
if (quickEval) {
if (editor == undefined) {
throw new Error('Can\'t run quick evaluation without an active editor.');
@@ -345,9 +405,10 @@ async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefine
throw new Error('The selected resource for quick evaluation should match the active editor.');
}
quickEvalPosition = await getSelectedPosition(editor);
quickEvalText = editor.document.getText(editor.selection);
}
return { queryPath, quickEvalPosition };
return { queryPath, quickEvalPosition, quickEvalText };
}
export async function compileAndRunQueryAgainstDatabase(
@@ -355,19 +416,21 @@ export async function compileAndRunQueryAgainstDatabase(
qs: qsClient.QueryServerClient,
db: DatabaseItem,
quickEval: boolean,
selectedQueryUri: vscode.Uri | undefined
selectedQueryUri: vscode.Uri | undefined,
templates?: messages.TemplateDefinitions,
): Promise<QueryWithResults> {
if (!db.contents || !db.contents.dbSchemeUri) {
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
}
// Determine which query to run, based on the selection and the active editor.
const { queryPath, quickEvalPosition } = await determineSelectedQuery(selectedQueryUri, quickEval);
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval);
// If this is quick query, store the query text
const historyItemOptions: QueryHistoryItemOptions = {};
if (isQuickQueryPath(queryPath)) {
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
if (quickEval) {
historyItemOptions.queryText = quickEvalText;
} else {
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
}
@@ -408,13 +471,27 @@ export async function compileAndRunQueryAgainstDatabase(
logger.log(`Couldn't resolve metadata for ${qlProgram.queryPath}: ${e}`);
}
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata);
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
await checkDbschemeCompatibility(cliServer, qs, query);
const errors = await query.compile(qs);
let errors;
try {
errors = await query.compile(qs);
} catch (e) {
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
} else {
throw e;
}
}
if (errors.length == 0) {
const result = await query.run(qs);
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const message = result.message || 'Failed to run query';
logger.log(message);
helpers.showAndLogErrorMessage(message);
}
return {
query,
result,
@@ -422,7 +499,11 @@ export async function compileAndRunQueryAgainstDatabase(
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
options: historyItemOptions
options: historyItemOptions,
logFileLocation: result.logFileLocation,
dispose: () => {
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
}
};
} else {
// Error dialogs are limited in size and scrollability,
@@ -431,37 +512,49 @@ export async function compileAndRunQueryAgainstDatabase(
// However we don't show quick eval errors there so we need to display them anyway.
qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
let formattedMessages: string[] = [];
const formattedMessages: string[] = [];
for (const error of errors) {
const message = error.message || "[no error message available]";
const message = error.message || '[no error message available]';
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
formattedMessages.push(formatted);
qs.logger.log(formatted);
}
if (quickEval && formattedMessages.length <= 3) {
helpers.showAndLogErrorMessage("Quick evaluation compilation failed: \n" + formattedMessages.join("\n"));
helpers.showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n'));
} else {
helpers.showAndLogErrorMessage((quickEval ? "Quick evaluation" : "Query") +
" compilation failed. Please make sure there are no errors in the query, the database is up to date," +
" and the query and database use the same target language. For more details on the error, go to View > Output," +
" and choose CodeQL Query Server from the dropdown.");
helpers.showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') +
' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
' and choose CodeQL Query Server from the dropdown.');
}
return {
query,
result: {
evaluationTime: 0,
resultType: messages.QueryResultType.OTHER_ERROR,
queryId: -1,
runId: -1,
message: "Query had compilation errors"
},
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
options: historyItemOptions,
};
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
}
}
function createSyntheticResult(
query: QueryInfo,
db: DatabaseItem,
historyItemOptions: QueryHistoryItemOptions,
message: string,
resultType: number
): QueryWithResults {
return {
query,
result: {
evaluationTime: 0,
resultType: resultType,
queryId: -1,
runId: -1,
message
},
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
options: historyItemOptions,
dispose: () => { /**/ },
};
}

View File

@@ -1,10 +1,10 @@
import * as Sarif from "sarif"
import * as path from "path"
import { LocationStyle, ResolvableLocationValue } from "semmle-bqrs";
import * as Sarif from 'sarif';
import * as path from 'path';
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
export interface SarifLink {
dest: number
text: string
dest: number;
text: string;
}
@@ -15,7 +15,7 @@ type ParsedSarifLocation =
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
& { userVisibleFile: string }
| { t: 'NoLocation', hint: string };
| { t: 'NoLocation'; hint: string };
export type SarifMessageComponent = string | SarifLink
@@ -23,11 +23,11 @@ export type SarifMessageComponent = string | SarifLink
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
export function unescapeSarifText(message: string): string {
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
return message.replace(/\\\[/g, '[').replace(/\\\]/g, ']').replace(/\\\\/, '\\');
}
export function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
let results: SarifMessageComponent[] = [];
const results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
@@ -38,8 +38,8 @@ export function parseSarifPlainTextMessage(message: string): SarifMessageCompone
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
const linkText = result.groups!['linkText'];
const linkTarget = +result.groups!['linkTarget'];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}

View File

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

View File

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

View File

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

View File

@@ -19,10 +19,11 @@ const MAX_UPGRADE_MESSAGE_LINES = 10;
* Reports errors to both the user and the console.
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
*/
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.UpgradeParams | undefined> {
async function checkAndConfirmDatabaseUpgrade(
qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]
): Promise<messages.UpgradeParams | undefined> {
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
helpers.showAndLogErrorMessage('Database is invalid, and cannot be upgraded.');
return;
}
const params: messages.UpgradeParams = {
@@ -79,17 +80,17 @@ async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
let dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
const noItem = { title: 'No', isCloseAffordance: true };
const dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
let messageLines = descriptionMessage.split('\n');
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
messageLines = messageLines.slice(0, MAX_UPGRADE_MESSAGE_LINES);
messageLines.push(`The list of upgrades was truncated, click "No, Show Changes" to see the full list.`);
messageLines.push('The list of upgrades was truncated, click "No, Show Changes" to see the full list.');
dialogOptions.push(showLogItem);
}
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join("\n")}`;
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join('\n')}`;
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
if (chosenItem === showLogItem) {
@@ -110,8 +111,9 @@ async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db
* First performs a dry-run and prompts the user to confirm the upgrade.
* Reports errors during compilation and evaluation of upgrades to the user.
*/
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.RunUpgradeResult | undefined> {
export async function upgradeDatabase(
qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]
): Promise<messages.RunUpgradeResult | undefined> {
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
if (upgradeParams === undefined) {
@@ -127,7 +129,7 @@ export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: Databa
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
qs.logger.log('Done compiling database upgrade.');
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
@@ -146,35 +148,38 @@ export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: Databa
return;
}
finally {
qs.logger.log('Done running database upgrade.')
qs.logger.log('Done running database upgrade.');
}
}
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CheckUpgradeResult> {
async function checkDatabaseUpgrade(
qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams
): Promise<messages.CheckUpgradeResult> {
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Checking for database upgrades",
title: 'Checking for database upgrades',
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
}
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CompileUpgradeResult> {
async function compileDatabaseUpgrade(
qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams
): Promise<messages.CompileUpgradeResult> {
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling database upgrades",
title: 'Compiling database upgrades',
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
}
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
Promise<messages.RunUpgradeResult> {
async function runDatabaseUpgrade(
qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades
): Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
@@ -192,7 +197,7 @@ async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseIt
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running database upgrades",
title: 'Running database upgrades',
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
}

View File

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

View File

@@ -0,0 +1,86 @@
import * as React from 'react';
import { vscode } from './vscode-api';
import { RawResultsSortState, SortDirection } from '../interface-types';
import { nextSortDirection } from './result-table-utils';
import { ColumnSchema } from 'semmle-bqrs';
interface Props {
readonly columns: readonly ColumnSchema[];
readonly schemaName: string;
readonly sortState?: RawResultsSortState;
readonly preventSort?: boolean;
}
function toggleSortStateForColumn(
index: number,
schemaName: string,
sortState: RawResultsSortState | undefined,
preventSort: boolean
): void {
if (preventSort) {
return;
}
const prevDirection =
sortState && sortState.columnIndex === index
? sortState.sortDirection
: undefined;
const nextDirection = nextSortDirection(prevDirection);
const nextSortState =
nextDirection === undefined
? undefined
: {
columnIndex: index,
sortDirection: nextDirection,
};
vscode.postMessage({
t: 'changeSort',
resultSetName: schemaName,
sortState: nextSortState,
});
}
export default function RawTableHeader(props: Props) {
return (
<thead>
<tr>
{[
(
<th key={-1}>
<b>#</b>
</th>
),
...props.columns.map((col, index) => {
const displayName = col.name || `[${index}]`;
const sortDirection =
props.sortState && index === props.sortState.columnIndex
? props.sortState.sortDirection
: undefined;
return (
<th
className={
'sort-' +
(sortDirection !== undefined
? SortDirection[sortDirection]
: 'none')
}
key={index}
onClick={() =>
toggleSortStateForColumn(
index,
props.schemaName,
props.sortState,
!!props.preventSort
)
}
>
<b>{displayName}</b>
</th>
);
}),
]}
</tr>
</thead>
);
}

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
import { ResultRow } from '../adapt';
import { zebraStripe } from './result-table-utils';
import RawTableValue from './RawTableValue';
interface Props {
rowIndex: number;
row: ResultRow;
databaseUri: string;
className?: string;
}
export default function RawTableRow(props: Props) {
return (
<tr key={props.rowIndex} {...zebraStripe(props.rowIndex, props.className || '')}>
<td key={-1}>{props.rowIndex + 1}</td>
{props.row.map((value, columnIndex) => (
<td key={columnIndex}>
<RawTableValue
value={value}
databaseUri={props.databaseUri}
/>
</td>
))}
</tr>
);
}

View File

@@ -0,0 +1,22 @@
import * as React from 'react';
import { ResultValue } from '../adapt';
import { renderLocation } from './result-table-utils';
interface Props {
value: ResultValue;
databaseUri: string;
}
export default function RawTableValue(props: Props): JSX.Element {
const v = props.value;
if (typeof v === 'string') {
return <span>{v}</span>;
}
else if ('uri' in v) {
return <a href={v.uri}>{v.uri}</a>;
}
else {
return renderLocation(v.location, v.label, props.databaseUri);
}
}

View File

@@ -5,9 +5,11 @@ 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 } from './results';
import { PathTableResultSet } from '../interface-types';
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
import { vscode } from './vscode-api';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState {
@@ -69,6 +71,14 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
});
}
renderNoResults(): JSX.Element {
if (this.props.nonemptyRawResults) {
return <span>No Alerts. See <a href='#' onClick={this.props.showRawResults}>raw results</a>.</span>;
} else {
return <span>No Alerts</span>;
}
}
render(): JSX.Element {
const { databaseUri, resultSet } = this.props;
@@ -84,7 +94,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
function renderRelatedLocations(msg: string, relatedLocations: Sarif.Location[]): JSX.Element[] {
const relatedLocationsById: { [k: string]: Sarif.Location } = {};
for (let loc of relatedLocations) {
for (const loc of relatedLocations) {
relatedLocationsById[loc.id!] = loc;
}
@@ -94,7 +104,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
for (const part of parts) {
if (typeof part === "string") {
if (typeof part === 'string') {
result.push(<span>{part} </span>);
} else {
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
@@ -116,7 +126,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 {
@@ -140,7 +150,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
let shortLocation, longLocation: string;
switch (parsedLoc.t) {
case 'NoLocation':
return renderNonLocation("[no location]", parsedLoc.hint);
return renderNonLocation('[no location]', parsedLoc.hint);
case LocationStyle.WholeFile:
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
longLocation = `${parsedLoc.userVisibleFile}`;
@@ -156,18 +166,19 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
return (e) => this.toggle(e, indices);
};
const noResults = <span>No Results</span>; // TODO: Maybe make this look nicer
if (resultSet.sarif.runs.length === 0 ||
resultSet.sarif.runs[0].results === undefined ||
resultSet.sarif.runs[0].results.length === 0) {
return this.renderNoResults();
}
let expansionIndex = 0;
if (resultSet.sarif.runs.length === 0) return noResults;
if (resultSet.sarif.runs[0].results === undefined) return noResults;
resultSet.sarif.runs[0].results.forEach((result, resultIndex) => {
const text = result.message.text || '[no text]';
const msg: JSX.Element[] =
result.relatedLocations === undefined ?
[<span>{text}</span>] :
[<span key="0">{text}</span>] :
renderRelatedLocations(text, result.relatedLocations);
const currentResultExpanded = this.state.expanded[expansionIndex];
@@ -178,11 +189,9 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
if (result.codeFlows === undefined) {
rows.push(
<tr {...zebraStripe(resultIndex)}>
<tr key={resultIndex} {...zebraStripe(resultIndex)}>
<td className="vscode-codeql__icon-cell">{octicons.info}</td>
<td colSpan={3}>
{msg}
</td>
<td colSpan={3}>{msg}</td>
{locationCells}
</tr>
);
@@ -239,7 +248,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
const additionalMsg = step.location !== undefined ?
renderSarifLocation(step.location, pathNodeKey) :
'';
let isSelected = Keys.equalsNotUndefined(this.state.selectedPathNode, pathNodeKey);
const isSelected = Keys.equalsNotUndefined(this.state.selectedPathNode, pathNodeKey);
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
const zebraIndex = resultIndex + stepIndex;
rows.push(
@@ -271,23 +280,23 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
private handleNavigationEvent(event: NavigationEvent) {
this.setState(prevState => {
let { selectedPathNode } = prevState;
const { selectedPathNode } = prevState;
if (selectedPathNode === undefined) return prevState;
let path = Keys.getPath(this.props.resultSet.sarif, selectedPathNode);
const path = Keys.getPath(this.props.resultSet.sarif, selectedPathNode);
if (path === undefined) return prevState;
let nextIndex = selectedPathNode.pathNodeIndex + event.direction;
const nextIndex = selectedPathNode.pathNodeIndex + event.direction;
if (nextIndex < 0 || nextIndex >= path.locations.length) return prevState;
let sarifLoc = path.locations[nextIndex].location;
const sarifLoc = path.locations[nextIndex].location;
if (sarifLoc === undefined) return prevState;
let loc = parseSarifLocation(sarifLoc, this.props.resultSet.sourceLocationPrefix);
const loc = parseSarifLocation(sarifLoc, this.props.resultSet.sourceLocationPrefix);
if (loc.t === 'NoLocation') return prevState;
jumpToLocation(loc, this.props.databaseUri);
let newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
const newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
return { ...prevState, selectedPathNode: newSelection };
});
}

View File

@@ -4,22 +4,22 @@ export type EventHandler<T> = (event: T) => void;
* A set of listeners for events of type `T`.
*/
export class EventHandlers<T> {
private handlers: EventHandler<T>[] = [];
private handlers: EventHandler<T>[] = [];
public addListener(handler: EventHandler<T>) {
this.handlers.push(handler);
}
public addListener(handler: EventHandler<T>) {
this.handlers.push(handler);
}
public removeListener(handler: EventHandler<T>) {
let index = this.handlers.indexOf(handler);
if (index !== -1) {
this.handlers.splice(index, 1);
}
public removeListener(handler: EventHandler<T>) {
const index = this.handlers.indexOf(handler);
if (index !== -1) {
this.handlers.splice(index, 1);
}
}
public fire(event: T) {
for (let handler of this.handlers) {
handler(event);
}
public fire(event: T) {
for (const handler of this.handlers) {
handler(event);
}
}
}

View File

@@ -1,4 +1,4 @@
import * as React from "react";
import * as React from 'react';
/**
* These icons come from https://github.com/microsoft/vscode-icons

View File

@@ -1,11 +1,15 @@
import * as React from "react";
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
import { RawTableResultSet, ResultValue, vscode } from "./results";
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
import * as React from 'react';
import { ResultTableProps, className } from './result-table-utils';
import { RAW_RESULTS_LIMIT, RawResultsSortState } from '../interface-types';
import { RawTableResultSet } from '../interface-types';
import RawTableHeader from './RawTableHeader';
import RawTableRow from './RawTableRow';
import { ResultRow } from '../adapt';
export type RawTableProps = ResultTableProps & {
resultSet: RawTableResultSet,
resultSet: RawTableResultSet;
sortState?: RawResultsSortState;
offset: number;
};
export class RawTable extends React.Component<RawTableProps, {}> {
@@ -16,27 +20,20 @@ export class RawTable extends React.Component<RawTableProps, {}> {
render(): React.ReactNode {
const { resultSet, databaseUri } = this.props;
let dataRows = this.props.resultSet.rows;
let dataRows = resultSet.rows;
let numTruncatedResults = 0;
if (dataRows.length > RAW_RESULTS_LIMIT) {
numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT;
dataRows = dataRows.slice(0, RAW_RESULTS_LIMIT);
}
const tableRows = dataRows.map((row, rowIndex) =>
<tr key={rowIndex} {...zebraStripe(rowIndex)}>
{
[
<td key={-1}>{rowIndex + 1}</td>,
...row.map((value, columnIndex) =>
<td key={columnIndex}>
{
renderTupleValue(value, databaseUri)
}
</td>)
]
}
</tr>
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) =>
<RawTableRow
key={rowIndex}
rowIndex={rowIndex}
row={row}
databaseUri={databaseUri}
/>
);
if (numTruncatedResults > 0) {
@@ -47,53 +44,14 @@ export class RawTable extends React.Component<RawTableProps, {}> {
}
return <table className={className}>
<thead>
<tr>
{
[
<th key={-1}><b>#</b></th>,
...resultSet.schema.columns.map((col, index) => {
const displayName = col.name || `[${index}]`;
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.sortDirection : undefined;
return <th className={"sort-" + (sortDirection !== undefined ? SortDirection[sortDirection] : "none")} key={index} onClick={() => this.toggleSortStateForColumn(index)}><b>{displayName}</b></th>;
})
]
}
</tr>
</thead>
<RawTableHeader
columns={resultSet.schema.columns}
schemaName={resultSet.schema.name}
sortState={this.props.sortState}
/>
<tbody>
{tableRows}
</tbody>
</table>;
}
private toggleSortStateForColumn(index: number) {
const sortState = this.props.sortState;
const prevDirection = sortState && sortState.columnIndex === index ? sortState.sortDirection : undefined;
const nextDirection = nextSortDirection(prevDirection);
const nextSortState = nextDirection === undefined ? undefined : {
columnIndex: index,
sortDirection: nextDirection
};
vscode.postMessage({
t: 'changeSort',
resultSetName: this.props.resultSet.schema.name,
sortState: nextSortState
});
}
}
/**
* Render one column of a tuple.
*/
function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element {
if (typeof v === 'string') {
return <span>{v}</span>
}
else if ('uri' in v) {
return <a href={v.uri}>{v.uri}</a>;
}
else {
return renderLocation(v.location, v.label, databaseUri);
}
}

View File

@@ -1,15 +1,29 @@
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 { assertNever } from '../helpers-pure';
import { ResultSet } from '../interface-types';
import { vscode } from './vscode-api';
export interface ResultTableProps {
resultSet: ResultSet;
databaseUri: string;
metadata?: QueryMetadata
metadata?: QueryMetadata;
resultsPath: string | undefined;
sortState?: RawResultsSortState;
offset: number;
/**
* Holds if there are any raw results. When that is the case, we
* want to direct users to pay attention to raw results if
* interpreted results are empty.
*/
nonemptyRawResults: boolean;
/**
* Callback to show raw results.
*/
showRawResults: () => void;
}
export const className = 'vscode-codeql__result-table';
@@ -34,7 +48,7 @@ export function jumpToLocationHandler(
};
}
export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string) {
export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string): void {
vscode.postMessage({
t: 'viewSourceFile',
loc,
@@ -66,7 +80,7 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
return <span title={title}>{displayLabel}</span>;
}
}
return <span />
return <span />;
}
/**
@@ -83,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,18 +1,33 @@
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,
ResultSet,
ALERTS_TABLE_NAME,
SELECT_TABLE_NAME,
getDefaultResultSetName,
} 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 { ParsedResultSets, ExtensionParsedResultSets } from '../adapt';
import { vscode } from './vscode-api';
/**
* Properties for the `ResultTables` component.
*/
export interface ResultTablesProps {
parsedResultSets: ParsedResultSets;
rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined;
database: DatabaseInfo;
metadata?: QueryMetadata
metadata?: QueryMetadata;
resultsPath: string;
origResultsPaths: ResultsPaths;
sortStates: Map<string, RawResultsSortState>;
@@ -25,20 +40,17 @@ 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";
const UPDATING_RESULTS_TEXT_CLASS_NAME = 'vscode-codeql__result-tables-updating-text';
function getResultCount(resultSet: ResultSet): number {
switch (resultSet.t) {
case 'RawResultSet':
return resultSet.schema.tupleCount;
case 'SarifResultSet':
if (resultSet.sarif.runs.length === 0) return 0;
if (resultSet.sarif.runs[0].results === undefined) return 0;
return resultSet.sarif.runs[0].results.length + resultSet.numTruncatedResults;
return resultSet.numTotalResults;
}
}
@@ -58,7 +70,9 @@ export class ResultTables
private getResultSets(): ResultSet[] {
const resultSets: ResultSet[] =
this.props.rawResultSets.map(rs => ({ t: 'RawResultSet', ...rs }));
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore 2783
this.props.rawResultSets.map((rs) => ({ t: 'RawResultSet', ...rs }));
if (this.props.interpretation != undefined) {
resultSets.push({
@@ -75,23 +89,53 @@ export class ResultTables
return resultSets;
}
constructor(props: ResultTablesProps) {
super(props);
this.state = {
// Get the result set that should be displayed by default
selectedTable: ResultTables.getDefaultResultSet(this.getResultSets())
};
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);
}
}
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];
/**
* 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';
}
constructor(props: ResultTablesProps) {
super(props);
const selectedTable = props.parsedResultSets.selectedTable || getDefaultResultSet(this.getResultSets());
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;
if (this.paginationAllowed()) {
vscode.postMessage({
t: 'changePage',
pageNumber: 0,
selectedTable
});
}
else
this.setState({ selectedTable });
}
private alertTableExtras(): JSX.Element | undefined {
@@ -115,26 +159,101 @@ 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;
// FIXME: The extension, not the view, should be in charge of deciding whether to initially show
// a raw or alerts page. We have to conditionally recompute the number of pages here, because
// on initial load of query results, resultSets.numPages will have the number of *raw* pages available,
// not interpreted pages, because the extension doesn't know the view will default to showing alerts
// instead.
const numPages = selectedTable == ALERTS_TABLE_NAME ?
resultSets.numInterpretedPages : resultSets.numPages;
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, 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, numPages - 1),
selectedTable,
});
};
return <span className="vscode-codeql__table-selection-header">
<button onClick={prevPage} >&#xab;</button>
<input
type="number"
size={3}
value={this.state.selectedPage}
onChange={onChange}
onBlur={e => choosePage(e.target.value)}
onKeyDown={e => { if (e.keyCode === 13) choosePage((e.target as HTMLInputElement).value); }}
/>
<span>
/ {numPages}
</span>
<button value=">" onClick={nextPage} >&#xbb;</button>
</span>;
}
renderButtons(): JSX.Element {
if (this.props.parsedResultSets.t === 'ExtensionParsed' && this.paginationAllowed())
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}
@@ -149,7 +268,10 @@ export class ResultTables
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
databaseUri={this.props.database.databaseUri}
resultsPath={this.props.resultsPath}
sortState={this.props.sortStates.get(resultSet.schema.name)} />
sortState={this.props.sortStates.get(resultSet.schema.name)}
nonemptyRawResults={nonemptyRawResults}
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }); }}
offset={this.getOffset()} />
}
</div>;
}
@@ -171,3 +293,9 @@ class ResultTable extends React.Component<ResultTableProps, {}> {
}
}
}
function getDefaultResultSet(resultSets: readonly ResultSet[]): string {
return getDefaultResultSetName(
resultSets.map((resultSet) => resultSet.schema.name)
);
}

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