Compare commits

...

513 Commits

Author SHA1 Message Date
Anders Starcke Henriksen
69ff2ed30c Merge pull request #3054 from github/v1.9.4
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
v1.9.4
2023-11-06 15:04:41 +01:00
Anders Starcke Henriksen
0884eb83ec v1.9.4 2023-11-06 14:41:42 +01:00
Kasper Svendsen
1fa7a93ec7 Merge pull request #3051 from github/kaspersv/parse-quoted-ra-idents
Extend join order badness RA parser
2023-11-06 13:50:46 +01:00
Koen Vlaswinkel
18d7fae817 Merge pull request #3053 from github/koesie10/ruby-arguments
Add support for Ruby keyword arguments
2023-11-06 10:59:27 +01:00
Koen Vlaswinkel
b3c5afbe4e Merge pull request #3052 from github/koesie10/argument-self
Extract creation of method argument options to languages
2023-11-06 10:59:20 +01:00
Koen Vlaswinkel
5f2a32ac8e Add support for Ruby keyword arguments
Ruby keyword arguments are represented differently than positional
arguments in the MaD format: they are represented as `Method[key:]`. The
framework endpoints query also returns the name as `key:`, so we can
detect these and format them as such.
2023-11-03 14:08:04 +01:00
Koen Vlaswinkel
b348356876 Extract creation of method argument options to languages
This moves the creation of possible method argument options from the
view to the languages. This allows differentiating between the
languages, for example by using `Argument[self]` for Ruby instead of
`Argument[this]`.
2023-11-03 13:50:13 +01:00
Koen Vlaswinkel
146732fa29 Merge pull request #3049 from github/koesie10/styled-transient-props
Use transient props for all styled components
2023-11-03 13:14:10 +01:00
Charis Kyriakou
3cc4f5c4a4 Remove logic that sets neutral models during automodeling (#3050) 2023-11-03 10:38:50 +00:00
Koen Vlaswinkel
4673bf56bd Merge pull request #3043 from github/koesie10/generate-model-unify
Unify model generation query execution
2023-11-03 11:27:36 +01:00
Koen Vlaswinkel
2f9f2f3d39 Merge remote-tracking branch 'origin/main' into koesie10/generate-model-unify 2023-11-03 11:08:01 +01:00
Koen Vlaswinkel
acc9ab30ed Add back 0 2023-11-03 10:48:11 +01:00
Kasper Svendsen
053708ab3b Extend join order badness RA parser 2023-11-03 10:10:39 +01:00
Koen Vlaswinkel
45f0669b85 Use transient prop for MethodModeling component 2023-11-02 14:16:34 +01:00
Koen Vlaswinkel
65f02f1c6f Use transient prop for CodeSnippetMessage component 2023-11-02 14:09:35 +01:00
Koen Vlaswinkel
fb45a0d409 Use transient prop for VerticalSpace component 2023-11-02 14:09:01 +01:00
Koen Vlaswinkel
3cd06021d3 Use transient prop for TextButton component 2023-11-02 14:08:29 +01:00
Koen Vlaswinkel
cd0b2fba8a Use transient prop for HorizontalSpace component 2023-11-02 14:07:55 +01:00
Koen Vlaswinkel
1cc63382c9 Remove unnecessary props type for Dropdown
This removes the `disabled` prop from the `Dropdown` component. This is
already included in the default HTML props of the `select` component,
so it's not necessary to add it again.
2023-11-02 14:06:03 +01:00
Koen Vlaswinkel
8e8399988e Use transient props for Alert component 2023-11-02 14:05:11 +01:00
Koen Vlaswinkel
eaf3a1ce1b Merge pull request #3031 from github/koesie10/modeled-method-union
Switch `ModeledMethod` to union of types
2023-11-02 09:52:33 +01:00
Koen Vlaswinkel
ccaf2ad0b6 Merge remote-tracking branch 'origin/main' into koesie10/modeled-method-union 2023-11-02 09:31:15 +01:00
Charis Kyriakou
7adc114002 Remove unnecessary ModelKindDropdown input (#3046) 2023-11-01 15:20:50 +00:00
Charis Kyriakou
3f90564ee3 Add telemetry for method modeling panel (#3041) 2023-11-01 15:15:22 +00:00
Koen Vlaswinkel
5378f1afa4 Merge remote-tracking branch 'origin/main' into koesie10/modeled-method-union 2023-11-01 16:11:21 +01:00
Koen Vlaswinkel
b47c561dfa Merge pull request #3044 from github/koesie10/fix-query-storage-path
Fix queries always being created in qlpack root
2023-11-01 16:02:17 +01:00
Charis Kyriakou
2f39364191 Merge pull request #3045 from github/charis/fix-broken-stories
Fix some broken storybook stories
2023-11-01 14:59:53 +00:00
Koen Vlaswinkel
ab67060279 Remove provenance from none modeling 2023-11-01 15:58:50 +01:00
Koen Vlaswinkel
dd8d7dfd58 Remove unnecessary nullish coalescing 2023-11-01 15:52:10 +01:00
Koen Vlaswinkel
e25398d1fa Use createEmptyModeledMethod in more places 2023-11-01 15:51:25 +01:00
Koen Vlaswinkel
5f25fe42c3 Change error message to be distinct 2023-11-01 15:49:01 +01:00
Charis Kyriakou
5ae136bc15 Fix some broken storybook stories 2023-11-01 14:39:09 +00:00
Koen Vlaswinkel
0bec013b73 Fix queries always being created in qlpack root
This passes through the query storage path to the qlpack generator so
it's able to create the query in the correct selected folder.
2023-11-01 15:13:43 +01:00
Koen Vlaswinkel
ccb08e19d7 Merge pull request #3040 from github/koesie10/duplicate-query-packs
Prevent duplicate query packs when creating a query
2023-11-01 15:02:18 +01:00
Koen Vlaswinkel
693adb5512 Remove separation between generate queries options 2023-11-01 14:55:49 +01:00
Koen Vlaswinkel
71f59b19b4 Move model generation functions to language definition 2023-11-01 14:53:05 +01:00
Koen Vlaswinkel
2a477140a6 Move language files to separate directories 2023-11-01 14:19:01 +01:00
Koen Vlaswinkel
9387d55263 Unify model generation query running 2023-11-01 14:19:00 +01:00
Koen Vlaswinkel
8a8a85fb9a Merge pull request #3033 from github/koesie10/generate-model
Add generation of Ruby models
2023-11-01 14:17:10 +01:00
Charis Kyriakou
978d8d38f1 Minor test plan fixes (#3042) 2023-11-01 12:17:59 +00:00
Koen Vlaswinkel
456163aba5 Prevent duplicate query packs when creating a query
This prevents the creation of duplicate query pack names when creating a
query in the following ways:
- When you have selected a folder, the query pack name will include the
  name of the folder. This should prevent duplicate query pack names
  when creating queries in different folders.
    - When the folder name includes `codeql` or `queries`, we will not
      add `codeql-extra-queries-` since that would be redundant.
- After generating the query pack name, we will resolve all qlpacks and
  check if one with this name already exists. If it does, we will start
  adding an index to the name until we find a unique name.
2023-11-01 11:03:20 +01:00
Charis Kyriakou
fe212c315c Merge pull request #3039 from github/charisk/method-modeling-language
Update method modeling panel language when new db opened
2023-11-01 09:31:53 +00:00
Charis Kyriakou
57fbb8e2e6 Update method modeling panel database item when new db opened 2023-11-01 09:09:25 +00:00
Koen Vlaswinkel
6685883ebf Merge pull request #3035 from github/koesie10/fix-ref
Fix scroll into view not working when revealing method
2023-11-01 09:56:44 +01:00
Koen Vlaswinkel
ad121a5f93 Merge pull request #3037 from github/koesie10/detect-language
Detect existing query packs when creating skeleton query
2023-11-01 09:56:24 +01:00
Charis Kyriakou
02c1d7ef9e Add ability to use a dev endpoint for auto-model (#3038) 2023-11-01 08:55:56 +00:00
Charis Kyriakou
e9fb9f52d8 Update method modeling panel language when new db opened 2023-11-01 08:52:44 +00:00
Charis Kyriakou
2988aceddf Change auto-model default batch size (#3036) 2023-10-31 14:46:01 +00:00
Koen Vlaswinkel
abafefdb5e Detect existing query packs when creating skeleton query
This will change the skeleton query wizard to detect existing query
packs when creating a skeleton query. This allows the user to create a
query in an existing query pack that is not named
`codeql-custom-queries-{language}`.
2023-10-31 15:45:48 +01:00
Koen Vlaswinkel
d24352be0a Merge pull request #3034 from github/koesie10/remove-extlogger-model-editor
Remove `extLogger` imports from model editor
2023-10-31 12:55:49 +01:00
Koen Vlaswinkel
50ae7d5b73 Fix scroll into view not working when revealing method
This fixes a bug where the method row would not scroll into view when
revealing a method. The problem was that the `DataGridRow` component
on which the `ref` was set is a `display: contents` element, which
does not have a visual representation in the DOM. Therefore, it wasn't
possible to scroll the method row into view. This fixes it by moving
the ref to the `DataGridCell` component of the first column, which is
a normal element.
2023-10-31 11:54:26 +01:00
Koen Vlaswinkel
8e4da4a20e Remove extLogger imports from model editor
We shouldn't be directly using the `extLogger` if we have access to the
app logger (either directly or by passing it in as a parameter). This
removes all imports of `extLogger` from the model editor directory.
2023-10-31 11:32:29 +01:00
Koen Vlaswinkel
2dbc50e009 Remove use of extLogger from generate model queries 2023-10-31 11:04:17 +01:00
Koen Vlaswinkel
5c2050d9bb Extract addModeledMethodsFromArray method 2023-10-31 10:31:54 +01:00
Koen Vlaswinkel
bb104b53ba Rename showFlowGeneration to showGenerateButton 2023-10-31 10:31:54 +01:00
Koen Vlaswinkel
474ec197a0 Add generation of Ruby models
This adds the ability to generate Ruby models from a database. It uses
the `GenerateModel.ql` query to do this. The query will essentially
return data in the data extensions format, so this will just parse it
and return it as `ModeledMethod` objects.
2023-10-31 10:23:36 +01:00
Shati Patel
135bce889e Minor fixes to testing.md (#3032) 2023-10-30 16:15:39 +00:00
Koen Vlaswinkel
b1aa5914c2 Merge pull request #3017 from hmac/hmac-model-editor-ruby
Add experimental model editor support for Ruby
2023-10-30 16:36:15 +01:00
Koen Vlaswinkel
80ae27a453 Extract isSupportedLanguage function 2023-10-30 16:08:00 +01:00
Koen Vlaswinkel
ba1bdacb50 Use INITIAL_MODE instead of hardcoding mode 2023-10-30 15:58:20 +01:00
Koen Vlaswinkel
98b0850f68 Switch ModeledMethod to union of types
This allows more disjointed models to be represented more accurately,
such as type models.
2023-10-30 14:26:47 +01:00
Koen Vlaswinkel
c482f2a058 Merge pull request #3025 from github/dependabot/npm_and_yarn/extensions/ql-vscode/msw-2.0.0
Bump msw from 0.0.0-fetch.rc-9 to 2.0.0 in /extensions/ql-vscode
2023-10-30 13:07:56 +01:00
Koen Vlaswinkel
f0efebbbc4 Merge pull request #3026 from github/koesie10/ask-for-display-language
Use language display name when asking for language
2023-10-30 11:58:57 +01:00
Koen Vlaswinkel
5e0caded52 Merge pull request #3016 from github/koesie10/improve-skeleton-db-download
Improve database download prompt when creating query
2023-10-30 11:58:01 +01:00
Koen Vlaswinkel
0951dde2c4 Implement changes for MSW 2.0.0 2023-10-30 11:28:51 +01:00
Nora
33992129ed Merge pull request #3029 from github/nora/progress-reporting-code-search
Code Search: use withProgress to indicate api request progress
2023-10-27 17:09:57 +02:00
Nora
5caf11e7b7 Polish naming 2023-10-27 14:51:16 +00:00
Koen Vlaswinkel
43e60b20db Merge pull request #3030 from github/koesie10/fix-data-flow-paths-dropdown
Fix data flow path dropdown not updating
2023-10-27 16:48:02 +02:00
Koen Vlaswinkel
c77a57f383 Store available modes per language 2023-10-27 15:50:46 +02:00
Koen Vlaswinkel
92ad718df1 Fix data flow path dropdown not updating 2023-10-27 14:40:19 +02:00
Nora
5c3c8ffa1b use withProgress 2023-10-27 12:28:47 +00:00
Koen Vlaswinkel
712b55768f Hide "Model as application" button for Ruby 2023-10-27 12:42:09 +02:00
Koen Vlaswinkel
8c7273efc6 Add Ruby queries as included queries for model editor
This adds the current version of the queries for Ruby to the model
editor included queries. This makes it work without needing to check out
a separate branch of the CodeQL repository/submodule. I've removed most
commented out code from the queries, but the other parts should match.
2023-10-27 12:39:59 +02:00
Koen Vlaswinkel
dde417ea7d Start model editor in framework mode for Ruby 2023-10-27 12:38:13 +02:00
Koen Vlaswinkel
b023431626 Add feature flag for Ruby in the model editor
This only makes Ruby available in the model editor when the following
is set in the settings.json (workspace or user) file:

```json5
{
  "codeQL.model.enableRuby": true,
}
```
2023-10-27 12:37:14 +02:00
Harry Maclean
9c5a963495 Add experimental model editor support for Ruby
Make the minimum changes necessary for prototype Ruby support in the
model editor.

This consists of:

- Reading/writing modelled methods from/to data extensions in the
  dynamic languages format
- Special-casing Ruby in a few places where Java/C# was previously
  assumed.
2023-10-27 12:36:22 +02:00
Koen Vlaswinkel
a3735c21a1 Use language display name for downloading databases 2023-10-27 12:32:34 +02:00
Koen Vlaswinkel
5ca084be91 Use language display name when asking for language 2023-10-27 12:08:46 +02:00
Koen Vlaswinkel
f4a2d8572c Merge pull request #3028 from github/koesie10/bqrs-decode-languages
Use MaD definition when decoding BQRS
2023-10-27 11:58:48 +02:00
Koen Vlaswinkel
ecb2503992 Use MaD definition when decoding BQRS
This will use the MaD's definition of a method signature when decoding
BQRS files. This will allow us to change the method signature definition
for dynamic languages.
2023-10-27 11:39:46 +02:00
Koen Vlaswinkel
b9fa79a76e Move predicates into a separate property 2023-10-27 11:29:29 +02:00
Koen Vlaswinkel
14c6f98289 Merge pull request #3027 from github/koesie10/flow-generation-button
Only show flow generation button when supported
2023-10-27 11:12:32 +02:00
Koen Vlaswinkel
05e3f2cba6 Only show flow generation button when supported 2023-10-27 10:46:54 +02:00
Koen Vlaswinkel
1404ab45fb Merge pull request #3020 from github/koesie10/refactor-predicates
Refactor model editor predicates
2023-10-27 10:39:52 +02:00
Koen Vlaswinkel
fa12671f4a Merge remote-tracking branch 'origin/main' into koesie10/improve-skeleton-db-download 2023-10-27 10:22:31 +02:00
Koen Vlaswinkel
a8404a5b01 Throw from getModelsAsDataLanguage 2023-10-27 10:18:38 +02:00
Koen Vlaswinkel
8a87db6cb4 Use URI for open file link 2023-10-27 10:15:58 +02:00
Koen Vlaswinkel
1151432ca2 Use language display name in notifications 2023-10-27 10:07:22 +02:00
Koen Vlaswinkel
42f1e81fdc Handle errors when downloading database 2023-10-27 10:04:37 +02:00
Koen Vlaswinkel
edbc65886d Use tryGetQueryLanguage function 2023-10-27 09:50:08 +02:00
Koen Vlaswinkel
407825e1cf Merge pull request #3021 from github/koesie10/external-api-to-model-editor
Rename external API methods/files/errors to model editor
2023-10-27 09:37:49 +02:00
dependabot[bot]
325cc05f36 Bump actions/setup-node from 3 to 4 (#3024)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 11:02:49 -07:00
dependabot[bot]
721d971a66 Bump msw from 0.0.0-fetch.rc-9 to 2.0.0 in /extensions/ql-vscode
Bumps [msw](https://github.com/mswjs/msw) from 0.0.0-fetch.rc-9 to 2.0.0.
- [Release notes](https://github.com/mswjs/msw/releases)
- [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mswjs/msw/compare/v0.0.0-fetch.rc-9...v2.0.0)

---
updated-dependencies:
- dependency-name: msw
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 13:28:50 +00:00
github-actions[bot]
cc8bcbbc5d Bump version to v1.9.4 (#3023)
Co-authored-by: github-actions[bot] <github-actions@github.com>
2023-10-26 11:20:22 +00:00
Charis Kyriakou
5375fcb26a v1.9.3 (#3022)
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2023-10-26 11:11:17 +01:00
Koen Vlaswinkel
f5d86777ae Update synthetic query pack name 2023-10-26 11:57:50 +02:00
Koen Vlaswinkel
ff36088ecc Merge pull request #3012 from github/koesie10/use-selected-queries-item
Create new queries in selected folder of queries panel
2023-10-26 11:55:42 +02:00
Koen Vlaswinkel
b19e970ec5 Fix error message when running model editor queries 2023-10-26 11:43:05 +02:00
Koen Vlaswinkel
f379036c18 Fix error message for bundled model editor queries 2023-10-26 11:42:41 +02:00
Koen Vlaswinkel
30daf49cb8 Rename external-api-usage-queries.ts to model-editor-queries.ts 2023-10-26 11:41:56 +02:00
Koen Vlaswinkel
ea2999fcc7 Rename model-editor-queries.ts to model-editor-queries-setup.ts 2023-10-26 11:41:32 +02:00
Koen Vlaswinkel
c548aa0ff9 Remove unnecessary nesting in external-api-usage-query.test.ts 2023-10-26 11:40:58 +02:00
Koen Vlaswinkel
e70bceb6dd Rename runExternalApiQueries to runModelEditorQueries 2023-10-26 11:40:26 +02:00
Koen Vlaswinkel
818e93e86b Rename prepareExternalApiQuery to prepareModelEditorQueries 2023-10-26 11:39:06 +02:00
Koen Vlaswinkel
322b376a2c Remove unused predicates file 2023-10-26 11:34:46 +02:00
Koen Vlaswinkel
0744b25a47 Use new language definitions for the view 2023-10-26 11:33:36 +02:00
Koen Vlaswinkel
8e721a6670 Use new language definitions for reading/writing 2023-10-26 11:33:34 +02:00
Koen Vlaswinkel
df3b94c081 Add language to model editor view 2023-10-26 11:33:10 +02:00
Koen Vlaswinkel
8a77a1fba2 Introduce separate files for predicate definitions 2023-10-26 11:33:08 +02:00
Koen Vlaswinkel
c9d1a6b447 Move predicates file to languages folder 2023-10-26 11:23:51 +02:00
Charis Kyriakou
234498a33e Enable multiple models feature (#3019) 2023-10-26 09:13:25 +00:00
Koen Vlaswinkel
40a77dfd4a Merge pull request #3018 from github/koesie10/fix-empty-location
Fix empty message with empty SARIF path
2023-10-26 11:04:45 +02:00
Koen Vlaswinkel
06b6595980 Update CHANGELOG 2023-10-26 10:42:11 +02:00
Shati Patel
9a97b7b0be Deselect current db when the language context changes (#3007) 2023-10-25 14:54:01 +01:00
Koen Vlaswinkel
6622d5e114 Fix empty message with empty SARIF path 2023-10-25 14:55:15 +02:00
Koen Vlaswinkel
f0f5538b51 Remove check for isFolderAlreadyInWorkspace 2023-10-25 14:37:11 +02:00
Nora
3f8302796f Merge pull request #3010 from github/nora/modeling-flow-testplan
Add method modeling flow to test plan
2023-10-25 14:28:59 +02:00
Koen Vlaswinkel
a3fad49577 Fix creation of nested query packs
Before, if you had selected a folder or file within for example
`codeql-custom-queries-java` and selected `java` as the language, it
would create a nested folder within `codeql-custom-queries-java` with
the name `codeql-custom-queries-java`. This is unexpected for the user,
who would expect a new query to be created within
`codeql-custom-queries-java`. This fixes that by checking for this
specific condition. It does not fix it for all scenarios, such as where
the selected file/folder is nested multiple levels deep within the
`codeql-custom-queries-java` folder.
2023-10-25 14:28:50 +02:00
Koen Vlaswinkel
68ab2fda2d Create new queries in selected folder of queries panel
This will change the behavior of the "Create new query" command to
create the new query in the same folder as the first selected item in
the queries panel. If no items are selected, the behavior is the same
as before.

I've used events to communicate the selection from the queries panel to
the local queries module. This is some more code and some extra
complexity, but it ensures that we don't have a dependency from the
local queries module to the queries panel module. This makes testing
easier.
2023-10-25 14:28:48 +02:00
Koen Vlaswinkel
f3eefc9418 Add tests for prompting for database download 2023-10-25 14:22:55 +02:00
Koen Vlaswinkel
15a8655931 Do not prompt for database download by default
This changes the skeleton query wizard to not prompt for database
download after creating a query by default. Instead, it will show a
message with a button to download a database which will launch the same
prompt.
2023-10-25 14:22:55 +02:00
Koen Vlaswinkel
fb33879a95 Merge pull request #3015 from github/koesie10/reveal-file-in-queries-panel
Reveal opened files in queries panel
2023-10-25 13:58:08 +02:00
Nora
0e5306742d Move test cases to optional and polish 2023-10-25 08:57:34 +00:00
Koen Vlaswinkel
3a07fa9e39 Show initially open file in queries panel 2023-10-25 10:13:48 +02:00
Koen Vlaswinkel
b6f7755908 Create subscribeToTreeSelectionEvents method 2023-10-25 10:06:14 +02:00
Koen Vlaswinkel
368f9c38ef Only change selection when tree view is visible 2023-10-25 10:04:31 +02:00
Koen Vlaswinkel
1e58e5a723 Reveal opened file in queries panel 2023-10-25 10:04:31 +02:00
Robert
2ebccd532f Merge pull request #3014 from github/robertbrignull/hidden_methods
Use the same logic for hiding methods in model editor and usages panel
2023-10-25 09:04:06 +01:00
Robert
231dcc0c55 Improve tests to cover showing supported modeled methods 2023-10-24 16:49:21 +01:00
Robert
675e2ec9f2 Use the same condition for hiding methods in model editor and usages panel 2023-10-24 16:34:39 +01:00
Shati Patel
f0f13f3569 Create query: open new query in editor before downloading DB (#3009) 2023-10-24 13:30:01 +00:00
Robert
8d336930c8 Merge pull request #3008 from github/robertbrignull/modeled_row_styling
Fix styling of "already modeled" rows when showing multiple methods
2023-10-24 14:13:18 +01:00
Koen Vlaswinkel
043cdab297 Merge pull request #3013 from github/koesie10/use-selected-language
Use selected language when creating query
2023-10-24 15:02:05 +02:00
Charis Kyriakou
b1172d7d64 Activate method modeling panel when model editor is opened (#3011) 2023-10-24 12:55:19 +01:00
Koen Vlaswinkel
8b5329fe08 Use selected language when creating query
This will change the "Create new query" command to use the selected
language when creating a new query. If no language is selected, it will
still prompt the user to pick a language.
2023-10-24 13:41:37 +02:00
Nora
7bade3e382 Include model panels in mandatory tests 2023-10-24 10:03:54 +00:00
Robert
eb42beee23 Move method classifications to start 2023-10-24 10:12:05 +01:00
Robert
2405628bcc Fix span size 2023-10-24 10:11:50 +01:00
Robert
0a75a0e835 Merge pull request #2983 from github/robertbrignull/remove_selected_model
Handle when the number of modeled methods decreases
2023-10-23 14:34:17 +01:00
Robert
07a4ffb306 Add tests that pagination is updated when a model is removed 2023-10-23 13:50:29 +01:00
Robert
1424afc7a4 Add tests that pagination is updated when add a new model 2023-10-23 13:46:10 +01:00
Koen Vlaswinkel
c62c054b95 Merge pull request #3001 from github/koesie10/validation-errors-focus
Add higlight on modeled method row when clicking in validation error
2023-10-23 13:17:59 +02:00
Koen Vlaswinkel
41aeb47a4e Merge pull request #3004 from github/koesie10/redactable-error-stack
Show stack for redactable error in log
2023-10-23 12:04:08 +02:00
Koen Vlaswinkel
4ca14f89df Revert "Use os.EOL for separating stack in error"
This reverts commit b33b5bb7c4.

The errors module is also imported in the webview, so we can't actually
use it.
2023-10-23 11:43:15 +02:00
Koen Vlaswinkel
8011481de2 Merge pull request #2996 from github/koesie10/query-save-dir
Fix results directory and evaluator log for cancelled queries
2023-10-23 10:45:41 +02:00
Koen Vlaswinkel
d682c520d5 Add tests for fullMessageWithStack 2023-10-23 10:44:25 +02:00
Koen Vlaswinkel
b33b5bb7c4 Use os.EOL for separating stack in error 2023-10-23 10:37:10 +02:00
Charis Kyriakou
1ab198fe49 Move modeling events to new ModelingEvents class (#3005) 2023-10-23 08:57:48 +01:00
Koen Vlaswinkel
48df8de2c2 Merge pull request #3003 from github/koesie10/fix-error-no-workspace-folders
Show error message for no workspace folders with model editor
2023-10-20 17:31:37 +02:00
Koen Vlaswinkel
78f832a73f Show stack for redactable error in log
When calling for example `showAndLogExceptionWithTelemetry`, the stack
trace would be sent to Application Insights, but there was no way to
see the stack trace from within VS Code. This will add the stack trace
to the log by returning it from `fullMessageWithStack` and using it in
the appropriate places.
2023-10-20 16:49:36 +02:00
Koen Vlaswinkel
8c594239cd Merge pull request #3002 from github/koesie10/jsx-key-rule
Enable `react/jsx-key` for fragment shorthands
2023-10-20 16:44:37 +02:00
Koen Vlaswinkel
89ccd70752 Show error message for no workspace folders with model editor
It is possible to open the model editor without opening a folder, but
this gave an unhelpful error message. This commit adds a more helpful
error message.
2023-10-20 16:43:58 +02:00
Koen Vlaswinkel
c928b1eb86 Enable react/jsx-key for fragment shorthands 2023-10-20 16:03:30 +02:00
Koen Vlaswinkel
faffe4590b Add focus on modeled method row on error click 2023-10-20 11:20:14 +02:00
Koen Vlaswinkel
91f6772ab9 Add interaction to MethodRow stories 2023-10-20 10:38:44 +02:00
Dave Bartolomeo
d20cf92eea Merge pull request #3000 from github/github-action/bump-cli
Bump CLI Version to v2.15.1 for integration tests
2023-10-19 13:57:08 -04:00
dependabot[bot]
1f34330052 Bump lint-staged from 14.0.0 to 15.0.2 in /extensions/ql-vscode (#2999)
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 14.0.0 to 15.0.2.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/master/CHANGELOG.md)
- [Commits](https://github.com/okonet/lint-staged/compare/v14.0.0...v15.0.2)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 10:39:57 -07:00
Robert
acb687cee7 Merge pull request #2997 from github/robertbrignull/model-table-validation
Show validation results in the model editor
2023-10-19 16:55:54 +01:00
github-actions[bot]
221b4392d3 Bump CLI version from v2.15.0 to v2.15.1 for integration tests 2023-10-19 15:44:43 +00:00
Robert
31d654d33d Add tests of showing validation errors 2023-10-19 16:26:08 +01:00
Robert
553435d5b7 Add stories for validation errors in MethodRow 2023-10-19 16:14:41 +01:00
Robert
0af77d086a Display validation errors in model editor 2023-10-19 12:52:45 +01:00
Robert
c69a310110 Add <> around inputs 2023-10-19 12:52:18 +01:00
Robert
1606829ceb Make setSelectedIndex optional 2023-10-19 12:47:50 +01:00
Robert
86b50560a4 Merge pull request #2995 from github/robertbrignull/model-table-multirow
Use one grid cell per model input, and have the method name span multiple rows
2023-10-19 12:40:03 +01:00
Robert
50f77e7918 Use Fragment to add key to map output 2023-10-19 10:53:06 +01:00
Robert
947f495d0b Merge pull request #2990 from github/robertbrignull/model-table-alignment
Use custom grid element instead of VSCodeDataGrid
2023-10-19 09:57:26 +01:00
Koen Vlaswinkel
18646ab637 Update CHANGELOG 2023-10-19 10:04:08 +02:00
Koen Vlaswinkel
046bc13fc3 Try generating evaluation log summary after failed query
When a local query fails (for example, if it is cancelled), it may still
have an evaluation log. We weren't generating evaluation log summaries
in these cases, so the options to view the summary text and to use the
evaluation log viewer would not be available. This fixes it by also
trying to generate the summary in the case of a failed query.
2023-10-19 10:04:08 +02:00
Koen Vlaswinkel
226274cb4e Fix showing of raw evaluator log for cancelled items
This will ensure that when "Show Evaluator Log (Raw JSON)" is used on a
cancelled query history item, we will still show it if it exists. This
changes the error messages on other cases to be more specific.
2023-10-19 10:04:08 +02:00
Koen Vlaswinkel
9928c338e9 Store query output dir on history items
This will add the `QueryOutputDir` to the `InitialQueryInfo` and
populate it when creating a local query history item. This will allow us
to open the results directory or show the evaluator log without a
completed query.
2023-10-19 10:04:08 +02:00
Andrew Eisenberg
df55e039a1 Merge pull request #2984 from github/aeisenberg/fix-log-path 2023-10-18 15:36:10 -07:00
Andrew Eisenberg
2e2051af6d Merge branch 'main' into aeisenberg/fix-log-path 2023-10-18 15:04:12 -07:00
Andrew Eisenberg
4ad3d962ec Add change note
Also, avoid a code scanning warning.
2023-10-18 15:03:16 -07:00
Robert
ec0e74bd9a Merge pull request #2992 from github/robertbrignull/columnCount
Add $ to columnCount prop used by styled component
2023-10-18 18:29:02 +01:00
dependabot[bot]
8a1da313ae Bump @babel/traverse from 7.22.8 to 7.23.2 in /extensions/ql-vscode (#2994)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.8 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 16:50:14 +01:00
Robert
c88ecf76aa Add documentation to all DataGrid components 2023-10-18 16:42:50 +01:00
Robert
93de35e7a3 Rename HiddenMethodsText => HiddenMethodsCell 2023-10-18 16:30:30 +01:00
Robert
8c339d07e8 Pass rowType to styled component instead of using another class name 2023-10-18 16:29:48 +01:00
Robert
cead0ea52e Import ReactNode 2023-10-18 16:26:37 +01:00
Robert
db67d93f83 Extract DataGridRow props in params 2023-10-18 16:24:34 +01:00
Robert
a79867732c Make gridTemplateColumns just a string 2023-10-18 16:21:04 +01:00
Robert
09a8d29ea5 Use one grid cell per input 2023-10-18 15:17:07 +01:00
Shati Patel
a2f85877a8 Add option to sort DB panel by language (#2993) 2023-10-18 14:58:39 +01:00
Robert
c528a389e5 Use forwardRef for DataGridRow 2023-10-18 14:41:26 +01:00
Robert
48f719fa9d Pass through data-testid explicitly 2023-10-18 14:35:49 +01:00
Robert
cac9efa41b Introduce wrapper component for DataGridRow 2023-10-18 14:20:19 +01:00
Robert
56d0f28814 Add styling for header rows 2023-10-18 14:20:16 +01:00
Robert
298656444f Make gridRow and gridColumn optional 2023-10-18 14:10:16 +01:00
Koen Vlaswinkel
30b51d98c8 Merge pull request #2988 from github/koesie10/error-default-branch
Add custom error handler for missing default branch
2023-10-18 14:12:03 +02:00
Robert
6c2718927e Adjust styling to not use --design-unit 2023-10-18 13:02:34 +01:00
Robert
579042cf84 Extract props in parameters instead of as statement 2023-10-18 12:22:06 +01:00
Robert
2c70c0b792 Remove unnecessary aria label 2023-10-18 12:15:17 +01:00
Robert
ec64b59b96 Pass className through DataGridCell 2023-10-18 12:07:10 +01:00
Robert
f886cd0dc8 Add $ to props used by styled component 2023-10-18 11:52:01 +01:00
Robert
20469b0da4 Merge branch 'main' into robertbrignull/remove_selected_model 2023-10-18 11:43:12 +01:00
Robert
26fcef8f5d Introduce selectNewMethod ref 2023-10-18 11:43:05 +01:00
Charis Kyriakou
96fb0046c5 Style update to in-progress drop-down (#2986) 2023-10-18 08:45:49 +01:00
Koen Vlaswinkel
903b272952 Merge pull request #2989 from github/koesie10/usage-panel-tree-items-reference
Keep reference to original items in usage data provider
2023-10-18 09:23:30 +02:00
Robert
54db867d15 Use custom grid element instead of VSCodeDataGrid 2023-10-17 17:38:06 +01:00
Koen Vlaswinkel
a852f16eb1 Keep reference to original items in usage data provider
This changes the usage data provider tree items to keep a reference to
the method and usage instead of only including their properties in the
tree item. This makes it easier to find the original method and usage
when revealing an item in the tree. It also removes the `getParent` call
in `getTreeItem`.

The main reason for this fix is to ensure
`codeQLModelEditor.jumpToMethod` gets the correct `usage` argument.
It received the tree item before, but now we can actually pass the
usage that was clicked on.
2023-10-17 16:03:57 +02:00
Koen Vlaswinkel
a7f8019bf4 Merge pull request #2987 from github/koesie10/source-map-fix
Fix decoding source map with VS Code internal files
2023-10-17 15:40:38 +02:00
Koen Vlaswinkel
2d5caa77bc Remove unnecessary casting 2023-10-17 15:19:04 +02:00
Koen Vlaswinkel
82c2952059 Add custom error handler for missing default branch
When the GitHub API returns an error for a missing default branch, we
will now show a custom error message. This custom error message includes
a link to the page to create the branch. The error is detected using the
`errors` field on the response that is now being returned.
2023-10-17 14:57:19 +02:00
Koen Vlaswinkel
67f6f8f160 Fix decoding source map with VS Code internal files
This makes it possible to decode source maps containing references to
code that is not part of the extension. If it finds any such references,
it will simply not decode the source map and use the original stack
trace instead.
2023-10-17 13:34:11 +02:00
Charis Kyriakou
abde8f3fae Make automodel batch size configurable (#2985) 2023-10-17 12:07:30 +01:00
Robert
8d5574e468 Merge pull request #2982 from github/robertbrignull/add-remove-models
Implement onClick for the add/remove model buttons in the model editor
2023-10-17 10:29:16 +01:00
Andrew Eisenberg
cc0e850c72 Ensure the logFileLocation is set
`logFileLocation` was not set after a query finishes running. I don't
know when this bug was introduced. I think it goes as far back as
the refactor to remove the old query server.
2023-10-16 16:44:49 -07:00
Robert
46e7dda6a6 Don't bother using slice when creating handlers 2023-10-16 16:29:36 +01:00
Robert
d937934737 Use the onChange handler in tests 2023-10-16 16:27:23 +01:00
Robert
96c0feb3e6 Handle when the number of modeled methods decreases 2023-10-16 16:00:49 +01:00
Robert
0ff523a64b Add tests for for the add/remove model buttons 2023-10-16 15:36:46 +01:00
Robert
1d0a1f56b1 Implment onClick handlers for add/remove model buttons 2023-10-16 15:07:44 +01:00
Koen Vlaswinkel
fca68edbb3 Merge pull request #2969 from github/koesie10/fix-unmodeled-unsaved
Fix unmodeled methods always being marked as unsaved
2023-10-16 15:07:18 +02:00
Charis Kyriakou
b9279dc64f Simplify modeling in progress state (#2980) 2023-10-16 12:54:13 +00:00
Koen Vlaswinkel
00b6ccdfe0 Simplify condition in useEffect 2023-10-16 13:53:53 +02:00
Koen Vlaswinkel
688b9863da Merge pull request #2981 from github/koesie10/fix-failing-test
Fix failing test
2023-10-16 13:47:01 +02:00
Koen Vlaswinkel
c80641866c Fix failing test 2023-10-16 13:25:35 +02:00
Koen Vlaswinkel
6a7ce9f4d2 Merge pull request #2976 from github/koesie10/methods-usage-panel-parent
Introduce separate tree item types in the methods usage panel
2023-10-16 13:25:05 +02:00
Koen Vlaswinkel
d0e0237b9e Merge pull request #2965 from github/koesie10/modeled-method-validation-neutral
Fix neutral model validation to consider kind
2023-10-16 13:23:26 +02:00
Anders Starcke Henriksen
e57f04e6b1 Merge pull request #2975 from github/starcke/remove-old-language-filter
Remove old language context filter.
2023-10-16 13:06:40 +02:00
Charis Kyriakou
b87dfa4471 Show in-progress state in method modeling panel (#2979) 2023-10-16 11:47:31 +01:00
Robert
b1a4586791 Merge pull request #2977 from github/robertbrignull/fix_jump_to_usage
Revert changes to codeQLModelEditor.jumpToMethod to allow jumping to usages other than the first usage
2023-10-16 11:21:18 +01:00
Robert
aa8896e553 Merge pull request #2964 from github/robertbrignull/enable-add-multiple-models
Enable/disable the add/remove model buttons at the right times
2023-10-16 11:04:06 +01:00
Robert
9134e0e917 Add comment explaining caveats of setSelectedMethod 2023-10-16 11:03:19 +01:00
Robert
1259a3e61d Use 'import type' in commands.ts 2023-10-16 10:59:12 +01:00
Robert
7aa0fe32c2 Revert changes to codeQLModelEditor.jumpToMethod 2023-10-16 10:42:18 +01:00
Anders Starcke Henriksen
cc2eec78bd Merge branch 'main' into starcke/remove-old-language-filter 2023-10-16 11:27:56 +02:00
Koen Vlaswinkel
d715ceea10 Introduce separate tree item types in the methods usage panel
This creates new tree item types for methods and usages such that these
can contain references to their parent and children. This allows us to
easily find the parent of a usage and to find the children of a method.
This removes an expensive `find` call in `getParent`.
2023-10-16 11:18:27 +02:00
Anders Starcke Henriksen
8b3786c621 Remove old language context filter. 2023-10-16 10:30:26 +02:00
Koen Vlaswinkel
39a9f4ce1e Merge pull request #2974 from github/koesie10/changelog-install-workspace-packs
Add CHANGELOG entry for #2960
2023-10-16 10:09:48 +02:00
Koen Vlaswinkel
b2de9e94cd Add CHANGELOG entry for #2960 2023-10-16 09:53:22 +02:00
Charis Kyriakou
23dc8f16c3 Move in-progress methods state to modeling store (#2971) 2023-10-16 08:51:46 +01:00
Koen Vlaswinkel
d78a4d19eb Merge pull request #2960 from github/koesie10/install-workspace-packs
Do not show non-workspace packs when installing pack dependencies
2023-10-16 09:51:29 +02:00
Anders Starcke Henriksen
3cbaa5aa24 Merge pull request #2935 from github/starcke/language-selection-panel
Add language filter panel.
2023-10-16 09:42:35 +02:00
Anders Starcke Henriksen
e8e6c6bbc7 Update comment. 2023-10-13 16:28:14 +02:00
Anders Starcke Henriksen
d5388576b5 Apply suggestions from code review
Co-authored-by: Koen Vlaswinkel <koesie10@users.noreply.github.com>
2023-10-13 16:00:17 +02:00
Charis Kyriakou
a5139b7fbf Convert InProgressMethods to an interface (#2970) 2023-10-13 14:56:33 +01:00
Nora Dimitrijević
f5f5b398fe Merge pull request #2928 from github/d10c/trim-cache-command
Add "CodeQL: Trim Cache" command that calls `evaluation/trimCache`
2023-10-13 15:12:55 +02:00
Koen Vlaswinkel
7baad1a5c6 Fix unmodeled methods always being marked as unsaved
When opening a library group in the model editor, unmodeled methods
would always be marked as unsaved, even if there were no changes. This
was because the `ModelKindDropdown` component did not properly take into
account that the `kind` for an unmodeled method should be an empty
string. It would always try setting it to `undefined`, which would cause
the method to be marked as unsaved. This fixes it by checking if there
are valid kinds before setting the kind to the first one.
2023-10-13 13:21:53 +02:00
Koen Vlaswinkel
5e8de88ee0 Merge pull request #2968 from github/koesie10/readonly-modeling-store
Improve immutability of modeling store state
2023-10-13 13:13:27 +02:00
Koen Vlaswinkel
6801a64148 Improve immutability of modeling store state
This improves the immutability of the modeling store state by using
TypeScript's readonly types to ensure that state can only be modified
from within the modeling store or when it's copied. This mostly consists
of adding `readonly` to properties and arrays, but this also adds a
`DeepReadonly` type to use in `postMessage` arguments to ensure that
readonly objects can be passed in. `postMessage` will never modify the
objects, so this is safe.
2023-10-13 11:20:54 +02:00
Nora
ee630b4a87 Merge pull request #2963 from github/nora/cancel-code-search
Improve code search cancelling
2023-10-13 10:23:02 +02:00
Anders Starcke Henriksen
a03e2c85f1 Address comments. 2023-10-13 10:03:34 +02:00
Koen Vlaswinkel
288f44e57d Merge remote-tracking branch 'origin/main' into koesie10/modeled-method-validation-neutral 2023-10-13 10:01:34 +02:00
Anders Starcke Henriksen
52d32a5051 Merge branch 'main' into starcke/language-selection-panel 2023-10-13 09:52:17 +02:00
Koen Vlaswinkel
fa9cc7c5f9 Merge pull request #2967 from github/koesie10/fix-multiple-models-method-panel-spec
Fix incorrect test for `MultipleModeledMethodsPanel`
2023-10-13 09:51:32 +02:00
Anders Starcke Henriksen
cc3feabe66 Apply suggestions from code review
Co-authored-by: Koen Vlaswinkel <koesie10@users.noreply.github.com>
2023-10-13 09:48:28 +02:00
Koen Vlaswinkel
1dbd5aa86e Fix test with non-conflicting neutral 2023-10-13 09:47:00 +02:00
Koen Vlaswinkel
19c30f1ee2 Fix incorrect test for MultipleModeledMethodsPanel
This was caused by a semantic merge conflict resulting in the wrong
`onChange` argument being used in the test.
2023-10-13 09:33:55 +02:00
Koen Vlaswinkel
3c505719f2 Merge pull request #2957 from github/koesie10/modeled-methods-panel-multiple-save
Use `SetMultipleModeledMethodsMessage` in modeled methods panel
2023-10-13 09:26:44 +02:00
Nora Dimitrijević
b097804ad7 Make the command conditional on a supported CLI version 2023-10-12 16:15:49 +00:00
Nora Dimitrijević
8b918bdb19 Add CHANGELOG.md entry 2023-10-12 16:15:49 +00:00
Nora Dimitrijević
d0f4188f3f Add "VSCode: Trim Cache" command that calls evaluation/trimCache
The purpose of this change is to add a command that clears the cache except for predicates marked `cached`.
In contrast, the existing "VSCode: Clear Cache" command clears everything (`--mode=brutal`).

This calls into the query server's `evaluation/trimCache` method;
however, its existing behaviour is to do a database cleanup with `--mode=gentle`.
This is not well documented, and `--mode=normal` would give the desired behaviour.

Accordingly, this approach is dependent on separately changing the backend behaviour to `--mode=normal`.

Other possible amendments to this commit would be to not touch the legacy client
(replacing required methods by failing promises, since the legacy server is fully deprecated already),
or to have less duplication (by introducing more arguments — however,
I'm applying the rule of thumb that >3 copy-pastes are required for the introduction of a deduplicating abstraction).
2023-10-12 16:15:49 +00:00
Robert
bf828bccb6 Correctly test when buttons are disabled 2023-10-12 17:10:25 +01:00
Robert
48732a817a Update test names 2023-10-12 16:59:04 +01:00
Koen Vlaswinkel
4ac21232cf Fix neutral model validation to consider kind
This fixes a bug where the validation of modeled methods would not
consider the kind of the modeled method, and would therefore give an
error when there was e.g. a neutral sink and a non-neutral summary.
2023-10-12 16:41:28 +02:00
Robert
fc9588a1ec Add tests for buttons 2023-10-12 15:31:35 +01:00
Robert
08522f9ae2 Enable/disable the add/remove model buttons in model editor 2023-10-12 15:31:34 +01:00
Robert
e50affeb56 Introduce shared methods for canAdd / canRemoveModeledMethod 2023-10-12 15:31:32 +01:00
Nora
43bc92e386 throw when cancelling 2023-10-12 13:52:56 +00:00
Kasper Svendsen
8ecc31fae7 Merge pull request #2955 from github/kaspersv/hash-result-set-in-bqrs-filename
Hash result set name in sorted result set path
2023-10-12 14:17:47 +02:00
Koen Vlaswinkel
d15c57ee29 Merge pull request #2961 from github/koesie10/storybook-a11y
Add @storybook/addon-a11y package
2023-10-12 14:08:40 +02:00
Nora
f4f799894e Merge pull request #2815 from github/nora/remove-unused-activation-events
Remove unnecessary `activationEvents` from extension's package.json
2023-10-12 14:03:43 +02:00
Kasper Svendsen
ee5b738e00 Hash result set name in sorted result set path 2023-10-12 13:58:55 +02:00
Koen Vlaswinkel
25ba9e436b Merge branch 'main' into koesie10/install-workspace-packs 2023-10-12 13:48:19 +02:00
Koen Vlaswinkel
b72e0352c4 Merge branch 'main' into koesie10/storybook-a11y 2023-10-12 13:48:17 +02:00
Robert
95f43b7d8c Merge pull request #2953 from github/robertbrignull/add-multiple-models
Add add/remove buttons for models
2023-10-12 12:47:46 +01:00
Nora
95dfb6e820 Remove onLanguage event 2023-10-12 11:44:41 +00:00
Koen Vlaswinkel
00a5717d78 Merge pull request #2962 from github/koesie10/use-memo
Add missing useMemo import
2023-10-12 13:33:41 +02:00
Koen Vlaswinkel
ea8e5c6cc2 Add missing useMemo import 2023-10-12 13:13:53 +02:00
Koen Vlaswinkel
26b4b98cbc Merge pull request #2959 from github/koesie10/modeled-methods-panel-add-button
Only disable add button when there are no models yet
2023-10-12 13:08:28 +02:00
Koen Vlaswinkel
7e3cb7541c Merge pull request #2936 from github/koesie10/method-modeling-panel-validation
Add model validation in method modeling panel
2023-10-12 13:08:17 +02:00
Robert
77dd376f92 Add content to table header for screen readers 2023-10-12 11:18:41 +01:00
Nora
f53ecde0a9 Remove events 2023-10-12 10:00:14 +00:00
Koen Vlaswinkel
56697a9c2f Add storybook/addon-a11y package 2023-10-12 11:52:47 +02:00
Koen Vlaswinkel
a2a7002263 Do not show non-workspace packs when installing pack dependencies
When using the "CodeQL: Install pack dependencies" command, we would
show packs that are located in the package cache or distribution. Since
there are no dependencies to install for these packs, these options are
not useful.

This will filter out any packs that are not in the workspace folders
when showing the quick pick. This should exclude most packs if you are
in a workspace without the `codeql` submodule and should be a lot more
intuitive in those cases. If you are in a workspace with the `codeql`
submodule, it will still show all the packs.
2023-10-12 11:39:29 +02:00
Robert
369258dc95 Add add/remove buttons for models 2023-10-12 10:32:40 +01:00
Anders Starcke Henriksen
ac4ccf4c65 Merge pull request #2958 from github/version/bump-to-v1.9.3
Bump version to v1.9.3
2023-10-12 11:26:03 +02:00
Koen Vlaswinkel
2453c64f51 Only disable add button when there are no models yet
This will change the add button in the method modeling panel to only be
disabled if there is exactly 1 unmodeled method and there are no
unmodeled methods. This should be more intuitive for users since they
are able to see in 1 screen that there is an unmodeled method.
2023-10-12 11:06:52 +02:00
github-actions[bot]
23927ec0f1 Bump version to v1.9.3 2023-10-12 08:59:04 +00:00
Koen Vlaswinkel
095f5aecc3 Use SetMultipleModeledMethodsMessage in modeled methods panel 2023-10-12 10:36:43 +02:00
Robert
1993db5122 Merge pull request #2952 from github/robertbrignull/save_multiple_modeled_methods
Implement editing / saving multiple modeled methods from the model editor
2023-10-12 09:19:16 +01:00
Anders Starcke Henriksen
53df2bcd87 Merge pull request #2956 from github/v1.9.2
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
Release v1.9.2
2023-10-12 10:18:15 +02:00
Koen Vlaswinkel
bcceae4f51 Add test for adding neutral classification 2023-10-12 10:06:27 +02:00
Koen Vlaswinkel
b6eb383696 Merge remote-tracking branch 'origin/main' into koesie10/method-modeling-panel-validation 2023-10-12 10:02:19 +02:00
Koen Vlaswinkel
0835b140e7 Merge pull request #2948 from github/koesie10/sort-usages-panel
Sort methods in usages panel according to model editor sort order
2023-10-12 09:55:00 +02:00
Anders Starcke Henriksen
0e62d2635c v1.9.2 2023-10-12 09:50:13 +02:00
Koen Vlaswinkel
52e3a71f9c Define order of JSON.stringify operation 2023-10-12 09:49:24 +02:00
Koen Vlaswinkel
cce5a989cf Clarify Alert ARIA documentation 2023-10-12 09:41:47 +02:00
Koen Vlaswinkel
78284cbc7a Merge remote-tracking branch 'origin/main' into koesie10/sort-usages-panel 2023-10-12 09:39:11 +02:00
Koen Vlaswinkel
0d7002273a Merge pull request #2949 from github/koesie10/single-model-editor-per-db
Only allow a single model editor per database
2023-10-12 09:37:56 +02:00
Charis Kyriakou
7041dd7698 Rename 'open database' button and only show if source available (#2945) 2023-10-12 08:09:05 +01:00
Robert
539ce245fc Add more tests covering MethodRow 2023-10-11 16:29:59 +01:00
Koen Vlaswinkel
a3d41a2afe Merge pull request #2924 from github/koesie10/modeling-panel-multiple-models-add-remove
Add ability to add/remove modelings to method modeling panel
2023-10-11 16:20:39 +02:00
Robert
b881a38703 Fix expected value in tests 2023-10-11 15:03:11 +01:00
Koen Vlaswinkel
0cbdadb271 Add newline for readability
Co-authored-by: Robert <robertbrignull@github.com>
2023-10-11 15:59:56 +02:00
Robert
6db59a84a2 Use 'type: "none"' instead of undefined 2023-10-11 14:12:58 +01:00
Robert
eaf81efd64 Extract nested ternary to method 2023-10-11 14:12:10 +01:00
Henry Mercer
5105187dbd Merge pull request #2951 from github/github-action/bump-cli
Bump CLI Version to v2.15.0 for integration tests
2023-10-11 14:03:49 +01:00
Robert
9da3dc9a25 Allow editing multiple models in model editor 2023-10-11 13:41:45 +01:00
Robert
c7451fc4c2 Remove need for forEachModeledMethod method 2023-10-11 13:41:42 +01:00
Koen Vlaswinkel
918362f39e Merge pull request #2950 from github/koesie10/variant-analysis-save-before-start
Use `saveBeforeStart` setting when running a variant analysis
2023-10-11 14:34:08 +02:00
github-actions[bot]
0b5d2d86cf Bump CLI version from v2.14.6 to v2.15.0 for integration tests 2023-10-11 12:30:29 +00:00
Koen Vlaswinkel
3a035708c5 Merge pull request #2947 from github/koesie10/update-set-selected-method-message
Convert `SetSelectedMethodMessage` to include multiple modeled methods
2023-10-11 13:59:48 +02:00
Koen Vlaswinkel
84211c63bb Merge remote-tracking branch 'origin/main' into koesie10/update-set-selected-method-message 2023-10-11 13:40:27 +02:00
Koen Vlaswinkel
11218522e7 Update CHANGELOG 2023-10-11 13:35:11 +02:00
Koen Vlaswinkel
0bdd441767 Use saveBeforeStart setting when running a variant analysis
This will respect the user's `saveBeforeStart` setting when running a
variant analysis. This re-uses the existing `saveBeforeStart` function
that is used when running local queries. The default behavior if the
setting is not set is to save all open named files.
2023-10-11 13:33:24 +02:00
Koen Vlaswinkel
667bf19f46 Add methodSignature to SetMultipleModeledMethodsMessage 2023-10-11 13:07:18 +02:00
Robert
c459d0ff65 Merge pull request #2942 from github/robertbrignull/SetModeledMethodsMessage
Convert SetModeledMethodsMessage to include multiple modeled methods
2023-10-11 11:45:57 +01:00
Koen Vlaswinkel
039b28235d Only allow 1 view in the view tracker
This will change the model editor view tracker to only store 1 view per
database item instead of an array of views per database item.
2023-10-11 11:54:47 +02:00
Koen Vlaswinkel
76db520ce7 Only allow a single model editor per database
This will add checks in the appropriate places to ensure that only a
single model editor is opened per database.
2023-10-11 11:51:05 +02:00
Anders Starcke Henriksen
d2b17e1676 Add tests. 2023-10-11 11:45:01 +02:00
Koen Vlaswinkel
0e1afcee64 Sort methods in usages panel according to model editor sort order
This sorts the methods in the methods usages panel the same as in the
model editor. Since this is dependent on the mode, we need to keep track
of the mode in the modeling store, so this also adds a mode field to the
db state.
2023-10-11 11:42:10 +02:00
Robert
62c9e51c25 Avoid unnnecessarily renaming field read from props 2023-10-11 10:41:48 +01:00
Charis Kyriakou
2b47d3d192 Ensure modeled methods are not undefined in usages panel (#2946) 2023-10-11 08:29:39 +00:00
Koen Vlaswinkel
4a62d05af6 Convert SetSelectedMethodMessage to include multiple modeled methods 2023-10-11 10:14:58 +02:00
Koen Vlaswinkel
e77cf28192 Merge pull request #2944 from github/dependabot/npm_and_yarn/extensions/ql-vscode/postcss-8.4.31
Bump postcss from 8.4.24 to 8.4.31 in /extensions/ql-vscode
2023-10-11 09:51:41 +02:00
Koen Vlaswinkel
59e23f35e2 Merge pull request #2943 from github/koesie10/state-confusion
Fix confusion between modeling store and view states
2023-10-11 09:45:58 +02:00
dependabot[bot]
33d55b1f0a Bump postcss from 8.4.24 to 8.4.31 in /extensions/ql-vscode
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.24 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.24...8.4.31)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-10 15:03:37 +00:00
Koen Vlaswinkel
e7ddb4406a Merge pull request #2937 from github/koesie10/remove-gulp-sourcemaps
Remove `gulp-sourcemaps` dependency
2023-10-10 17:02:00 +02:00
Koen Vlaswinkel
9a12a12065 Unexport DbModelingState 2023-10-10 16:57:19 +02:00
Koen Vlaswinkel
073440914d Set instance variables on webview load 2023-10-10 16:55:55 +02:00
Robert
c6a9f23e7e Delete convertToLegacyModeledMethods 2023-10-10 15:50:02 +01:00
Robert
d531bc642d Convert SetModeledMethodsMessage to include multiple modeled methods 2023-10-10 15:50:02 +01:00
Robert
a6625334f0 Merge pull request #2940 from github/robertbrignull/CommonFromViewMessages
Add CommonFromViewMessages to FromModelEditorMessage
2023-10-10 15:49:04 +01:00
Koen Vlaswinkel
77bb9780ec Fix confusion between modeling store an view states
This fixes three bugs related to the modeling store and view states:
- In the model editor view, when `setModeledMethods` was called, it
would do it on the active database, instead of the database that the
view was showing. This should not result in any visible bugs since the
active database is always the one that is being shown (in theory), but
I can imagine that it could cause issues if showing multiple model
editors next to each other.
- In the method modeling panel, the "reveal in editor" button would
always show the already active model editor. Therefore, if you had
multiple open and were still viewing the method of the first one, it
would always show the second one.
- In the method modeling panel, the same bug would cause the incorrect
modeled methods to be updated.
2023-10-10 16:44:40 +02:00
Koen Vlaswinkel
bb88c148aa Merge pull request #2941 from github/koesie10/modeling-store-modeled-methods-bug
Fix bug when selecting method without modeled methods
2023-10-10 16:18:42 +02:00
Robert
6df7ea3ddc Remove unused import 2023-10-10 15:15:29 +01:00
Robert
2beaf9a88c ViewLoadedMsg is already included 2023-10-10 15:11:30 +01:00
Robert
a7dbae02e0 Use ap logger 2023-10-10 15:11:19 +01:00
Robert
28ac019929 Fix typo 2023-10-10 15:11:09 +01:00
Koen Vlaswinkel
2e2ab11e4f Merge pull request #2939 from github/koesie10/unhandled-error-stack
Add stack trace to unhandled error log message
2023-10-10 16:10:01 +02:00
Robert
edf3cad6e4 Add CommonFromViewMessages to FromModelEditorMessage 2023-10-10 15:03:23 +01:00
Robert
246c347b04 Merge pull request #2938 from github/robertbrignull/legacy-conversion
Make use of modeled-methods-legacy.ts in the webview code
2023-10-10 15:00:52 +01:00
Koen Vlaswinkel
e332b26f29 Fix bug when selecting method without modeled methods
When selecting a method that has no modeled methods, the modeling state
would not contain an entry for the method signature. This would cause
the `modeledMethods` to be `undefined`, which is not allowed according
to its type.
2023-10-10 15:51:59 +02:00
Koen Vlaswinkel
22f6ac7974 Add stack trace to unhandled error log message
This change sets the `fullMessage` of the
`showAndLogExceptionWithTelemetry` to include the stack trace. This
makes it possible to find the source of the error rather than only
knowing that a specific error occurred. If the error does not have a
stack trace (which should be rare) the message will be the same as
before.
2023-10-10 15:45:49 +02:00
Robert
e45f4bd0d9 Merge branch 'main' into robertbrignull/legacy-conversion 2023-10-10 14:37:20 +01:00
Charis Kyriakou
f4d74c7d3f Deal with already modeled methods in the modeling panel (#2933) 2023-10-10 14:35:21 +01:00
Robert
7ba58b6298 Use legacy modeled method conversions instead of custom conversions 2023-10-10 14:23:06 +01:00
Robert
2b59c041b8 Make convertFromLegacyModeledMethod handle undefined 2023-10-10 14:23:06 +01:00
Robert
a434fbffbc Move modeled-methods-legacy.ts to /shared directory 2023-10-10 14:23:06 +01:00
Koen Vlaswinkel
649f69234e Remove gulp-sourcemaps 2023-10-10 14:37:20 +02:00
Anders Starcke Henriksen
c668b39b30 Add language filter panel. 2023-10-10 14:34:04 +02:00
Koen Vlaswinkel
5f4e9d4879 Add alert to show validation errors 2023-10-10 14:29:10 +02:00
Koen Vlaswinkel
6b965afe4f Add validation function for modeled methods 2023-10-10 14:20:44 +02:00
Charis Kyriakou
4ee86c15ad Add some spacing in modeling method empty states (#2934) 2023-10-10 10:52:15 +00:00
Robert
fcfd4f37a6 Merge pull request #2926 from github/robertbrignull/JumpToUsageMessage
Only include method signature in JumpToUsageMessage
2023-10-10 11:12:36 +01:00
Robert
6561bb0543 Merge branch 'main' into robertbrignull/JumpToUsageMessage 2023-10-10 10:50:21 +01:00
Koen Vlaswinkel
319a54c32f Merge pull request #2906 from github/koesie10/convert-remaining-multiple-models
Convert remaining extension host code to handle multiple models
2023-10-10 11:43:09 +02:00
Koen Vlaswinkel
385b0e8d1a Merge remote-tracking branch 'origin/main' into koesie10/convert-remaining-multiple-models 2023-10-10 11:22:08 +02:00
Robert
4dcfa8b679 Merge pull request #2927 from github/robertbrignull/generateMethodsFromLlm
Only include method signatures in generateMethodsFromLlm message
2023-10-10 10:19:20 +01:00
Robert
47509a922a Rename jumpToUsage => jumpToMethod throughout 2023-10-10 10:14:27 +01:00
Charis Kyriakou
7eab7e4e48 Set modeling mode when initialising method modeling panel (#2932) 2023-10-10 10:12:01 +01:00
Charis Kyriakou
f794a19d96 Track db related to modeled method and react when it closes (#2930) 2023-10-10 09:51:50 +01:00
Charis Kyriakou
e98611fd21 Ensure modified methods updated after changing a method from the modeling panel (#2929) 2023-10-10 08:57:36 +01:00
Koen Vlaswinkel
461cf15a47 Merge remote-tracking branch 'origin/main' into koesie10/convert-remaining-multiple-models 2023-10-10 09:30:29 +02:00
Koen Vlaswinkel
54e1b29940 Add explicit return type to convertToLegacyModeledMethod 2023-10-10 09:26:27 +02:00
Dave Bartolomeo
623890adc9 Merge pull request #2922 from github/dbartol/passthru
Add `additionalArgs` option to `launch.json`
2023-10-09 15:04:13 -04:00
Dave Bartolomeo
e7f75ab928 Merge branch 'dbartol/passthru' of https://github.com/github/vscode-codeql into dbartol/passthru 2023-10-09 14:44:49 -04:00
Dave Bartolomeo
df02fecf3c Fix test expectations 2023-10-09 14:44:33 -04:00
Dave Bartolomeo
a861346b10 Merge branch 'main' into dbartol/passthru 2023-10-09 14:06:14 -04:00
Philip Ginsbach
86b2157552 Merge pull request #2792 from github/ginsbach/TextMateInstantiationSyntax
fix syntax highlighting after imports with instantiation arguments
2023-10-09 17:17:55 +01:00
Philip Ginsbach
47fa163cb9 Update compiled grammar 2023-10-09 16:58:00 +01:00
Dave Bartolomeo
c78f01758a Alternate fix for import highlighting with instantiations 2023-10-09 16:58:00 +01:00
Philip Ginsbach
353e22d6e8 link to the PR from the changelog entry 2023-10-09 16:58:00 +01:00
Philip Ginsbach
599d31e5ac add changelog entry for TextMate instantiation argument syntax 2023-10-09 16:57:25 +01:00
Philip Ginsbach
64df792eda TextMate grammar: imports can have instantiation arguments 2023-10-09 16:56:21 +01:00
Robert
feebf7c3fd Only include method signatures in generateMethodsFromLlm 2023-10-09 16:17:46 +01:00
Robert
27c4bd8349 Merge pull request #2910 from github/robertbrignull/multiple-models-method-row
Add ability for MethodRow to render multiple modelings of the same method
2023-10-09 16:17:17 +01:00
Dave Bartolomeo
d4cbfbb70e Rename to additionalRunQueryArgs 2023-10-09 11:04:22 -04:00
Robert
153424ae5a Only include method signature in JumpToUsageMessage 2023-10-09 16:03:41 +01:00
Robert
edd2aa5e8f Merge branch 'main' into robertbrignull/multiple-models-method-row 2023-10-09 15:38:00 +01:00
Robert
0265353370 Use index as react key 2023-10-09 14:44:43 +01:00
Koen Vlaswinkel
623df4c2ee Merge pull request #2919 from github/koesie10/show-multiple-models-listener
Listen to modeling setting changes in the model editor and method modeling panel
2023-10-09 15:38:30 +02:00
Robert
8b0825ab3c Use better name for variable 2023-10-09 14:32:56 +01:00
Robert
c620939895 Merge pull request #2913 from github/robertbrignull/SaveModeledMethods
Remove method and modeledMethods from SaveModeledMethods message
2023-10-09 14:16:30 +01:00
Koen Vlaswinkel
e4a290fe37 Merge pull request #2925 from github/koesie10/method-modeling-codeowner
Set SecExp as codeowner for modeling panel
2023-10-09 15:13:56 +02:00
Koen Vlaswinkel
3230cc9166 Set SecExp as codeowner for modeling panel 2023-10-09 14:56:38 +02:00
Koen Vlaswinkel
4dae9b87d4 Add ability to add/remove modelings 2023-10-09 14:54:32 +02:00
Koen Vlaswinkel
5c368cec8b Merge pull request #2921 from github/koesie10/modeling-panel-multiple-models
Add ability for method modeling panel to render multiple modelings of the same method
2023-10-09 14:27:30 +02:00
Koen Vlaswinkel
b76369330d Convert remaining extension host code to handle multiple models
This converts all remaining extension host code to handle multiple
models per method. The only place where we're using the legacy format
is in the webview and in the boundary between the webview and the
extension host.
2023-10-09 13:56:41 +02:00
Koen Vlaswinkel
323c5368ba Switch from CodiconButton to VSCodeButton 2023-10-09 13:52:55 +02:00
Robert
c3e18910fc Fix typo in comment 2023-10-09 12:26:09 +01:00
Robert
603c799717 Merge pull request #2923 from github/robertbrignull/telemetry-changelog
Update CHANGELOG.md for telemetry changes
2023-10-09 12:24:39 +01:00
Robert
aa045835fd Merge branch 'main' into robertbrignull/SaveModeledMethods 2023-10-09 11:35:47 +01:00
Robert
850c04a289 Clarify method docs 2023-10-09 11:30:35 +01:00
Robert
4d19955740 Update CHANGELOG.md for telemetry changes 2023-10-09 11:08:31 +01:00
Robert
b623f92c4a Merge pull request #2824 from github/robertbrignull/telemetry
Respect "telemetry.telemetryLevel" as well as "telemetry.enableTelemetry"
2023-10-09 10:59:26 +01:00
Koen Vlaswinkel
08342f1960 Merge pull request #2905 from github/koesie10/convert-modeled-method-fs
Convert `modeled-method-fs.ts` to handle multiple models per method
2023-10-09 11:54:31 +02:00
Robert
af334a98e3 Merge pull request #2912 from github/robertbrignull/simplify_messages/RevealMethodMessage
Convert RevealMethodMessage to only include method signature
2023-10-09 10:46:19 +01:00
Dave Bartolomeo
9e26c29ddb Fix tests for older CLIs 2023-10-06 18:35:20 -04:00
Dave Bartolomeo
705a7975c5 Add additionalArgs option to launch.json 2023-10-06 18:03:10 -04:00
Koen Vlaswinkel
6be98f3f8d Fix test 2023-10-06 16:57:31 +02:00
Koen Vlaswinkel
3b6263fb07 Remove size from CodiconButton 2023-10-06 16:48:49 +02:00
Koen Vlaswinkel
29aa4a3f29 Add tests for multiple modeled methods panel 2023-10-06 16:42:41 +02:00
Koen Vlaswinkel
3a494dff36 Add support for unmodeled methods 2023-10-06 16:26:53 +02:00
Koen Vlaswinkel
a01a76cb73 Add initial multiple modelings panel 2023-10-06 16:25:25 +02:00
Koen Vlaswinkel
a29112e045 Add multiple models to MethodModeling story 2023-10-06 15:54:02 +02:00
Koen Vlaswinkel
ee1bf8896e Change MethodModeling to accept multiple models 2023-10-06 15:50:31 +02:00
Koen Vlaswinkel
2410d2bfdd Merge pull request #2918 from github/koesie10/modeling-panel-view-state
Add view state to method modeling panel
2023-10-06 15:49:37 +02:00
Charis Kyriakou
0e5551b650 Make method modeling panel available to canary users (#2920) 2023-10-06 13:25:33 +00:00
Koen Vlaswinkel
951bd13881 Use ModelConfig for all model settings
This switches all places where we're retrieving some model configuration
to use the `ModelConfig` or `ModelConfigListener` types. This makes it
much easier to mock these settings in tests.

This also adds a listener to the `ModelEditorView` to send the new view
state when any of the settings is changed. This should make it easier
to test settings changes in the model editor without having to re-open
the model editor.
2023-10-06 14:29:07 +02:00
Koen Vlaswinkel
08944a292c Update method modeling panel after changes to config
This updates the method modeling panel's view state when the
`codeQL.model.showMultipleModels` setting changes. This will ensure that
the setting updates without needing to restart VS Code since this view
is much harder to restart than the model editor.
2023-10-06 14:15:23 +02:00
Koen Vlaswinkel
cf0057ecd9 Add view state to method modeling panel
This adds a view state to the method modeling panel similar to the
model editor. This will be used to send the state of the show multiple
models feature flag to the webview so this can be used to selectively
show/hide components in the method modeling panel.
2023-10-06 14:04:06 +02:00
Charis Kyriakou
1d691c2b7f UI polish for modeling panel (#2917)
* Don't push content down with unsaved tag

* Adjust spacing between components
2023-10-06 11:57:52 +01:00
Charis Kyriakou
20c63921f7 Wire up modeling panel empty states (#2915) 2023-10-06 11:27:16 +01:00
Koen Vlaswinkel
9c275130a5 Fix duplicate modeled methods
Co-authored-by: Robert <robertbrignull@github.com>
2023-10-06 12:09:00 +02:00
Henry Mercer
b1df4a4f0a Merge pull request #2916 from github/henrymercer/remove-ml-powered-queries
Packaging: Remove ML-powered queries pack from known list
2023-10-06 11:02:27 +01:00
Henry Mercer
08a4457e39 Packaging: Remove ML-powered queries pack from known list
ML-powered queries are [now deprecated](https://github.blog/changelog/2023-09-29-codeql-code-scanning-deprecates-ml-powered-alerts/).
2023-10-06 10:38:14 +01:00
Charis Kyriakou
db3d24236c Add empty states for modeling panel (#2914) 2023-10-06 09:34:54 +01:00
Robert
9d40d9a703 Remove method and modeledMethods from SaveModeledMethods message 2023-10-05 16:56:05 +01:00
Robert
0fabcda49b Convert RevealMethodMessage to only include method signature 2023-10-05 16:41:56 +01:00
Robert
2b0bd840e6 Place setup before initialising listener 2023-10-05 15:25:42 +01:00
Robert
d499ff3cf4 Merge branch 'main' into robertbrignull/telemetry 2023-10-05 15:19:07 +01:00
Charis Kyriakou
1806108166 Remove unnecessary TODOs (#2900) 2023-10-05 08:04:17 +00:00
Andrew Eisenberg
02af432d5f Merge pull request #2885 from github/aeisenberg/multi-token
Adds `MultiCancellationToken`
2023-10-04 13:33:46 -07:00
Andrew Eisenberg
76ec9e2e50 Make DisposableObject a concrete class 2023-10-04 20:10:42 +00:00
Robert
5ba64a1db3 Add test for when there's no modeled method 2023-10-04 18:46:17 +01:00
Robert
f87b1c46de Add some simple tests of rendering multiple models 2023-10-04 18:46:17 +01:00
Robert
8eef4eb4f2 Add story with multiple modelings 2023-10-04 18:46:17 +01:00
Robert
252c7a20c4 Convert MethodRow to display multiple modelings 2023-10-04 18:46:13 +01:00
Robert
86d7d8345c Convert ApiOrMethodCell to ApiOrMethodRow 2023-10-04 18:17:34 +01:00
Robert
a704cd7bae Convert getModelingStatus to take ModeledMethod[] 2023-10-04 18:17:34 +01:00
Robert
e75eccb416 Change MethodRow to accept ModelEditorViewState object instead of just Mode 2023-10-04 18:17:34 +01:00
Charis Kyriakou
17947fb7c2 Use base postMessage instead of webview one (#2909) 2023-10-04 17:23:49 +01:00
Charis Kyriakou
c00207cccc Move method modeling panel out of explorer (#2908) 2023-10-04 15:38:07 +01:00
Andrew Eisenberg
801df7b14a Merge branch 'main' into aeisenberg/multi-token 2023-10-04 07:09:21 -07:00
Koen Vlaswinkel
a6c7af09d4 Merge pull request #2907 from github/koesie10/eslint-memory
Increase memory limit when running ESLint
2023-10-04 15:11:26 +02:00
Koen Vlaswinkel
96668928aa Increase memory limit when running ESLint 2023-10-04 14:46:32 +02:00
Koen Vlaswinkel
a6c97077fb Merge pull request #2904 from github/koesie10/convert-yaml-modeled-methods
Convert `yaml.ts` to handle multiple models per method
2023-10-04 14:01:02 +02:00
Robert
6d7fbfc4f8 Merge pull request #2903 from github/robertbrignull/multiple-models-feature-flag
Add feature flag for showing multiple modelings
2023-10-04 12:55:42 +01:00
Koen Vlaswinkel
c5175e040e Convert modeled-method-fs.ts to handle multiple models per method
This will change the input/output types for modeled methods in the
`modeled-method-fs.ts` file to take in multiple models per method. This
removes the need for conversion functions between this file and
`yaml.ts` files. Instead, the conversion functions are done when calling
any functions defined in `modeled-method-fs.ts` files.
2023-10-04 13:47:30 +02:00
Anders Starcke Henriksen
55af9bc47c Merge pull request #2894 from github/starcke/lang-context-history
Add support for filtering history panel.
2023-10-04 13:38:38 +02:00
Koen Vlaswinkel
ada62ffd33 Convert yaml.ts to handle multiple models per method
This changes YAML parsing/creating functions for the model editor to
handle multiple models per method. The changes in the actual YAML
handling are fairly small because the format itself already supports
multiple models per method.

I've introduced a few helper functions to convert between the old and
new types. This should only be necessary while we're in the middle of
the transition to the new types and can be removed later. For now,
we'll just take the first model in the array when converting from the
new to the old type. This is a change in the behavior since currently
we always take the last model in the array but this behavior is
undocumented and unsupported, so it should be fine to change it.
2023-10-04 13:23:54 +02:00
Robert
e10e3adc14 Add feature flag for showing multiple modelings 2023-10-04 12:21:47 +01:00
Koen Vlaswinkel
16af4c49ea Merge pull request #2902 from github/koesie10/actions-nvmrc
Use .nvmrc file for setting up Node version in Actions
2023-10-04 11:36:22 +02:00
Koen Vlaswinkel
c970c3bc19 Use .nvmrc file for setting up Node version in Actions 2023-10-04 10:00:56 +02:00
Andrew Eisenberg
3699f0b3b3 Add BasicDisposableObject
Use it for `MultiCancellationToken`. And ensure that adding a
cancellation requested listener to the `MultiCancellationToken` will
forward any cancellation requests to all constituent tokens.
2023-10-03 21:42:00 +00:00
Andrew Eisenberg
ccbe4ee974 Merge branch 'main' into aeisenberg/multi-token 2023-10-03 07:52:54 -07:00
Anders Starcke Henriksen
486180d149 Fix and add tests. 2023-10-03 13:57:16 +02:00
Anders Starcke Henriksen
9362447338 Add support for filtering history panel. 2023-10-03 13:55:01 +02:00
Anders Starcke Henriksen
ff34079247 Merge pull request #2892 from github/starcke/local-query-lang-dto
Add language to local query history items
2023-10-03 13:53:32 +02:00
Koen Vlaswinkel
7cd99cb000 Merge pull request #2897 from github/dependabot/npm_and_yarn/extensions/ql-vscode/p-queue-7.4.1
Bump p-queue from 6.6.2 to 7.4.1 in /extensions/ql-vscode
2023-10-03 12:53:55 +02:00
Koen Vlaswinkel
21fd0cfd29 Transform p-queue and p-timeout modules in Jest tests 2023-10-03 11:52:51 +02:00
dependabot[bot]
db3337cc1b Bump p-queue from 6.6.2 to 7.4.1 in /extensions/ql-vscode
Bumps [p-queue](https://github.com/sindresorhus/p-queue) from 6.6.2 to 7.4.1.
- [Release notes](https://github.com/sindresorhus/p-queue/releases)
- [Commits](https://github.com/sindresorhus/p-queue/compare/v6.6.2...v7.4.1)

---
updated-dependencies:
- dependency-name: p-queue
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 09:29:01 +00:00
Koen Vlaswinkel
a525499c2a Merge pull request #2818 from github/dependabot/npm_and_yarn/extensions/ql-vscode/nanoid-5.0.1
Bump nanoid from 3.3.6 to 5.0.1 in /extensions/ql-vscode
2023-10-03 11:26:00 +02:00
Anders Starcke Henriksen
6d2859cee5 Fix typo. 2023-10-03 11:23:32 +02:00
Koen Vlaswinkel
ac9128735f Merge pull request #2896 from github/koesie10/reveal-in-editor
Add reveal in editor button to method modeling panel
2023-10-03 11:12:44 +02:00
Anders Starcke Henriksen
2c0838e393 Merge branch 'main' into starcke/local-query-lang-dto 2023-10-03 11:01:46 +02:00
Anders Starcke Henriksen
b294e16c44 Add to changelog. 2023-10-03 11:01:03 +02:00
Koen Vlaswinkel
f678f49bc9 Transform nanoid module in Jest tests 2023-10-03 10:48:08 +02:00
dependabot[bot]
99dabb0779 Bump nanoid from 3.3.6 to 5.0.1 in /extensions/ql-vscode
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.6 to 5.0.1.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.6...5.0.1)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 08:13:33 +00:00
Koen Vlaswinkel
add7a25578 Merge pull request #2877 from github/robertbrignull/upgrade_msw
Upgrade MSW
2023-10-03 10:08:34 +02:00
Koen Vlaswinkel
487a753ede Reveal method in editor view
This will reveal a method for which "Review in editor" is clicked in the
model editor view: it will expand the group (library/package) in which
the method is located, scroll to the method, and highlight the method.

If the user clicks anywhere on the page, the highlight will be removed,
but the group will remain expanded.
2023-10-03 09:49:00 +02:00
Koen Vlaswinkel
79ea901a52 Send message to model editor view on revealing method
This will call a method on the correct model editor view when the user
clicks on "Review in editor". This does not yet do anything to the view;
this will be added in a follow-up commit.
2023-10-03 09:48:59 +02:00
Koen Vlaswinkel
2872a2dcaf Add model editor view tracker
This is used for registering which model editor views are currently
active. This will be used to determine which view to send the "reveal
method" command to. It can also be used in the future to limit the
number of instances of the model editor that can be opened for a
database.

This uses the same pattern as variant analyses with a separate interface
for the view to avoid having circular dependencies.
2023-10-03 09:48:17 +02:00
Koen Vlaswinkel
8b8bacb718 Add button for reveal method in editor 2023-10-03 09:47:52 +02:00
Koen Vlaswinkel
83e38c811f Update CHANGELOG 2023-10-03 09:36:59 +02:00
Koen Vlaswinkel
8107bf7cb1 Merge remote-tracking branch 'origin/main' into robertbrignull/upgrade_msw 2023-10-03 09:36:03 +02:00
Andrew Eisenberg
625b3a5c37 Merge branch 'main' into aeisenberg/multi-token 2023-10-02 20:07:31 -07:00
Andrew Eisenberg
0fa3cf5d8e Use EventEmitter in MultiCancellationToken 2023-10-02 10:21:17 -07:00
Charis Kyriakou
558b9329c5 Extract base functionality for WebviewViewProviders into an abstract class (#2895) 2023-10-02 16:31:52 +01:00
Koen Vlaswinkel
e15b7681db Bump @types/vscode to 1.82.0 2023-10-02 11:14:42 +02:00
Koen Vlaswinkel
6539417d2d Fix recording of binary response bodies 2023-10-02 11:13:18 +02:00
Koen Vlaswinkel
095d56ee37 Fix typo in BasicErrorResponse 2023-10-02 11:02:44 +02:00
Koen Vlaswinkel
7578697e92 Fix response bodies when recording 2023-10-02 11:02:20 +02:00
Anders Starcke Henriksen
6e06e7934b Merge pull request #2889 from github/starcke/lang-context-queries
Apply language context to queries panel
2023-10-02 10:27:00 +02:00
Charis Kyriakou
58249e3efe Dispose event subscriptions in method modeling view (#2882) 2023-10-02 08:57:33 +01:00
Andrew Eisenberg
75540b449f Merge pull request #2884 from github/aeisenberg/avoid-double-restart
Avoid double restarts of the query server
2023-09-29 11:04:37 -07:00
Andrew Eisenberg
04dfc4e647 Move location of multi-cancellation-token
This avoids a code-scanning warning.
2023-09-29 08:31:20 -07:00
Andrew Eisenberg
3ba1712be0 Update Changelog 2023-09-29 08:30:51 -07:00
Andrew Eisenberg
e8f68c1b5f Adds MultiCancellationToken
This is a cancellation token that cancels when any of its constituent
cancellation tokens are cancelled.

This token is used to fix a bug in Find Definitions. Previously, when
clicking `CTRL` (or `CMD` on macs) inside a source file in an archive
and hovering over a token, this will automatically invoke the
definitions finder (in preparation for navigating to the definition).
The only way to cancel is to move down to the message popup and click
cancel there.

However, this is a bug. What _should_ happen is that if a user moves
their mouse away from the token, the operation should cancel.

The underlying problem is that the extension was only listening to the
cancellation token from inside `getLocationsForUriString` the
cancellation token used by the Language Server protocol to cancel
operations in flight was being ignored.

This fix will ensure we are listening to _both_ cancellation tokens
and cancel the query if either are cancelled.
2023-09-29 08:28:35 -07:00
Andrew Eisenberg
a9edb36242 Merge branch 'main' into aeisenberg/avoid-double-restart 2023-09-29 08:26:53 -07:00
Koen Vlaswinkel
40b79f2e61 Improve scenario recording 2023-09-29 14:34:12 +02:00
Charis Kyriakou
3489c26ef6 Styling updates to method modeling panel (#2888) 2023-09-29 13:02:29 +01:00
Koen Vlaswinkel
6b522819fd Merge pull request #2893 from github/version/bump-to-v1.9.2
Bump version to v1.9.2
2023-09-29 13:20:59 +02:00
Koen Vlaswinkel
15b8d2bdb4 Merge pull request #2890 from github/koesie10/add-missing-feature-flag-to-test-plan
Add missing feature flag to test plan
2023-09-29 13:10:27 +02:00
github-actions[bot]
213a03555a Bump version to v1.9.2 2023-09-29 10:27:33 +00:00
Anders Starcke Henriksen
c4fe868826 Add a way to show language for qury history. 2023-09-29 12:15:55 +02:00
Anders Starcke Henriksen
c43d0fa805 Add language to local queries. 2023-09-29 11:54:45 +02:00
Koen Vlaswinkel
e798663bbb Add missing feature flag to test plan 2023-09-29 11:12:04 +02:00
Anders Starcke Henriksen
169a425e0b Update and add tests. 2023-09-29 11:07:06 +02:00
Anders Starcke Henriksen
1e6b7a6619 Add support for filtering queries panel. 2023-09-29 09:28:44 +02:00
Andrew Eisenberg
2b17979b6c Update changelog 2023-09-28 14:29:40 -07:00
Andrew Eisenberg
3999ae3728 Avoid double restarts of the query server
Previously, if there was an explicit restart of the query server (eg-
by changing a configuration setting), then the query server process
would be started twice: once by the `close` handler and once by the
restart command.

By adding the `removeAllListeners` to the dispose method, we ensure that
when the query server shuts down gracefully, there won't be a `close`
listener that is going to restart it a second time if there is a
different way of restarting it.
2023-09-28 14:25:35 -07:00
Koen Vlaswinkel
ae2d6ce16e Upgrade Node version to 18.15.0 2023-09-28 16:38:54 +02:00
Koen Vlaswinkel
8b1c52886a Fix typing error for Octokit 2023-09-28 16:09:16 +02:00
Koen Vlaswinkel
f9ddb4080c Switch to stable VS Code version for tests 2023-09-28 16:07:34 +02:00
Koen Vlaswinkel
3afa15a0ce Remove redundant query params 2023-09-28 16:07:05 +02:00
Koen Vlaswinkel
f22714777c Use AppOctokit in tests 2023-09-28 16:06:34 +02:00
Koen Vlaswinkel
9b10a09727 Fix MSW JSON responses 2023-09-28 14:32:33 +02:00
Koen Vlaswinkel
3c10e87529 Switch Octokit to use node-fetch
It seems like Node's native `fetch` implementation isn't quite working
right with Octokit and MSW. This switches to using `node-fetch` like
we're already doing for all other requests (e.g. downloading databases).
2023-09-28 14:32:32 +02:00
Koen Vlaswinkel
a1ea1f8135 Upgrade Octokit and MSW 2023-09-28 14:32:16 +02:00
Robert
ffc90a0c30 Remove startServer method as it now does nothing 2023-09-28 14:32:15 +02:00
Robert
e34f4ed485 Only start server when loading a scenario 2023-09-28 14:32:15 +02:00
Robert
7c1b6e2ff6 Use new types 2023-09-28 14:32:15 +02:00
Robert
e719df711a Upgrade msw to the @next tag 2023-09-28 14:32:15 +02:00
Robert
4227ff6338 Mock env.itTelemetryEnable 2023-09-19 17:24:53 +01:00
Robert
ca96cdf879 Mock sendTelemetryErrorEvent and check appropriately 2023-09-19 17:23:47 +01:00
Robert
aad1fee787 Stop mocking sendTelemetryException because we don't use that anywhere in the program 2023-09-19 17:18:38 +01:00
Robert
48399a9aeb Merge branch 'main' into robertbrignull/telemetry 2023-09-19 16:40:07 +01:00
Robert
177c770f4d Delete config values that we on longer read directly 2023-09-18 11:15:34 +01:00
Robert
d4033615c8 Use onDidChangeTelemetryEnabled instead of listening for config value changes 2023-09-18 11:14:11 +01:00
Robert
9ec6590548 Use env.isTelemetryEnabled instead of checking configuration values ourselves 2023-09-18 11:14:11 +01:00
Robert
5fc9a73e20 Add telemtry tag to settings 2023-09-18 11:14:11 +01:00
Robert
af5547fb77 Use correct values of telemtetry level 2023-09-18 11:14:03 +01:00
Robert
a506276635 Update custom telemetry setting description 2023-09-15 16:14:34 +01:00
Robert
f3e72a0ab8 Delete unused relevantSettings field 2023-09-15 16:04:58 +01:00
Robert
c7a9337ac0 Use .qualifiedName instead of writing out manually 2023-09-15 16:04:58 +01:00
Robert
c7bd343f54 Use sendTelemetryErrorEvent when sending errors 2023-09-15 16:04:58 +01:00
Robert
9c76ba35f1 Check both telemetry.telemetryLevel and telemetry.enableTelemetry 2023-09-15 16:04:13 +01:00
Robert
90093fb9f5 Rearrange settings so the telemetry settings are clearer 2023-09-15 15:01:32 +01:00
251 changed files with 13405 additions and 5110 deletions

View File

@@ -60,9 +60,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.17.1'
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json

View File

@@ -20,9 +20,9 @@ jobs:
with:
fetch-depth: 1
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.17.1'
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json
@@ -62,9 +62,9 @@ jobs:
with:
fetch-depth: 1
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.17.1'
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json
@@ -81,6 +81,8 @@ jobs:
- name: Lint
working-directory: extensions/ql-vscode
env:
NODE_OPTIONS: '--max-old-space-size=4096'
run: |
npm run lint
@@ -108,9 +110,9 @@ jobs:
with:
fetch-depth: 1
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.17.1'
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json
@@ -147,9 +149,9 @@ jobs:
with:
fetch-depth: 1
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.17.1'
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json
@@ -181,9 +183,9 @@ jobs:
with:
fetch-depth: 1
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.17.1'
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json
@@ -249,9 +251,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.17.1'
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json

View File

@@ -20,9 +20,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.17.1'
node-version-file: extensions/ql-vscode/.nvmrc
- name: Install dependencies
run: |

View File

@@ -1,5 +1,6 @@
**/* @github/codeql-vscode-reviewers
**/variant-analysis/ @github/code-scanning-secexp-reviewers
**/databases/ @github/code-scanning-secexp-reviewers
**/method-modeling/ @github/code-scanning-secexp-reviewers
**/model-editor/ @github/code-scanning-secexp-reviewers
**/queries-panel/ @github/code-scanning-secexp-reviewers

View File

@@ -15,12 +15,13 @@ You can find this info by seleting "About Visual Studio Code" from the top menu.
The following files will need to be updated:
- `.github/workflows/cli-test.yml` - the "node-version: '[VERSION]'" setting
- `.github/workflows/main.yml` - all the "node-version: '[VERSION]'" settings
- `.github/workflows/release.yml` - the "node-version: '[VERSION]'" setting
- `extensions/ql-vscode/.nvmrc` - this will enable nvm to automatically switch to the correct node version when you're in the project folder
- `extensions/ql-vscode/package-lock.json` - the "engines.node: '[VERSION]'" setting
- `extensions/ql-vscode/.nvmrc` - this will enable nvm to automatically switch to the correct Node
version when you're in the project folder. It will also change the Node version the GitHub Actions
workflows use.
- `extensions/ql-vscode/package.json` - the "engines.node: '[VERSION]'" setting
- `extensions/ql-vscode/package.json` - the "@types/node: '[VERSION]'" dependency
Then run `npm install` to update the `extensions/ql-vscode/package-lock.json` file.
## Node.js version used in tests

View File

@@ -151,17 +151,20 @@ Run one of the above MRVAs, but cancel it from within VS Code:
2. Open the Model Editor with the "CodeQL: Open CodeQL Model Editor" command from the command palette.
- Check that the editor loads and shows methods to model.
- Check that methods are grouped per library (e.g. `rocksdbjni@7.7.3` or `asm@6.0`)
- Check that the "Open database" link works.
- Check that the "Open source" link works.
- Check that the 'View' button works and the Method Usage panel highlight the correct method and usage
- Check that the Method Modeling panel shows the correct method and modeling state
#### Test Case 2: Model methods
1. Expand one of the libraries.
- Change the model type and check that the other dropdowns change.
- Check that the method modeling panel updates accordingly
2. Save the modeled methods.
3. Click "Open extension pack"
- Check that the file explorer opens a directory with a "models" directory
4. Open the ".model.yml" file corresponding to the library that was changed.
- Check that the file contrains the entries that was modeled.
- Check that the file contains entries for the methods that were modeled.
#### Test Case 3: Model with AI
@@ -173,6 +176,8 @@ Note that this test requires the feature flag: `codeQL.model.llmGeneration`
#### Test Case 4: Model as dependency
Note that this test requires the feature flag: `codeQL.model.flowGeneration`
1. Click "Model as dependency"
- Check that grouping are now per package (e.g. `com.alipay.sofa.rraft.option` or `com.google.protobuf`)
2. Click "Generate".
@@ -187,9 +192,28 @@ Are there any components that are not showing up?
## Optional Test Cases
These are mostly aimed at MRVA, but some of them are also applicable to non-MRVA queries.
### Modeling Flow
### Selecting repositories to run on
1. Check that a method can have multiple models:
- Add a couple of new models for one method in the model editor
- Save and check that the modeling file (use the 'open extension pack' button to open it) shows multiple methods
- Check that the Method Modeling Panel shows the correct multiple models
- Check that you can browse through different models in the Method Modeling Panel
- Check that a 'duplicated classification' error appears in both model editor and modeling panel when a duplicate modeling occurs
- Check that a 'conflicting classification' error appears when a neutral model type is paired with a model of the same kind
- Check that clicking on the error highlights the correct modeling in both the editor and the modeling panel
2. Check the Method Usage Panel
- Check that the Method Usage Panel opens and jumps to the correct usage when clicking on 'View' in the model editor
- Check that the first and following usages are opening when clicking on a usage
- Check that the usage icon color turns green when saving a newly modeled method
- Check that the usage icon color turns red when saving a newly unmodeld method
3. Check the Method Modeling Panel
- Check that the 'Start modeling' button opens a new model editor
- Check that it refreshes the blank state when a model editor is opened/closed
- Check that when modeling in the editor the modeling panel updates accordingly
- Check that when modeling in the modeling panel the model editor updates accordingly
### Selecting MRVA repositories to run on
#### Test case 1: Running a query on a single repository
@@ -219,7 +243,7 @@ These are mostly aimed at MRVA, but some of them are also applicable to non-MRVA
4. The org contains private repositories that are inaccessible
2. The org does not exist
### Using different types of controller repos
### Using different types of controller repos for MRVA
#### Test case 1: Running a query when the controller repository is public

View File

@@ -27,7 +27,7 @@ Pre-requisites:
Then, from the `extensions/ql-vscode` directory, use the appropriate command to run the tests:
* Unit tests: `npm run test:unit`
* View Tests: `npm test:view`
* View Tests: `npm run test:view`
* VSCode integration tests: `npm run test:vscode-integration`
#### Running CLI integration tests from the terminal
@@ -48,8 +48,8 @@ Alternatively, you can run the tests inside of VSCode. There are several VSCode
You will need to run tests using a task from inside of VS Code, under the "Run and Debug" view:
* Unit tests: run the _Launch Unit Tests - React_ task
* View Tests: run the _Launch Unit Tests_ task
* Unit tests: run the _Launch Unit Tests_ task
* View Tests: run the _Launch Unit Tests - React_ task
* VSCode integration tests: run the _Launch Unit Tests - No Workspace_ and _Launch Unit Tests - Minimal Workspace_ tasks
#### Running CLI integration tests from VSCode

View File

@@ -71,6 +71,7 @@ const baseConfig = {
"no-shadow": "off",
"github/array-foreach": "off",
"github/no-then": "off",
"react/jsx-key": ["error", { checkFragmentShorthand: true }],
},
};

View File

@@ -1 +1 @@
v16.17.1
v18.15.0

View File

@@ -6,6 +6,7 @@ const config: StorybookConfig = {
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-a11y",
"./vscode-theme-addon/preset.ts",
],
framework: {

View File

@@ -1,5 +1,30 @@
# CodeQL for Visual Studio Code: Changelog
## 1.9.4 - 6 November 2023
No user facing changes.
## 1.9.3 - 26 October 2023
- Sorted result set filenames now include a hash of the result set name instead of the full name. [#2955](https://github.com/github/vscode-codeql/pull/2955)
- The "Install Pack Dependencies" will now only list CodeQL packs located in the workspace. [#2960](https://github.com/github/vscode-codeql/pull/2960)
- Fix a bug where the "View Query Log" action for a query history item was not working. [#2984](https://github.com/github/vscode-codeql/pull/2984)
- Add a command to sort items in the databases view by language. [#2993](https://github.com/github/vscode-codeql/pull/2993)
- Fix not being able to open the results directory or evaluator log for a cancelled local query run. [#2996](https://github.com/github/vscode-codeql/pull/2996)
- Fix empty row in alert path when the SARIF location was empty. [#3018](https://github.com/github/vscode-codeql/pull/3018)
## 1.9.2 - 12 October 2023
- Fix a bug where the query to Find Definitions in database source files would not be cancelled appropriately. [#2885](https://github.com/github/vscode-codeql/pull/2885)
- It is now possible to show the language of query history items using the `%l` specifier in the `codeQL.queryHistory.format` setting. Note that this only works for queries run after this upgrade, and older items will show `unknown` as a language. [#2892](https://github.com/github/vscode-codeql/pull/2892)
- Increase the required version of VS Code to 1.82.0. [#2877](https://github.com/github/vscode-codeql/pull/2877)
- Fix a bug where the query server was restarted twice after configuration changes. [#2884](https://github.com/github/vscode-codeql/pull/2884).
- Add support for the `telemetry.telemetryLevel` setting. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code). [#2824](https://github.com/github/vscode-codeql/pull/2824).
- Add a "CodeQL: Trim Cache" command that clears the evaluation cache of a database except for predicates annotated with the `cached` keyword. Its purpose is to get accurate performance measurements when tuning the final stage of a query, like a data-flow configuration. This is equivalent to the `codeql database cleanup --mode=normal` CLI command. In contrast, the existing "CodeQL: Clear Cache" command clears the entire cache. CodeQL CLI v2.15.1 or later is required. [#2928](https://github.com/github/vscode-codeql/pull/2928)
- Fix syntax highlighting directly after import statements with instantiation arguments. [#2792](https://github.com/github/vscode-codeql/pull/2792)
- The `debug.saveBeforeStart` setting is now respected when running variant analyses. [#2950](https://github.com/github/vscode-codeql/pull/2950)
- The 'open database' button of the model editor was renamed to 'open source'. Also, it's now only available if the source archive is available as a workspace folder. [#2945](https://github.com/github/vscode-codeql/pull/2945)
## 1.9.1 - 29 September 2023
- Add warning when using a VS Code version older than 1.82.0. [#2854](https://github.com/github/vscode-codeql/pull/2854)

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.9.1",
"version": "1.9.4",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -13,8 +13,8 @@
"url": "https://github.com/github/vscode-codeql"
},
"engines": {
"vscode": "^1.67.0",
"node": "^16.17.1",
"vscode": "^1.82.0",
"node": "^18.15.0",
"npm": ">=7.20.6"
},
"categories": [
@@ -34,27 +34,7 @@
}
},
"activationEvents": [
"onLanguage:ql",
"onLanguage:ql-summary",
"onView:codeQLQueries",
"onView:codeQLDatabases",
"onView:codeQLVariantAnalysisRepositories",
"onView:codeQLQueryHistory",
"onView:codeQLAstViewer",
"onView:codeQLEvalLogViewer",
"onView:test-explorer",
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.authenticateToGitHub",
"onCommand:codeQL.viewAst",
"onCommand:codeQL.viewCfg",
"onCommand:codeQL.openReferencedFile",
"onCommand:codeQL.previewQueryHelp",
"onCommand:codeQL.chooseDatabaseFolder",
"onCommand:codeQL.chooseDatabaseArchive",
"onCommand:codeQL.chooseDatabaseInternet",
"onCommand:codeQL.chooseDatabaseGithub",
"onCommand:codeQL.quickQuery",
"onCommand:codeQL.restartQueryServer",
"onWebviewPanel:resultsView",
"onWebviewPanel:codeQL.variantAnalysis",
"onWebviewPanel:codeQL.dataFlowPaths",
@@ -110,6 +90,10 @@
"string"
],
"description": "Names of extension packs to include in the evaluation. These are resolved from the locations specified in `additionalPacks`."
},
"additionalRunQueryArgs": {
"type": "object",
"description": "**Internal use only**. Additional arguments to pass to the `runQuery` command of the query server, without validation."
}
}
}
@@ -446,13 +430,20 @@
"type": "boolean",
"default": false,
"scope": "application",
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)"
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the one of the global telemetry settings (`#telemetry.enableTelemetry#` or `#telemetry.telemetryLevel#`) must be enabled for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)",
"tags": [
"telemetry",
"usesOnlineServices"
]
},
"codeQL.telemetry.logTelemetry": {
"type": "boolean",
"default": false,
"scope": "application",
"description": "Specifies whether or not to write telemetry events to the extension log."
"description": "Specifies whether or not to write telemetry events to the extension log.",
"tags": [
"telemetry"
]
}
}
}
@@ -570,6 +561,10 @@
"command": "codeQL.copyVersion",
"title": "CodeQL: Copy Version Information"
},
{
"command": "codeQLLanguageSelection.setSelectedItem",
"title": "Select"
},
{
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
"title": "Run local query",
@@ -721,6 +716,10 @@
"command": "codeQL.clearCache",
"title": "CodeQL: Clear Cache"
},
{
"command": "codeQL.trimCache",
"title": "CodeQL: Trim Cache"
},
{
"command": "codeQL.installPackDependencies",
"title": "CodeQL: Install Pack Dependencies"
@@ -753,78 +752,6 @@
"command": "codeQLDatabases.addDatabaseSource",
"title": "Add Database Source to Workspace"
},
{
"command": "codeQLDatabases.displayAllLanguages",
"title": "All languages"
},
{
"command": "codeQLDatabases.displayAllLanguagesSelected",
"title": "All languages (selected)"
},
{
"command": "codeQLDatabases.displayCpp",
"title": "C/C++"
},
{
"command": "codeQLDatabases.displayCppSelected",
"title": "C/C++ (selected)"
},
{
"command": "codeQLDatabases.displayCsharp",
"title": "C#"
},
{
"command": "codeQLDatabases.displayCsharpSelected",
"title": "C# (selected)"
},
{
"command": "codeQLDatabases.displayGo",
"title": "Go"
},
{
"command": "codeQLDatabases.displayGoSelected",
"title": "Go (selected)"
},
{
"command": "codeQLDatabases.displayJava",
"title": "Java/Kotlin"
},
{
"command": "codeQLDatabases.displayJavaSelected",
"title": "Java/Kotlin (selected)"
},
{
"command": "codeQLDatabases.displayJavascript",
"title": "JavaScript/TypeScript"
},
{
"command": "codeQLDatabases.displayJavascriptSelected",
"title": "JavaScript/TypeScript (selected)"
},
{
"command": "codeQLDatabases.displayPython",
"title": "Python"
},
{
"command": "codeQLDatabases.displayPythonSelected",
"title": "Python (selected)"
},
{
"command": "codeQLDatabases.displayRuby",
"title": "Ruby"
},
{
"command": "codeQLDatabases.displayRubySelected",
"title": "Ruby (selected)"
},
{
"command": "codeQLDatabases.displaySwift",
"title": "Swift"
},
{
"command": "codeQLDatabases.displaySwiftSelected",
"title": "Swift (selected)"
},
{
"command": "codeQL.chooseDatabaseFolder",
"title": "CodeQL: Choose Database from Folder"
@@ -845,6 +772,10 @@
"command": "codeQLDatabases.sortByName",
"title": "Sort by Name"
},
{
"command": "codeQLDatabases.sortByLanguage",
"title": "Sort by Language"
},
{
"command": "codeQLDatabases.sortByDateAdded",
"title": "Sort by Date Added"
@@ -1071,14 +1002,14 @@
"group": "1_databases@0"
},
{
"command": "codeQLDatabases.sortByDateAdded",
"command": "codeQLDatabases.sortByLanguage",
"when": "view == codeQLDatabases",
"group": "1_databases@1"
},
{
"submenu": "codeQLDatabases.languages",
"when": "view == codeQLDatabases && config.codeQL.canary && config.codeQL.showLanguageFilter",
"group": "2_databases@0"
"command": "codeQLDatabases.sortByDateAdded",
"when": "view == codeQLDatabases",
"group": "1_databases@2"
},
{
"command": "codeQLQueries.createQuery",
@@ -1152,6 +1083,11 @@
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/",
"group": "2_qlContextMenu@1"
},
{
"command": "codeQLLanguageSelection.setSelectedItem",
"when": "view == codeQLLanguageSelection && viewItem =~ /canBeSelected/",
"group": "inline"
},
{
"command": "codeQLDatabases.setCurrentDatabase",
"group": "inline",
@@ -1500,6 +1436,10 @@
{
"command": "codeQL.openModelEditor"
},
{
"command": "codeQLLanguageSelection.setSelectedItem",
"when": "false"
},
{
"command": "codeQLQueries.runLocalQueryContextMenu",
"when": "false"
@@ -1568,6 +1508,10 @@
"command": "codeQLDatabases.sortByName",
"when": "false"
},
{
"command": "codeQLDatabases.sortByLanguage",
"when": "false"
},
{
"command": "codeQLDatabases.sortByDateAdded",
"when": "false"
@@ -1600,78 +1544,6 @@
"command": "codeQLDatabases.upgradeDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.displayAllLanguages",
"when": "false"
},
{
"command": "codeQLDatabases.displayAllLanguagesSelected",
"when": "false"
},
{
"command": "codeQLDatabases.displayCpp",
"when": "false"
},
{
"command": "codeQLDatabases.displayCppSelected",
"when": "false"
},
{
"command": "codeQLDatabases.displayCsharp",
"when": "false"
},
{
"command": "codeQLDatabases.displayCsharpSelected",
"when": "false"
},
{
"command": "codeQLDatabases.displayGo",
"when": "false"
},
{
"command": "codeQLDatabases.displayGoSelected",
"when": "false"
},
{
"command": "codeQLDatabases.displayJava",
"when": "false"
},
{
"command": "codeQLDatabases.displayJavaSelected",
"when": "false"
},
{
"command": "codeQLDatabases.displayJavascript",
"when": "false"
},
{
"command": "codeQLDatabases.displayJavascriptSelected",
"when": "false"
},
{
"command": "codeQLDatabases.displayPython",
"when": "false"
},
{
"command": "codeQLDatabases.displayPythonSelected",
"when": "false"
},
{
"command": "codeQLDatabases.displayRuby",
"when": "false"
},
{
"command": "codeQLDatabases.displayRubySelected",
"when": "false"
},
{
"command": "codeQLDatabases.displaySwift",
"when": "false"
},
{
"command": "codeQLDatabases.displaySwiftSelected",
"when": "false"
},
{
"command": "codeQLQueryHistory.openQueryContextMenu",
"when": "false"
@@ -1815,6 +1687,10 @@
{
"command": "codeQL.gotoQLContextEditor",
"when": "false"
},
{
"command": "codeQL.trimCache",
"when": "codeql.supportsTrimCache"
}
],
"editor/context": [
@@ -1866,88 +1742,8 @@
"command": "codeQL.gotoQLContextEditor",
"when": "editorLangId == ql-summary && config.codeQL.canary"
}
],
"codeQLDatabases.languages": [
{
"command": "codeQLDatabases.displayAllLanguages",
"when": "codeQLDatabases.languageFilter"
},
{
"command": "codeQLDatabases.displayAllLanguagesSelected",
"when": "!codeQLDatabases.languageFilter"
},
{
"command": "codeQLDatabases.displayCpp",
"when": "codeQLDatabases.languageFilter != cpp"
},
{
"command": "codeQLDatabases.displayCppSelected",
"when": "codeQLDatabases.languageFilter == cpp"
},
{
"command": "codeQLDatabases.displayCsharp",
"when": "codeQLDatabases.languageFilter != csharp"
},
{
"command": "codeQLDatabases.displayCsharpSelected",
"when": "codeQLDatabases.languageFilter == csharp"
},
{
"command": "codeQLDatabases.displayGo",
"when": "codeQLDatabases.languageFilter != go"
},
{
"command": "codeQLDatabases.displayGoSelected",
"when": "codeQLDatabases.languageFilter == go"
},
{
"command": "codeQLDatabases.displayJava",
"when": "codeQLDatabases.languageFilter != java"
},
{
"command": "codeQLDatabases.displayJavaSelected",
"when": "codeQLDatabases.languageFilter == java"
},
{
"command": "codeQLDatabases.displayJavascript",
"when": "codeQLDatabases.languageFilter != javascript"
},
{
"command": "codeQLDatabases.displayJavascriptSelected",
"when": "codeQLDatabases.languageFilter == javascript"
},
{
"command": "codeQLDatabases.displayPython",
"when": "codeQLDatabases.languageFilter != python"
},
{
"command": "codeQLDatabases.displayPythonSelected",
"when": "codeQLDatabases.languageFilter == python"
},
{
"command": "codeQLDatabases.displayRuby",
"when": "codeQLDatabases.languageFilter != ruby"
},
{
"command": "codeQLDatabases.displayRubySelected",
"when": "codeQLDatabases.languageFilter == ruby"
},
{
"command": "codeQLDatabases.displaySwift",
"when": "codeQLDatabases.languageFilter != swift"
},
{
"command": "codeQLDatabases.displaySwiftSelected",
"when": "codeQLDatabases.languageFilter == swift"
}
]
},
"submenus": [
{
"id": "codeQLDatabases.languages",
"label": "Languages"
}
],
"viewsContainers": {
"activitybar": [
{
@@ -1966,6 +1762,11 @@
},
"views": {
"ql-container": [
{
"id": "codeQLLanguageSelection",
"name": "Language",
"when": "config.codeQL.canary && config.codeQL.showLanguageFilter"
},
{
"id": "codeQLDatabases",
"name": "Databases"
@@ -1991,6 +1792,12 @@
"id": "codeQLEvalLogViewer",
"name": "Evaluator Log Viewer",
"when": "config.codeQL.canary"
},
{
"id": "codeQLMethodModeling",
"type": "webview",
"name": "CodeQL Method Modeling",
"when": "config.codeQL.canary"
}
],
"codeql-methods-usage": [
@@ -1999,14 +1806,6 @@
"name": "CodeQL Methods Usage",
"when": "config.codeQL.canary && codeql.modelEditorOpen"
}
],
"explorer": [
{
"type": "webview",
"id": "codeQLMethodModeling",
"name": "CodeQL Method Modeling",
"when": "config.codeQL.canary && config.codeQL.model.methodModelingView && codeql.modelEditorOpen && !codeql.modelEditorActive"
}
]
},
"viewsWelcome": [
@@ -2070,8 +1869,8 @@
"prepare": "cd ../.. && husky install"
},
"dependencies": {
"@octokit/plugin-retry": "^4.1.6",
"@octokit/rest": "^19.0.4",
"@octokit/plugin-retry": "^6.0.1",
"@octokit/rest": "^20.0.2",
"@vscode/codicons": "^0.0.31",
"@vscode/debugadapter": "^1.59.0",
"@vscode/debugprotocol": "^1.59.0",
@@ -2085,10 +1884,10 @@
"fs-extra": "^11.1.1",
"immutable": "^4.0.0",
"js-yaml": "^4.1.0",
"msw": "^1.2.0",
"nanoid": "^3.2.0",
"msw": "^2.0.0",
"nanoid": "^5.0.1",
"node-fetch": "^2.6.7",
"p-queue": "^6.0.0",
"p-queue": "^7.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"semver": "^7.5.2",
@@ -2115,7 +1914,8 @@
"@babel/preset-typescript": "^7.21.4",
"@faker-js/faker": "^8.0.2",
"@github/markdownlint-github": "^0.3.0",
"@octokit/plugin-throttling": "^5.0.1",
"@octokit/plugin-throttling": "^8.0.0",
"@storybook/addon-a11y": "^7.4.6",
"@storybook/addon-actions": "^7.1.0",
"@storybook/addon-essentials": "^7.1.0",
"@storybook/addon-interactions": "^7.1.0",
@@ -2141,7 +1941,7 @@
"@types/jest": "^29.0.2",
"@types/js-yaml": "^4.0.6",
"@types/nanoid": "^3.0.0",
"@types/node": "^16.11.25",
"@types/node": "18.15.0",
"@types/node-fetch": "^2.5.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
@@ -2153,7 +1953,7 @@
"@types/through2": "^2.0.36",
"@types/tmp": "^0.1.0",
"@types/unzipper": "^0.10.1",
"@types/vscode": "^1.67.0",
"@types/vscode": "^1.82.0",
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.18.0",
"@typescript-eslint/eslint-plugin": "^6.2.1",
@@ -2181,13 +1981,12 @@
"gulp": "^4.0.2",
"gulp-esbuild": "^0.10.5",
"gulp-replace": "^1.1.3",
"gulp-sourcemaps": "^3.0.0",
"gulp-typescript": "^5.0.1",
"husky": "^8.0.0",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
"jest-runner-vscode": "^3.0.1",
"lint-staged": "^14.0.0",
"lint-staged": "^15.0.2",
"markdownlint-cli2": "^0.6.0",
"markdownlint-cli2-formatter-pretty": "^0.0.4",
"mini-css-extract-plugin": "^2.6.1",

View File

@@ -14,7 +14,8 @@
import { pathExists, readJson, writeJson } from "fs-extra";
import { resolve, relative } from "path";
import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
import { Octokit } from "@octokit/core";
import { type RestEndpointMethodTypes } from "@octokit/rest";
import { throttling } from "@octokit/plugin-throttling";
import { getFiles } from "./util/files";
@@ -22,6 +23,7 @@ import type { GitHubApiRequest } from "../src/common/mock-gh-api/gh-api-request"
import { isGetVariantAnalysisRequest } from "../src/common/mock-gh-api/gh-api-request";
import { VariantAnalysis } from "../src/variant-analysis/gh-api/variant-analysis";
import { RepositoryWithMetadata } from "../src/variant-analysis/gh-api/repository";
import { AppOctokit } from "../src/common/octokit";
const extensionDirectory = resolve(__dirname, "..");
const scenariosDirectory = resolve(
@@ -31,7 +33,7 @@ const scenariosDirectory = resolve(
// Make sure we don't run into rate limits by automatically waiting until we can
// make another request.
const MyOctokit = Octokit.plugin(throttling);
const MyOctokit = AppOctokit.plugin(throttling);
const auth = process.env.GITHUB_TOKEN;

View File

@@ -115,21 +115,35 @@ async function extractSourceMap() {
}
if (stacktrace.includes("at")) {
const rawSourceMaps = new Map<string, RawSourceMap>();
const rawSourceMaps = new Map<string, RawSourceMap | null>();
const mappedStacktrace = await replaceAsync(
stacktrace,
stackLineRegex,
async (match, name, file, line, column) => {
if (!rawSourceMaps.has(file)) {
const rawSourceMap: RawSourceMap = await readJSON(
resolve(sourceMapsDirectory, `${basename(file)}.map`),
);
rawSourceMaps.set(file, rawSourceMap);
try {
const rawSourceMap: RawSourceMap = await readJSON(
resolve(sourceMapsDirectory, `${basename(file)}.map`),
);
rawSourceMaps.set(file, rawSourceMap);
} catch (e: unknown) {
// If the file is not found, we will not decode it and not try reading this source map again
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
rawSourceMaps.set(file, null);
} else {
throw e;
}
}
}
const sourceMap = rawSourceMaps.get(file);
if (!sourceMap) {
return match;
}
const originalPosition = await SourceMapConsumer.with(
rawSourceMaps.get(file) as RawSourceMap,
sourceMap,
null,
async function (consumer) {
return consumer.originalPositionFor({

View File

@@ -10,7 +10,11 @@ import tk from "tree-kill";
import { promisify } from "util";
import { CancellationToken, Disposable, Uri } from "vscode";
import { BQRSInfo, DecodedBqrsChunk } from "../common/bqrs-cli-types";
import {
BQRSInfo,
DecodedBqrs,
DecodedBqrsChunk,
} from "../common/bqrs-cli-types";
import { allowCanaryQueryServer, CliConfig } from "../config";
import {
DistributionProvider,
@@ -1040,6 +1044,18 @@ export class CodeQLCliServer implements Disposable {
);
}
/**
* Gets all results from a bqrs.
* @param bqrsPath The path to the bqrs.
*/
async bqrsDecodeAll(bqrsPath: string): Promise<DecodedBqrs> {
return await this.runJsonCodeQlCliCommand<DecodedBqrs>(
["bqrs", "decode"],
[bqrsPath],
"Reading all bqrs data",
);
}
async runInterpretCommand(
format: string,
additonalArgs: string[],
@@ -1244,11 +1260,13 @@ export class CodeQLCliServer implements Disposable {
* @param additionalPacks A list of directories to search for qlpacks.
* @param extensionPacksOnly Whether to only search for extension packs. If true, only extension packs will
* be returned. If false, all packs will be returned.
* @param kind Whether to only search for qlpacks with a certain kind.
* @returns A dictionary mapping qlpack name to the directory it comes from
*/
async resolveQlpacks(
additionalPacks: string[],
extensionPacksOnly = false,
kind?: "query" | "library" | "all",
): Promise<QlpacksInfo> {
const args = this.getAdditionalPacksArg(additionalPacks);
if (extensionPacksOnly) {
@@ -1259,6 +1277,8 @@ export class CodeQLCliServer implements Disposable {
return {};
}
args.push("--kind", "extension", "--no-recursive");
} else if (kind) {
args.push("--kind", kind);
}
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
@@ -1492,6 +1512,13 @@ export class CodeQLCliServer implements Disposable {
CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT,
) >= 0,
);
await this.app.commands.execute(
"setContext",
"codeql.supportsTrimCache",
newVersion.compare(
CliVersionConstraint.CLI_VERSION_WITH_TRIM_CACHE,
) >= 0,
);
} catch (e) {
this._versionChangedListeners.forEach((listener) =>
listener(undefined),
@@ -1755,6 +1782,12 @@ export class CliVersionConstraint {
"2.14.0",
);
/**
* CLI version where the query server supports the `evaluation/trimCache` method
* with `codeql database cleanup --mode=trim` semantics.
*/
public static CLI_VERSION_WITH_TRIM_CACHE = new SemVer("2.15.1");
constructor(private readonly cli: CodeQLCliServer) {
/**/
}

View File

@@ -1,6 +1,10 @@
import { CodeQLCliServer } from "./cli";
import { Uri, window } from "vscode";
import { isQueryLanguage, QueryLanguage } from "../common/query-language";
import {
getLanguageDisplayName,
isQueryLanguage,
QueryLanguage,
} from "../common/query-language";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { extLogger } from "../common/logging/vscode";
import { UserCancellationException } from "../common/vscode/progress";
@@ -46,14 +50,22 @@ export async function askForLanguage(
cliServer: CodeQLCliServer,
throwOnEmpty = true,
): Promise<QueryLanguage | undefined> {
const language = await window.showQuickPick(
await cliServer.getSupportedLanguages(),
{
placeHolder: "Select target language for your query",
ignoreFocusOut: true,
},
);
if (!language) {
const supportedLanguages = await cliServer.getSupportedLanguages();
const items = supportedLanguages
.filter((language) => isQueryLanguage(language))
.map((language) => ({
label: getLanguageDisplayName(language),
description: language,
language,
}))
.sort((a, b) => a.label.localeCompare(b.label));
const selectedItem = await window.showQuickPick(items, {
placeHolder: "Select target language for your query",
ignoreFocusOut: true,
});
if (!selectedItem) {
// This only happens if the user cancels the quick pick.
if (throwOnEmpty) {
throw new UserCancellationException("Cancelled.");
@@ -66,6 +78,8 @@ export async function askForLanguage(
return undefined;
}
const language = selectedItem.language;
if (!isQueryLanguage(language)) {
void showAndLogErrorMessage(
extLogger,

View File

@@ -121,3 +121,5 @@ export interface DecodedBqrsChunk {
next?: number;
columns: BqrsColumn[];
}
export type DecodedBqrs = Record<string, DecodedBqrsChunk>;

View File

@@ -12,6 +12,7 @@ import type {
} from "../variant-analysis/shared/variant-analysis";
import type { QLDebugConfiguration } from "../debugger/debug-configuration";
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import type { LanguageSelectionTreeViewItem } from "../language-selection-panel/language-selection-data-provider";
import type { Method, Usage } from "../model-editor/method";
// A command function matching the signature that VS Code calls when
@@ -199,6 +200,13 @@ export type QueryHistoryCommands = {
"codeQL.exportSelectedVariantAnalysisResults": () => Promise<void>;
};
// Commands user for the language selector panel
export type LanguageSelectionCommands = {
"codeQLLanguageSelection.setSelectedItem": (
item: LanguageSelectionTreeViewItem,
) => Promise<void>;
};
// Commands used for the local databases panel
export type LocalDatabasesCommands = {
// Command palette commands
@@ -208,6 +216,7 @@ export type LocalDatabasesCommands = {
"codeQL.chooseDatabaseGithub": () => Promise<void>;
"codeQL.upgradeCurrentDatabase": () => Promise<void>;
"codeQL.clearCache": () => Promise<void>;
"codeQL.trimCache": () => Promise<void>;
// Explorer context menu
"codeQL.setCurrentDatabase": (uri: Uri) => Promise<void>;
@@ -218,25 +227,8 @@ export type LocalDatabasesCommands = {
"codeQLDatabases.chooseDatabaseInternet": () => Promise<void>;
"codeQLDatabases.chooseDatabaseGithub": () => Promise<void>;
"codeQLDatabases.sortByName": () => Promise<void>;
"codeQLDatabases.sortByLanguage": () => Promise<void>;
"codeQLDatabases.sortByDateAdded": () => Promise<void>;
"codeQLDatabases.displayAllLanguages": () => Promise<void>;
"codeQLDatabases.displayCpp": () => Promise<void>;
"codeQLDatabases.displayCsharp": () => Promise<void>;
"codeQLDatabases.displayGo": () => Promise<void>;
"codeQLDatabases.displayJava": () => Promise<void>;
"codeQLDatabases.displayJavascript": () => Promise<void>;
"codeQLDatabases.displayPython": () => Promise<void>;
"codeQLDatabases.displayRuby": () => Promise<void>;
"codeQLDatabases.displaySwift": () => Promise<void>;
"codeQLDatabases.displayAllLanguagesSelected": () => Promise<void>;
"codeQLDatabases.displayCppSelected": () => Promise<void>;
"codeQLDatabases.displayCsharpSelected": () => Promise<void>;
"codeQLDatabases.displayGoSelected": () => Promise<void>;
"codeQLDatabases.displayJavaSelected": () => Promise<void>;
"codeQLDatabases.displayJavascriptSelected": () => Promise<void>;
"codeQLDatabases.displayPythonSelected": () => Promise<void>;
"codeQLDatabases.displayRubySelected": () => Promise<void>;
"codeQLDatabases.displaySwiftSelected": () => Promise<void>;
// Database panel context menu
"codeQLDatabases.setCurrentDatabase": (
@@ -323,7 +315,8 @@ export type PackagingCommands = {
export type ModelEditorCommands = {
"codeQL.openModelEditor": () => Promise<void>;
"codeQLModelEditor.jumpToUsageLocation": (
"codeQL.openModelEditorFromModelingPanel": () => Promise<void>;
"codeQLModelEditor.jumpToMethod": (
method: Method,
usage: Usage,
databaseItem: DatabaseItem,
@@ -360,6 +353,7 @@ export type AllExtensionCommands = BaseCommands &
QueryEditorCommands &
ResultsViewCommands &
QueryHistoryCommands &
LanguageSelectionCommands &
LocalDatabasesCommands &
DebuggerCommands &
VariantAnalysisCommands &

View File

@@ -9,10 +9,16 @@ export type DisposeHandler = (disposable: Disposable) => void;
/**
* Base class to make it easier to implement a `Disposable` that owns other disposable object.
*/
export abstract class DisposableObject implements Disposable {
export class DisposableObject implements Disposable {
private disposables: Disposable[] = [];
private tracked?: Set<Disposable> = undefined;
constructor(...dispoables: Disposable[]) {
for (const d of dispoables) {
this.push(d);
}
}
/**
* Adds `obj` to a list of objects to dispose when `this` is disposed. Objects added by `push` are
* disposed in reverse order of being added.

View File

@@ -22,6 +22,14 @@ export class RedactableError extends Error {
.join("");
}
public get fullMessageWithStack(): string {
if (!this.stack) {
return this.fullMessage;
}
return `${this.fullMessage}\n${this.stack}`;
}
public get redactedMessage(): string {
return this.strings
.map((s, i) => s + (this.hasValue(i) ? this.getRedactedValue(i) : ""))

View File

@@ -17,10 +17,14 @@ import {
} from "../variant-analysis/shared/variant-analysis-filter-sort";
import { ErrorLike } from "../common/errors";
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
import { Method, Usage } from "../model-editor/method";
import { Method } from "../model-editor/method";
import { ModeledMethod } from "../model-editor/modeled-method";
import { ModelEditorViewState } from "../model-editor/shared/view-state";
import {
MethodModelingPanelViewState,
ModelEditorViewState,
} from "../model-editor/shared/view-state";
import { Mode } from "../model-editor/shared/mode";
import { QueryLanguage } from "./query-language";
/**
* This module contains types and code that are shared between
@@ -51,6 +55,7 @@ export const RAW_RESULTS_LIMIT = 10000;
export interface DatabaseInfo {
name: string;
databaseUri: string;
language?: QueryLanguage;
}
/** Arbitrary query metadata */
@@ -502,7 +507,7 @@ interface SetMethodsMessage {
interface SetModeledMethodsMessage {
t: "setModeledMethods";
methods: Record<string, ModeledMethod>;
methods: Record<string, ModeledMethod[]>;
}
interface SetModifiedMethodsMessage {
@@ -512,8 +517,7 @@ interface SetModifiedMethodsMessage {
interface SetInProgressMethodsMessage {
t: "setInProgressMethods";
packageName: string;
inProgressMethods: string[];
methods: string[];
}
interface SwitchModeMessage {
@@ -521,10 +525,9 @@ interface SwitchModeMessage {
mode: Mode;
}
interface JumpToUsageMessage {
t: "jumpToUsage";
method: Method;
usage: Usage;
interface JumpToMethodMessage {
t: "jumpToMethod";
methodSignature: string;
}
interface OpenDatabaseMessage {
@@ -541,8 +544,7 @@ interface RefreshMethods {
interface SaveModeledMethods {
t: "saveModeledMethods";
methods: Method[];
modeledMethods: Record<string, ModeledMethod>;
methodSignatures?: string[];
}
interface GenerateMethodMessage {
@@ -552,8 +554,7 @@ interface GenerateMethodMessage {
interface GenerateMethodsFromLlmMessage {
t: "generateMethodsFromLlm";
packageName: string;
methods: Method[];
modeledMethods: Record<string, ModeledMethod>;
methodSignatures: string[];
}
interface StopGeneratingMethodsFromLlmMessage {
@@ -570,9 +571,25 @@ interface HideModeledMethodsMessage {
hideModeledMethods: boolean;
}
interface SetModeledMethodMessage {
t: "setModeledMethod";
method: ModeledMethod;
interface SetMultipleModeledMethodsMessage {
t: "setMultipleModeledMethods";
methodSignature: string;
modeledMethods: ModeledMethod[];
}
interface SetInModelingModeMessage {
t: "setInModelingMode";
inModelingMode: boolean;
}
interface SetInProgressMessage {
t: "setInProgress";
inProgress: boolean;
}
interface RevealMethodMessage {
t: "revealMethod";
methodSignature: string;
}
export type ToModelEditorMessage =
@@ -580,31 +597,47 @@ export type ToModelEditorMessage =
| SetMethodsMessage
| SetModeledMethodsMessage
| SetModifiedMethodsMessage
| SetInProgressMethodsMessage;
| SetInProgressMethodsMessage
| RevealMethodMessage;
export type FromModelEditorMessage =
| ViewLoadedMsg
| CommonFromViewMessages
| SwitchModeMessage
| RefreshMethods
| OpenDatabaseMessage
| OpenExtensionPackMessage
| JumpToUsageMessage
| JumpToMethodMessage
| SaveModeledMethods
| GenerateMethodMessage
| GenerateMethodsFromLlmMessage
| StopGeneratingMethodsFromLlmMessage
| ModelDependencyMessage
| HideModeledMethodsMessage
| SetModeledMethodMessage;
| SetMultipleModeledMethodsMessage;
interface RevealInEditorMessage {
t: "revealInModelEditor";
method: Method;
}
interface StartModelingMessage {
t: "startModeling";
}
export type FromMethodModelingMessage =
| TelemetryMessage
| UnhandledErrorMessage
| SetModeledMethodMessage;
| CommonFromViewMessages
| SetMultipleModeledMethodsMessage
| RevealInEditorMessage
| StartModelingMessage;
interface SetMethodModelingPanelViewStateMessage {
t: "setMethodModelingPanelViewState";
viewState: MethodModelingPanelViewState;
}
interface SetMethodMessage {
t: "setMethod";
method: Method;
method: Method | undefined;
}
interface SetMethodModifiedMessage {
@@ -615,12 +648,16 @@ interface SetMethodModifiedMessage {
interface SetSelectedMethodMessage {
t: "setSelectedMethod";
method: Method;
modeledMethod: ModeledMethod;
modeledMethods: ModeledMethod[];
isModified: boolean;
isInProgress: boolean;
}
export type ToMethodModelingMessage =
| SetMethodModelingPanelViewStateMessage
| SetMethodMessage
| SetModeledMethodMessage
| SetMultipleModeledMethodsMessage
| SetMethodModifiedMessage
| SetSelectedMethodMessage;
| SetSelectedMethodMessage
| SetInModelingModeMessage
| SetInProgressMessage;

View File

@@ -112,5 +112,5 @@ export async function showAndLogExceptionWithTelemetry(
options: ShowAndLogExceptionOptions = {},
): Promise<void> {
telemetry?.sendError(error, options.extraTelemetryProperties);
return showAndLogErrorMessage(logger, error.fullMessage, options);
return showAndLogErrorMessage(logger, error.fullMessageWithStack, options);
}

View File

@@ -17,7 +17,7 @@ export enum RequestKind {
AutoModel = "autoModel",
}
interface BasicErorResponse {
export interface BasicErrorResponse {
message: string;
}
@@ -27,7 +27,7 @@ interface GetRepoRequest {
};
response: {
status: number;
body: Repository | BasicErorResponse | undefined;
body: Repository | BasicErrorResponse | undefined;
};
}
@@ -37,7 +37,7 @@ interface SubmitVariantAnalysisRequest {
};
response: {
status: number;
body?: VariantAnalysis | BasicErorResponse;
body?: VariantAnalysis | BasicErrorResponse;
};
}
@@ -47,7 +47,7 @@ interface GetVariantAnalysisRequest {
};
response: {
status: number;
body?: VariantAnalysis | BasicErorResponse;
body?: VariantAnalysis | BasicErrorResponse;
};
}
@@ -58,7 +58,7 @@ interface GetVariantAnalysisRepoRequest {
};
response: {
status: number;
body?: VariantAnalysisRepoTask | BasicErorResponse;
body?: VariantAnalysisRepoTask | BasicErrorResponse;
};
}
@@ -74,6 +74,13 @@ export interface GetVariantAnalysisRepoResultRequest {
};
}
export interface CodeSearchResponse {
total_count: number;
items: Array<{
repository: Repository;
}>;
}
interface CodeSearchRequest {
request: {
kind: RequestKind.CodeSearch;
@@ -81,16 +88,14 @@ interface CodeSearchRequest {
};
response: {
status: number;
body?: {
total_count?: number;
items?: Array<{
repository: Repository;
}>;
};
message?: string;
body?: CodeSearchResponse | BasicErrorResponse;
};
}
export interface AutoModelResponse {
models: string;
}
interface AutoModelRequest {
request: {
kind: RequestKind.AutoModel;
@@ -100,10 +105,7 @@ interface AutoModelRequest {
};
response: {
status: number;
body?: {
models: string;
};
message?: string;
body?: AutoModelResponse | BasicErrorResponse;
};
}

View File

@@ -1,30 +1,33 @@
import { ensureDir, writeFile } from "fs-extra";
import { join } from "path";
import { MockedRequest } from "msw";
import { SetupServer } from "msw/node";
import { IsomorphicResponse } from "@mswjs/interceptors";
import { Headers } from "headers-polyfill";
import fetch from "node-fetch";
import { SetupServer } from "msw/node";
import { DisposableObject } from "../disposable-object";
import { gzipDecode } from "../zlib";
import {
AutoModelResponse,
BasicErrorResponse,
CodeSearchResponse,
GetVariantAnalysisRepoResultRequest,
GitHubApiRequest,
RequestKind,
} from "./gh-api-request";
import {
VariantAnalysis,
VariantAnalysisRepoTask,
} from "../../variant-analysis/gh-api/variant-analysis";
import { Repository } from "../../variant-analysis/gh-api/repository";
export class Recorder extends DisposableObject {
private readonly allRequests = new Map<string, MockedRequest>();
private currentRecordedScenario: GitHubApiRequest[] = [];
private _isRecording = false;
constructor(private readonly server: SetupServer) {
super();
this.onRequestStart = this.onRequestStart.bind(this);
this.onResponseBypass = this.onResponseBypass.bind(this);
}
@@ -45,7 +48,6 @@ export class Recorder extends DisposableObject {
this.clear();
this.server.events.on("request:start", this.onRequestStart);
this.server.events.on("response:bypass", this.onResponseBypass);
}
@@ -56,13 +58,11 @@ export class Recorder extends DisposableObject {
this._isRecording = false;
this.server.events.removeListener("request:start", this.onRequestStart);
this.server.events.removeListener("response:bypass", this.onResponseBypass);
}
public clear() {
this.currentRecordedScenario = [];
this.allRequests.clear();
}
public async save(scenariosPath: string, name: string): Promise<string> {
@@ -91,7 +91,7 @@ export class Recorder extends DisposableObject {
let bodyFileLink = undefined;
if (writtenRequest.response.body) {
await writeFile(bodyFilePath, writtenRequest.response.body || "");
await writeFile(bodyFilePath, writtenRequest.response.body);
bodyFileLink = `file:${bodyFileName}`;
}
@@ -112,33 +112,21 @@ export class Recorder extends DisposableObject {
return scenarioDirectory;
}
private onRequestStart(request: MockedRequest): void {
private async onResponseBypass({
response,
request,
}: {
response: Response;
request: Request;
requestId: string;
}): Promise<void> {
if (request.headers.has("x-vscode-codeql-msw-bypass")) {
return;
}
this.allRequests.set(request.id, request);
}
private async onResponseBypass(
response: IsomorphicResponse,
requestId: string,
): Promise<void> {
const request = this.allRequests.get(requestId);
this.allRequests.delete(requestId);
if (!request) {
return;
}
if (response.body === undefined) {
return;
}
const gitHubApiRequest = await createGitHubApiRequest(
request.url.toString(),
response.status,
response.body,
response.headers,
request.url,
response,
);
if (!gitHubApiRequest) {
return;
@@ -150,14 +138,14 @@ export class Recorder extends DisposableObject {
async function createGitHubApiRequest(
url: string,
status: number,
body: string,
headers: Headers,
response: Response,
): Promise<GitHubApiRequest | undefined> {
if (!url) {
return undefined;
}
const status = response.status;
if (url.match(/\/repos\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_.]+$/)) {
return {
request: {
@@ -165,7 +153,9 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await jsonResponseBody<
Repository | BasicErrorResponse | undefined
>(response),
},
};
}
@@ -179,7 +169,9 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await jsonResponseBody<
VariantAnalysis | BasicErrorResponse | undefined
>(response),
},
};
}
@@ -195,7 +187,9 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await jsonResponseBody<
VariantAnalysis | BasicErrorResponse | undefined
>(response),
},
};
}
@@ -211,7 +205,9 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await jsonResponseBody<
VariantAnalysisRepoTask | BasicErrorResponse | undefined
>(response),
},
};
}
@@ -238,9 +234,10 @@ async function createGitHubApiRequest(
repositoryId: parseInt(repoDownloadMatch.groups.repositoryId, 10),
},
response: {
status,
status: response.status,
body: responseBuffer,
contentType: headers.get("content-type") ?? "application/octet-stream",
contentType:
response.headers.get("content-type") ?? "application/octet-stream",
},
};
}
@@ -254,7 +251,9 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await jsonResponseBody<
CodeSearchResponse | BasicErrorResponse | undefined
>(response),
},
};
}
@@ -269,7 +268,9 @@ async function createGitHubApiRequest(
},
response: {
status,
body: JSON.parse(body),
body: await jsonResponseBody<
BasicErrorResponse | AutoModelResponse | undefined
>(response),
},
};
}
@@ -277,6 +278,26 @@ async function createGitHubApiRequest(
return undefined;
}
async function responseBody(response: Response): Promise<Uint8Array> {
const body = await response.arrayBuffer();
const view = new Uint8Array(body);
if (view[0] === 0x1f && view[1] === 0x8b) {
// Response body is gzipped, so we need to un-gzip it.
return await gzipDecode(view);
} else {
return view;
}
}
async function jsonResponseBody<T>(response: Response): Promise<T> {
const body = await responseBody(response);
const text = new TextDecoder("utf-8").decode(body);
return JSON.parse(text);
}
function shouldWriteBodyToFile(
request: GitHubApiRequest,
): request is GetVariantAnalysisRepoResultRequest {

View File

@@ -1,6 +1,6 @@
import { join } from "path";
import { readdir, readJson, readFile } from "fs-extra";
import { DefaultBodyType, MockedRequest, rest, RestHandler } from "msw";
import { http, RequestHandler } from "msw";
import {
GitHubApiRequest,
isAutoModelRequest,
@@ -14,7 +14,19 @@ import {
const baseUrl = "https://api.github.com";
type RequestHandler = RestHandler<MockedRequest<DefaultBodyType>>;
const jsonResponse = <T>(
body: T,
init?: ResponseInit,
contentType = "application/json",
): Response => {
return new Response(JSON.stringify(body), {
...init,
headers: {
"Content-Type": contentType,
...init?.headers,
},
});
};
export async function createRequestHandlers(
scenarioDirPath: string,
@@ -82,11 +94,10 @@ function createGetRepoRequestHandler(
const getRepoRequest = getRepoRequests[0];
return rest.get(`${baseUrl}/repos/:owner/:name`, (_req, res, ctx) => {
return res(
ctx.status(getRepoRequest.response.status),
ctx.json(getRepoRequest.response.body),
);
return http.get(`${baseUrl}/repos/:owner/:name`, () => {
return jsonResponse(getRepoRequest.response.body, {
status: getRepoRequest.response.status,
});
});
}
@@ -103,13 +114,12 @@ function createSubmitVariantAnalysisRequestHandler(
const getRepoRequest = submitVariantAnalysisRequests[0];
return rest.post(
return http.post(
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses`,
(_req, res, ctx) => {
return res(
ctx.status(getRepoRequest.response.status),
ctx.json(getRepoRequest.response.body),
);
() => {
return jsonResponse(getRepoRequest.response.body, {
status: getRepoRequest.response.status,
});
},
);
}
@@ -125,9 +135,9 @@ function createGetVariantAnalysisRequestHandler(
// During the lifetime of a variant analysis run, there are multiple requests
// to get the variant analysis. We need to return different responses for each
// request, so keep an index of the request and return the appropriate response.
return rest.get(
return http.get(
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId`,
(_req, res, ctx) => {
() => {
const request = getVariantAnalysisRequests[requestIndex];
if (requestIndex < getVariantAnalysisRequests.length - 1) {
@@ -135,10 +145,9 @@ function createGetVariantAnalysisRequestHandler(
requestIndex++;
}
return res(
ctx.status(request.response.status),
ctx.json(request.response.body),
);
return jsonResponse(request.response.body, {
status: request.response.status,
});
},
);
}
@@ -150,20 +159,19 @@ function createGetVariantAnalysisRepoRequestHandler(
isGetVariantAnalysisRepoRequest,
);
return rest.get(
return http.get(
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId/repositories/:repoId`,
(req, res, ctx) => {
({ request, params }) => {
const scenarioRequest = getVariantAnalysisRepoRequests.find(
(r) => r.request.repositoryId.toString() === req.params.repoId,
(r) => r.request.repositoryId.toString() === params.repoId,
);
if (!scenarioRequest) {
throw Error(`No scenario request found for ${req.url}`);
throw Error(`No scenario request found for ${request.url}`);
}
return res(
ctx.status(scenarioRequest.response.status),
ctx.json(scenarioRequest.response.body),
);
return jsonResponse(scenarioRequest.response.body, {
status: scenarioRequest.response.status,
});
},
);
}
@@ -175,24 +183,25 @@ function createGetVariantAnalysisRepoResultRequestHandler(
isGetVariantAnalysisRepoResultRequest,
);
return rest.get(
return http.get(
"https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/:variantAnalysisId/:repoId/*",
(req, res, ctx) => {
({ request, params }) => {
const scenarioRequest = getVariantAnalysisRepoResultRequests.find(
(r) => r.request.repositoryId.toString() === req.params.repoId,
(r) => r.request.repositoryId.toString() === params.repoId,
);
if (!scenarioRequest) {
throw Error(`No scenario request found for ${req.url}`);
throw Error(`No scenario request found for ${request.url}`);
}
if (scenarioRequest.response.body) {
return res(
ctx.status(scenarioRequest.response.status),
ctx.set("Content-Type", scenarioRequest.response.contentType),
ctx.body(scenarioRequest.response.body),
);
return new Response(scenarioRequest.response.body, {
status: scenarioRequest.response.status,
headers: {
"Content-Type": scenarioRequest.response.contentType,
},
});
} else {
return res(ctx.status(scenarioRequest.response.status));
return new Response(null, { status: scenarioRequest.response.status });
}
},
);
@@ -207,7 +216,7 @@ function createCodeSearchRequestHandler(
// During a code search, there are multiple request to get pages of results. We
// need to return different responses for each request, so keep an index of the
// request and return the appropriate response.
return rest.get(`${baseUrl}/search/code?q=*`, (_req, res, ctx) => {
return http.get(`${baseUrl}/search/code`, () => {
const request = codeSearchRequests[requestIndex];
if (requestIndex < codeSearchRequests.length - 1) {
@@ -215,10 +224,9 @@ function createCodeSearchRequestHandler(
requestIndex++;
}
return res(
ctx.status(request.response.status),
ctx.json(request.response.body),
);
return jsonResponse(request.response.body, {
status: request.response.status,
});
});
}
@@ -231,9 +239,9 @@ function createAutoModelRequestHandler(
// During automodeling there can be multiple API requests for each batch
// of candidates we want to model. We need to return different responses for each request,
// so keep an index of the request and return the appropriate response.
return rest.post(
return http.post(
`${baseUrl}/repos/github/codeql/code-scanning/codeql/auto-model`,
(_req, res, ctx) => {
() => {
const request = autoModelRequests[requestIndex];
if (requestIndex < autoModelRequests.length - 1) {
@@ -241,10 +249,9 @@ function createAutoModelRequestHandler(
requestIndex++;
}
return res(
ctx.status(request.response.status),
ctx.json(request.response.body),
);
return jsonResponse(request.response.body, {
status: request.response.status,
});
},
);
}

View File

@@ -0,0 +1,6 @@
/**
* Remove all readonly modifiers from a type.
*/
export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

View File

@@ -0,0 +1,10 @@
import * as Octokit from "@octokit/rest";
import { retry } from "@octokit/plugin-retry";
import fetch from "node-fetch";
export const AppOctokit = Octokit.Octokit.defaults({
request: {
fetch,
},
retry,
});

View File

@@ -40,10 +40,7 @@ export const PACKS_BY_QUERY_LANGUAGE = {
],
[QueryLanguage.Go]: ["codeql/go-queries"],
[QueryLanguage.Java]: ["codeql/java-queries"],
[QueryLanguage.Javascript]: [
"codeql/javascript-queries",
"codeql/javascript-experimental-atm-queries",
],
[QueryLanguage.Javascript]: ["codeql/javascript-queries"],
[QueryLanguage.Python]: ["codeql/python-queries"],
[QueryLanguage.Ruby]: ["codeql/ruby-queries"],
};

View File

@@ -0,0 +1,14 @@
export type DeepReadonly<T> = T extends Array<infer R>
? DeepReadonlyArray<R>
: // eslint-disable-next-line @typescript-eslint/ban-types
T extends Function
? T
: T extends object
? DeepReadonlyObject<T>
: T;
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};

View File

@@ -1,6 +1,7 @@
import * as Sarif from "sarif";
import type { HighlightedRegion } from "../variant-analysis/shared/analysis-result";
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
import { isEmptyPath } from "./bqrs-utils";
export interface SarifLink {
dest: number;
@@ -111,6 +112,9 @@ export function parseSarifLocation(
return { hint: "no artifact location" };
if (physicalLocation.artifactLocation.uri === undefined)
return { hint: "artifact location has no uri" };
if (isEmptyPath(physicalLocation.artifactLocation.uri)) {
return { hint: "artifact location has empty uri" };
}
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.

View File

@@ -0,0 +1,86 @@
import * as vscode from "vscode";
import { Uri, WebviewViewProvider } from "vscode";
import { WebviewKind, WebviewMessage, getHtmlForWebview } from "./webview-html";
import { Disposable } from "../disposable-object";
import { App } from "../app";
import { DeepReadonly } from "../readonly";
export abstract class AbstractWebviewViewProvider<
ToMessage extends WebviewMessage,
FromMessage extends WebviewMessage,
> implements WebviewViewProvider
{
protected webviewView: vscode.WebviewView | undefined = undefined;
private disposables: Disposable[] = [];
constructor(
protected readonly app: App,
private readonly webviewKind: WebviewKind,
) {}
/**
* This is called when a view first becomes visible. This may happen when the view is
* first loaded or when the user hides and then shows a view again.
*/
public resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [Uri.file(this.app.extensionPath)],
};
const html = getHtmlForWebview(
this.app,
webviewView.webview,
this.webviewKind,
{
allowInlineStyles: true,
allowWasmEval: false,
},
);
webviewView.webview.html = html;
this.webviewView = webviewView;
webviewView.webview.onDidReceiveMessage(async (msg) => this.onMessage(msg));
webviewView.onDidDispose(() => this.dispose());
}
protected get isShowingView() {
return this.webviewView?.visible ?? false;
}
protected async postMessage(msg: DeepReadonly<ToMessage>): Promise<void> {
await this.webviewView?.webview.postMessage(msg);
}
protected dispose() {
while (this.disposables.length > 0) {
const disposable = this.disposables.pop()!;
disposable.dispose();
}
this.webviewView = undefined;
}
protected push<T extends Disposable>(obj: T): T {
if (obj !== undefined) {
this.disposables.push(obj);
}
return obj;
}
protected abstract onMessage(msg: FromMessage): Promise<void>;
/**
* This is called when a view first becomes visible. This may happen when the view is
* first loaded or when the user hides and then shows a view again.
*/
protected onWebViewLoaded(): void {
// Do nothing by default.
}
}

View File

@@ -12,6 +12,7 @@ import { App } from "../app";
import { Disposable } from "../disposable-object";
import { tmpDir } from "../../tmp-dir";
import { getHtmlForWebview, WebviewMessage, WebviewKind } from "./webview-html";
import { DeepReadonly } from "../readonly";
export type WebviewPanelConfig = {
viewId: string;
@@ -146,7 +147,7 @@ export abstract class AbstractWebview<
this.panelLoadedCallBacks = [];
}
protected async postMessage(msg: ToMessage): Promise<boolean> {
protected async postMessage(msg: DeepReadonly<ToMessage>): Promise<boolean> {
const panel = await this.getPanel();
return panel.webview.postMessage(msg);
}

View File

@@ -1,7 +1,7 @@
import * as vscode from "vscode";
import * as Octokit from "@octokit/rest";
import { retry } from "@octokit/plugin-retry";
import { Credentials } from "../authentication";
import { AppOctokit } from "../octokit";
export const GITHUB_AUTH_PROVIDER_ID = "github";
@@ -32,9 +32,8 @@ export class VSCodeCredentials implements Credentials {
const accessToken = await this.getAccessToken();
return new Octokit.Octokit({
return new AppOctokit({
auth: accessToken,
retry,
});
}

View File

@@ -6,11 +6,7 @@ import {
showAndLogExceptionWithTelemetry,
} from "../logging";
import { extLogger } from "../logging/vscode";
import {
asError,
getErrorMessage,
getErrorStack,
} from "../../common/helpers-pure";
import { asError, getErrorMessage } from "../../common/helpers-pure";
import { redactableError } from "../../common/errors";
import { UserCancellationException } from "./progress";
import { telemetryListener } from "./telemetry";
@@ -66,10 +62,7 @@ export function registerCommandWithErrorHandling(
}
} else {
// Include the full stack in the error log only.
const errorStack = getErrorStack(e);
const fullMessage = errorStack
? `${errorMessage.fullMessage}\n${errorStack}`
: errorMessage.fullMessage;
const fullMessage = errorMessage.fullMessageWithStack;
void showAndLogExceptionWithTelemetry(logger, telemetry, errorMessage, {
fullMessage,
extraTelemetryProperties: {

View File

@@ -0,0 +1,24 @@
import { CancellationToken, Disposable } from "vscode";
import { DisposableObject } from "../disposable-object";
/**
* A cancellation token that cancels when any of its constituent
* cancellation tokens are cancelled.
*/
export class MultiCancellationToken implements CancellationToken {
private readonly tokens: CancellationToken[];
constructor(...tokens: CancellationToken[]) {
this.tokens = tokens;
}
get isCancellationRequested(): boolean {
return this.tokens.some((t) => t.isCancellationRequested);
}
onCancellationRequested<T>(listener: (e: T) => any): Disposable {
return new DisposableObject(
...this.tokens.map((t) => t.onCancellationRequested(listener)),
);
}
}

View File

@@ -3,13 +3,13 @@ import {
Extension,
ExtensionContext,
ConfigurationChangeEvent,
env,
} from "vscode";
import TelemetryReporter from "vscode-extension-telemetry";
import {
ConfigListener,
CANARY_FEATURES,
ENABLE_TELEMETRY,
GLOBAL_ENABLE_TELEMETRY,
LOG_TELEMETRY,
isIntegrationTestMode,
isCanary,
@@ -59,8 +59,6 @@ export class ExtensionTelemetryListener
extends ConfigListener
implements AppTelemetry
{
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
private reporter?: TelemetryReporter;
private cliVersionStr = NOT_SET_CLI_VERSION;
@@ -72,6 +70,10 @@ export class ExtensionTelemetryListener
private readonly ctx: ExtensionContext,
) {
super();
env.onDidChangeTelemetryEnabled(async () => {
await this.initialize();
});
}
/**
@@ -91,10 +93,7 @@ export class ExtensionTelemetryListener
async handleDidChangeConfiguration(
e: ConfigurationChangeEvent,
): Promise<void> {
if (
e.affectsConfiguration("codeQL.telemetry.enableTelemetry") ||
e.affectsConfiguration("telemetry.enableTelemetry")
) {
if (e.affectsConfiguration(ENABLE_TELEMETRY.qualifiedName)) {
await this.initialize();
}
@@ -102,7 +101,7 @@ export class ExtensionTelemetryListener
// Re-request if codeQL.canary is being set to `true` and telemetry
// is not currently enabled.
if (
e.affectsConfiguration("codeQL.canary") &&
e.affectsConfiguration(CANARY_FEATURES.qualifiedName) &&
CANARY_FEATURES.getValue() &&
!ENABLE_TELEMETRY.getValue()
) {
@@ -212,7 +211,7 @@ export class ExtensionTelemetryListener
properties.stack = error.stack;
}
this.reporter.sendTelemetryEvent("error", properties, {});
this.reporter.sendTelemetryErrorEvent("error", properties, {});
}
/**
@@ -224,7 +223,7 @@ export class ExtensionTelemetryListener
// if global telemetry is disabled, avoid showing the dialog or making any changes
let result = undefined;
if (
GLOBAL_ENABLE_TELEMETRY.getValue() &&
env.isTelemetryEnabled &&
// Avoid showing the dialog if we are in integration test mode.
!isIntegrationTestMode()
) {

View File

@@ -72,15 +72,8 @@ export const VSCODE_SAVE_BEFORE_START_SETTING = new Setting(
const ROOT_SETTING = new Setting("codeQL");
// Global configuration
// Telemetry configuration
const TELEMETRY_SETTING = new Setting("telemetry", ROOT_SETTING);
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
const CONTEXTUAL_QUERIES_SETTINGS = new Setting(
"contextualQueries",
ROOT_SETTING,
);
const GLOBAL_TELEMETRY_SETTING = new Setting("telemetry");
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
export const LOG_TELEMETRY = new Setting("logTelemetry", TELEMETRY_SETTING);
export const ENABLE_TELEMETRY = new Setting(
@@ -88,11 +81,6 @@ export const ENABLE_TELEMETRY = new Setting(
TELEMETRY_SETTING,
);
export const GLOBAL_ENABLE_TELEMETRY = new Setting(
"enableTelemetry",
GLOBAL_TELEMETRY_SETTING,
);
// Distribution configuration
const DISTRIBUTION_SETTING = new Setting("cli", ROOT_SETTING);
export const CUSTOM_CODEQL_PATH_SETTING = new Setting(
@@ -475,6 +463,7 @@ export function allowCanaryQueryServer() {
return value === undefined ? true : !!value;
}
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
export const JOIN_ORDER_WARNING_THRESHOLD = new Setting(
"joinOrderWarningThreshold",
LOG_INSIGHTS_SETTING,
@@ -484,6 +473,7 @@ export function joinOrderWarningThreshold(): number {
return JOIN_ORDER_WARNING_THRESHOLD.getValue<number>();
}
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
/**
* Hidden setting: Avoids caching in the AST viewer if the user is also a canary user.
*/
@@ -492,6 +482,10 @@ export const NO_CACHE_AST_VIEWER = new Setting(
AST_VIEWER_SETTING,
);
const CONTEXTUAL_QUERIES_SETTINGS = new Setting(
"contextualQueries",
ROOT_SETTING,
);
/**
* Hidden setting: Avoids caching in jump to def and find refs contextual queries if the user is also a canary user.
*/
@@ -708,18 +702,65 @@ export function showQueriesPanel(): boolean {
const MODEL_SETTING = new Setting("model", ROOT_SETTING);
const FLOW_GENERATION = new Setting("flowGeneration", MODEL_SETTING);
const LLM_GENERATION = new Setting("llmGeneration", MODEL_SETTING);
const LLM_GENERATION_BATCH_SIZE = new Setting(
"llmGenerationBatchSize",
MODEL_SETTING,
);
const LLM_GENERATION_DEV_ENDPOINT = new Setting(
"llmGenerationDevEndpoint",
MODEL_SETTING,
);
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
const ENABLE_RUBY = new Setting("enableRuby", MODEL_SETTING);
export function showFlowGeneration(): boolean {
return !!FLOW_GENERATION.getValue<boolean>();
export interface ModelConfig {
flowGeneration: boolean;
llmGeneration: boolean;
getExtensionsDirectory(languageId: string): string | undefined;
showMultipleModels: boolean;
enableRuby: boolean;
}
export function showLlmGeneration(): boolean {
return !!LLM_GENERATION.getValue<boolean>();
}
export class ModelConfigListener extends ConfigListener implements ModelConfig {
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings([MODEL_SETTING], e);
}
export function getExtensionsDirectory(languageId: string): string | undefined {
return EXTENSIONS_DIRECTORY.getValue<string>({
languageId,
});
public get flowGeneration(): boolean {
return !!FLOW_GENERATION.getValue<boolean>();
}
public get llmGeneration(): boolean {
return !!LLM_GENERATION.getValue<boolean>();
}
/**
* Limits the number of candidates we send to the model in each request to avoid long requests.
* Note that the model may return fewer than this number of candidates.
*/
public get llmGenerationBatchSize(): number {
return LLM_GENERATION_BATCH_SIZE.getValue<number | null>() || 5;
}
/**
* The URL of the endpoint to use for LLM generation. This should only be set
* if you want to test against a dev server.
*/
public get llmGenerationDevEndpoint(): string | undefined {
return LLM_GENERATION_DEV_ENDPOINT.getValue<string | undefined>();
}
public getExtensionsDirectory(languageId: string): string | undefined {
return EXTENSIONS_DIRECTORY.getValue<string>({
languageId,
});
}
public get showMultipleModels(): boolean {
return isCanary();
}
public get enableRuby(): boolean {
return !!ENABLE_RUBY.getValue<boolean>();
}
}

View File

@@ -1,22 +1,24 @@
import { retry } from "@octokit/plugin-retry";
import { throttling } from "@octokit/plugin-throttling";
import { Octokit } from "@octokit/rest";
import { Progress, CancellationToken } from "vscode";
import { CancellationToken } from "vscode";
import { Credentials } from "../common/authentication";
import { BaseLogger } from "../common/logging";
import { AppOctokit } from "../common/octokit";
import {
ProgressCallback,
UserCancellationException,
} from "../common/vscode/progress";
export async function getCodeSearchRepositories(
query: string,
progress: Progress<{
message?: string | undefined;
increment?: number | undefined;
}>,
progress: ProgressCallback,
token: CancellationToken,
credentials: Credentials,
logger: BaseLogger,
): Promise<string[]> {
let nwos: string[] = [];
const nwos: string[] = [];
const octokit = await provideOctokitWithThrottling(credentials, logger);
let i = 0;
for await (const response of octokit.paginate.iterator(
octokit.rest.search.code,
@@ -25,17 +27,19 @@ export async function getCodeSearchRepositories(
per_page: 100,
},
)) {
i++;
nwos.push(...response.data.map((item) => item.repository.full_name));
// calculate progress bar: 80% of the progress bar is used for the code search
const totalNumberOfRequests = Math.ceil(response.data.total_count / 100);
// Since we have a maximum of 1000 responses of the api, we can use a fixed increment whenever the totalNumberOfRequests would be greater than 10
const increment =
totalNumberOfRequests < 10 ? 80 / totalNumberOfRequests : 8;
progress.report({ increment });
const totalNumberOfResultPages = Math.ceil(response.data.total_count / 100);
const totalNumberOfRequests =
totalNumberOfResultPages > 10 ? 10 : totalNumberOfResultPages;
progress({
maxStep: totalNumberOfRequests,
step: i,
message: "Sending API requests to get Code Search results.",
});
if (token.isCancellationRequested) {
nwos = [];
break;
throw new UserCancellationException("Code search cancelled.", true);
}
}
@@ -46,12 +50,11 @@ async function provideOctokitWithThrottling(
credentials: Credentials,
logger: BaseLogger,
): Promise<Octokit> {
const MyOctokit = Octokit.plugin(throttling);
const MyOctokit = AppOctokit.plugin(throttling);
const auth = await credentials.getAccessToken();
const octokit = new MyOctokit({
auth,
retry,
throttle: {
onRateLimit: (retryAfter: number, options: any): boolean => {
void logger.log(

View File

@@ -14,7 +14,6 @@ import {
} from "fs-extra";
import { basename, join } from "path";
import * as Octokit from "@octokit/rest";
import { retry } from "@octokit/plugin-retry";
import { DatabaseManager, DatabaseItem } from "./local-databases";
import { tmpDir } from "../tmp-dir";
@@ -32,6 +31,8 @@ import { Credentials } from "../common/authentication";
import { AppCommandManager } from "../common/commands";
import { allowHttp } from "../config";
import { showAndLogInformationMessage } from "../common/logging";
import { AppOctokit } from "../common/octokit";
import { getLanguageDisplayName } from "../common/query-language";
/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
@@ -186,7 +187,7 @@ export async function downloadGitHubDatabase(
const octokit = credentials
? await credentials.getOctokit()
: new Octokit.Octokit({ retry });
: new AppOctokit();
const result = await convertGithubNwoToDatabaseUrl(
nwo,
@@ -579,10 +580,23 @@ export async function promptForLanguage(
return languages[0];
}
return await window.showQuickPick(languages, {
const items = languages
.map((language) => ({
label: getLanguageDisplayName(language),
description: language,
language,
}))
.sort((a, b) => a.label.localeCompare(b.label));
const selectedItem = await window.showQuickPick(items, {
placeHolder: "Select the database language to download:",
ignoreFocusOut: true,
});
if (!selectedItem) {
return undefined;
}
return selectedItem.language;
}
/**

View File

@@ -51,12 +51,14 @@ import {
createMultiSelectionCommand,
createSingleSelectionCommand,
} from "../common/vscode/selection-commands";
import { QueryLanguage, tryGetQueryLanguage } from "../common/query-language";
import { tryGetQueryLanguage } from "../common/query-language";
import { LanguageContextStore } from "../language-context-store";
enum SortOrder {
NameAsc = "NameAsc",
NameDesc = "NameDesc",
LanguageAsc = "LanguageAsc",
LanguageDesc = "LanguageDesc",
DateAddedAsc = "DateAddedAsc",
DateAddedDesc = "DateAddedDesc",
}
@@ -155,6 +157,10 @@ class DatabaseTreeDataProvider
return db1.name.localeCompare(db2.name, env.language);
case SortOrder.NameDesc:
return db2.name.localeCompare(db1.name, env.language);
case SortOrder.LanguageAsc:
return db1.language.localeCompare(db2.language, env.language);
case SortOrder.LanguageDesc:
return db2.language.localeCompare(db1.language, env.language);
case SortOrder.DateAddedAsc:
return (db1.dateAdded || 0) - (db2.dateAdded || 0);
case SortOrder.DateAddedDesc:
@@ -218,7 +224,7 @@ export class DatabaseUI extends DisposableObject {
public constructor(
private app: App,
private databaseManager: DatabaseManager,
private languageContext: LanguageContextStore,
languageContext: LanguageContextStore,
private readonly queryServer: QueryRunner | undefined,
private readonly storagePath: string,
readonly extensionPath: string,
@@ -252,6 +258,7 @@ export class DatabaseUI extends DisposableObject {
"codeQL.upgradeCurrentDatabase":
this.handleUpgradeCurrentDatabase.bind(this),
"codeQL.clearCache": this.handleClearCache.bind(this),
"codeQL.trimCache": this.handleTrimCache.bind(this),
"codeQLDatabases.chooseDatabaseFolder":
this.handleChooseDatabaseFolder.bind(this),
"codeQLDatabases.chooseDatabaseArchive":
@@ -263,61 +270,8 @@ export class DatabaseUI extends DisposableObject {
"codeQLDatabases.setCurrentDatabase":
this.handleMakeCurrentDatabase.bind(this),
"codeQLDatabases.sortByName": this.handleSortByName.bind(this),
"codeQLDatabases.sortByLanguage": this.handleSortByLanguage.bind(this),
"codeQLDatabases.sortByDateAdded": this.handleSortByDateAdded.bind(this),
"codeQLDatabases.displayAllLanguages":
this.handleClearLanguageFilter.bind(this),
"codeQLDatabases.displayCpp": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.Cpp,
),
"codeQLDatabases.displayCsharp": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.CSharp,
),
"codeQLDatabases.displayGo": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.Go,
),
"codeQLDatabases.displayJava": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.Java,
),
"codeQLDatabases.displayJavascript": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.Javascript,
),
"codeQLDatabases.displayPython": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.Python,
),
"codeQLDatabases.displayRuby": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.Ruby,
),
"codeQLDatabases.displaySwift": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.Swift,
),
"codeQLDatabases.displayAllLanguagesSelected":
this.handleClearLanguageFilter.bind(this),
"codeQLDatabases.displayCppSelected":
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Cpp),
"codeQLDatabases.displayCsharpSelected":
this.handleChangeLanguageFilter.bind(this, QueryLanguage.CSharp),
"codeQLDatabases.displayGoSelected": this.handleChangeLanguageFilter.bind(
this,
QueryLanguage.Go,
),
"codeQLDatabases.displayJavaSelected":
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Java),
"codeQLDatabases.displayJavascriptSelected":
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Javascript),
"codeQLDatabases.displayPythonSelected":
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Python),
"codeQLDatabases.displayRubySelected":
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Ruby),
"codeQLDatabases.displaySwiftSelected":
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Swift),
"codeQLDatabases.removeDatabase": createMultiSelectionCommand(
this.handleRemoveDatabase.bind(this),
),
@@ -600,6 +554,14 @@ export class DatabaseUI extends DisposableObject {
}
}
private async handleSortByLanguage() {
if (this.treeDataProvider.sortOrder === SortOrder.LanguageAsc) {
this.treeDataProvider.sortOrder = SortOrder.LanguageDesc;
} else {
this.treeDataProvider.sortOrder = SortOrder.LanguageAsc;
}
}
private async handleSortByDateAdded() {
if (this.treeDataProvider.sortOrder === SortOrder.DateAddedAsc) {
this.treeDataProvider.sortOrder = SortOrder.DateAddedDesc;
@@ -608,14 +570,6 @@ export class DatabaseUI extends DisposableObject {
}
}
private async handleClearLanguageFilter() {
await this.languageContext.clearLanguageContext();
}
private async handleChangeLanguageFilter(languageFilter: QueryLanguage) {
await this.languageContext.setLanguageContext(languageFilter);
}
private async handleUpgradeCurrentDatabase(): Promise<void> {
return withProgress(
async (progress, token) => {
@@ -703,6 +657,25 @@ export class DatabaseUI extends DisposableObject {
);
}
private async handleTrimCache(): Promise<void> {
return withProgress(
async (_progress, token) => {
if (
this.queryServer !== undefined &&
this.databaseManager.currentDatabaseItem !== undefined
) {
await this.queryServer.trimCacheInDatabase(
this.databaseManager.currentDatabaseItem,
token,
);
}
},
{
title: "Trimming cache",
},
);
}
private async handleGetCurrentDatabase(): Promise<string | undefined> {
const dbItem = await this.getDatabaseItemInternal(undefined);
return dbItem?.databaseUri.fsPath;

View File

@@ -167,6 +167,15 @@ export class DatabaseItemImpl implements DatabaseItem {
return encodeArchiveBasePath(sourceArchive.fsPath);
}
/**
* Returns true if the database's source archive is in the workspace.
*/
public hasSourceArchiveInExplorer(): boolean {
return (vscode.workspace.workspaceFolders || []).some((folder) =>
this.belongsToSourceArchiveExplorerUri(folder.uri),
);
}
public verifyZippedSources(): string | undefined {
const sourceArchive = this.sourceArchive;
if (sourceArchive === undefined) {

View File

@@ -56,6 +56,11 @@ export interface DatabaseItem {
*/
getSourceArchiveExplorerUri(): vscode.Uri;
/**
* Returns true if the database's source archive is in the workspace.
*/
hasSourceArchiveInExplorer(): boolean;
/**
* Holds if `uri` belongs to this database's source archive.
*/

View File

@@ -19,7 +19,10 @@ import {
getFirstWorkspaceFolder,
isFolderAlreadyInWorkspace,
} from "../../common/vscode/workspace-folders";
import { isQueryLanguage } from "../../common/query-language";
import {
isQueryLanguage,
tryGetQueryLanguage,
} from "../../common/query-language";
import { existsSync } from "fs";
import { QlPackGenerator } from "../../local-queries/qlpack-generator";
import { asError, getErrorMessage } from "../../common/helpers-pure";
@@ -30,6 +33,7 @@ import { containsPath } from "../../common/files";
import { DatabaseChangedEvent, DatabaseEventKind } from "./database-events";
import { DatabaseResolver } from "./database-resolver";
import { telemetryListener } from "../../common/vscode/telemetry";
import { LanguageContextStore } from "../../language-context-store";
/**
* The name of the key in the workspaceState dictionary in which we
@@ -100,11 +104,25 @@ export class DatabaseManager extends DisposableObject {
private readonly app: App,
private readonly qs: QueryRunner,
private readonly cli: cli.CodeQLCliServer,
private readonly languageContext: LanguageContextStore,
public logger: Logger,
) {
super();
qs.onStart(this.reregisterDatabases.bind(this));
this.push(
this.languageContext.onLanguageContextChanged(async () => {
if (
this.currentDatabaseItem !== undefined &&
!this.languageContext.isSelectedLanguage(
tryGetQueryLanguage(this.currentDatabaseItem.language),
)
) {
await this.setCurrentDatabaseItem(undefined);
}
}),
);
}
/**
@@ -230,8 +248,10 @@ export class DatabaseManager extends DisposableObject {
const firstWorkspaceFolder = getFirstWorkspaceFolder();
const folderName = `codeql-custom-queries-${databaseItem.language}`;
const qlpackStoragePath = join(firstWorkspaceFolder, folderName);
if (
existsSync(join(firstWorkspaceFolder, folderName)) ||
existsSync(qlpackStoragePath) ||
isFolderAlreadyInWorkspace(folderName)
) {
return;
@@ -256,10 +276,10 @@ export class DatabaseManager extends DisposableObject {
try {
const qlPackGenerator = new QlPackGenerator(
folderName,
databaseItem.language,
this.cli,
firstWorkspaceFolder,
qlpackStoragePath,
qlpackStoragePath,
);
await qlPackGenerator.generate();
} catch (e: unknown) {

View File

@@ -1,5 +1,4 @@
import {
ProgressLocation,
QuickPickItem,
TreeView,
TreeViewExpansionEvent,
@@ -7,7 +6,10 @@ import {
window,
workspace,
} from "vscode";
import { UserCancellationException } from "../../common/vscode/progress";
import {
UserCancellationException,
withProgress,
} from "../../common/vscode/progress";
import {
getNwoFromGitHubUrl,
isValidGitHubNwo,
@@ -34,10 +36,7 @@ import { DatabasePanelCommands } from "../../common/commands";
import { App } from "../../common/app";
import { QueryLanguage } from "../../common/query-language";
import { getCodeSearchRepositories } from "../code-search-api";
import {
showAndLogErrorMessage,
showAndLogInformationMessage,
} from "../../common/logging";
import { showAndLogErrorMessage } from "../../common/logging";
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
remoteDatabaseKind: string;
@@ -409,15 +408,8 @@ export class DbPanel extends DisposableObject {
return;
}
await window.withProgress(
{
location: ProgressLocation.Notification,
title: "Searching for repositories... This might take a while",
cancellable: true,
},
await withProgress(
async (progress, token) => {
progress.report({ increment: 10 });
const repositories = await getCodeSearchRepositories(
`${codeSearchQuery} ${languagePrompt}`,
progress,
@@ -426,18 +418,22 @@ export class DbPanel extends DisposableObject {
this.app.logger,
);
token.onCancellationRequested(() => {
void showAndLogInformationMessage(
this.app.logger,
"Code search cancelled",
);
return;
if (token.isCancellationRequested) {
throw new UserCancellationException("Code search cancelled.", true);
}
progress({
maxStep: 12,
step: 12,
message: "Processing results...",
});
progress.report({ increment: 10, message: "Processing results..." });
await this.dbManager.addNewRemoteReposToList(repositories, listName);
},
{
title: "Searching for repositories...",
cancellable: true,
},
);
}

View File

@@ -22,6 +22,7 @@ export interface QLDebugArgs {
extensionPacks?: string[] | string;
quickEval?: boolean;
noDebug?: boolean;
additionalRunQueryArgs?: Record<string, any>;
}
/**
@@ -120,6 +121,7 @@ export class QLDebugConfigurationProvider
extensionPacks,
quickEvalContext,
noDebug: qlConfiguration.noDebug ?? false,
additionalRunQueryArgs: qlConfiguration.additionalRunQueryArgs ?? {},
};
return resultConfiguration;

View File

@@ -70,6 +70,8 @@ export interface LaunchConfig {
quickEvalContext: QuickEvalContext | undefined;
/** Run the query without debugging it. */
noDebug: boolean;
/** Undocumented: Additional arguments to be passed to the `runQuery` API on the query server. */
additionalRunQueryArgs: Record<string, any>;
}
export interface LaunchRequest extends Request, DebugProtocol.LaunchRequest {

View File

@@ -161,6 +161,7 @@ class RunningQuery extends DisposableObject {
true,
config.additionalPacks,
config.extensionPacks,
config.additionalRunQueryArgs,
queryStorageDir,
undefined,
undefined,

View File

@@ -136,6 +136,7 @@ import { NewQueryRunner, QueryRunner, QueryServerClient } from "./query-server";
import { QueriesModule } from "./queries-panel/queries-module";
import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider";
import { LanguageContextStore } from "./language-context-store";
import { LanguageSelectionPanel } from "./language-selection-panel/language-selection-panel";
/**
* extension.ts
@@ -768,17 +769,28 @@ async function activateWithInstalledDistribution(
fsWatcher.onDidDelete(clearPackCache);
}
void extLogger.log("Initializing language context.");
const languageContext = new LanguageContextStore(app);
void extLogger.log("Initializing language selector.");
const languageSelectionPanel = new LanguageSelectionPanel(languageContext);
ctx.subscriptions.push(languageSelectionPanel);
void extLogger.log("Initializing database manager.");
const dbm = new DatabaseManager(ctx, app, qs, cliServer, extLogger);
const dbm = new DatabaseManager(
ctx,
app,
qs,
cliServer,
languageContext,
extLogger,
);
// Let this run async.
void dbm.loadPersistedState();
ctx.subscriptions.push(dbm);
void extLogger.log("Initializing language context.");
const languageContext = new LanguageContextStore(app);
void extLogger.log("Initializing database panel.");
const databaseUI = new DatabaseUI(
app,
@@ -790,7 +802,11 @@ async function activateWithInstalledDistribution(
);
ctx.subscriptions.push(databaseUI);
QueriesModule.initialize(app, cliServer);
const queriesModule = QueriesModule.initialize(
app,
languageContext,
cliServer,
);
void extLogger.log("Initializing evaluator log viewer.");
const evalLogViewer = new EvalLogViewer();
@@ -871,6 +887,7 @@ async function activateWithInstalledDistribution(
ctx,
queryHistoryConfigurationListener,
labelProvider,
languageContext,
async (
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo,
@@ -924,9 +941,14 @@ async function activateWithInstalledDistribution(
databaseUI,
localQueryResultsView,
queryStorageDir,
languageContext,
);
ctx.subscriptions.push(localQueries);
queriesModule.onDidChangeSelection((event) =>
localQueries.setSelectedQueryTreeViewItems(event.selection),
);
void extLogger.log("Initializing debugger factory.");
ctx.subscriptions.push(
new QLDebugAdapterDescriptorFactory(queryStorageDir, qs, localQueries),
@@ -1015,6 +1037,7 @@ async function activateWithInstalledDistribution(
...getPackagingCommands({
cliServer,
}),
...languageSelectionPanel.getCommands(),
...modelEditorModule.getCommands(),
...evalLogViewer.getCommands(),
...summaryLanguageSupport.getCommands(),
@@ -1163,13 +1186,13 @@ function addUnhandledRejectionListener() {
const message = redactableError(
asError(error),
)`Unhandled error: ${getErrorMessage(error)}`;
const fullMessage = message.fullMessageWithStack;
// Add a catch so that showAndLogExceptionWithTelemetry fails, we avoid
// triggering "unhandledRejection" and avoid an infinite loop
showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
message,
).catch((telemetryError: unknown) => {
showAndLogExceptionWithTelemetry(extLogger, telemetryListener, message, {
fullMessage,
}).catch((telemetryError: unknown) => {
void extLogger.log(
`Failed to send error telemetry: ${getErrorMessage(telemetryError)}`,
);

View File

@@ -43,7 +43,36 @@ export class LanguageContextStore extends DisposableObject {
);
}
/**
* This returns true if the given language should be included.
*
* That means that either the given language is selected or the "All" option is selected.
*
* @param language a query language or undefined if the language is unknown.
*/
public shouldInclude(language: QueryLanguage | undefined): boolean {
return this.languageFilter === "All" || this.languageFilter === language;
}
/**
* This returns true if the given language is selected.
*
* If no language is given then it returns true if the "All" option is selected.
*
* @param language a query language or undefined.
*/
public isSelectedLanguage(language: QueryLanguage | undefined): boolean {
return (
(this.languageFilter === "All" && language === undefined) ||
this.languageFilter === language
);
}
public get selectedLanguage(): QueryLanguage | undefined {
if (this.languageFilter === "All") {
return undefined;
}
return this.languageFilter;
}
}

View File

@@ -0,0 +1,91 @@
import { DisposableObject } from "../common/disposable-object";
import { LanguageContextStore } from "../language-context-store";
import {
Event,
EventEmitter,
ThemeIcon,
TreeDataProvider,
TreeItem,
} from "vscode";
import {
QueryLanguage,
getLanguageDisplayName,
} from "../common/query-language";
const ALL_LANGUAGE_SELECTION_OPTIONS = [
undefined, // All languages
QueryLanguage.Cpp,
QueryLanguage.CSharp,
QueryLanguage.Go,
QueryLanguage.Java,
QueryLanguage.Javascript,
QueryLanguage.Python,
QueryLanguage.Ruby,
QueryLanguage.Swift,
];
// A tree view items consisting of of a language (or undefined for all languages)
// and a boolean indicating whether it is selected or not.
export class LanguageSelectionTreeViewItem extends TreeItem {
constructor(
public readonly language: QueryLanguage | undefined,
public readonly selected: boolean = false,
) {
const label = language ? getLanguageDisplayName(language) : "All languages";
super(label);
this.iconPath = selected ? new ThemeIcon("check") : undefined;
this.contextValue = selected ? undefined : "canBeSelected";
}
}
export class LanguageSelectionTreeDataProvider
extends DisposableObject
implements TreeDataProvider<LanguageSelectionTreeViewItem>
{
private treeItems: LanguageSelectionTreeViewItem[];
private readonly onDidChangeTreeDataEmitter = this.push(
new EventEmitter<void>(),
);
public constructor(private readonly languageContext: LanguageContextStore) {
super();
this.treeItems = this.createTree();
// If the language context changes, we need to update the tree.
this.push(
this.languageContext.onLanguageContextChanged(() => {
this.treeItems = this.createTree();
this.onDidChangeTreeDataEmitter.fire();
}),
);
}
public get onDidChangeTreeData(): Event<void> {
return this.onDidChangeTreeDataEmitter.event;
}
public getTreeItem(item: LanguageSelectionTreeViewItem): TreeItem {
return item;
}
public getChildren(
item?: LanguageSelectionTreeViewItem,
): LanguageSelectionTreeViewItem[] {
if (!item) {
return this.treeItems;
} else {
return [];
}
}
private createTree(): LanguageSelectionTreeViewItem[] {
return ALL_LANGUAGE_SELECTION_OPTIONS.map((language) => {
return new LanguageSelectionTreeViewItem(
language,
this.languageContext.isSelectedLanguage(language),
);
});
}
}

View File

@@ -0,0 +1,41 @@
import { DisposableObject } from "../common/disposable-object";
import { window } from "vscode";
import {
LanguageSelectionTreeDataProvider,
LanguageSelectionTreeViewItem,
} from "./language-selection-data-provider";
import { LanguageContextStore } from "../language-context-store";
import { LanguageSelectionCommands } from "../common/commands";
// This panel allows the selection of a single language, that will
// then filter all other relevant views (e.g. db panel, query history).
export class LanguageSelectionPanel extends DisposableObject {
constructor(private readonly languageContext: LanguageContextStore) {
super();
const dataProvider = new LanguageSelectionTreeDataProvider(languageContext);
this.push(dataProvider);
const treeView = window.createTreeView("codeQLLanguageSelection", {
treeDataProvider: dataProvider,
});
this.push(treeView);
}
public getCommands(): LanguageSelectionCommands {
return {
"codeQLLanguageSelection.setSelectedItem":
this.handleSetSelectedLanguage.bind(this),
};
}
private async handleSetSelectedLanguage(
item: LanguageSelectionTreeViewItem,
): Promise<void> {
if (item.language) {
await this.languageContext.setLanguageContext(item.language);
} else {
await this.languageContext.clearLanguageContext();
}
}
}

View File

@@ -44,6 +44,7 @@ export async function runContextualQuery(
false,
getOnDiskWorkspaceFolders(),
undefined,
{},
queryStorageDir,
undefined,
templates,

View File

@@ -36,6 +36,7 @@ import {
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
import { AstBuilder } from "../ast-viewer/ast-builder";
import { qlpackOfDatabase } from "../../local-queries";
import { MultiCancellationToken } from "../../common/vscode/multi-cancellation-token";
/**
* Runs templated CodeQL queries to find definitions in
@@ -43,6 +44,7 @@ import { qlpackOfDatabase } from "../../local-queries";
* generalize this to other custom queries, e.g. showing dataflow to
* or from a selected identifier.
*/
export class TemplateQueryDefinitionProvider implements DefinitionProvider {
private cache: CachedOperation<LocationLink[]>;
@@ -60,11 +62,11 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
async provideDefinition(
document: TextDocument,
position: Position,
_token: CancellationToken,
token: CancellationToken,
): Promise<LocationLink[]> {
const fileLinks = this.shouldUseCache()
? await this.cache.get(document.uri.toString())
: await this.getDefinitions(document.uri.toString());
? await this.cache.get(document.uri.toString(), token)
: await this.getDefinitions(document.uri.toString(), token);
const locLinks: LocationLink[] = [];
for (const link of fileLinks) {
@@ -79,9 +81,13 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
return !(isCanary() && NO_CACHE_CONTEXTUAL_QUERIES.getValue<boolean>());
}
private async getDefinitions(uriString: string): Promise<LocationLink[]> {
private async getDefinitions(
uriString: string,
token: CancellationToken,
): Promise<LocationLink[]> {
return withProgress(
async (progress, token) => {
async (progress, tokenInner) => {
const multiToken = new MultiCancellationToken(token, tokenInner);
return getLocationsForUriString(
this.cli,
this.qs,
@@ -90,7 +96,7 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
KeyType.DefinitionQuery,
this.queryStorageDir,
progress,
token,
multiToken,
(src, _dest) => src === uriString,
);
},
@@ -126,11 +132,11 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
document: TextDocument,
position: Position,
_context: ReferenceContext,
_token: CancellationToken,
token: CancellationToken,
): Promise<Location[]> {
const fileLinks = this.shouldUseCache()
? await this.cache.get(document.uri.toString())
: await this.getReferences(document.uri.toString());
? await this.cache.get(document.uri.toString(), token)
: await this.getReferences(document.uri.toString(), token);
const locLinks: Location[] = [];
for (const link of fileLinks) {
@@ -148,9 +154,14 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
return !(isCanary() && NO_CACHE_CONTEXTUAL_QUERIES.getValue<boolean>());
}
private async getReferences(uriString: string): Promise<FullLocationLink[]> {
private async getReferences(
uriString: string,
token: CancellationToken,
): Promise<FullLocationLink[]> {
return withProgress(
async (progress, token) => {
async (progress, tokenInner) => {
const multiToken = new MultiCancellationToken(token, tokenInner);
return getLocationsForUriString(
this.cli,
this.qs,
@@ -159,7 +170,7 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
KeyType.DefinitionQuery,
this.queryStorageDir,
progress,
token,
multiToken,
(src, _dest) => src === uriString,
);
},

View File

@@ -1,5 +1,6 @@
export * from "./local-queries";
export * from "./local-query-run";
export * from "./query-constraints";
export * from "./query-resolver";
export * from "./quick-eval-code-lens-provider";
export * from "./quick-query";

View File

@@ -49,6 +49,8 @@ import { LocalQueryRun } from "./local-query-run";
import { createMultiSelectionCommand } from "../common/vscode/selection-commands";
import { findLanguage } from "../codeql-cli/query-language";
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import { tryGetQueryLanguage } from "../common/query-language";
import { LanguageContextStore } from "../language-context-store";
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
@@ -61,6 +63,8 @@ export enum QuickEvalType {
}
export class LocalQueries extends DisposableObject {
private selectedQueryTreeViewItems: readonly QueryTreeViewItem[] = [];
public constructor(
private readonly app: App,
private readonly queryRunner: QueryRunner,
@@ -70,10 +74,17 @@ export class LocalQueries extends DisposableObject {
private readonly databaseUI: DatabaseUI,
private readonly localQueryResultsView: ResultsView,
private readonly queryStorageDir: string,
private readonly languageContextStore: LanguageContextStore,
) {
super();
}
public setSelectedQueryTreeViewItems(
selection: readonly QueryTreeViewItem[],
) {
this.selectedQueryTreeViewItems = selection;
}
public getCommands(): LocalQueryCommands {
return {
"codeQL.runQuery": this.runQuery.bind(this),
@@ -322,13 +333,16 @@ export class LocalQueries extends DisposableObject {
const credentials = isCanary() ? this.app.credentials : undefined;
const contextStoragePath =
this.app.workspaceStoragePath || this.app.globalStoragePath;
const language = this.languageContextStore.selectedLanguage;
const skeletonQueryWizard = new SkeletonQueryWizard(
this.cliServer,
progress,
credentials,
this.app.logger,
this.app,
this.databaseManager,
contextStoragePath,
this.selectedQueryTreeViewItems,
language,
);
await skeletonQueryWizard.execute();
},
@@ -361,10 +375,15 @@ export class LocalQueries extends DisposableObject {
);
}
const initialInfo = await createInitialQueryInfo(selectedQuery, {
databaseUri: dbItem.databaseUri.toString(),
name: dbItem.name,
});
const initialInfo = await createInitialQueryInfo(
selectedQuery,
{
databaseUri: dbItem.databaseUri.toString(),
name: dbItem.name,
language: tryGetQueryLanguage(dbItem.language),
},
outputDir,
);
// When cancellation is requested from the query history view, we just stop the debug session.
const queryInfo = new LocalQueryInfo(initialInfo, tokenSource);
@@ -454,6 +473,7 @@ export class LocalQueries extends DisposableObject {
true,
additionalPacks,
extensionPacks,
{},
this.queryStorageDir,
undefined,
templates,

View File

@@ -97,6 +97,15 @@ export class LocalQueryRun {
* Updates the UI in the case where query evaluation throws an exception.
*/
public async fail(err: Error): Promise<void> {
const evalLogPaths = await this.summarizeEvalLog(
QueryResultType.OTHER_ERROR,
this.outputDir,
this.logger,
);
if (evalLogPaths !== undefined) {
this.queryInfo.setEvaluatorLogPaths(evalLogPaths);
}
err.message = `Error running query: ${err.message}`;
this.queryInfo.failureReason = err.message;
await this.queryHistoryManager.refreshTreeView();

View File

@@ -1,35 +1,36 @@
import { mkdir, writeFile } from "fs-extra";
import { dump } from "js-yaml";
import { join } from "path";
import { dirname, join } from "path";
import { Uri } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { QueryLanguage } from "../common/query-language";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { basename } from "../common/path";
export class QlPackGenerator {
private readonly qlpackName: string;
private qlpackName: string | undefined;
private readonly qlpackVersion: string;
private readonly header: string;
private readonly qlpackFileName: string;
private readonly folderUri: Uri;
constructor(
private readonly folderName: string,
private readonly queryLanguage: QueryLanguage,
private readonly cliServer: CodeQLCliServer,
private readonly storagePath: string | undefined,
private readonly storagePath: string,
private readonly queryStoragePath: string,
private readonly includeFolderNameInQlpackName: boolean = false,
) {
if (this.storagePath === undefined) {
throw new Error("Workspace storage path is undefined");
}
this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
this.qlpackVersion = "1.0.0";
this.header = "# This is an automatically generated file.\n\n";
this.qlpackFileName = "codeql-pack.yml";
this.folderUri = Uri.file(join(this.storagePath, this.folderName));
this.folderUri = Uri.file(this.storagePath);
}
public async generate() {
this.qlpackName = await this.determineQlpackName();
// create QL pack folder and add to workspace
await this.createWorkspaceFolder();
@@ -43,6 +44,37 @@ export class QlPackGenerator {
await this.createCodeqlPackLockYaml();
}
private async determineQlpackName(): Promise<string> {
let qlpackBaseName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
if (this.includeFolderNameInQlpackName) {
const folderBasename = basename(dirname(this.folderUri.fsPath));
if (
folderBasename.includes("codeql") ||
folderBasename.includes("queries")
) {
// If the user has already included "codeql" or "queries" in the folder name, don't include it twice
qlpackBaseName = `getting-started/${folderBasename}-${this.queryLanguage}`;
} else {
qlpackBaseName = `getting-started/codeql-extra-queries-${folderBasename}-${this.queryLanguage}`;
}
}
const existingQlPacks = await this.cliServer.resolveQlpacks(
getOnDiskWorkspaceFolders(),
);
const existingQlPackNames = Object.keys(existingQlPacks);
let qlpackName = qlpackBaseName;
let i = 0;
while (existingQlPackNames.includes(qlpackName)) {
i++;
qlpackName = `${qlpackBaseName}-${i}`;
}
return qlpackName;
}
private async createWorkspaceFolder() {
await mkdir(this.folderUri.fsPath);
}
@@ -60,7 +92,7 @@ export class QlPackGenerator {
}
public async createExampleQlFile(fileName = "example.ql") {
const exampleQlFilePath = join(this.folderUri.fsPath, fileName);
const exampleQlFilePath = join(this.queryStoragePath, fileName);
const exampleQl = `
/**

View File

@@ -0,0 +1,7 @@
export interface QueryConstraints {
kind?: string;
"tags contain"?: string[];
"tags contain all"?: string[];
"query filename"?: string;
"query path"?: string;
}

View File

@@ -14,6 +14,7 @@ import { showAndLogExceptionWithTelemetry } from "../common/logging";
import { extLogger } from "../common/logging/vscode";
import { telemetryListener } from "../common/vscode/telemetry";
import { SuiteInstruction } from "../packaging/suite-instruction";
import { QueryConstraints } from "./query-constraints";
export async function qlpackOfDatabase(
cli: Pick<CodeQLCliServer, "resolveQlpacks">,
@@ -27,12 +28,6 @@ export async function qlpackOfDatabase(
return await getQlPackForDbscheme(cli, dbscheme);
}
export interface QueryConstraints {
kind?: string;
"tags contain"?: string[];
"tags contain all"?: string[];
}
/**
* Finds the queries with the specified kind and tags in a list of CodeQL packs.
*
@@ -132,6 +127,14 @@ export async function resolveQueries(
`tagged all of "${constraints["tags contain all"].join(" ")}"`,
);
}
if (constraints["query filename"] !== undefined) {
humanConstraints.push(
`with query filename "${constraints["query filename"]}"`,
);
}
if (constraints["query path"] !== undefined) {
humanConstraints.push(`with query path "${constraints["query path"]}"`);
}
const joinedPacksToSearch = packsToSearch.join(", ");
const error = redactableError`No ${name} queries (${humanConstraints.join(

View File

@@ -41,6 +41,7 @@ export async function runQuery({
false,
additionalPacks,
extensionPacks,
{},
queryStorageDir,
undefined,
undefined,

View File

@@ -1,19 +1,23 @@
import { join } from "path";
import { Uri, workspace, window as Window } from "vscode";
import { dirname, join } from "path";
import { Uri, window, window as Window, workspace } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { BaseLogger } from "../common/logging";
import { showAndLogExceptionWithTelemetry } from "../common/logging";
import { Credentials } from "../common/authentication";
import { QueryLanguage } from "../common/query-language";
import {
getLanguageDisplayName,
QueryLanguage,
} from "../common/query-language";
import {
getFirstWorkspaceFolder,
isFolderAlreadyInWorkspace,
getOnDiskWorkspaceFolders,
} from "../common/vscode/workspace-folders";
import { getErrorMessage } from "../common/helpers-pure";
import { asError, getErrorMessage } from "../common/helpers-pure";
import { QlPackGenerator } from "./qlpack-generator";
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
import {
ProgressCallback,
UserCancellationException,
withProgress,
} from "../common/vscode/progress";
import {
askForGitHubRepo,
@@ -24,8 +28,16 @@ import {
isCodespacesTemplate,
setQlPackLocation,
} from "../config";
import { existsSync } from "fs-extra";
import { lstat, pathExists, readFile } from "fs-extra";
import { askForLanguage } from "../codeql-cli/query-language";
import { showInformationMessageWithAction } from "../common/vscode/dialog";
import { redactableError } from "../common/errors";
import { App } from "../common/app";
import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import { containsPath, pathsEqual } from "../common/files";
import { getQlPackPath } from "../common/ql";
import { load } from "js-yaml";
import { QlPackFile } from "../packaging/qlpack-file";
type QueryLanguagesToDatabaseMap = Record<string, string>;
@@ -41,73 +53,139 @@ export const QUERY_LANGUAGE_TO_DATABASE_REPO: QueryLanguagesToDatabaseMap = {
};
export class SkeletonQueryWizard {
private language: QueryLanguage | undefined;
private fileName = "example.ql";
private qlPackStoragePath: string | undefined;
private queryStoragePath: string | undefined;
private downloadPromise: Promise<void> | undefined;
constructor(
private readonly cliServer: CodeQLCliServer,
private readonly progress: ProgressCallback,
private readonly credentials: Credentials | undefined,
private readonly logger: BaseLogger,
private readonly app: App,
private readonly databaseManager: DatabaseManager,
private readonly databaseStoragePath: string | undefined,
private readonly selectedItems: readonly QueryTreeViewItem[],
private language: QueryLanguage | undefined = undefined,
) {}
private get folderName() {
return `codeql-custom-queries-${this.language}`;
/**
* Wait for the download process to complete by waiting for the user to select
* either "Download database" or closing the dialog. This is used for testing.
*/
public async waitForDownload() {
if (this.downloadPromise) {
await this.downloadPromise;
}
}
public async execute() {
// show quick pick to choose language
this.language = await this.chooseLanguage();
// First try detecting the language based on the existing qlpacks.
// This will override the selected language if there is an existing query pack.
const detectedLanguage = await this.detectLanguage();
if (detectedLanguage) {
this.language = detectedLanguage;
}
// If no existing qlpack was found, we need to ask the user for the language
if (!this.language) {
// show quick pick to choose language
this.language = await this.chooseLanguage();
}
if (!this.language) {
return;
}
this.qlPackStoragePath = await this.determineStoragePath();
let createSkeletonQueryPack: boolean = false;
const skeletonPackAlreadyExists =
existsSync(join(this.qlPackStoragePath, this.folderName)) ||
isFolderAlreadyInWorkspace(this.folderName);
if (!this.qlPackStoragePath) {
// This means no existing qlpack was detected in the selected folder, so we need
// to find a new location to store the qlpack. This new location could potentially
// already exist.
const storagePath = await this.determineStoragePath();
this.qlPackStoragePath = join(
storagePath,
`codeql-custom-queries-${this.language}`,
);
if (skeletonPackAlreadyExists) {
// just create a new example query file in skeleton QL pack
await this.createExampleFile();
// Try to detect if there is already a qlpack in this location. We will assume that
// the user hasn't changed the language of the qlpack.
const qlPackPath = await getQlPackPath(this.qlPackStoragePath);
// If we are creating or using a qlpack in the user's selected folder, we will also
// create the query in that folder
this.queryStoragePath = this.qlPackStoragePath;
createSkeletonQueryPack = qlPackPath === undefined;
} else {
// A query pack was detected in the selected folder or one of its ancestors, so we
// directly use the selected folder as the storage path for the query.
this.queryStoragePath = await this.determineStoragePathFromSelection();
}
if (createSkeletonQueryPack) {
// generate a new skeleton QL pack with query file
await this.createQlPack();
} else {
// just create a new example query file in skeleton QL pack
await this.createExampleFile();
}
// open the query file
try {
await this.openExampleFile();
} catch (e: unknown) {
void this.app.logger.log(
`Could not open example query file: ${getErrorMessage(e)}`,
);
}
// select existing database for language or download a new one
await this.selectOrDownloadDatabase();
// open a query file
try {
await this.openExampleFile();
} catch (e: unknown) {
void this.logger.log(
`Could not open example query file: ${getErrorMessage(e)}`,
);
}
}
private async openExampleFile() {
if (this.folderName === undefined || this.qlPackStoragePath === undefined) {
if (this.queryStoragePath === undefined) {
throw new Error("Path to folder is undefined");
}
const queryFileUri = Uri.file(
join(this.qlPackStoragePath, this.folderName, this.fileName),
);
const queryFileUri = Uri.file(join(this.queryStoragePath, this.fileName));
void workspace.openTextDocument(queryFileUri).then((doc) => {
void Window.showTextDocument(doc);
void Window.showTextDocument(doc, {
preview: false,
});
});
}
public async determineStoragePath() {
public async determineStoragePath(): Promise<string> {
if (this.selectedItems.length === 0) {
return this.determineRootStoragePath();
}
return this.determineStoragePathFromSelection();
}
private async determineStoragePathFromSelection(): Promise<string> {
// Just like VS Code's "New File" command, if the user has selected multiple files/folders in the queries panel,
// we will create the new file in the same folder as the first selected item.
// See https://github.com/microsoft/vscode/blob/a8b7239d0311d4915b57c837972baf4b01394491/src/vs/workbench/contrib/files/browser/fileActions.ts#L893-L900
const selectedItem = this.selectedItems[0];
const path = selectedItem.path;
// We use stat to protect against outdated query tree items
const fileStat = await lstat(path);
if (fileStat.isDirectory()) {
return path;
}
return dirname(path);
}
public async determineRootStoragePath() {
const firstStorageFolder = getFirstWorkspaceFolder();
if (isCodespacesTemplate()) {
@@ -116,7 +194,7 @@ export class SkeletonQueryWizard {
let storageFolder = getQlPackLocation();
if (storageFolder === undefined || !existsSync(storageFolder)) {
if (storageFolder === undefined || !(await pathExists(storageFolder))) {
storageFolder = await Window.showInputBox({
title:
"Please choose a folder in which to create your new query pack. You can change this in the extension settings.",
@@ -129,7 +207,7 @@ export class SkeletonQueryWizard {
throw new UserCancellationException("No storage folder entered.");
}
if (!existsSync(storageFolder)) {
if (!(await pathExists(storageFolder))) {
throw new UserCancellationException(
"Invalid folder. Must be a folder that already exists.",
);
@@ -139,6 +217,62 @@ export class SkeletonQueryWizard {
return storageFolder;
}
private async detectLanguage(): Promise<QueryLanguage | undefined> {
if (this.selectedItems.length < 1) {
return undefined;
}
this.progress({
message: "Resolving existing query packs",
step: 1,
maxStep: 3,
});
const storagePath = await this.determineStoragePathFromSelection();
const queryPacks = await this.cliServer.resolveQlpacks(
getOnDiskWorkspaceFolders(),
false,
"query",
);
const matchingQueryPacks = Object.values(queryPacks)
.map((paths) => paths.find((path) => containsPath(path, storagePath)))
.filter((path): path is string => path !== undefined)
// Find the longest matching path
.sort((a, b) => b.length - a.length);
if (matchingQueryPacks.length === 0) {
return undefined;
}
const matchingQueryPackPath = matchingQueryPacks[0];
const qlPackPath = await getQlPackPath(matchingQueryPackPath);
if (!qlPackPath) {
return undefined;
}
const qlPack = load(await readFile(qlPackPath, "utf8")) as
| QlPackFile
| undefined;
const dependencies = qlPack?.dependencies;
if (!dependencies || typeof dependencies !== "object") {
return;
}
const matchingLanguages = Object.values(QueryLanguage).filter(
(language) => `codeql/${language}-all` in dependencies,
);
if (matchingLanguages.length !== 1) {
return undefined;
}
this.qlPackStoragePath = matchingQueryPackPath;
return matchingLanguages[0];
}
private async chooseLanguage() {
this.progress({
message: "Choose language",
@@ -150,13 +284,6 @@ export class SkeletonQueryWizard {
}
private async createQlPack() {
if (this.folderName === undefined) {
throw new Error("Folder name is undefined");
}
if (this.language === undefined) {
throw new Error("Language is undefined");
}
this.progress({
message: "Creating skeleton QL pack around query",
step: 2,
@@ -164,29 +291,17 @@ export class SkeletonQueryWizard {
});
try {
const qlPackGenerator = new QlPackGenerator(
this.folderName,
this.language,
this.cliServer,
this.qlPackStoragePath,
);
const qlPackGenerator = this.createQlPackGenerator();
await qlPackGenerator.generate();
} catch (e: unknown) {
void this.logger.log(
void this.app.logger.log(
`Could not create skeleton QL pack: ${getErrorMessage(e)}`,
);
}
}
private async createExampleFile() {
if (this.folderName === undefined) {
throw new Error("Folder name is undefined");
}
if (this.language === undefined) {
throw new Error("Language is undefined");
}
this.progress({
message:
"Skeleton query pack already exists. Creating additional query example file.",
@@ -195,29 +310,29 @@ export class SkeletonQueryWizard {
});
try {
const qlPackGenerator = new QlPackGenerator(
this.folderName,
this.language,
this.cliServer,
this.qlPackStoragePath,
);
const qlPackGenerator = this.createQlPackGenerator();
this.fileName = await this.determineNextFileName(this.folderName);
this.fileName = await this.determineNextFileName();
await qlPackGenerator.createExampleQlFile(this.fileName);
} catch (e: unknown) {
void this.logger.log(
`Could not create skeleton QL pack: ${getErrorMessage(e)}`,
void this.app.logger.log(
`Could not create query example file: ${getErrorMessage(e)}`,
);
}
}
private async determineNextFileName(folderName: string): Promise<string> {
if (this.qlPackStoragePath === undefined) {
throw new Error("QL Pack storage path is undefined");
private async determineNextFileName(): Promise<string> {
if (this.queryStoragePath === undefined) {
throw new Error("Query storage path is undefined");
}
const folderUri = Uri.file(join(this.qlPackStoragePath, folderName));
const folderUri = Uri.file(this.queryStoragePath);
const files = await workspace.fs.readDirectory(folderUri);
// If the example.ql file doesn't exist yet, use that name
if (!files.some(([filename, _fileType]) => filename === this.fileName)) {
return this.fileName;
}
const qlFiles = files.filter(([filename, _fileType]) =>
filename.match(/^example[0-9]*\.ql$/),
);
@@ -225,11 +340,43 @@ export class SkeletonQueryWizard {
return `example${qlFiles.length + 1}.ql`;
}
private async downloadDatabase() {
if (this.qlPackStoragePath === undefined) {
throw new Error("QL Pack storage path is undefined");
private async promptDownloadDatabase() {
if (this.language === undefined) {
throw new Error("Language is undefined");
}
const openFileLink = this.openFileMarkdownLink;
const displayLanguage = getLanguageDisplayName(this.language);
const action = await showInformationMessageWithAction(
`New CodeQL query for ${displayLanguage} ${openFileLink} created, but no CodeQL databases for ${displayLanguage} were detected in your workspace. Would you like to download a CodeQL database for ${displayLanguage} to analyze with ${openFileLink}?`,
"Download database",
);
if (action) {
void withProgress(async (progress) => {
try {
await this.downloadDatabase(progress);
} catch (e: unknown) {
if (e instanceof UserCancellationException) {
return;
}
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(e),
)`An error occurred while downloading the GitHub repository: ${getErrorMessage(
e,
)}`,
);
}
});
}
}
private async downloadDatabase(progress: ProgressCallback) {
if (this.databaseStoragePath === undefined) {
throw new Error("Database storage path is undefined");
}
@@ -238,10 +385,10 @@ export class SkeletonQueryWizard {
throw new Error("Language is undefined");
}
this.progress({
progress({
message: "Downloading database",
step: 3,
maxStep: 3,
step: 1,
maxStep: 2,
});
const githubRepoNwo = QUERY_LANGUAGE_TO_DATABASE_REPO[this.language];
@@ -256,7 +403,7 @@ export class SkeletonQueryWizard {
this.databaseManager,
this.databaseStoragePath,
this.credentials,
this.progress,
progress,
this.cliServer,
this.language,
);
@@ -267,10 +414,6 @@ export class SkeletonQueryWizard {
throw new Error("Language is undefined");
}
if (this.qlPackStoragePath === undefined) {
throw new Error("QL Pack storage path is undefined");
}
const existingDatabaseItem =
await SkeletonQueryWizard.findExistingDatabaseItem(
this.language,
@@ -278,14 +421,65 @@ export class SkeletonQueryWizard {
);
if (existingDatabaseItem) {
// select the found database
await this.databaseManager.setCurrentDatabaseItem(existingDatabaseItem);
const openFileLink = this.openFileMarkdownLink;
if (this.databaseManager.currentDatabaseItem !== existingDatabaseItem) {
// select the found database
await this.databaseManager.setCurrentDatabaseItem(existingDatabaseItem);
const displayLanguage = getLanguageDisplayName(this.language);
void window.showInformationMessage(
`New CodeQL query for ${displayLanguage} ${openFileLink} created. We have automatically selected your existing CodeQL ${displayLanguage} database ${existingDatabaseItem.name} for you to analyze with ${openFileLink}.`,
);
}
} else {
// download new database and select it
await this.downloadDatabase();
this.downloadPromise = this.promptDownloadDatabase().finally(() => {
this.downloadPromise = undefined;
});
}
}
private get openFileMarkdownLink() {
if (this.queryStoragePath === undefined) {
throw new Error("QL Pack storage path is undefined");
}
const queryPath = join(this.queryStoragePath, this.fileName);
const queryPathUri = Uri.file(queryPath);
const openFileArgs = [queryPathUri.toString(true)];
const queryString = encodeURI(JSON.stringify(openFileArgs));
return `[${this.fileName}](command:vscode.open?${queryString})`;
}
private createQlPackGenerator() {
if (this.qlPackStoragePath === undefined) {
throw new Error("QL pack storage path is undefined");
}
if (this.queryStoragePath === undefined) {
throw new Error("Query storage path is undefined");
}
if (this.language === undefined) {
throw new Error("Language is undefined");
}
const parentFolder = dirname(this.qlPackStoragePath);
// Only include the folder name in the qlpack name if the qlpack is not in the root of the workspace.
const includeFolderNameInQlpackName = !getOnDiskWorkspaceFolders().some(
(workspaceFolder) => pathsEqual(workspaceFolder, parentFolder),
);
return new QlPackGenerator(
this.language,
this.cliServer,
this.qlPackStoragePath,
this.queryStoragePath,
includeFolderNameInQlpackName,
);
}
public static async findDatabaseItemByNwo(
language: string,
databaseNwo: string,

View File

@@ -40,17 +40,19 @@ function makeKey(
const DEPENDENT_PREDICATES_REGEXP = (() => {
const regexps = [
// SCAN id
String.raw`SCAN\s+([0-9a-zA-Z:#_]+)\s`,
String.raw`SCAN\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s`,
// JOIN id WITH id
String.raw`JOIN\s+([0-9a-zA-Z:#_]+)\s+WITH\s+([0-9a-zA-Z:#_]+)\s`,
String.raw`JOIN\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s+WITH\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s`,
// AGGREGATE id, id
String.raw`AGGREGATE\s+([0-9a-zA-Z:#_]+)\s*,\s+([0-9a-zA-Z:#_]+)`,
String.raw`AGGREGATE\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s*,\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)`,
// id AND NOT id
String.raw`([0-9a-zA-Z:#_]+)\s+AND\s+NOT\s+([0-9a-zA-Z:#_]+)`,
String.raw`([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s+AND\s+NOT\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)`,
// INVOKE HIGHER-ORDER RELATION rel ON <id, ..., id>
String.raw`INVOKE\s+HIGHER-ORDER\s+RELATION\s[^\s]+\sON\s+<([0-9a-zA-Z:#_<>]+)((?:,[0-9a-zA-Z:#_<>]+)*)>`,
String.raw`INVOKE\s+HIGHER-ORDER\s+RELATION\s[^\s]+\sON\s+<([0-9a-zA-Z:#_<>]+|\`[^\`\r\n]*\`)((?:,[0-9a-zA-Z:#_<>]+|,\`[^\`\r\n]*\`)*)>`,
// SELECT id
String.raw`SELECT\s+([0-9a-zA-Z:#_]+)`,
String.raw`SELECT\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)`,
// REWRITE id WITH
String.raw`REWRITE\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s+WITH\s`,
];
return new RegExp(
`${String.raw`\{[0-9]+\}\s+[0-9a-zA-Z]+\s=\s(?:` + regexps.join("|")})`,
@@ -65,7 +67,12 @@ function getDependentPredicates(operations: string[]): I.List<string> {
.rest() // Skip the first group as it's just the entire string
.filter((x) => !!x && !x.match("r[0-9]+|PRIMITIVE")) // Only keep the references to predicates.
.flatMap((x) => x.split(",")) // Group 2 in the INVOKE HIGHER_ORDER RELATION case is a comma-separated list of identifiers.
.filter((x) => !!x); // Remove empty strings
.filter((x) => !!x) // Remove empty strings
.map((x) =>
x.startsWith("`") && x.endsWith("`")
? x.substring(1, x.length - 1)
: x,
); // Remove quotes from quoted identifiers
} else {
return I.List();
}

View File

@@ -1,5 +1,7 @@
import { Credentials } from "../common/authentication";
import { OctokitResponse } from "@octokit/types";
import fetch from "node-fetch";
import { ModelConfigListener } from "../config";
export enum AutomodelMode {
Unspecified = "AUTOMODEL_MODE_UNSPECIFIED",
@@ -20,15 +22,44 @@ export interface ModelResponse {
export async function autoModel(
credentials: Credentials,
request: ModelRequest,
modelingConfig: ModelConfigListener,
): Promise<ModelResponse> {
const octokit = await credentials.getOctokit();
const devEndpoint = modelingConfig.llmGenerationDevEndpoint;
if (devEndpoint) {
return callAutoModelDevEndpoint(devEndpoint, request);
} else {
const octokit = await credentials.getOctokit();
const response: OctokitResponse<ModelResponse> = await octokit.request(
"POST /repos/github/codeql/code-scanning/codeql/auto-model",
{
data: request,
},
);
const response: OctokitResponse<ModelResponse> = await octokit.request(
"POST /repos/github/codeql/code-scanning/codeql/auto-model",
{
data: request,
},
);
return response.data;
return response.data;
}
}
async function callAutoModelDevEndpoint(
endpoint: string,
request: ModelRequest,
): Promise<ModelResponse> {
const json = JSON.stringify(request);
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: json,
});
if (!response.ok) {
throw new Error(
`Error calling auto-model API: ${response.status} ${response.statusText}`,
);
}
const data = await response.json();
return data as ModelResponse;
}

View File

@@ -14,13 +14,13 @@ import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
* the order in the UI.
* @param mode Whether it is application or framework mode.
* @param methods all methods.
* @param modeledMethods the currently modeled methods.
* @param modeledMethodsBySignature the currently modeled methods.
* @returns list of modeled methods that are candidates for modeling.
*/
export function getCandidates(
mode: Mode,
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
methods: readonly Method[],
modeledMethodsBySignature: Record<string, readonly ModeledMethod[]>,
): MethodSignature[] {
// Sort the same way as the UI so we send the first ones listed in the UI first
const grouped = groupMethods(methods, mode);
@@ -32,12 +32,12 @@ export function getCandidates(
const candidates: MethodSignature[] = [];
for (const method of sortedMethods) {
const modeledMethod: ModeledMethod = modeledMethods[method.signature] ?? {
type: "none",
};
const modeledMethods: ModeledMethod[] = [
...(modeledMethodsBySignature[method.signature] ?? []),
];
// Anything that is modeled is not a candidate
if (modeledMethod.type !== "none") {
if (modeledMethods.some((m) => m.type !== "none")) {
continue;
}

View File

@@ -1,6 +1,5 @@
import { Method, MethodSignature } from "./method";
import { ModeledMethod } from "./modeled-method";
import { extLogger } from "../common/logging/vscode";
import { load as loadYaml } from "js-yaml";
import { ProgressCallback, withProgress } from "../common/vscode/progress";
import { createAutoModelRequest, getCandidates } from "./auto-model";
@@ -16,11 +15,9 @@ import { QueryRunner } from "../query-server";
import { DatabaseItem } from "../databases/local-databases";
import { Mode } from "./shared/mode";
import { CancellationTokenSource } from "vscode";
// Limit the number of candidates we send to the model in each request
// to avoid long requests.
// Note that the model may return fewer than this number of candidates.
const candidateBatchSize = 20;
import { ModelingStore } from "./modeling-store";
import { ModelConfigListener } from "../config";
import { QueryLanguage } from "../common/query-language";
/**
* The auto-modeler holds state around auto-modeling jobs and allows
@@ -35,14 +32,13 @@ export class AutoModeler {
private readonly app: App,
private readonly cliServer: CodeQLCliServer,
private readonly queryRunner: QueryRunner,
private readonly modelConfig: ModelConfigListener,
private readonly modelingStore: ModelingStore,
private readonly queryStorageDir: string,
private readonly databaseItem: DatabaseItem,
private readonly setInProgressMethods: (
packageName: string,
inProgressMethods: string[],
) => Promise<void>,
private readonly language: QueryLanguage,
private readonly addModeledMethods: (
modeledMethods: Record<string, ModeledMethod>,
modeledMethods: Record<string, ModeledMethod[]>,
) => Promise<void>,
) {
this.jobs = new Map<string, CancellationTokenSource>();
@@ -58,8 +54,8 @@ export class AutoModeler {
*/
public async startModeling(
packageName: string,
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
methods: readonly Method[],
modeledMethods: Record<string, readonly ModeledMethod[]>,
mode: Mode,
): Promise<void> {
if (this.jobs.has(packageName)) {
@@ -87,7 +83,7 @@ export class AutoModeler {
* @param packageName The name of the package to stop modeling.
*/
public async stopModeling(packageName: string): Promise<void> {
void extLogger.log(`Stopping modeling for package ${packageName}`);
void this.app.logger.log(`Stopping modeling for package ${packageName}`);
const cancellationTokenSource = this.jobs.get(packageName);
if (cancellationTokenSource) {
cancellationTokenSource.cancel();
@@ -105,19 +101,22 @@ export class AutoModeler {
private async modelPackage(
packageName: string,
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
methods: readonly Method[],
modeledMethods: Record<string, readonly ModeledMethod[]>,
mode: Mode,
cancellationTokenSource: CancellationTokenSource,
): Promise<void> {
void extLogger.log(`Modeling package ${packageName}`);
void this.app.logger.log(`Modeling package ${packageName}`);
const candidateBatchSize = this.modelConfig.llmGenerationBatchSize;
await withProgress(async (progress) => {
// Fetch the candidates to send to the model
const allCandidateMethods = getCandidates(mode, methods, modeledMethods);
// If there are no candidates, there is nothing to model and we just return
if (allCandidateMethods.length === 0) {
void extLogger.log("No candidates to model. Stopping.");
void this.app.logger.log("No candidates to model. Stopping.");
return;
}
@@ -135,11 +134,14 @@ export class AutoModeler {
const start = i * candidateBatchSize;
const end = start + candidateBatchSize;
const candidatesToProcess = allCandidateMethods.slice(start, end);
const candidateSignatures = candidatesToProcess.map(
(c) => c.signature,
);
// Let the UI know which candidates we are modeling
await this.setInProgressMethods(
packageName,
candidatesToProcess.map((c) => c.signature),
this.modelingStore.addInProgressMethods(
this.databaseItem,
candidateSignatures,
);
// Kick off the process to model the slice of candidates
@@ -149,10 +151,19 @@ export class AutoModeler {
progress,
cancellationTokenSource,
);
// Let the UI know which candidates we are done modeling
this.modelingStore.removeInProgressMethods(
this.databaseItem,
candidateSignatures,
);
}
} finally {
// Clear out in progress methods
await this.setInProgressMethods(packageName, []);
// Clear out in progress methods in case anything went wrong
this.modelingStore.removeInProgressMethods(
this.databaseItem,
allCandidateMethods.map((c) => c.signature),
);
}
});
}
@@ -163,7 +174,7 @@ export class AutoModeler {
progress: ProgressCallback,
cancellationTokenSource: CancellationTokenSource,
): Promise<void> {
void extLogger.log("Executing auto-model queries");
void this.app.logger.log("Executing auto-model queries");
const usages = await runAutoModelQueries({
mode,
@@ -181,7 +192,7 @@ export class AutoModeler {
const request = await createAutoModelRequest(mode, usages);
void extLogger.log("Calling auto-model API");
void this.app.logger.log("Calling auto-model API");
const response = await this.callAutoModelApi(request);
if (!response) {
@@ -192,32 +203,11 @@ export class AutoModeler {
filename: "auto-model.yml",
});
const loadedMethods = loadDataExtensionYaml(models);
const loadedMethods = loadDataExtensionYaml(models, this.language);
if (!loadedMethods) {
return;
}
// Any candidate that was part of the response is a negative result
// meaning that the canidate is not a sink for the kinds that the LLM is checking for.
// For now we model this as a sink neutral method, however this is subject
// to discussion.
for (const candidate of candidateMethods) {
if (!(candidate.signature in loadedMethods)) {
loadedMethods[candidate.signature] = {
type: "neutral",
kind: "sink",
input: "",
output: "",
provenance: "ai-generated",
signature: candidate.signature,
packageName: candidate.packageName,
typeName: candidate.typeName,
methodName: candidate.methodName,
methodParameters: candidate.methodParameters,
};
}
}
await this.addModeledMethods(loadedMethods);
}
@@ -225,7 +215,7 @@ export class AutoModeler {
request: ModelRequest,
): Promise<ModelResponse | null> {
try {
return await autoModel(this.app.credentials, request);
return await autoModel(this.app.credentials, request, this.modelConfig);
} catch (e) {
if (e instanceof RequestError && e.status === 429) {
void showAndLogExceptionWithTelemetry(

View File

@@ -4,13 +4,18 @@ import { ModeledMethodType } from "./modeled-method";
import { parseLibraryFilename } from "./library";
import { Mode } from "./shared/mode";
import { ApplicationModeTuple, FrameworkModeTuple } from "./queries/query";
import { QueryLanguage } from "../common/query-language";
import { getModelsAsDataLanguage } from "./languages";
export function decodeBqrsToMethods(
chunk: DecodedBqrsChunk,
mode: Mode,
language: QueryLanguage,
): Method[] {
const methodsByApiName = new Map<string, Method>();
const definition = getModelsAsDataLanguage(language);
chunk?.tuples.forEach((tuple) => {
let usage: Call;
let packageName: string;
@@ -51,7 +56,12 @@ export function decodeBqrsToMethods(
classification = CallClassification.Unknown;
}
const signature = `${packageName}.${typeName}#${methodName}${methodParameters}`;
const signature = definition.createMethodSignature({
packageName,
typeName,
methodName,
methodParameters,
});
// For Java, we'll always get back a .jar file, and the library version may be bad because not all library authors
// properly specify the version. Therefore, we'll always try to parse the name and version from the library filename
@@ -88,9 +98,16 @@ export function decodeBqrsToMethods(
}
const method = methodsByApiName.get(signature)!;
method.usages.push({
...usage,
classification,
const usages = [
...method.usages,
{
...usage,
classification,
},
];
methodsByApiName.set(signature, {
...method,
usages,
});
});

View File

@@ -11,7 +11,7 @@ import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql";
import { getErrorMessage } from "../common/helpers-pure";
import { ExtensionPack } from "./shared/extension-pack";
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
import { getExtensionsDirectory } from "../config";
import { ModelConfig } from "../config";
import {
autoNameExtensionPack,
ExtensionPackName,
@@ -28,6 +28,7 @@ const extensionPackValidate = ajv.compile(extensionPackMetadataSchemaJson);
export async function pickExtensionPack(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
databaseItem: Pick<DatabaseItem, "name" | "language">,
modelConfig: ModelConfig,
logger: NotificationLogger,
progress: ProgressCallback,
maxStep: number,
@@ -56,12 +57,14 @@ export async function pickExtensionPack(
});
// Get the `codeQL.model.extensionsDirectory` setting for the language
const userExtensionsDirectory = getExtensionsDirectory(databaseItem.language);
const userExtensionsDirectory = modelConfig.getExtensionsDirectory(
databaseItem.language,
);
// If the setting is not set, automatically pick a suitable directory
const extensionsDirectory = userExtensionsDirectory
? Uri.file(userExtensionsDirectory)
: await autoPickExtensionsDirectory();
: await autoPickExtensionsDirectory(logger);
if (!extensionsDirectory) {
return undefined;

View File

@@ -1,7 +1,7 @@
import { FileType, Uri, workspace, WorkspaceFolder } from "vscode";
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
import { extLogger } from "../common/logging/vscode";
import { tmpdir } from "../common/files";
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
/**
* Returns the ancestors of this path in order from furthest to closest (i.e. root of filesystem to parent directory)
@@ -143,9 +143,20 @@ async function findGitFolder(
* for which the .git directory is closest to a workspace folder
* 6. If none of the above apply, return `undefined`
*/
export async function autoPickExtensionsDirectory(): Promise<Uri | undefined> {
export async function autoPickExtensionsDirectory(
logger: NotificationLogger,
): Promise<Uri | undefined> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
// If there are no on-disk workspace folders, we can't do anything
if (workspaceFolders.length === 0) {
void showAndLogErrorMessage(
logger,
`Could not find any on-disk workspace folders. Please ensure that you have opened a folder or workspace.`,
);
return undefined;
}
// If there's only 1 workspace folder, use the `.github/codeql/extensions` directory in that folder
if (workspaceFolders.length === 1) {
return Uri.joinPath(
@@ -168,7 +179,7 @@ export async function autoPickExtensionsDirectory(): Promise<Uri | undefined> {
// Get the root workspace directory, i.e. the common root directory of all workspace folders
const rootDirectory = await getRootWorkspaceDirectory();
if (!rootDirectory) {
void extLogger.log("Unable to determine root workspace directory");
void logger.log("Unable to determine root workspace directory");
return undefined;
}
@@ -192,7 +203,7 @@ export async function autoPickExtensionsDirectory(): Promise<Uri | undefined> {
},
)
) {
void extLogger.log(
void logger.log(
`Failed to add workspace folder for extensions at ${extensionsUri.fsPath}`,
);
return undefined;

View File

@@ -1,189 +0,0 @@
import { QueryRunner } from "../query-server";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { extLogger } from "../common/logging/vscode";
import { showAndLogExceptionWithTelemetry } from "../common/logging";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { DatabaseItem } from "../databases/local-databases";
import { ProgressCallback } from "../common/vscode/progress";
import { redactableError } from "../common/errors";
import { telemetryListener } from "../common/vscode/telemetry";
import { join } from "path";
import { Mode } from "./shared/mode";
import { writeFile } from "fs-extra";
import { QueryLanguage } from "../common/query-language";
import { fetchExternalApiQueries } from "./queries";
import { Method } from "./method";
import { runQuery } from "../local-queries/run-query";
import { decodeBqrsToMethods } from "./bqrs";
import {
resolveEndpointsQuery,
syntheticQueryPackName,
} from "./model-editor-queries";
type RunQueryOptions = {
cliServer: CodeQLCliServer;
queryRunner: QueryRunner;
databaseItem: DatabaseItem;
queryStorageDir: string;
queryDir: string;
progress: ProgressCallback;
token: CancellationToken;
};
export async function prepareExternalApiQuery(
queryDir: string,
language: QueryLanguage,
): Promise<boolean> {
// Resolve the query that we want to run.
const query = fetchExternalApiQueries[language];
if (!query) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`No external API usage query found for language ${language}`,
);
return false;
}
// Create the query file.
Object.values(Mode).map(async (mode) => {
const queryFile = join(queryDir, queryNameFromMode(mode));
await writeFile(queryFile, query[`${mode}ModeQuery`], "utf8");
});
// Create any dependencies
if (query.dependencies) {
for (const [filename, contents] of Object.entries(query.dependencies)) {
const dependencyFile = join(queryDir, filename);
await writeFile(dependencyFile, contents, "utf8");
}
}
return true;
}
export const externalApiQueriesProgressMaxStep = 2000;
export async function runExternalApiQueries(
mode: Mode,
{
cliServer,
queryRunner,
databaseItem,
queryStorageDir,
queryDir,
progress,
token,
}: RunQueryOptions,
): Promise<Method[] | undefined> {
// The below code is temporary to allow for rapid prototyping of the queries. Once the queries are stabilized, we will
// move these queries into the `github/codeql` repository and use them like any other contextual (e.g. AST) queries.
// This is intentionally not pretty code, as it will be removed soon.
// For a reference of what this should do in the future, see the previous implementation in
// https://github.com/github/vscode-codeql/blob/089d3566ef0bc67d9b7cc66e8fd6740b31c1c0b0/extensions/ql-vscode/src/data-extensions-editor/external-api-usage-query.ts#L33-L72
progress({
message: "Resolving QL packs",
step: 1,
maxStep: externalApiQueriesProgressMaxStep,
});
const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = Object.keys(
await cliServer.resolveQlpacks(additionalPacks, true),
);
progress({
message: "Resolving query",
step: 2,
maxStep: externalApiQueriesProgressMaxStep,
});
// Resolve the queries from either codeql/java-queries or from the temporary queryDir
const queryPath = await resolveEndpointsQuery(
cliServer,
databaseItem.language,
mode,
[syntheticQueryPackName],
[queryDir],
);
if (!queryPath) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`The ${mode} model editor query could not be found. Try re-opening the model editor. If that doesn't work, try upgrading the CodeQL libraries.`,
);
return;
}
// Run the actual query
const completedQuery = await runQuery({
queryRunner,
databaseItem,
queryPath,
queryStorageDir,
additionalPacks,
extensionPacks,
progress: (update) =>
progress({
step: update.step + 500,
maxStep: externalApiQueriesProgressMaxStep,
message: update.message,
}),
token,
});
if (!completedQuery) {
return;
}
// Read the results and covert to internal representation
progress({
message: "Decoding results",
step: 1600,
maxStep: externalApiQueriesProgressMaxStep,
});
const bqrsChunk = await readQueryResults({
cliServer,
bqrsPath: completedQuery.outputDir.bqrsPath,
});
if (!bqrsChunk) {
return;
}
progress({
message: "Finalizing results",
step: 1950,
maxStep: externalApiQueriesProgressMaxStep,
});
return decodeBqrsToMethods(bqrsChunk, mode);
}
type GetResultsOptions = {
cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">;
bqrsPath: string;
};
export async function readQueryResults({
cliServer,
bqrsPath,
}: GetResultsOptions) {
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
);
return undefined;
}
const resultSet = bqrsInfo["result-sets"][0];
return cliServer.bqrsDecode(bqrsPath, resultSet.name);
}
function queryNameFromMode(mode: Mode): string {
return `${mode.charAt(0).toUpperCase() + mode.slice(1)}ModeEndpoints.ql`;
}

View File

@@ -1,174 +0,0 @@
import { CancellationToken } from "vscode";
import { DatabaseItem } from "../databases/local-databases";
import { basename } from "path";
import { QueryRunner } from "../query-server";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { showAndLogExceptionWithTelemetry } from "../common/logging";
import { extLogger } from "../common/logging/vscode";
import { extensiblePredicateDefinitions } from "./predicates";
import { ProgressCallback } from "../common/vscode/progress";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
import { redactableError } from "../common/errors";
import { telemetryListener } from "../common/vscode/telemetry";
import { runQuery } from "../local-queries/run-query";
import { resolveQueries } from "../local-queries";
type FlowModelOptions = {
cliServer: CodeQLCliServer;
queryRunner: QueryRunner;
queryStorageDir: string;
databaseItem: DatabaseItem;
progress: ProgressCallback;
token: CancellationToken;
onResults: (results: ModeledMethod[]) => void | Promise<void>;
};
export async function runFlowModelQueries({
onResults,
...options
}: FlowModelOptions) {
const queries = await resolveFlowQueries(
options.cliServer,
options.databaseItem,
);
const queriesByBasename: Record<string, string> = {};
for (const query of queries) {
queriesByBasename[basename(query)] = query;
}
const summaryResults = await runSingleFlowQuery(
"summary",
queriesByBasename["CaptureSummaryModels.ql"],
0,
options,
);
if (summaryResults) {
await onResults(summaryResults);
}
const sinkResults = await runSingleFlowQuery(
"sink",
queriesByBasename["CaptureSinkModels.ql"],
1,
options,
);
if (sinkResults) {
await onResults(sinkResults);
}
const sourceResults = await runSingleFlowQuery(
"source",
queriesByBasename["CaptureSourceModels.ql"],
2,
options,
);
if (sourceResults) {
await onResults(sourceResults);
}
const neutralResults = await runSingleFlowQuery(
"neutral",
queriesByBasename["CaptureNeutralModels.ql"],
3,
options,
);
if (neutralResults) {
await onResults(neutralResults);
}
}
async function resolveFlowQueries(
cliServer: CodeQLCliServer,
databaseItem: DatabaseItem,
): Promise<string[]> {
const packsToSearch = [`codeql/${databaseItem.language}-queries`];
return await resolveQueries(
cliServer,
packsToSearch,
"flow model generator",
{
"tags contain": ["modelgenerator"],
},
);
}
async function runSingleFlowQuery(
type: Exclude<ModeledMethodType, "none">,
queryPath: string | undefined,
queryStep: number,
{
cliServer,
queryRunner,
queryStorageDir,
databaseItem,
progress,
token,
}: Omit<FlowModelOptions, "onResults">,
): Promise<ModeledMethod[]> {
// Check that the right query was found
if (queryPath === undefined) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`Failed to find ${type} query`,
);
return [];
}
// Run the query
const completedQuery = await runQuery({
queryRunner,
databaseItem,
queryPath,
queryStorageDir,
additionalPacks: getOnDiskWorkspaceFolders(),
extensionPacks: undefined,
progress: ({ step, message }) =>
progress({
message: `Generating ${type} model: ${message}`,
step: queryStep * 1000 + step,
maxStep: 4000,
}),
token,
});
if (!completedQuery) {
return [];
}
// Interpret the results
const definition = extensiblePredicateDefinitions[type];
const bqrsPath = completedQuery.outputDir.bqrsPath;
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`Expected exactly one result set, got ${
bqrsInfo["result-sets"].length
} for ${basename(queryPath)}`,
);
}
const resultSet = bqrsInfo["result-sets"][0];
const decodedResults = await cliServer.bqrsDecode(bqrsPath, resultSet.name);
const results = decodedResults.tuples;
return (
results
// This is just a sanity check. The query should only return strings.
.filter((result) => typeof result[0] === "string")
.map((result) => {
const row = result[0] as string;
return definition.readModeledMethod(row.split(";"));
})
);
}

View File

@@ -0,0 +1,99 @@
import { CancellationToken } from "vscode";
import { DatabaseItem } from "../databases/local-databases";
import { basename } from "path";
import { QueryRunner } from "../query-server";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { ProgressCallback } from "../common/vscode/progress";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { ModeledMethod } from "./modeled-method";
import { runQuery } from "../local-queries/run-query";
import { QueryConstraints, resolveQueries } from "../local-queries";
import { DecodedBqrs } from "../common/bqrs-cli-types";
type GenerateQueriesOptions = {
queryConstraints: QueryConstraints;
filterQueries?: (queryPath: string) => boolean;
parseResults: (
queryPath: string,
results: DecodedBqrs,
) => ModeledMethod[] | Promise<ModeledMethod[]>;
onResults: (results: ModeledMethod[]) => void | Promise<void>;
cliServer: CodeQLCliServer;
queryRunner: QueryRunner;
queryStorageDir: string;
databaseItem: DatabaseItem;
progress: ProgressCallback;
token: CancellationToken;
};
export async function runGenerateQueries(options: GenerateQueriesOptions) {
const { queryConstraints, filterQueries, parseResults, onResults } = options;
options.progress({
message: "Resolving queries",
step: 1,
maxStep: 5000,
});
const packsToSearch = [`codeql/${options.databaseItem.language}-queries`];
const queryPaths = await resolveQueries(
options.cliServer,
packsToSearch,
"generate model",
queryConstraints,
);
const filteredQueryPaths = filterQueries
? queryPaths.filter(filterQueries)
: queryPaths;
const maxStep = filteredQueryPaths.length * 1000;
for (let i = 0; i < filteredQueryPaths.length; i++) {
const queryPath = filteredQueryPaths[i];
const bqrs = await runSingleGenerateQuery(queryPath, i, maxStep, options);
if (bqrs) {
await onResults(await parseResults(queryPath, bqrs));
}
}
}
async function runSingleGenerateQuery(
queryPath: string,
queryStep: number,
maxStep: number,
{
cliServer,
queryRunner,
queryStorageDir,
databaseItem,
progress,
token,
}: GenerateQueriesOptions,
): Promise<DecodedBqrs | undefined> {
const queryBasename = basename(queryPath);
// Run the query
const completedQuery = await runQuery({
queryRunner,
databaseItem,
queryPath,
queryStorageDir,
additionalPacks: getOnDiskWorkspaceFolders(),
extensionPacks: undefined,
progress: ({ step, message }) =>
progress({
message: `Generating model from ${queryBasename}: ${message}`,
step: queryStep * 1000 + step,
maxStep,
}),
token,
});
if (!completedQuery) {
return undefined;
}
return cliServer.bqrsDecodeAll(completedQuery.outputDir.bqrsPath);
}

View File

@@ -0,0 +1,2 @@
export * from "./languages";
export * from "./models-as-data";

View File

@@ -0,0 +1,36 @@
import { QueryLanguage } from "../../common/query-language";
import {
ModelsAsDataLanguage,
ModelsAsDataLanguagePredicates,
} from "./models-as-data";
import { ruby } from "./ruby";
import { staticLanguage } from "./static";
const languages: Partial<Record<QueryLanguage, ModelsAsDataLanguage>> = {
[QueryLanguage.CSharp]: staticLanguage,
[QueryLanguage.Java]: staticLanguage,
[QueryLanguage.Ruby]: ruby,
};
export function getModelsAsDataLanguage(
language: QueryLanguage,
): ModelsAsDataLanguage {
const definition = languages[language];
if (!definition) {
throw new Error(`No models-as-data definition for ${language}`);
}
return definition;
}
export function getModelsAsDataLanguageModel<
T extends keyof ModelsAsDataLanguagePredicates,
>(
language: QueryLanguage,
model: T,
): NonNullable<ModelsAsDataLanguagePredicates[T]> {
const definition = getModelsAsDataLanguage(language).predicates[model];
if (!definition) {
throw new Error(`No models-as-data predicate for ${model}`);
}
return definition;
}

View File

@@ -0,0 +1,67 @@
import { MethodArgument, MethodDefinition } from "../method";
import {
ModeledMethod,
NeutralModeledMethod,
SinkModeledMethod,
SourceModeledMethod,
SummaryModeledMethod,
} from "../modeled-method";
import { DataTuple } from "../model-extension-file";
import { Mode } from "../shared/mode";
import type { QueryConstraints } from "../../local-queries/query-constraints";
import { DecodedBqrs } from "../../common/bqrs-cli-types";
import { BaseLogger } from "../../common/logging";
type GenerateMethodDefinition<T> = (method: T) => DataTuple[];
type ReadModeledMethod = (row: DataTuple[]) => ModeledMethod;
export type ModelsAsDataLanguagePredicate<T> = {
extensiblePredicate: string;
supportedKinds: string[];
generateMethodDefinition: GenerateMethodDefinition<T>;
readModeledMethod: ReadModeledMethod;
};
type ModelsAsDataLanguageModelGeneration = {
queryConstraints: QueryConstraints;
filterQueries?: (queryPath: string) => boolean;
parseResults: (
// The path to the query that generated the results.
queryPath: string,
// The results of the query.
bqrs: DecodedBqrs,
// The language-specific predicate that was used to generate the results. This is passed to allow
// sharing of code between different languages.
modelsAsDataLanguage: ModelsAsDataLanguage,
// The logger to use for logging.
logger: BaseLogger,
) => ModeledMethod[];
};
export type ModelsAsDataLanguagePredicates = {
source?: ModelsAsDataLanguagePredicate<SourceModeledMethod>;
sink?: ModelsAsDataLanguagePredicate<SinkModeledMethod>;
summary?: ModelsAsDataLanguagePredicate<SummaryModeledMethod>;
neutral?: ModelsAsDataLanguagePredicate<NeutralModeledMethod>;
};
export type MethodArgumentOptions = {
options: MethodArgument[];
defaultArgumentPath: string;
};
export type ModelsAsDataLanguage = {
/**
* The modes that are available for this language. If not specified, all
* modes are available.
*/
availableModes?: Mode[];
createMethodSignature: (method: MethodDefinition) => string;
predicates: ModelsAsDataLanguagePredicates;
modelGeneration?: ModelsAsDataLanguageModelGeneration;
/**
* Returns the list of valid arguments that can be selected for the given method.
* @param method The method to get the valid arguments for.
*/
getArgumentOptions: (method: MethodDefinition) => MethodArgumentOptions;
};

View File

@@ -0,0 +1,50 @@
import { BaseLogger } from "../../../common/logging";
import { DecodedBqrs } from "../../../common/bqrs-cli-types";
import { ModelsAsDataLanguage } from "../models-as-data";
import { ModeledMethod } from "../../modeled-method";
import { DataTuple } from "../../model-extension-file";
export function parseGenerateModelResults(
_queryPath: string,
bqrs: DecodedBqrs,
modelsAsDataLanguage: ModelsAsDataLanguage,
logger: BaseLogger,
): ModeledMethod[] {
const modeledMethods: ModeledMethod[] = [];
for (const resultSetName in bqrs) {
const definition = Object.values(modelsAsDataLanguage.predicates).find(
(definition) => definition.extensiblePredicate === resultSetName,
);
if (definition === undefined) {
void logger.log(`No predicate found for ${resultSetName}`);
continue;
}
const resultSet = bqrs[resultSetName];
if (
resultSet.tuples.some((tuple) =>
tuple.some((value) => typeof value === "object"),
)
) {
void logger.log(
`Skipping ${resultSetName} because it contains undefined values`,
);
continue;
}
modeledMethods.push(
...resultSet.tuples.map((tuple) => {
const row = tuple.filter(
(value): value is DataTuple => typeof value !== "object",
);
return definition.readModeledMethod(row);
}),
);
}
return modeledMethods;
}

View File

@@ -0,0 +1,191 @@
import { ModelsAsDataLanguage } from "../models-as-data";
import { sharedExtensiblePredicates, sharedKinds } from "../shared";
import { Mode } from "../../shared/mode";
import { parseGenerateModelResults } from "./generate";
import { getArgumentsList, MethodArgument } from "../../method";
function parseRubyMethodFromPath(path: string): string {
const match = path.match(/Method\[([^\]]+)].*/);
if (match) {
return match[1];
} else {
return "";
}
}
function parseRubyAccessPath(path: string): {
methodName: string;
path: string;
} {
const match = path.match(/Method\[([^\]]+)]\.(.*)/);
if (match) {
return { methodName: match[1], path: match[2] };
} else {
return { methodName: "", path: "" };
}
}
function rubyMethodSignature(typeName: string, methodName: string) {
return `${typeName}#${methodName}`;
}
export const ruby: ModelsAsDataLanguage = {
availableModes: [Mode.Framework],
createMethodSignature: ({ typeName, methodName }) =>
`${typeName}#${methodName}`,
predicates: {
source: {
extensiblePredicate: sharedExtensiblePredicates.source,
supportedKinds: sharedKinds.source,
// extensible predicate sourceModel(
// string type, string path, string kind
// );
generateMethodDefinition: (method) => [
method.typeName,
`Method[${method.methodName}].${method.output}`,
method.kind,
],
readModeledMethod: (row) => {
const typeName = row[0] as string;
const { methodName, path: output } = parseRubyAccessPath(
row[1] as string,
);
return {
type: "source",
input: "",
output,
kind: row[2] as string,
provenance: "manual",
signature: rubyMethodSignature(typeName, methodName),
packageName: "",
typeName,
methodName,
methodParameters: "",
};
},
},
sink: {
extensiblePredicate: sharedExtensiblePredicates.sink,
supportedKinds: sharedKinds.sink,
// extensible predicate sinkModel(
// string type, string path, string kind
// );
generateMethodDefinition: (method) => {
const path = `Method[${method.methodName}].${method.input}`;
return [method.typeName, path, method.kind];
},
readModeledMethod: (row) => {
const typeName = row[0] as string;
const { methodName, path: input } = parseRubyAccessPath(
row[1] as string,
);
return {
type: "sink",
input,
output: "",
kind: row[2] as string,
provenance: "manual",
signature: rubyMethodSignature(typeName, methodName),
packageName: "",
typeName,
methodName,
methodParameters: "",
};
},
},
summary: {
extensiblePredicate: sharedExtensiblePredicates.summary,
supportedKinds: sharedKinds.summary,
// extensible predicate summaryModel(
// string type, string path, string input, string output, string kind
// );
generateMethodDefinition: (method) => [
method.typeName,
`Method[${method.methodName}]`,
method.input,
method.output,
method.kind,
],
readModeledMethod: (row) => {
const typeName = row[0] as string;
const methodName = parseRubyMethodFromPath(row[1] as string);
return {
type: "summary",
input: row[2] as string,
output: row[3] as string,
kind: row[4] as string,
provenance: "manual",
signature: rubyMethodSignature(typeName, methodName),
packageName: "",
typeName,
methodName,
methodParameters: "",
};
},
},
neutral: {
extensiblePredicate: sharedExtensiblePredicates.neutral,
supportedKinds: sharedKinds.neutral,
// extensible predicate neutralModel(
// string type, string path, string kind
// );
generateMethodDefinition: (method) => [
method.typeName,
`Method[${method.methodName}]`,
method.kind,
],
readModeledMethod: (row) => {
const typeName = row[0] as string;
const methodName = parseRubyMethodFromPath(row[1] as string);
return {
type: "neutral",
input: "",
output: "",
kind: row[2] as string,
provenance: "manual",
signature: rubyMethodSignature(typeName, methodName),
packageName: "",
typeName,
methodName,
methodParameters: "",
};
},
},
},
modelGeneration: {
queryConstraints: {
"query path": "queries/modeling/GenerateModel.ql",
},
parseResults: parseGenerateModelResults,
},
getArgumentOptions: (method) => {
const argumentsList = getArgumentsList(method.methodParameters).map(
(argument, index): MethodArgument => {
if (argument.endsWith(":")) {
return {
path: `Argument[${argument}]`,
label: `Argument[${argument}]`,
};
}
return {
path: `Argument[${index}]`,
label: `Argument[${index}]: ${argument}`,
};
},
);
return {
options: [
{
path: "Argument[self]",
label: "Argument[self]",
},
...argumentsList,
],
// If there are no arguments, we will default to "Argument[self]"
defaultArgumentPath:
argumentsList.length > 0 ? argumentsList[0].path : "Argument[self]",
};
},
};

View File

@@ -0,0 +1,25 @@
export const sharedExtensiblePredicates = {
source: "sourceModel",
sink: "sinkModel",
summary: "summaryModel",
neutral: "neutralModel",
};
export const sharedKinds = {
source: ["local", "remote"],
sink: [
"code-injection",
"command-injection",
"file-content-store",
"html-injection",
"js-injection",
"ldap-injection",
"log-injection",
"path-injection",
"request-forgery",
"sql-injection",
"url-redirection",
],
summary: ["taint", "value"],
neutral: ["summary", "source", "sink"],
};

View File

@@ -0,0 +1,60 @@
import { BaseLogger } from "../../../common/logging";
import {
ModelsAsDataLanguage,
ModelsAsDataLanguagePredicates,
} from "../models-as-data";
import { DecodedBqrs } from "../../../common/bqrs-cli-types";
import { ModeledMethod } from "../../modeled-method";
import { basename } from "../../../common/path";
const queriesToModel: Record<string, keyof ModelsAsDataLanguagePredicates> = {
"CaptureSummaryModels.ql": "summary",
"CaptureSinkModels.ql": "sink",
"CaptureSourceModels.ql": "source",
"CaptureNeutralModels.ql": "neutral",
};
export function filterFlowModelQueries(queryPath: string): boolean {
return Object.keys(queriesToModel).includes(basename(queryPath));
}
export function parseFlowModelResults(
queryPath: string,
bqrs: DecodedBqrs,
modelsAsDataLanguage: ModelsAsDataLanguage,
logger: BaseLogger,
): ModeledMethod[] {
if (Object.keys(bqrs).length !== 1) {
throw new Error(
`Expected exactly one result set from ${queryPath}, but got ${
Object.keys(bqrs).length
}`,
);
}
const modelType = queriesToModel[basename(queryPath)];
if (!modelType) {
void logger.log(`Unknown model type for ${queryPath}`);
return [];
}
const resultSet = bqrs[Object.keys(bqrs)[0]];
const results = resultSet.tuples;
const definition = modelsAsDataLanguage.predicates[modelType];
if (!definition) {
throw new Error(`No definition for ${modelType}`);
}
return (
results
// This is just a sanity check. The query should only return strings.
.filter((result) => typeof result[0] === "string")
.map((result) => {
const row = result[0] as string;
return definition.readModeledMethod(row.split(";"));
})
);
}

View File

@@ -0,0 +1,170 @@
import { ModelsAsDataLanguage } from "../models-as-data";
import { Provenance } from "../../modeled-method";
import { DataTuple } from "../../model-extension-file";
import { sharedExtensiblePredicates, sharedKinds } from "../shared";
import { filterFlowModelQueries, parseFlowModelResults } from "./generate";
import { getArgumentsList, MethodArgument } from "../../method";
function readRowToMethod(row: DataTuple[]): string {
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
}
export const staticLanguage: ModelsAsDataLanguage = {
createMethodSignature: ({
packageName,
typeName,
methodName,
methodParameters,
}) => `${packageName}.${typeName}#${methodName}${methodParameters}`,
predicates: {
source: {
extensiblePredicate: sharedExtensiblePredicates.source,
supportedKinds: sharedKinds.source,
// extensible predicate sourceModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.packageName,
method.typeName,
true,
method.methodName,
method.methodParameters,
"",
method.output,
method.kind,
method.provenance,
],
readModeledMethod: (row) => ({
type: "source",
input: "",
output: row[6] as string,
kind: row[7] as string,
provenance: row[8] as Provenance,
signature: readRowToMethod(row),
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
},
sink: {
extensiblePredicate: sharedExtensiblePredicates.sink,
supportedKinds: sharedKinds.sink,
// extensible predicate sinkModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.packageName,
method.typeName,
true,
method.methodName,
method.methodParameters,
"",
method.input,
method.kind,
method.provenance,
],
readModeledMethod: (row) => ({
type: "sink",
input: row[6] as string,
output: "",
kind: row[7] as string,
provenance: row[8] as Provenance,
signature: readRowToMethod(row),
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
},
summary: {
extensiblePredicate: sharedExtensiblePredicates.summary,
supportedKinds: sharedKinds.summary,
// extensible predicate summaryModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.packageName,
method.typeName,
true,
method.methodName,
method.methodParameters,
"",
method.input,
method.output,
method.kind,
method.provenance,
],
readModeledMethod: (row) => ({
type: "summary",
input: row[6] as string,
output: row[7] as string,
kind: row[8] as string,
provenance: row[9] as Provenance,
signature: readRowToMethod(row),
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
},
neutral: {
extensiblePredicate: sharedExtensiblePredicates.neutral,
supportedKinds: sharedKinds.neutral,
// extensible predicate neutralModel(
// string package, string type, string name, string signature, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.packageName,
method.typeName,
method.methodName,
method.methodParameters,
method.kind,
method.provenance,
],
readModeledMethod: (row) => ({
type: "neutral",
input: "",
output: "",
kind: row[4] as string,
provenance: row[5] as Provenance,
signature: `${row[0]}.${row[1]}#${row[2]}${row[3]}`,
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[2] as string,
methodParameters: row[3] as string,
}),
},
},
modelGeneration: {
queryConstraints: {
"tags contain": ["modelgenerator"],
},
filterQueries: filterFlowModelQueries,
parseResults: parseFlowModelResults,
},
getArgumentOptions: (method) => {
const argumentsList = getArgumentsList(method.methodParameters).map(
(argument, index): MethodArgument => ({
path: `Argument[${index}]`,
label: `Argument[${index}]: ${argument}`,
}),
);
return {
options: [
{
path: "Argument[this]",
label: "Argument[this]",
},
...argumentsList,
],
// If there are no arguments, we will default to "Argument[this]"
defaultArgumentPath:
argumentsList.length > 0 ? argumentsList[0].path : "Argument[this]",
};
},
};

View File

@@ -4,15 +4,34 @@ import { DisposableObject } from "../../common/disposable-object";
import { MethodModelingViewProvider } from "./method-modeling-view-provider";
import { Method } from "../method";
import { ModelingStore } from "../modeling-store";
import { ModelEditorViewTracker } from "../model-editor-view-tracker";
import { ModelConfigListener } from "../../config";
import { DatabaseItem } from "../../databases/local-databases";
import { ModelingEvents } from "../modeling-events";
export class MethodModelingPanel extends DisposableObject {
private readonly provider: MethodModelingViewProvider;
constructor(app: App, modelingStore: ModelingStore) {
constructor(
app: App,
modelingStore: ModelingStore,
modelingEvents: ModelingEvents,
editorViewTracker: ModelEditorViewTracker,
) {
super();
this.provider = new MethodModelingViewProvider(app, modelingStore);
this.push(this.provider);
// This is here instead of in MethodModelingViewProvider because we need to
// dispose this when the extension gets disposed, not when the webview gets
// disposed.
const modelConfig = this.push(new ModelConfigListener());
this.provider = new MethodModelingViewProvider(
app,
modelingStore,
modelingEvents,
editorViewTracker,
modelConfig,
);
this.push(
window.registerWebviewViewProvider(
MethodModelingViewProvider.viewType,
@@ -21,7 +40,10 @@ export class MethodModelingPanel extends DisposableObject {
);
}
public async setMethod(method: Method): Promise<void> {
await this.provider.setMethod(method);
public async setMethod(
databaseItem: DatabaseItem,
method: Method,
): Promise<void> {
await this.provider.setMethod(databaseItem, method);
}
}

View File

@@ -1,153 +1,265 @@
import * as vscode from "vscode";
import { Uri, WebviewViewProvider } from "vscode";
import { getHtmlForWebview } from "../../common/vscode/webview-html";
import { FromMethodModelingMessage } from "../../common/interface-types";
import {
FromMethodModelingMessage,
ToMethodModelingMessage,
} from "../../common/interface-types";
import { telemetryListener } from "../../common/vscode/telemetry";
import { showAndLogExceptionWithTelemetry } from "../../common/logging/notifications";
import { extLogger } from "../../common/logging/vscode/loggers";
import { App } from "../../common/app";
import { redactableError } from "../../common/errors";
import { Method } from "../method";
import { DisposableObject } from "../../common/disposable-object";
import { ModelingStore } from "../modeling-store";
import { AbstractWebviewViewProvider } from "../../common/vscode/abstract-webview-view-provider";
import { assertNever } from "../../common/helpers-pure";
import { ModelEditorViewTracker } from "../model-editor-view-tracker";
import { ModelConfigListener } from "../../config";
import { DatabaseItem } from "../../databases/local-databases";
import { ModelingEvents } from "../modeling-events";
import {
QueryLanguage,
tryGetQueryLanguage,
} from "../../common/query-language";
export class MethodModelingViewProvider
extends DisposableObject
implements WebviewViewProvider
{
export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
ToMethodModelingMessage,
FromMethodModelingMessage
> {
public static readonly viewType = "codeQLMethodModeling";
private webviewView: vscode.WebviewView | undefined = undefined;
private method: Method | undefined = undefined;
private databaseItem: DatabaseItem | undefined = undefined;
private language: QueryLanguage | undefined = undefined;
constructor(
private readonly app: App,
app: App,
private readonly modelingStore: ModelingStore,
private readonly modelingEvents: ModelingEvents,
private readonly editorViewTracker: ModelEditorViewTracker,
private readonly modelConfig: ModelConfigListener,
) {
super();
super(app, "method-modeling");
}
/**
* This is called when a view first becomes visible. This may happen when the view is
* first loaded or when the user hides and then shows a view again.
*/
public resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [Uri.file(this.app.extensionPath)],
};
protected override async onWebViewLoaded(): Promise<void> {
await Promise.all([this.setViewState(), this.setInitialState()]);
this.registerToModelingEvents();
this.registerToModelConfigEvents();
}
const html = getHtmlForWebview(
this.app,
webviewView.webview,
"method-modeling",
{
allowInlineStyles: true,
allowWasmEval: false,
private async setViewState(): Promise<void> {
await this.postMessage({
t: "setMethodModelingPanelViewState",
viewState: {
language: this.language,
showMultipleModels: this.modelConfig.showMultipleModels,
},
);
webviewView.webview.html = html;
webviewView.webview.onDidReceiveMessage(async (msg) => this.onMessage(msg));
this.webviewView = webviewView;
this.setInitialState(webviewView);
this.registerToModelingStoreEvents();
});
}
public async setMethod(method: Method): Promise<void> {
public async setMethod(
databaseItem: DatabaseItem | undefined,
method: Method | undefined,
): Promise<void> {
this.method = method;
this.databaseItem = databaseItem;
this.language = databaseItem && tryGetQueryLanguage(databaseItem.language);
if (this.webviewView) {
await this.webviewView.webview.postMessage({
if (this.isShowingView) {
await this.postMessage({
t: "setMethod",
method,
});
}
}
private setInitialState(webviewView: vscode.WebviewView): void {
const selectedMethod = this.modelingStore.getSelectedMethodDetails();
if (selectedMethod) {
void webviewView.webview.postMessage({
t: "setSelectedMethod",
method: selectedMethod.method,
modeledMethod: selectedMethod.modeledMethod,
isModified: selectedMethod.isModified,
private async setInitialState(): Promise<void> {
if (this.modelingStore.hasStateForActiveDb()) {
const selectedMethod = this.modelingStore.getSelectedMethodDetails();
if (selectedMethod) {
this.databaseItem = selectedMethod.databaseItem;
this.language = tryGetQueryLanguage(
selectedMethod.databaseItem.language,
);
this.method = selectedMethod.method;
await this.postMessage({
t: "setSelectedMethod",
method: selectedMethod.method,
modeledMethods: selectedMethod.modeledMethods,
isModified: selectedMethod.isModified,
isInProgress: selectedMethod.isInProgress,
});
}
await this.postMessage({
t: "setInModelingMode",
inModelingMode: true,
});
}
}
private async onMessage(msg: FromMethodModelingMessage): Promise<void> {
protected override async onMessage(
msg: FromMethodModelingMessage,
): Promise<void> {
switch (msg.t) {
case "setModeledMethod": {
const activeState = this.modelingStore.getStateForActiveDb();
if (!activeState) {
throw new Error("No active state found in modeling store");
}
this.modelingStore.updateModeledMethod(
activeState.databaseItem,
msg.method,
);
case "viewLoaded":
await this.onWebViewLoaded();
break;
}
case "telemetry": {
case "telemetry":
telemetryListener?.sendUIInteraction(msg.action);
break;
}
case "unhandledError":
void showAndLogExceptionWithTelemetry(
extLogger,
this.app.logger,
telemetryListener,
redactableError(
msg.error,
)`Unhandled error in method modeling view: ${msg.error.message}`,
);
break;
case "setMultipleModeledMethods": {
if (!this.databaseItem) {
return;
}
this.modelingStore.updateModeledMethods(
this.databaseItem,
msg.methodSignature,
msg.modeledMethods,
);
this.modelingStore.addModifiedMethod(
this.databaseItem,
msg.methodSignature,
);
break;
}
case "revealInModelEditor":
await this.revealInModelEditor(msg.method);
void telemetryListener?.sendUIInteraction(
"method-modeling-reveal-in-model-editor",
);
break;
case "startModeling":
await this.app.commands.execute(
"codeQL.openModelEditorFromModelingPanel",
);
break;
default:
assertNever(msg);
}
}
private registerToModelingStoreEvents(): void {
this.modelingStore.onModeledMethodsChanged(async (e) => {
if (this.webviewView && e.isActiveDb) {
const modeledMethod = e.modeledMethods[this.method?.signature ?? ""];
if (modeledMethod) {
await this.webviewView.webview.postMessage({
t: "setModeledMethod",
method: modeledMethod,
private async revealInModelEditor(method: Method): Promise<void> {
if (!this.databaseItem) {
return;
}
const view = this.editorViewTracker.getView(
this.databaseItem.databaseUri.toString(),
);
await view?.revealMethod(method);
}
private registerToModelingEvents(): void {
this.push(
this.modelingEvents.onModeledMethodsChanged(async (e) => {
if (this.webviewView && e.isActiveDb && this.method) {
const modeledMethods = e.modeledMethods[this.method.signature];
if (modeledMethods) {
await this.postMessage({
t: "setMultipleModeledMethods",
methodSignature: this.method.signature,
modeledMethods,
});
}
}
}),
);
this.push(
this.modelingEvents.onModifiedMethodsChanged(async (e) => {
if (this.webviewView && e.isActiveDb && this.method) {
const isModified = e.modifiedMethods.has(this.method.signature);
await this.postMessage({
t: "setMethodModified",
isModified,
});
}
}
});
}),
);
this.modelingStore.onModifiedMethodsChanged(async (e) => {
if (this.webviewView && e.isActiveDb && this.method) {
const isModified = e.modifiedMethods.has(this.method.signature);
await this.webviewView.webview.postMessage({
t: "setMethodModified",
isModified,
});
}
});
this.push(
this.modelingEvents.onSelectedMethodChanged(async (e) => {
if (this.webviewView) {
this.method = e.method;
this.databaseItem = e.databaseItem;
this.language = tryGetQueryLanguage(e.databaseItem.language);
this.modelingStore.onSelectedMethodChanged(async (e) => {
if (this.webviewView) {
this.method = e.method;
await this.webviewView.webview.postMessage({
t: "setSelectedMethod",
method: e.method,
modeledMethod: e.modeledMethod,
isModified: e.isModified,
await this.postMessage({
t: "setSelectedMethod",
method: e.method,
modeledMethods: e.modeledMethods,
isModified: e.isModified,
isInProgress: e.isInProgress,
});
}
}),
);
this.push(
this.modelingEvents.onDbOpened(async (databaseItem) => {
this.databaseItem = databaseItem;
await this.postMessage({
t: "setInModelingMode",
inModelingMode: true,
});
}
});
this.language = tryGetQueryLanguage(databaseItem.language);
await this.setViewState();
}),
);
this.push(
this.modelingEvents.onDbClosed(async (dbUri) => {
if (!this.modelingStore.anyDbsBeingModeled()) {
await this.postMessage({
t: "setInModelingMode",
inModelingMode: false,
});
}
if (dbUri === this.databaseItem?.databaseUri.toString()) {
await this.setMethod(undefined, undefined);
}
}),
);
this.push(
this.modelingEvents.onInProgressMethodsChanged(async (e) => {
if (this.method && this.databaseItem) {
const dbUri = this.databaseItem.databaseUri.toString();
if (e.dbUri === dbUri) {
const inProgress = e.methods.has(this.method.signature);
await this.postMessage({
t: "setInProgress",
inProgress,
});
}
}
}),
);
}
private registerToModelConfigEvents(): void {
this.push(
this.modelConfig.onDidChangeConfiguration(() => {
void this.setViewState();
}),
);
}
}

View File

@@ -1,9 +1,9 @@
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
import { ModeledMethodType } from "./modeled-method";
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
export type Call = {
label: string;
url: ResolvableLocationValue;
readonly label: string;
readonly url: Readonly<ResolvableLocationValue>;
};
export enum CallClassification {
@@ -14,14 +14,29 @@ export enum CallClassification {
}
export type Usage = Call & {
classification: CallClassification;
readonly classification: CallClassification;
};
export interface MethodSignature {
export interface MethodDefinition {
/**
* The package name in Java, or the namespace in C#, e.g. `org.sql2o` or `System.Net.Http.Headers`.
*
* If the class is not in a package, the value should be an empty string.
*/
readonly packageName: string;
readonly typeName: string;
readonly methodName: string;
/**
* The method parameters, including enclosing parentheses, e.g. `(String, String)`
*/
readonly methodParameters: string;
}
export interface MethodSignature extends MethodDefinition {
/**
* Contains the version of the library if it can be determined by CodeQL, e.g. `4.2.2.2`
*/
libraryVersion?: string;
readonly libraryVersion?: string;
/**
* A unique signature that can be used to identify this external API usage.
*
@@ -29,33 +44,26 @@ export interface MethodSignature {
* in the form "packageName.typeName#methodName(methodParameters)".
* e.g. `org.sql2o.Connection#createQuery(String)`
*/
signature: string;
/**
* The package name in Java, or the namespace in C#, e.g. `org.sql2o` or `System.Net.Http.Headers`.
*
* If the class is not in a package, the value should be an empty string.
*/
packageName: string;
typeName: string;
methodName: string;
/**
* The method parameters, including enclosing parentheses, e.g. `(String, String)`
*/
methodParameters: string;
readonly signature: string;
}
export interface Method extends MethodSignature {
/**
* Contains the name of the library containing the method declaration, e.g. `sql2o-1.6.0.jar` or `System.Runtime.dll`
*/
library: string;
readonly library: string;
/**
* Is this method already supported by CodeQL standard libraries.
* If so, there is no need for the user to model it themselves.
*/
supported: boolean;
supportedType: ModeledMethodType;
usages: Usage[];
readonly supported: boolean;
readonly supportedType: ModeledMethodType;
readonly usages: readonly Usage[];
}
export interface MethodArgument {
path: string;
label: string;
}
export function getArgumentsList(methodParameters: string): string[] {
@@ -65,3 +73,21 @@ export function getArgumentsList(methodParameters: string): string[] {
return methodParameters.substring(1, methodParameters.length - 1).split(",");
}
/**
* Should we present the user with the ability to edit to modelings for this method.
*
* A method may be unmodelable if it is already modeled by CodeQL or by an extension
* pack other than the one currently being edited.
*/
export function canMethodBeModeled(
method: Method,
modeledMethods: readonly ModeledMethod[],
methodIsUnsaved: boolean,
): boolean {
return (
!method.supported ||
modeledMethods.some((modeledMethod) => modeledMethod.type !== "none") ||
methodIsUnsaved
);
}

View File

@@ -9,7 +9,7 @@ import {
Uri,
} from "vscode";
import { DisposableObject } from "../../common/disposable-object";
import { Method, Usage } from "../method";
import { Method, Usage, canMethodBeModeled } from "../method";
import { DatabaseItem } from "../../databases/local-databases";
import { relative } from "path";
import { CodeQLCliServer } from "../../codeql-cli/cli";
@@ -17,17 +17,24 @@ import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "../shared/hide-modeled-metho
import { getModelingStatus } from "../shared/modeling-status";
import { assertNever } from "../../common/helpers-pure";
import { ModeledMethod } from "../modeled-method";
import { groupMethods, sortGroupNames, sortMethods } from "../shared/sorting";
import { INITIAL_MODE, Mode } from "../shared/mode";
export class MethodsUsageDataProvider
extends DisposableObject
implements TreeDataProvider<MethodsUsageTreeViewItem>
{
private methods: Method[] = [];
private methods: readonly Method[] = [];
// sortedMethods is a separate field so we can check if the methods have changed
// by reference, which is faster than checking if the methods have changed by value.
private sortedTreeItems: readonly MethodTreeViewItem[] = [];
private databaseItem: DatabaseItem | undefined = undefined;
private sourceLocationPrefix: string | undefined = undefined;
private hideModeledMethods: boolean = INITIAL_HIDE_MODELED_METHODS_VALUE;
private modeledMethods: Record<string, ModeledMethod> = {};
private modifiedMethodSignatures: Set<string> = new Set();
private mode: Mode = INITIAL_MODE;
private modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>> =
{};
private modifiedMethodSignatures: ReadonlySet<string> = new Set();
private readonly onDidChangeTreeDataEmitter = this.push(
new EventEmitter<void>(),
@@ -49,24 +56,30 @@ export class MethodsUsageDataProvider
* method and instead always pass new objects/arrays.
*/
public async setState(
methods: Method[],
methods: readonly Method[],
databaseItem: DatabaseItem,
hideModeledMethods: boolean,
modeledMethods: Record<string, ModeledMethod>,
modifiedMethodSignatures: Set<string>,
mode: Mode,
modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>>,
modifiedMethodSignatures: ReadonlySet<string>,
): Promise<void> {
if (
this.methods !== methods ||
this.databaseItem !== databaseItem ||
this.hideModeledMethods !== hideModeledMethods ||
this.mode !== mode ||
this.modeledMethods !== modeledMethods ||
this.modifiedMethodSignatures !== modifiedMethodSignatures
) {
this.methods = methods;
this.sortedTreeItems = createTreeItems(
sortMethodsInGroups(methods, mode),
);
this.databaseItem = databaseItem;
this.sourceLocationPrefix =
await this.databaseItem.getSourceLocationPrefix(this.cliServer);
this.hideModeledMethods = hideModeledMethods;
this.mode = mode;
this.modeledMethods = modeledMethods;
this.modifiedMethodSignatures = modifiedMethodSignatures;
@@ -75,34 +88,37 @@ export class MethodsUsageDataProvider
}
getTreeItem(item: MethodsUsageTreeViewItem): TreeItem {
if (isExternalApiUsage(item)) {
if (isMethodTreeViewItem(item)) {
const { method } = item;
return {
label: `${item.packageName}.${item.typeName}.${item.methodName}${item.methodParameters}`,
label: `${method.packageName}.${method.typeName}.${method.methodName}${method.methodParameters}`,
collapsibleState: TreeItemCollapsibleState.Collapsed,
iconPath: this.getModelingStatusIcon(item),
iconPath: this.getModelingStatusIcon(method),
};
} else {
const method = this.getParent(item);
const { method, usage } = item;
return {
label: item.label,
description: `${this.relativePathWithinDatabase(item.url.uri)} [${
item.url.startLine
}, ${item.url.endLine}]`,
label: usage.label,
description: `${this.relativePathWithinDatabase(usage.url.uri)} [${
usage.url.startLine
}, ${usage.url.endLine}]`,
collapsibleState: TreeItemCollapsibleState.None,
command: {
title: "Show usage",
command: "codeQLModelEditor.jumpToUsageLocation",
arguments: [method, item, this.databaseItem],
command: "codeQLModelEditor.jumpToMethod",
arguments: [method, usage, this.databaseItem],
},
};
}
}
private getModelingStatusIcon(method: Method): ThemeIcon {
const modeledMethod = this.modeledMethods[method.signature];
const modeledMethods = this.modeledMethods[method.signature] ?? [];
const modifiedMethod = this.modifiedMethodSignatures.has(method.signature);
const status = getModelingStatus(modeledMethod, modifiedMethod);
const status = getModelingStatus(modeledMethods, modifiedMethod);
switch (status) {
case "unmodeled":
return new ThemeIcon("error", new ThemeColor("errorForeground"));
@@ -130,12 +146,18 @@ export class MethodsUsageDataProvider
getChildren(item?: MethodsUsageTreeViewItem): MethodsUsageTreeViewItem[] {
if (item === undefined) {
if (this.hideModeledMethods) {
return this.methods.filter((api) => !api.supported);
return this.sortedTreeItems.filter((api) =>
canMethodBeModeled(
api.method,
this.modeledMethods[api.method.signature] ?? [],
this.modifiedMethodSignatures.has(api.method.signature),
),
);
} else {
return this.methods;
return [...this.sortedTreeItems];
}
} else if (isExternalApiUsage(item)) {
return item.usages;
} else if (isMethodTreeViewItem(item)) {
return item.children;
} else {
return [];
}
@@ -144,29 +166,45 @@ export class MethodsUsageDataProvider
getParent(
item: MethodsUsageTreeViewItem,
): MethodsUsageTreeViewItem | undefined {
if (isExternalApiUsage(item)) {
if (isMethodTreeViewItem(item)) {
return undefined;
} else {
return this.methods.find((e) => e.usages.includes(item));
return item.parent;
}
}
public resolveCanonicalUsage(usage: Usage): Usage | undefined {
for (const method of this.methods) {
for (const u of method.usages) {
if (usagesAreEqual(u, usage)) {
return u;
}
}
public resolveUsageTreeViewItem(
methodSignature: string,
usage: Usage,
): UsageTreeViewItem | undefined {
const method = this.sortedTreeItems.find(
(m) => m.method.signature === methodSignature,
);
if (!method) {
return undefined;
}
return undefined;
return method.children.find((u) => usagesAreEqual(u.usage, usage));
}
}
export type MethodsUsageTreeViewItem = Method | Usage;
type MethodTreeViewItem = {
method: Method;
children: UsageTreeViewItem[];
};
function isExternalApiUsage(item: MethodsUsageTreeViewItem): item is Method {
return (item as any).usages !== undefined;
type UsageTreeViewItem = {
method: Method;
usage: Usage;
parent: MethodTreeViewItem;
};
export type MethodsUsageTreeViewItem = MethodTreeViewItem | UsageTreeViewItem;
function isMethodTreeViewItem(
item: MethodsUsageTreeViewItem,
): item is MethodTreeViewItem {
return "children" in item && "method" in item;
}
function usagesAreEqual(u1: Usage, u2: Usage): boolean {
@@ -180,3 +218,33 @@ function usagesAreEqual(u1: Usage, u2: Usage): boolean {
u1.url.endColumn === u2.url.endColumn
);
}
function sortMethodsInGroups(methods: readonly Method[], mode: Mode): Method[] {
const grouped = groupMethods(methods, mode);
const sortedGroupNames = sortGroupNames(grouped);
return sortedGroupNames.flatMap((groupName) => {
const group = grouped[groupName];
return sortMethods(group);
});
}
function createTreeItems(methods: readonly Method[]): MethodTreeViewItem[] {
return methods.map((method) => {
const newMethod: MethodTreeViewItem = {
method,
children: [],
};
newMethod.children = method.usages.map((usage) => ({
method,
usage,
// This needs to be a reference to the parent method, not a copy of it.
parent: newMethod,
}));
return newMethod;
});
}

View File

@@ -9,6 +9,8 @@ import { DatabaseItem } from "../../databases/local-databases";
import { CodeQLCliServer } from "../../codeql-cli/cli";
import { ModelingStore } from "../modeling-store";
import { ModeledMethod } from "../modeled-method";
import { Mode } from "../shared/mode";
import { ModelingEvents } from "../modeling-events";
export class MethodsUsagePanel extends DisposableObject {
private readonly dataProvider: MethodsUsageDataProvider;
@@ -16,6 +18,7 @@ export class MethodsUsagePanel extends DisposableObject {
public constructor(
private readonly modelingStore: ModelingStore,
private readonly modelingEvents: ModelingEvents,
cliServer: CodeQLCliServer,
) {
super();
@@ -27,20 +30,22 @@ export class MethodsUsagePanel extends DisposableObject {
});
this.push(this.treeView);
this.registerToModelingStoreEvents();
this.registerToModelingEvents();
}
public async setState(
methods: Method[],
methods: readonly Method[],
databaseItem: DatabaseItem,
hideModeledMethods: boolean,
modeledMethods: Record<string, ModeledMethod>,
modifiedMethodSignatures: Set<string>,
mode: Mode,
modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>>,
modifiedMethodSignatures: ReadonlySet<string>,
): Promise<void> {
await this.dataProvider.setState(
methods,
databaseItem,
hideModeledMethods,
mode,
modeledMethods,
modifiedMethodSignatures,
);
@@ -53,22 +58,28 @@ export class MethodsUsagePanel extends DisposableObject {
};
}
public async revealItem(usage: Usage): Promise<void> {
const canonicalUsage = this.dataProvider.resolveCanonicalUsage(usage);
if (canonicalUsage !== undefined) {
await this.treeView.reveal(canonicalUsage);
public async revealItem(
methodSignature: string,
usage: Usage,
): Promise<void> {
const usageTreeViewItem = this.dataProvider.resolveUsageTreeViewItem(
methodSignature,
usage,
);
if (usageTreeViewItem !== undefined) {
await this.treeView.reveal(usageTreeViewItem);
}
}
private registerToModelingStoreEvents(): void {
private registerToModelingEvents(): void {
this.push(
this.modelingStore.onActiveDbChanged(async () => {
this.modelingEvents.onActiveDbChanged(async () => {
await this.handleStateChangeEvent();
}),
);
this.push(
this.modelingStore.onMethodsChanged(async (event) => {
this.modelingEvents.onMethodsChanged(async (event) => {
if (event.isActiveDb) {
await this.handleStateChangeEvent();
}
@@ -76,7 +87,7 @@ export class MethodsUsagePanel extends DisposableObject {
);
this.push(
this.modelingStore.onHideModeledMethodsChanged(async (event) => {
this.modelingEvents.onHideModeledMethodsChanged(async (event) => {
if (event.isActiveDb) {
await this.handleStateChangeEvent();
}
@@ -84,7 +95,15 @@ export class MethodsUsagePanel extends DisposableObject {
);
this.push(
this.modelingStore.onModifiedMethodsChanged(async (event) => {
this.modelingEvents.onModeChanged(async (event) => {
if (event.isActiveDb) {
await this.handleStateChangeEvent();
}
}),
);
this.push(
this.modelingEvents.onModifiedMethodsChanged(async (event) => {
if (event.isActiveDb) {
await this.handleStateChangeEvent();
}
@@ -99,6 +118,7 @@ export class MethodsUsagePanel extends DisposableObject {
activeState.methods,
activeState.databaseItem,
activeState.hideModeledMethods,
activeState.mode,
activeState.modeledMethods,
activeState.modifiedMethodSignatures,
);

View File

@@ -14,20 +14,26 @@ import { dir } from "tmp-promise";
import { isQueryLanguage } from "../common/query-language";
import { DisposableObject } from "../common/disposable-object";
import { MethodsUsagePanel } from "./methods-usage/methods-usage-panel";
import { Mode } from "./shared/mode";
import { Method, Usage } from "./method";
import { setUpPack } from "./model-editor-queries";
import { setUpPack } from "./model-editor-queries-setup";
import { MethodModelingPanel } from "./method-modeling/method-modeling-panel";
import { ModelingStore } from "./modeling-store";
import { showResolvableLocation } from "../databases/local-databases/locations";
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
import { ModelConfigListener } from "../config";
import { ModelingEvents } from "./modeling-events";
import { getModelsAsDataLanguage } from "./languages";
import { INITIAL_MODE } from "./shared/mode";
import { isSupportedLanguage } from "./supported-languages";
export class ModelEditorModule extends DisposableObject {
private readonly queryStorageDir: string;
private readonly modelingStore: ModelingStore;
private readonly modelingEvents: ModelingEvents;
private readonly editorViewTracker: ModelEditorViewTracker<ModelEditorView>;
private readonly methodsUsagePanel: MethodsUsagePanel;
private readonly methodModelingPanel: MethodModelingPanel;
private readonly modelConfig: ModelConfigListener;
private constructor(
private readonly app: App,
@@ -38,15 +44,23 @@ export class ModelEditorModule extends DisposableObject {
) {
super();
this.queryStorageDir = join(baseQueryStorageDir, "model-editor-results");
this.modelingStore = new ModelingStore(app);
this.modelingEvents = new ModelingEvents(app);
this.modelingStore = new ModelingStore(this.modelingEvents);
this.editorViewTracker = new ModelEditorViewTracker();
this.methodsUsagePanel = this.push(
new MethodsUsagePanel(this.modelingStore, cliServer),
new MethodsUsagePanel(this.modelingStore, this.modelingEvents, cliServer),
);
this.methodModelingPanel = this.push(
new MethodModelingPanel(app, this.modelingStore),
new MethodModelingPanel(
app,
this.modelingStore,
this.modelingEvents,
this.editorViewTracker,
),
);
this.modelConfig = this.push(new ModelConfigListener());
this.registerToModelingStoreEvents();
this.registerToModelingEvents();
}
public static async initialize(
@@ -70,115 +84,10 @@ export class ModelEditorModule extends DisposableObject {
public getCommands(): ModelEditorCommands {
return {
"codeQL.openModelEditor": async () => {
const db = this.databaseManager.currentDatabaseItem;
if (!db) {
void showAndLogErrorMessage(this.app.logger, "No database selected");
return;
}
const language = db.language;
if (
!SUPPORTED_LANGUAGES.includes(language) ||
!isQueryLanguage(language)
) {
void showAndLogErrorMessage(
this.app.logger,
`The CodeQL Model Editor is not supported for ${language} databases.`,
);
return;
}
return withProgress(
async (progress) => {
const maxStep = 4;
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
void showAndLogErrorMessage(
this.app.logger,
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
);
return;
}
if (
!(await this.cliServer.cliConstraints.supportsResolveExtensions())
) {
void showAndLogErrorMessage(
this.app.logger,
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`,
);
return;
}
const modelFile = await pickExtensionPack(
this.cliServer,
db,
this.app.logger,
progress,
maxStep,
);
if (!modelFile) {
return;
}
progress({
message: "Installing dependencies...",
step: 3,
maxStep,
});
// Create new temporary directory for query files and pack dependencies
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
unsafeCleanup: true,
});
const success = await setUpPack(this.cliServer, queryDir, language);
if (!success) {
await cleanupQueryDir();
return;
}
progress({
message: "Opening editor...",
step: 4,
maxStep,
});
const view = new ModelEditorView(
this.app,
this.modelingStore,
this.databaseManager,
this.cliServer,
this.queryRunner,
this.queryStorageDir,
queryDir,
db,
modelFile,
Mode.Application,
);
this.modelingStore.onDbClosed(async (dbUri) => {
if (dbUri === db.databaseUri.toString()) {
await cleanupQueryDir();
}
});
this.push(view);
this.push({
dispose(): void {
void cleanupQueryDir();
},
});
await view.openView();
},
{
title: "Opening CodeQL Model Editor",
},
);
},
"codeQLModelEditor.jumpToUsageLocation": async (
"codeQL.openModelEditor": this.openModelEditor.bind(this),
"codeQL.openModelEditorFromModelingPanel":
this.openModelEditor.bind(this),
"codeQLModelEditor.jumpToMethod": async (
method: Method,
usage: Usage,
databaseItem: DatabaseItem,
@@ -192,9 +101,9 @@ export class ModelEditorModule extends DisposableObject {
await ensureDir(this.queryStorageDir);
}
private registerToModelingStoreEvents(): void {
private registerToModelingEvents(): void {
this.push(
this.modelingStore.onSelectedMethodChanged(async (event) => {
this.modelingEvents.onSelectedMethodChanged(async (event) => {
await this.showMethod(event.databaseItem, event.method, event.usage);
}),
);
@@ -205,8 +114,155 @@ export class ModelEditorModule extends DisposableObject {
method: Method,
usage: Usage,
): Promise<void> {
await this.methodsUsagePanel.revealItem(usage);
await this.methodModelingPanel.setMethod(method);
await this.methodsUsagePanel.revealItem(method.signature, usage);
await this.methodModelingPanel.setMethod(databaseItem, method);
await showResolvableLocation(usage.url, databaseItem, this.app.logger);
}
private async openModelEditor(): Promise<void> {
{
const db = this.databaseManager.currentDatabaseItem;
if (!db) {
void showAndLogErrorMessage(this.app.logger, "No database selected");
return;
}
const language = db.language;
if (
!isQueryLanguage(language) ||
!isSupportedLanguage(language, this.modelConfig)
) {
void showAndLogErrorMessage(
this.app.logger,
`The CodeQL Model Editor is not supported for ${language} databases.`,
);
return;
}
const definition = getModelsAsDataLanguage(language);
const initialMode = definition.availableModes?.[0] ?? INITIAL_MODE;
const existingView = this.editorViewTracker.getView(
db.databaseUri.toString(),
);
if (existingView) {
await existingView.focusView();
return;
}
return withProgress(
async (progress) => {
const maxStep = 4;
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
void showAndLogErrorMessage(
this.app.logger,
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
);
return;
}
if (
!(await this.cliServer.cliConstraints.supportsResolveExtensions())
) {
void showAndLogErrorMessage(
this.app.logger,
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`,
);
return;
}
const modelFile = await pickExtensionPack(
this.cliServer,
db,
this.modelConfig,
this.app.logger,
progress,
maxStep,
);
if (!modelFile) {
return;
}
progress({
message: "Installing dependencies...",
step: 3,
maxStep,
});
// Create new temporary directory for query files and pack dependencies
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
unsafeCleanup: true,
});
const success = await setUpPack(
this.cliServer,
this.app.logger,
queryDir,
language,
this.modelConfig,
);
if (!success) {
await cleanupQueryDir();
return;
}
progress({
message: "Opening editor...",
step: 4,
maxStep,
});
// Check again just before opening the editor to ensure no model editor has been opened between
// our first check and now.
const existingView = this.editorViewTracker.getView(
db.databaseUri.toString(),
);
if (existingView) {
await existingView.focusView();
return;
}
const view = new ModelEditorView(
this.app,
this.modelingStore,
this.modelingEvents,
this.editorViewTracker,
this.modelConfig,
this.databaseManager,
this.cliServer,
this.queryRunner,
this.queryStorageDir,
queryDir,
db,
modelFile,
language,
initialMode,
);
this.modelingEvents.onDbClosed(async (dbUri) => {
if (dbUri === db.databaseUri.toString()) {
await cleanupQueryDir();
}
});
this.push(view);
this.push({
dispose(): void {
void cleanupQueryDir();
},
});
await view.openView();
},
{
title: "Opening CodeQL Model Editor",
},
);
}
}
}

View File

@@ -0,0 +1,144 @@
import { join } from "path";
import { QueryLanguage } from "../common/query-language";
import { writeFile } from "fs-extra";
import { dump } from "js-yaml";
import { prepareModelEditorQueries } from "./model-editor-queries";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { ModelConfig } from "../config";
import { Mode } from "./shared/mode";
import { resolveQueriesFromPacks } from "../local-queries";
import { modeTag } from "./mode-tag";
import { NotificationLogger } from "../common/logging";
export const syntheticQueryPackName = "codeql/model-editor-queries";
/**
* setUpPack sets up a directory to use for the data extension editor queries if required.
*
* There are two cases (example language is Java):
* - In case the queries are present in the codeql/java-queries, we don't need to write our own queries
* to disk. We still need to create a synthetic query pack so we can pass the queryDir to the query
* resolver without caring about whether the queries are present in the pack or not.
* - In case the queries are not present in the codeql/java-queries, we need to write our own queries
* to disk. We will create a synthetic query pack and install its dependencies so it is fully independent
* and we can simply pass it through when resolving the queries.
*
* These steps together ensure that later steps of the process don't need to keep track of whether the queries
* are present in codeql/java-queries or in our own query pack. They just need to resolve the query.
*
* @param cliServer The CodeQL CLI server to use.
* @param logger The logger to use.
* @param queryDir The directory to set up.
* @param language The language to use for the queries.
* @param modelConfig The model config to use.
* @returns true if the setup was successful, false otherwise.
*/
export async function setUpPack(
cliServer: CodeQLCliServer,
logger: NotificationLogger,
queryDir: string,
language: QueryLanguage,
modelConfig: ModelConfig,
): Promise<boolean> {
// Download the required query packs
await cliServer.packDownload([`codeql/${language}-queries`]);
// We'll only check if the application mode query exists in the pack and assume that if it does,
// the framework mode query will also exist.
const applicationModeQuery = await resolveEndpointsQuery(
cliServer,
language,
Mode.Application,
[],
[],
);
if (applicationModeQuery) {
// Set up a synthetic pack so CodeQL doesn't crash later when we try
// to resolve a query within this directory
const syntheticQueryPack = {
name: syntheticQueryPackName,
version: "0.0.0",
dependencies: {},
};
const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
} else {
// If we can't resolve the query, we need to write them to desk ourselves.
const externalApiQuerySuccess = await prepareModelEditorQueries(
logger,
queryDir,
language,
);
if (!externalApiQuerySuccess) {
return false;
}
// Set up a synthetic pack so that the query can be resolved later.
const syntheticQueryPack = {
name: syntheticQueryPackName,
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
},
};
const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
await cliServer.packInstall(queryDir);
}
// Download any other required packs
if (language === "java" && modelConfig.llmGeneration) {
await cliServer.packDownload([`codeql/${language}-automodel-queries`]);
}
return true;
}
/**
* Resolve the query path to the model editor endpoints query. All queries are tagged like this:
* modeleditor endpoints <mode>
* Example: modeleditor endpoints framework-mode
*
* @param cliServer The CodeQL CLI server to use.
* @param language The language of the query pack to use.
* @param mode The mode to resolve the query for.
* @param additionalPackNames Additional pack names to search.
* @param additionalPackPaths Additional pack paths to search.
*/
export async function resolveEndpointsQuery(
cliServer: CodeQLCliServer,
language: string,
mode: Mode,
additionalPackNames: string[] = [],
additionalPackPaths: string[] = [],
): Promise<string | undefined> {
const packsToSearch = [`codeql/${language}-queries`, ...additionalPackNames];
// First, resolve the query that we want to run.
// All queries are tagged like this:
// internal extract automodel <mode> <queryTag>
// Example: internal extract automodel framework-mode candidates
const queries = await resolveQueriesFromPacks(
cliServer,
packsToSearch,
{
kind: "table",
"tags contain all": ["modeleditor", "endpoints", modeTag(mode)],
},
additionalPackPaths,
);
if (queries.length > 1) {
throw new Error(
`Found multiple endpoints queries for ${mode}. Can't continue`,
);
}
if (queries.length === 0) {
return undefined;
}
return queries[0];
}

View File

@@ -1,138 +1,199 @@
import { join } from "path";
import { QueryLanguage } from "../common/query-language";
import { writeFile } from "fs-extra";
import { dump } from "js-yaml";
import { prepareExternalApiQuery } from "./external-api-usage-queries";
import { QueryRunner } from "../query-server";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import {
NotificationLogger,
showAndLogExceptionWithTelemetry,
} from "../common/logging";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { showLlmGeneration } from "../config";
import { DatabaseItem } from "../databases/local-databases";
import { ProgressCallback } from "../common/vscode/progress";
import { redactableError } from "../common/errors";
import { telemetryListener } from "../common/vscode/telemetry";
import { join } from "path";
import { Mode } from "./shared/mode";
import { resolveQueriesFromPacks } from "../local-queries";
import { modeTag } from "./mode-tag";
import { outputFile, writeFile } from "fs-extra";
import { QueryLanguage } from "../common/query-language";
import { fetchExternalApiQueries } from "./queries";
import { Method } from "./method";
import { runQuery } from "../local-queries/run-query";
import { decodeBqrsToMethods } from "./bqrs";
import {
resolveEndpointsQuery,
syntheticQueryPackName,
} from "./model-editor-queries-setup";
export const syntheticQueryPackName = "codeql/external-api-usage";
type RunQueryOptions = {
cliServer: CodeQLCliServer;
queryRunner: QueryRunner;
logger: NotificationLogger;
databaseItem: DatabaseItem;
language: QueryLanguage;
queryStorageDir: string;
queryDir: string;
/**
* setUpPack sets up a directory to use for the data extension editor queries if required.
*
* There are two cases (example language is Java):
* - In case the queries are present in the codeql/java-queries, we don't need to write our own queries
* to disk. We still need to create a synthetic query pack so we can pass the queryDir to the query
* resolver without caring about whether the queries are present in the pack or not.
* - In case the queries are not present in the codeql/java-queries, we need to write our own queries
* to disk. We will create a synthetic query pack and install its dependencies so it is fully independent
* and we can simply pass it through when resolving the queries.
*
* These steps together ensure that later steps of the process don't need to keep track of whether the queries
* are present in codeql/java-queries or in our own query pack. They just need to resolve the query.
*
* @param cliServer The CodeQL CLI server to use.
* @param queryDir The directory to set up.
* @param language The language to use for the queries.
* @returns true if the setup was successful, false otherwise.
*/
export async function setUpPack(
cliServer: CodeQLCliServer,
progress: ProgressCallback;
token: CancellationToken;
};
export async function prepareModelEditorQueries(
logger: NotificationLogger,
queryDir: string,
language: QueryLanguage,
): Promise<boolean> {
// Download the required query packs
await cliServer.packDownload([`codeql/${language}-queries`]);
// We'll only check if the application mode query exists in the pack and assume that if it does,
// the framework mode query will also exist.
const applicationModeQuery = await resolveEndpointsQuery(
cliServer,
language,
Mode.Application,
[],
[],
);
if (applicationModeQuery) {
// Set up a synthetic pack so CodeQL doesn't crash later when we try
// to resolve a query within this directory
const syntheticQueryPack = {
name: syntheticQueryPackName,
version: "0.0.0",
dependencies: {},
};
const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
} else {
// If we can't resolve the query, we need to write them to desk ourselves.
const externalApiQuerySuccess = await prepareExternalApiQuery(
queryDir,
language,
// Resolve the query that we want to run.
const query = fetchExternalApiQueries[language];
if (!query) {
void showAndLogExceptionWithTelemetry(
logger,
telemetryListener,
redactableError`No bundled model editor query found for language ${language}`,
);
if (!externalApiQuerySuccess) {
return false;
return false;
}
// Create the query file.
Object.values(Mode).map(async (mode) => {
const queryFile = join(queryDir, queryNameFromMode(mode));
await writeFile(queryFile, query[`${mode}ModeQuery`], "utf8");
});
// Create any dependencies
if (query.dependencies) {
for (const [filename, contents] of Object.entries(query.dependencies)) {
const dependencyFile = join(queryDir, filename);
await outputFile(dependencyFile, contents, "utf8");
}
// Set up a synthetic pack so that the query can be resolved later.
const syntheticQueryPack = {
name: syntheticQueryPackName,
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
},
};
const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
await cliServer.packInstall(queryDir);
}
// Download any other required packs
if (language === "java" && showLlmGeneration()) {
await cliServer.packDownload([`codeql/${language}-automodel-queries`]);
}
return true;
}
/**
* Resolve the query path to the model editor endpoints query. All queries are tagged like this:
* modeleditor endpoints <mode>
* Example: modeleditor endpoints framework-mode
*
* @param cliServer The CodeQL CLI server to use.
* @param language The language of the query pack to use.
* @param mode The mode to resolve the query for.
* @param additionalPackNames Additional pack names to search.
* @param additionalPackPaths Additional pack paths to search.
*/
export async function resolveEndpointsQuery(
cliServer: CodeQLCliServer,
language: string,
mode: Mode,
additionalPackNames: string[] = [],
additionalPackPaths: string[] = [],
): Promise<string | undefined> {
const packsToSearch = [`codeql/${language}-queries`, ...additionalPackNames];
export const externalApiQueriesProgressMaxStep = 2000;
// First, resolve the query that we want to run.
// All queries are tagged like this:
// internal extract automodel <mode> <queryTag>
// Example: internal extract automodel framework-mode candidates
const queries = await resolveQueriesFromPacks(
export async function runModelEditorQueries(
mode: Mode,
{
cliServer,
packsToSearch,
{
kind: "table",
"tags contain all": ["modeleditor", "endpoints", modeTag(mode)],
},
additionalPackPaths,
queryRunner,
logger,
databaseItem,
language,
queryStorageDir,
queryDir,
progress,
token,
}: RunQueryOptions,
): Promise<Method[] | undefined> {
// The below code is temporary to allow for rapid prototyping of the queries. Once the queries are stabilized, we will
// move these queries into the `github/codeql` repository and use them like any other contextual (e.g. AST) queries.
// This is intentionally not pretty code, as it will be removed soon.
// For a reference of what this should do in the future, see the previous implementation in
// https://github.com/github/vscode-codeql/blob/089d3566ef0bc67d9b7cc66e8fd6740b31c1c0b0/extensions/ql-vscode/src/data-extensions-editor/external-api-usage-query.ts#L33-L72
progress({
message: "Resolving QL packs",
step: 1,
maxStep: externalApiQueriesProgressMaxStep,
});
const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = Object.keys(
await cliServer.resolveQlpacks(additionalPacks, true),
);
if (queries.length > 1) {
throw new Error(
`Found multiple endpoints queries for ${mode}. Can't continue`,
progress({
message: "Resolving query",
step: 2,
maxStep: externalApiQueriesProgressMaxStep,
});
// Resolve the queries from either codeql/java-queries or from the temporary queryDir
const queryPath = await resolveEndpointsQuery(
cliServer,
databaseItem.language,
mode,
[syntheticQueryPackName],
[queryDir],
);
if (!queryPath) {
void showAndLogExceptionWithTelemetry(
logger,
telemetryListener,
redactableError`The ${mode} model editor query could not be found. Try re-opening the model editor. If that doesn't work, try upgrading the CodeQL libraries.`,
);
return;
}
if (queries.length === 0) {
// Run the actual query
const completedQuery = await runQuery({
queryRunner,
databaseItem,
queryPath,
queryStorageDir,
additionalPacks,
extensionPacks,
progress: (update) =>
progress({
step: update.step + 500,
maxStep: externalApiQueriesProgressMaxStep,
message: update.message,
}),
token,
});
if (!completedQuery) {
return;
}
// Read the results and covert to internal representation
progress({
message: "Decoding results",
step: 1600,
maxStep: externalApiQueriesProgressMaxStep,
});
const bqrsChunk = await readQueryResults({
cliServer,
logger,
bqrsPath: completedQuery.outputDir.bqrsPath,
});
if (!bqrsChunk) {
return;
}
progress({
message: "Finalizing results",
step: 1950,
maxStep: externalApiQueriesProgressMaxStep,
});
return decodeBqrsToMethods(bqrsChunk, mode, language);
}
type GetResultsOptions = {
cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">;
logger: NotificationLogger;
bqrsPath: string;
};
export async function readQueryResults({
cliServer,
logger,
bqrsPath,
}: GetResultsOptions) {
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) {
void showAndLogExceptionWithTelemetry(
logger,
telemetryListener,
redactableError`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
);
return undefined;
}
return queries[0];
const resultSet = bqrsInfo["result-sets"][0];
return cliServer.bqrsDecode(bqrsPath, resultSet.name);
}
function queryNameFromMode(mode: Mode): string {
return `${mode.charAt(0).toUpperCase() + mode.slice(1)}ModeEndpoints.ql`;
}

View File

@@ -0,0 +1,33 @@
import { Method } from "./method";
interface ModelEditorViewInterface {
databaseUri: string;
revealMethod(method: Method): Promise<void>;
}
export class ModelEditorViewTracker<
T extends ModelEditorViewInterface = ModelEditorViewInterface,
> {
private readonly views = new Map<string, T>();
constructor() {}
public registerView(view: T): void {
const databaseUri = view.databaseUri;
if (this.views.has(databaseUri)) {
throw new Error(`View for database ${databaseUri} already registered`);
}
this.views.set(databaseUri, view);
}
public unregisterView(view: T): void {
this.views.delete(view.databaseUri);
}
public getView(databaseUri: string): T | undefined {
return this.views.get(databaseUri);
}
}

View File

@@ -17,41 +17,51 @@ import {
import { ProgressCallback, withProgress } from "../common/vscode/progress";
import { QueryRunner } from "../query-server";
import {
showAndLogExceptionWithTelemetry,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
} from "../common/logging";
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { asError, assertNever, getErrorMessage } from "../common/helpers-pure";
import { runFlowModelQueries } from "./flow-model-queries";
import { promptImportGithubDatabase } from "../databases/database-fetcher";
import { App } from "../common/app";
import { redactableError } from "../common/errors";
import {
externalApiQueriesProgressMaxStep,
runExternalApiQueries,
} from "./external-api-usage-queries";
import { Method, Usage } from "./method";
runModelEditorQueries,
} from "./model-editor-queries";
import { Method } from "./method";
import { ModeledMethod } from "./modeled-method";
import { ExtensionPack } from "./shared/extension-pack";
import { showFlowGeneration, showLlmGeneration } from "../config";
import { ModelConfigListener } from "../config";
import { Mode } from "./shared/mode";
import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
import { pickExtensionPack } from "./extension-pack-picker";
import { getLanguageDisplayName } from "../common/query-language";
import {
getLanguageDisplayName,
QueryLanguage,
} from "../common/query-language";
import { AutoModeler } from "./auto-modeler";
import { telemetryListener } from "../common/vscode/telemetry";
import { ModelingStore } from "./modeling-store";
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
import { ModelingEvents } from "./modeling-events";
import { getModelsAsDataLanguage, ModelsAsDataLanguage } from "./languages";
import { runGenerateQueries } from "./generate";
export class ModelEditorView extends AbstractWebview<
ToModelEditorMessage,
FromModelEditorMessage
> {
private readonly autoModeler: AutoModeler;
private readonly languageDefinition: ModelsAsDataLanguage;
public constructor(
protected readonly app: App,
private readonly modelingStore: ModelingStore,
private readonly modelingEvents: ModelingEvents,
private readonly viewTracker: ModelEditorViewTracker<ModelEditorView>,
private readonly modelConfig: ModelConfigListener,
private readonly databaseManager: DatabaseManager,
private readonly cliServer: CodeQLCliServer,
private readonly queryRunner: QueryRunner,
@@ -59,30 +69,32 @@ export class ModelEditorView extends AbstractWebview<
private readonly queryDir: string,
private readonly databaseItem: DatabaseItem,
private readonly extensionPack: ExtensionPack,
private mode: Mode,
// The language is equal to databaseItem.language but is properly typed as QueryLanguage
private readonly language: QueryLanguage,
initialMode: Mode,
) {
super(app);
this.modelingStore.initializeStateForDb(databaseItem);
this.registerToModelingStoreEvents();
this.modelingStore.initializeStateForDb(databaseItem, initialMode);
this.registerToModelingEvents();
this.registerToModelConfigEvents();
this.viewTracker.registerView(this);
this.autoModeler = new AutoModeler(
app,
cliServer,
queryRunner,
this.modelConfig,
modelingStore,
queryStorageDir,
databaseItem,
async (packageName, inProgressMethods) => {
await this.postMessage({
t: "setInProgressMethods",
packageName,
inProgressMethods,
});
},
language,
async (modeledMethods) => {
this.addModeledMethods(modeledMethods);
},
);
this.languageDefinition = getModelsAsDataLanguage(language);
}
public async openView() {
@@ -92,9 +104,6 @@ export class ModelEditorView extends AbstractWebview<
panel.onDidChangeViewState(async () => {
if (panel.active) {
this.modelingStore.setActiveDb(this.databaseItem);
await this.markModelEditorAsActive();
} else {
await this.updateModelEditorActiveContext();
}
});
@@ -118,36 +127,12 @@ export class ModelEditorView extends AbstractWebview<
);
}
private async markModelEditorAsActive(): Promise<void> {
void this.app.commands.execute(
"setContext",
"codeql.modelEditorActive",
true,
);
}
private async updateModelEditorActiveContext(): Promise<void> {
await this.app.commands.execute(
"setContext",
"codeql.modelEditorActive",
this.isAModelEditorActive(),
);
}
private isAModelEditorOpen(): boolean {
return window.tabGroups.all.some((tabGroup) =>
tabGroup.tabs.some((tab) => this.isTabModelEditorView(tab)),
);
}
private isAModelEditorActive(): boolean {
return window.tabGroups.all.some((tabGroup) =>
tabGroup.tabs.some(
(tab) => this.isTabModelEditorView(tab) && tab.isActive,
),
);
}
private isTabModelEditorView(tab: Tab): boolean {
if (!(tab.input instanceof TabInputWebview)) {
return false;
@@ -181,7 +166,7 @@ export class ModelEditorView extends AbstractWebview<
}
protected onPanelDispose(): void {
// Nothing to do here
this.viewTracker.unregisterView(this);
}
protected async onMessage(msg: FromModelEditorMessage): Promise<void> {
@@ -214,57 +199,72 @@ export class ModelEditorView extends AbstractWebview<
);
break;
case "jumpToUsage":
await this.handleJumpToUsage(msg.method, msg.usage);
void telemetryListener?.sendUIInteraction("model-editor-jump-to-usage");
case "jumpToMethod":
await this.handleJumpToMethod(msg.methodSignature);
void telemetryListener?.sendUIInteraction(
"model-editor-jump-to-method",
);
break;
case "saveModeledMethods":
await withProgress(
async (progress) => {
progress({
step: 1,
maxStep: 500 + externalApiQueriesProgressMaxStep,
message: "Writing model files",
});
await saveModeledMethods(
this.extensionPack,
this.databaseItem.language,
msg.methods,
msg.modeledMethods,
this.mode,
this.cliServer,
this.app.logger,
);
{
const methods = this.modelingStore.getMethods(
this.databaseItem,
msg.methodSignatures,
);
const modeledMethods = this.modelingStore.getModeledMethods(
this.databaseItem,
msg.methodSignatures,
);
const mode = this.modelingStore.getMode(this.databaseItem);
await Promise.all([
this.setViewState(),
this.loadMethods((update) =>
progress({
...update,
step: update.step + 500,
maxStep: 500 + externalApiQueriesProgressMaxStep,
}),
),
]);
},
{
cancellable: false,
},
);
await withProgress(
async (progress) => {
progress({
step: 1,
maxStep: 500 + externalApiQueriesProgressMaxStep,
message: "Writing model files",
});
await saveModeledMethods(
this.extensionPack,
this.language,
methods,
modeledMethods,
mode,
this.cliServer,
this.app.logger,
);
this.modelingStore.removeModifiedMethods(
this.databaseItem,
Object.keys(msg.modeledMethods),
);
await Promise.all([
this.setViewState(),
this.loadMethods((update) =>
progress({
...update,
step: update.step + 500,
maxStep: 500 + externalApiQueriesProgressMaxStep,
}),
),
]);
},
{
cancellable: false,
},
);
void telemetryListener?.sendUIInteraction(
"model-editor-save-modeled-methods",
);
this.modelingStore.removeModifiedMethods(
this.databaseItem,
Object.keys(modeledMethods),
);
void telemetryListener?.sendUIInteraction(
"model-editor-save-modeled-methods",
);
}
break;
case "generateMethod":
await this.generateModeledMethods();
void telemetryListener?.sendUIInteraction(
"model-editor-generate-modeled-methods",
);
@@ -273,8 +273,7 @@ export class ModelEditorView extends AbstractWebview<
case "generateMethodsFromLlm":
await this.generateModeledMethodsFromLlm(
msg.packageName,
msg.methods,
msg.modeledMethods,
msg.methodSignatures,
);
void telemetryListener?.sendUIInteraction(
"model-editor-generate-methods-from-llm",
@@ -293,7 +292,7 @@ export class ModelEditorView extends AbstractWebview<
);
break;
case "switchMode":
this.mode = msg.mode;
this.modelingStore.setMode(this.databaseItem, msg.mode);
this.modelingStore.setMethods(this.databaseItem, []);
await Promise.all([
this.postMessage({
@@ -317,10 +316,22 @@ export class ModelEditorView extends AbstractWebview<
"model-editor-hide-modeled-methods",
);
break;
case "setModeledMethod": {
this.setModeledMethod(msg.method);
case "setMultipleModeledMethods": {
this.setModeledMethods(msg.methodSignature, msg.modeledMethods);
break;
}
case "telemetry":
telemetryListener?.sendUIInteraction(msg.action);
break;
case "unhandledError":
void showAndLogExceptionWithTelemetry(
this.app.logger,
telemetryListener,
redactableError(
msg.error,
)`Unhandled error in model editor view: ${msg.error.message}`,
);
break;
default:
assertNever(msg);
}
@@ -338,29 +349,73 @@ export class ModelEditorView extends AbstractWebview<
]);
}
public get databaseUri(): string {
return this.databaseItem.databaseUri.toString();
}
public async focusView(): Promise<void> {
this.panel?.reveal();
}
public async revealMethod(method: Method): Promise<void> {
this.panel?.reveal();
await this.postMessage({
t: "revealMethod",
methodSignature: method.signature,
});
}
private async setViewState(): Promise<void> {
const modelsAsDataLanguage = getModelsAsDataLanguage(this.language);
const showGenerateButton =
this.modelConfig.flowGeneration && !!modelsAsDataLanguage.modelGeneration;
const showLlmButton =
this.databaseItem.language === "java" && showLlmGeneration();
this.databaseItem.language === "java" && this.modelConfig.llmGeneration;
const sourceArchiveAvailable =
this.databaseItem.hasSourceArchiveInExplorer();
const showModeSwitchButton =
this.languageDefinition.availableModes === undefined ||
this.languageDefinition.availableModes.length > 1;
await this.postMessage({
t: "setModelEditorViewState",
viewState: {
extensionPack: this.extensionPack,
showFlowGeneration: showFlowGeneration(),
language: this.language,
showGenerateButton,
showLlmButton,
mode: this.mode,
showMultipleModels: this.modelConfig.showMultipleModels,
mode: this.modelingStore.getMode(this.databaseItem),
showModeSwitchButton,
sourceArchiveAvailable,
},
});
}
protected async handleJumpToUsage(method: Method, usage: Usage) {
this.modelingStore.setSelectedMethod(this.databaseItem, method, usage);
protected async handleJumpToMethod(methodSignature: string) {
const method = this.modelingStore.getMethod(
this.databaseItem,
methodSignature,
);
if (method) {
this.modelingStore.setSelectedMethod(
this.databaseItem,
method,
method.usages[0],
);
}
}
protected async loadExistingModeledMethods(): Promise<void> {
try {
const modeledMethods = await loadModeledMethods(
this.extensionPack,
this.language,
this.cliServer,
this.app.logger,
);
@@ -374,12 +429,16 @@ export class ModelEditorView extends AbstractWebview<
}
protected async loadMethods(progress: ProgressCallback): Promise<void> {
const mode = this.modelingStore.getMode(this.databaseItem);
try {
const cancellationTokenSource = new CancellationTokenSource();
const queryResult = await runExternalApiQueries(this.mode, {
const queryResult = await runModelEditorQueries(mode, {
cliServer: this.cliServer,
queryRunner: this.queryRunner,
logger: this.app.logger,
databaseItem: this.databaseItem,
language: this.language,
queryStorageDir: this.queryStorageDir,
queryDir: this.queryDir,
progress: (update) =>
@@ -398,9 +457,9 @@ export class ModelEditorView extends AbstractWebview<
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(err),
)`Failed to load external API usages: ${getErrorMessage(err)}`,
redactableError(asError(err))`Failed to load results: ${getErrorMessage(
err,
)}`,
);
}
}
@@ -410,17 +469,37 @@ export class ModelEditorView extends AbstractWebview<
async (progress) => {
const tokenSource = new CancellationTokenSource();
const mode = this.modelingStore.getMode(this.databaseItem);
const modelsAsDataLanguage = getModelsAsDataLanguage(this.language);
const modelGeneration = modelsAsDataLanguage.modelGeneration;
if (!modelGeneration) {
void showAndLogErrorMessage(
this.app.logger,
`Model generation is not supported for ${this.language}.`,
);
return;
}
let addedDatabase: DatabaseItem | undefined;
// In application mode, we need the database of a specific library to generate
// the modeled methods. In framework mode, we'll use the current database.
if (this.mode === Mode.Application) {
if (mode === Mode.Application) {
addedDatabase = await this.promptChooseNewOrExistingDatabase(
progress,
);
if (!addedDatabase) {
return;
}
if (addedDatabase.language !== this.language) {
void showAndLogErrorMessage(
this.app.logger,
`The selected database is for ${addedDatabase.language}, but the current database is for ${this.language}.`,
);
return;
}
}
progress({
@@ -430,20 +509,23 @@ export class ModelEditorView extends AbstractWebview<
});
try {
await runFlowModelQueries({
await runGenerateQueries({
queryConstraints: modelGeneration.queryConstraints,
filterQueries: modelGeneration.filterQueries,
parseResults: (queryPath, results) =>
modelGeneration.parseResults(
queryPath,
results,
modelsAsDataLanguage,
this.app.logger,
),
onResults: async (modeledMethods) => {
this.addModeledMethodsFromArray(modeledMethods);
},
cliServer: this.cliServer,
queryRunner: this.queryRunner,
queryStorageDir: this.queryStorageDir,
databaseItem: addedDatabase ?? this.databaseItem,
onResults: async (modeledMethods) => {
const modeledMethodsByName: Record<string, ModeledMethod> = {};
for (const modeledMethod of modeledMethods) {
modeledMethodsByName[modeledMethod.signature] = modeledMethod;
}
this.addModeledMethods(modeledMethodsByName);
},
progress,
token: tokenSource.token,
});
@@ -453,7 +535,7 @@ export class ModelEditorView extends AbstractWebview<
this.app.telemetry,
redactableError(
asError(e),
)`Failed to generate flow model: ${getErrorMessage(e)}`,
)`Failed to generate models: ${getErrorMessage(e)}`,
);
}
},
@@ -463,14 +545,22 @@ export class ModelEditorView extends AbstractWebview<
private async generateModeledMethodsFromLlm(
packageName: string,
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
methodSignatures: string[],
): Promise<void> {
const methods = this.modelingStore.getMethods(
this.databaseItem,
methodSignatures,
);
const modeledMethods = this.modelingStore.getModeledMethods(
this.databaseItem,
methodSignatures,
);
const mode = this.modelingStore.getMode(this.databaseItem);
await this.autoModeler.startModeling(
packageName,
methods,
modeledMethods,
this.mode,
mode,
);
}
@@ -483,9 +573,19 @@ export class ModelEditorView extends AbstractWebview<
return;
}
let existingView = this.viewTracker.getView(
addedDatabase.databaseUri.toString(),
);
if (existingView) {
await existingView.focusView();
return;
}
const modelFile = await pickExtensionPack(
this.cliServer,
addedDatabase,
this.modelConfig,
this.app.logger,
progress,
3,
@@ -494,9 +594,23 @@ export class ModelEditorView extends AbstractWebview<
return;
}
// Check again just before opening the editor to ensure no model editor has been opened between
// our first check and now.
existingView = this.viewTracker.getView(
addedDatabase.databaseUri.toString(),
);
if (existingView) {
await existingView.focusView();
return;
}
const view = new ModelEditorView(
this.app,
this.modelingStore,
this.modelingEvents,
this.viewTracker,
this.modelConfig,
this.databaseManager,
this.cliServer,
this.queryRunner,
@@ -504,6 +618,7 @@ export class ModelEditorView extends AbstractWebview<
this.queryDir,
addedDatabase,
modelFile,
this.language,
Mode.Framework,
);
await view.openView();
@@ -583,9 +698,9 @@ export class ModelEditorView extends AbstractWebview<
return addedDatabase;
}
private registerToModelingStoreEvents() {
private registerToModelingEvents() {
this.push(
this.modelingStore.onMethodsChanged(async (event) => {
this.modelingEvents.onMethodsChanged(async (event) => {
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
await this.postMessage({
t: "setMethods",
@@ -596,7 +711,7 @@ export class ModelEditorView extends AbstractWebview<
);
this.push(
this.modelingStore.onModeledMethodsChanged(async (event) => {
this.modelingEvents.onModeledMethodsChanged(async (event) => {
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
await this.postMessage({
t: "setModeledMethods",
@@ -607,7 +722,7 @@ export class ModelEditorView extends AbstractWebview<
);
this.push(
this.modelingStore.onModifiedMethodsChanged(async (event) => {
this.modelingEvents.onModifiedMethodsChanged(async (event) => {
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
await this.postMessage({
t: "setModifiedMethods",
@@ -616,9 +731,28 @@ export class ModelEditorView extends AbstractWebview<
}
}),
);
this.push(
this.modelingEvents.onInProgressMethodsChanged(async (event) => {
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
await this.postMessage({
t: "setInProgressMethods",
methods: Array.from(event.methods),
});
}
}),
);
}
private addModeledMethods(modeledMethods: Record<string, ModeledMethod>) {
private registerToModelConfigEvents() {
this.push(
this.modelConfig.onDidChangeConfiguration(() => {
void this.setViewState();
}),
);
}
private addModeledMethods(modeledMethods: Record<string, ModeledMethod[]>) {
this.modelingStore.addModeledMethods(this.databaseItem, modeledMethods);
this.modelingStore.addModifiedMethods(
@@ -627,13 +761,26 @@ export class ModelEditorView extends AbstractWebview<
);
}
private setModeledMethod(method: ModeledMethod) {
const state = this.modelingStore.getStateForActiveDb();
if (!state) {
throw new Error("Attempting to set modeled method without active db");
private addModeledMethodsFromArray(modeledMethods: ModeledMethod[]) {
const modeledMethodsByName: Record<string, ModeledMethod[]> = {};
for (const modeledMethod of modeledMethods) {
if (!(modeledMethod.signature in modeledMethodsByName)) {
modeledMethodsByName[modeledMethod.signature] = [];
}
modeledMethodsByName[modeledMethod.signature].push(modeledMethod);
}
this.modelingStore.updateModeledMethod(state.databaseItem, method);
this.modelingStore.addModifiedMethod(state.databaseItem, method.signature);
this.addModeledMethods(modeledMethodsByName);
}
private setModeledMethods(signature: string, methods: ModeledMethod[]) {
this.modelingStore.updateModeledMethods(
this.databaseItem,
signature,
methods,
);
this.modelingStore.addModifiedMethod(this.databaseItem, signature);
}
}

View File

@@ -0,0 +1,88 @@
import { ModeledMethod, SinkModeledMethod } from "./modeled-method";
import { MethodSignature } from "./method";
import { assertNever } from "../common/helpers-pure";
export function createEmptyModeledMethod(
type: ModeledMethod["type"],
methodSignature: MethodSignature,
) {
const canonicalMethodSignature: MethodSignature = {
packageName: methodSignature.packageName,
typeName: methodSignature.typeName,
methodName: methodSignature.methodName,
methodParameters: methodSignature.methodParameters,
signature: methodSignature.signature,
};
switch (type) {
case "none":
return createEmptyNoneModeledMethod(canonicalMethodSignature);
case "source":
return createEmptySourceModeledMethod(canonicalMethodSignature);
case "sink":
return createEmptySinkModeledMethod(canonicalMethodSignature);
case "summary":
return createEmptySummaryModeledMethod(canonicalMethodSignature);
case "neutral":
return createEmptyNeutralModeledMethod(canonicalMethodSignature);
default:
assertNever(type);
}
}
function createEmptyNoneModeledMethod(
methodSignature: MethodSignature,
): ModeledMethod {
return {
...methodSignature,
type: "none",
};
}
function createEmptySourceModeledMethod(
methodSignature: MethodSignature,
): ModeledMethod {
return {
...methodSignature,
type: "source",
output: "",
kind: "",
provenance: "manual",
};
}
function createEmptySinkModeledMethod(
methodSignature: MethodSignature,
): SinkModeledMethod {
return {
...methodSignature,
type: "sink",
input: "",
kind: "",
provenance: "manual",
};
}
function createEmptySummaryModeledMethod(
methodSignature: MethodSignature,
): ModeledMethod {
return {
...methodSignature,
type: "summary",
input: "",
output: "",
kind: "",
provenance: "manual",
};
}
function createEmptyNeutralModeledMethod(
methodSignature: MethodSignature,
): ModeledMethod {
return {
...methodSignature,
type: "neutral",
kind: "",
provenance: "manual",
};
}

View File

@@ -10,18 +10,20 @@ import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { load as loadYaml } from "js-yaml";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { pathsEqual } from "../common/files";
import { QueryLanguage } from "../common/query-language";
export async function saveModeledMethods(
extensionPack: ExtensionPack,
language: string,
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
language: QueryLanguage,
methods: readonly Method[],
modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>>,
mode: Mode,
cliServer: CodeQLCliServer,
logger: NotificationLogger,
): Promise<void> {
const existingModeledMethods = await loadModeledMethodFiles(
extensionPack,
language,
cliServer,
logger,
);
@@ -43,14 +45,15 @@ export async function saveModeledMethods(
async function loadModeledMethodFiles(
extensionPack: ExtensionPack,
language: QueryLanguage,
cliServer: CodeQLCliServer,
logger: NotificationLogger,
): Promise<Record<string, Record<string, ModeledMethod>>> {
): Promise<Record<string, Record<string, ModeledMethod[]>>> {
const modelFiles = await listModelFiles(extensionPack.path, cliServer);
const modeledMethodsByFile: Record<
string,
Record<string, ModeledMethod>
Record<string, ModeledMethod[]>
> = {};
for (const modelFile of modelFiles) {
@@ -60,7 +63,7 @@ async function loadModeledMethodFiles(
filename: modelFile,
});
const modeledMethods = loadDataExtensionYaml(data);
const modeledMethods = loadDataExtensionYaml(data, language);
if (!modeledMethods) {
void showAndLogErrorMessage(
logger,
@@ -76,19 +79,25 @@ async function loadModeledMethodFiles(
export async function loadModeledMethods(
extensionPack: ExtensionPack,
language: QueryLanguage,
cliServer: CodeQLCliServer,
logger: NotificationLogger,
): Promise<Record<string, ModeledMethod>> {
const existingModeledMethods: Record<string, ModeledMethod> = {};
): Promise<Record<string, ModeledMethod[]>> {
const existingModeledMethods: Record<string, ModeledMethod[]> = {};
const modeledMethodsByFile = await loadModeledMethodFiles(
extensionPack,
language,
cliServer,
logger,
);
for (const modeledMethods of Object.values(modeledMethodsByFile)) {
for (const [key, value] of Object.entries(modeledMethods)) {
existingModeledMethods[key] = value;
if (!(key in existingModeledMethods)) {
existingModeledMethods[key] = [];
}
existingModeledMethods[key].push(...value);
}
}

View File

@@ -19,12 +19,85 @@ export type Provenance =
// Entered by the user in the editor manually
| "manual";
export interface ModeledMethod extends MethodSignature {
type: ModeledMethodType;
input: string;
output: string;
kind: ModeledMethodKind;
provenance: Provenance;
export interface NoneModeledMethod extends MethodSignature {
readonly type: "none";
}
export interface SourceModeledMethod extends MethodSignature {
readonly type: "source";
readonly output: string;
readonly kind: ModeledMethodKind;
readonly provenance: Provenance;
}
export interface SinkModeledMethod extends MethodSignature {
readonly type: "sink";
readonly input: string;
readonly kind: ModeledMethodKind;
readonly provenance: Provenance;
}
export interface SummaryModeledMethod extends MethodSignature {
readonly type: "summary";
readonly input: string;
readonly output: string;
readonly kind: ModeledMethodKind;
readonly provenance: Provenance;
}
export interface NeutralModeledMethod extends MethodSignature {
readonly type: "neutral";
readonly kind: ModeledMethodKind;
readonly provenance: Provenance;
}
export type ModeledMethod =
| NoneModeledMethod
| SourceModeledMethod
| SinkModeledMethod
| SummaryModeledMethod
| NeutralModeledMethod;
export type ModeledMethodKind = string;
export function modeledMethodSupportsKind(
modeledMethod: ModeledMethod,
): modeledMethod is
| SourceModeledMethod
| SinkModeledMethod
| SummaryModeledMethod
| NeutralModeledMethod {
return (
modeledMethod.type === "source" ||
modeledMethod.type === "sink" ||
modeledMethod.type === "summary" ||
modeledMethod.type === "neutral"
);
}
export function modeledMethodSupportsInput(
modeledMethod: ModeledMethod,
): modeledMethod is SinkModeledMethod | SummaryModeledMethod {
return modeledMethod.type === "sink" || modeledMethod.type === "summary";
}
export function modeledMethodSupportsOutput(
modeledMethod: ModeledMethod,
): modeledMethod is SourceModeledMethod | SummaryModeledMethod {
return modeledMethod.type === "source" || modeledMethod.type === "summary";
}
export function modeledMethodSupportsProvenance(
modeledMethod: ModeledMethod,
): modeledMethod is
| SourceModeledMethod
| SinkModeledMethod
| SummaryModeledMethod
| NeutralModeledMethod {
return (
modeledMethod.type === "source" ||
modeledMethod.type === "sink" ||
modeledMethod.type === "summary" ||
modeledMethod.type === "neutral"
);
}

View File

@@ -0,0 +1,223 @@
import { App } from "../common/app";
import { DisposableObject } from "../common/disposable-object";
import { AppEvent, AppEventEmitter } from "../common/events";
import { DatabaseItem } from "../databases/local-databases";
import { Method, Usage } from "./method";
import { ModeledMethod } from "./modeled-method";
import { Mode } from "./shared/mode";
interface MethodsChangedEvent {
readonly methods: readonly Method[];
readonly dbUri: string;
readonly isActiveDb: boolean;
}
interface HideModeledMethodsChangedEvent {
readonly hideModeledMethods: boolean;
readonly isActiveDb: boolean;
}
interface ModeChangedEvent {
readonly mode: Mode;
readonly isActiveDb: boolean;
}
interface ModeledMethodsChangedEvent {
readonly modeledMethods: Readonly<Record<string, ModeledMethod[]>>;
readonly dbUri: string;
readonly isActiveDb: boolean;
}
interface ModifiedMethodsChangedEvent {
readonly modifiedMethods: ReadonlySet<string>;
readonly dbUri: string;
readonly isActiveDb: boolean;
}
interface SelectedMethodChangedEvent {
readonly databaseItem: DatabaseItem;
readonly method: Method;
readonly usage: Usage;
readonly modeledMethods: readonly ModeledMethod[];
readonly isModified: boolean;
readonly isInProgress: boolean;
}
interface InProgressMethodsChangedEvent {
readonly dbUri: string;
readonly methods: ReadonlySet<string>;
}
export class ModelingEvents extends DisposableObject {
public readonly onActiveDbChanged: AppEvent<void>;
public readonly onDbOpened: AppEvent<DatabaseItem>;
public readonly onDbClosed: AppEvent<string>;
public readonly onMethodsChanged: AppEvent<MethodsChangedEvent>;
public readonly onHideModeledMethodsChanged: AppEvent<HideModeledMethodsChangedEvent>;
public readonly onModeChanged: AppEvent<ModeChangedEvent>;
public readonly onModeledMethodsChanged: AppEvent<ModeledMethodsChangedEvent>;
public readonly onModifiedMethodsChanged: AppEvent<ModifiedMethodsChangedEvent>;
public readonly onSelectedMethodChanged: AppEvent<SelectedMethodChangedEvent>;
public readonly onInProgressMethodsChanged: AppEvent<InProgressMethodsChangedEvent>;
private readonly onActiveDbChangedEventEmitter: AppEventEmitter<void>;
private readonly onDbOpenedEventEmitter: AppEventEmitter<DatabaseItem>;
private readonly onDbClosedEventEmitter: AppEventEmitter<string>;
private readonly onMethodsChangedEventEmitter: AppEventEmitter<MethodsChangedEvent>;
private readonly onHideModeledMethodsChangedEventEmitter: AppEventEmitter<HideModeledMethodsChangedEvent>;
private readonly onModeChangedEventEmitter: AppEventEmitter<ModeChangedEvent>;
private readonly onModeledMethodsChangedEventEmitter: AppEventEmitter<ModeledMethodsChangedEvent>;
private readonly onModifiedMethodsChangedEventEmitter: AppEventEmitter<ModifiedMethodsChangedEvent>;
private readonly onSelectedMethodChangedEventEmitter: AppEventEmitter<SelectedMethodChangedEvent>;
private readonly onInProgressMethodsChangedEventEmitter: AppEventEmitter<InProgressMethodsChangedEvent>;
constructor(app: App) {
super();
this.onActiveDbChangedEventEmitter = this.push(
app.createEventEmitter<void>(),
);
this.onActiveDbChanged = this.onActiveDbChangedEventEmitter.event;
this.onDbOpenedEventEmitter = this.push(
app.createEventEmitter<DatabaseItem>(),
);
this.onDbOpened = this.onDbOpenedEventEmitter.event;
this.onDbClosedEventEmitter = this.push(app.createEventEmitter<string>());
this.onDbClosed = this.onDbClosedEventEmitter.event;
this.onMethodsChangedEventEmitter = this.push(
app.createEventEmitter<MethodsChangedEvent>(),
);
this.onMethodsChanged = this.onMethodsChangedEventEmitter.event;
this.onHideModeledMethodsChangedEventEmitter = this.push(
app.createEventEmitter<HideModeledMethodsChangedEvent>(),
);
this.onHideModeledMethodsChanged =
this.onHideModeledMethodsChangedEventEmitter.event;
this.onModeChangedEventEmitter = this.push(
app.createEventEmitter<ModeChangedEvent>(),
);
this.onModeChanged = this.onModeChangedEventEmitter.event;
this.onModeledMethodsChangedEventEmitter = this.push(
app.createEventEmitter<ModeledMethodsChangedEvent>(),
);
this.onModeledMethodsChanged =
this.onModeledMethodsChangedEventEmitter.event;
this.onModifiedMethodsChangedEventEmitter = this.push(
app.createEventEmitter<ModifiedMethodsChangedEvent>(),
);
this.onModifiedMethodsChanged =
this.onModifiedMethodsChangedEventEmitter.event;
this.onSelectedMethodChangedEventEmitter = this.push(
app.createEventEmitter<SelectedMethodChangedEvent>(),
);
this.onSelectedMethodChanged =
this.onSelectedMethodChangedEventEmitter.event;
this.onInProgressMethodsChangedEventEmitter = this.push(
app.createEventEmitter<InProgressMethodsChangedEvent>(),
);
this.onInProgressMethodsChanged =
this.onInProgressMethodsChangedEventEmitter.event;
}
public fireActiveDbChangedEvent() {
this.onActiveDbChangedEventEmitter.fire();
}
public fireDbOpenedEvent(databaseItem: DatabaseItem) {
this.onDbOpenedEventEmitter.fire(databaseItem);
}
public fireDbClosedEvent(dbUri: string) {
this.onDbClosedEventEmitter.fire(dbUri);
}
public fireMethodsChangedEvent(
methods: Method[],
dbUri: string,
isActiveDb: boolean,
) {
this.onMethodsChangedEventEmitter.fire({
methods,
dbUri,
isActiveDb,
});
}
public fireHideModeledMethodsChangedEvent(
hideModeledMethods: boolean,
isActiveDb: boolean,
) {
this.onHideModeledMethodsChangedEventEmitter.fire({
hideModeledMethods,
isActiveDb,
});
}
public fireModeChangedEvent(mode: Mode, isActiveDb: boolean) {
this.onModeChangedEventEmitter.fire({
mode,
isActiveDb,
});
}
public fireModeledMethodsChangedEvent(
modeledMethods: Record<string, ModeledMethod[]>,
dbUri: string,
isActiveDb: boolean,
) {
this.onModeledMethodsChangedEventEmitter.fire({
modeledMethods,
dbUri,
isActiveDb,
});
}
public fireModifiedMethodsChangedEvent(
modifiedMethods: ReadonlySet<string>,
dbUri: string,
isActiveDb: boolean,
) {
this.onModifiedMethodsChangedEventEmitter.fire({
modifiedMethods,
dbUri,
isActiveDb,
});
}
public fireSelectedMethodChangedEvent(
databaseItem: DatabaseItem,
method: Method,
usage: Usage,
modeledMethods: ModeledMethod[],
isModified: boolean,
isInProgress: boolean,
) {
this.onSelectedMethodChangedEventEmitter.fire({
databaseItem,
method,
usage,
modeledMethods,
isModified,
isInProgress,
});
}
public fireInProgressMethodsChangedEvent(
dbUri: string,
methods: ReadonlySet<string>,
) {
this.onInProgressMethodsChangedEventEmitter.fire({
dbUri,
methods,
});
}
}

View File

@@ -1,133 +1,75 @@
import { App } from "../common/app";
import { DisposableObject } from "../common/disposable-object";
import { AppEvent, AppEventEmitter } from "../common/events";
import { DatabaseItem } from "../databases/local-databases";
import { Method, Usage } from "./method";
import { ModeledMethod } from "./modeled-method";
import { ModelingEvents } from "./modeling-events";
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "./shared/hide-modeled-methods";
import { Mode } from "./shared/mode";
interface DbModelingState {
interface InternalDbModelingState {
databaseItem: DatabaseItem;
methods: Method[];
hideModeledMethods: boolean;
modeledMethods: Record<string, ModeledMethod>;
mode: Mode;
modeledMethods: Record<string, ModeledMethod[]>;
modifiedMethodSignatures: Set<string>;
inProgressMethods: Set<string>;
selectedMethod: Method | undefined;
selectedUsage: Usage | undefined;
}
interface MethodsChangedEvent {
methods: Method[];
dbUri: string;
isActiveDb: boolean;
interface DbModelingState {
readonly databaseItem: DatabaseItem;
readonly methods: readonly Method[];
readonly hideModeledMethods: boolean;
readonly mode: Mode;
readonly modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>>;
readonly modifiedMethodSignatures: ReadonlySet<string>;
readonly inProgressMethods: ReadonlySet<string>;
readonly selectedMethod: Method | undefined;
readonly selectedUsage: Usage | undefined;
}
interface HideModeledMethodsChangedEvent {
hideModeledMethods: boolean;
isActiveDb: boolean;
}
interface ModeledMethodsChangedEvent {
modeledMethods: Record<string, ModeledMethod>;
dbUri: string;
isActiveDb: boolean;
}
interface ModifiedMethodsChangedEvent {
modifiedMethods: Set<string>;
dbUri: string;
isActiveDb: boolean;
}
interface SelectedMethodChangedEvent {
databaseItem: DatabaseItem;
method: Method;
usage: Usage;
modeledMethod: ModeledMethod | undefined;
isModified: boolean;
interface SelectedMethodDetails {
readonly databaseItem: DatabaseItem;
readonly method: Method;
readonly usage: Usage | undefined;
readonly modeledMethods: readonly ModeledMethod[];
readonly isModified: boolean;
readonly isInProgress: boolean;
}
export class ModelingStore extends DisposableObject {
public readonly onActiveDbChanged: AppEvent<void>;
public readonly onDbClosed: AppEvent<string>;
public readonly onMethodsChanged: AppEvent<MethodsChangedEvent>;
public readonly onHideModeledMethodsChanged: AppEvent<HideModeledMethodsChangedEvent>;
public readonly onModeledMethodsChanged: AppEvent<ModeledMethodsChangedEvent>;
public readonly onModifiedMethodsChanged: AppEvent<ModifiedMethodsChangedEvent>;
public readonly onSelectedMethodChanged: AppEvent<SelectedMethodChangedEvent>;
private readonly state: Map<string, DbModelingState>;
private readonly state: Map<string, InternalDbModelingState>;
private activeDb: string | undefined;
private readonly onActiveDbChangedEventEmitter: AppEventEmitter<void>;
private readonly onDbClosedEventEmitter: AppEventEmitter<string>;
private readonly onMethodsChangedEventEmitter: AppEventEmitter<MethodsChangedEvent>;
private readonly onHideModeledMethodsChangedEventEmitter: AppEventEmitter<HideModeledMethodsChangedEvent>;
private readonly onModeledMethodsChangedEventEmitter: AppEventEmitter<ModeledMethodsChangedEvent>;
private readonly onModifiedMethodsChangedEventEmitter: AppEventEmitter<ModifiedMethodsChangedEvent>;
private readonly onSelectedMethodChangedEventEmitter: AppEventEmitter<SelectedMethodChangedEvent>;
constructor(app: App) {
constructor(private readonly modelingEvents: ModelingEvents) {
super();
// State initialization
this.state = new Map<string, DbModelingState>();
// Event initialization
this.onActiveDbChangedEventEmitter = this.push(
app.createEventEmitter<void>(),
);
this.onActiveDbChanged = this.onActiveDbChangedEventEmitter.event;
this.onDbClosedEventEmitter = this.push(app.createEventEmitter<string>());
this.onDbClosed = this.onDbClosedEventEmitter.event;
this.onMethodsChangedEventEmitter = this.push(
app.createEventEmitter<MethodsChangedEvent>(),
);
this.onMethodsChanged = this.onMethodsChangedEventEmitter.event;
this.onHideModeledMethodsChangedEventEmitter = this.push(
app.createEventEmitter<HideModeledMethodsChangedEvent>(),
);
this.onHideModeledMethodsChanged =
this.onHideModeledMethodsChangedEventEmitter.event;
this.onModeledMethodsChangedEventEmitter = this.push(
app.createEventEmitter<ModeledMethodsChangedEvent>(),
);
this.onModeledMethodsChanged =
this.onModeledMethodsChangedEventEmitter.event;
this.onModifiedMethodsChangedEventEmitter = this.push(
app.createEventEmitter<ModifiedMethodsChangedEvent>(),
);
this.onModifiedMethodsChanged =
this.onModifiedMethodsChangedEventEmitter.event;
this.onSelectedMethodChangedEventEmitter = this.push(
app.createEventEmitter<SelectedMethodChangedEvent>(),
);
this.onSelectedMethodChanged =
this.onSelectedMethodChangedEventEmitter.event;
this.state = new Map<string, InternalDbModelingState>();
}
public initializeStateForDb(databaseItem: DatabaseItem) {
public initializeStateForDb(databaseItem: DatabaseItem, mode: Mode) {
const dbUri = databaseItem.databaseUri.toString();
this.state.set(dbUri, {
databaseItem,
methods: [],
hideModeledMethods: INITIAL_HIDE_MODELED_METHODS_VALUE,
mode,
modeledMethods: {},
modifiedMethodSignatures: new Set(),
selectedMethod: undefined,
selectedUsage: undefined,
inProgressMethods: new Set(),
});
this.modelingEvents.fireDbOpenedEvent(databaseItem);
}
public setActiveDb(databaseItem: DatabaseItem) {
this.activeDb = databaseItem.databaseUri.toString();
this.onActiveDbChangedEventEmitter.fire();
this.modelingEvents.fireActiveDbChangedEvent();
}
public removeDb(databaseItem: DatabaseItem) {
@@ -139,11 +81,11 @@ export class ModelingStore extends DisposableObject {
if (this.activeDb === dbUri) {
this.activeDb = undefined;
this.onActiveDbChangedEventEmitter.fire();
this.modelingEvents.fireActiveDbChangedEvent();
}
this.state.delete(dbUri);
this.onDbClosedEventEmitter.fire(dbUri);
this.modelingEvents.fireDbClosedEvent(dbUri);
}
public getStateForActiveDb(): DbModelingState | undefined {
@@ -154,17 +96,63 @@ export class ModelingStore extends DisposableObject {
return this.state.get(this.activeDb);
}
private getInternalStateForActiveDb(): InternalDbModelingState | undefined {
if (!this.activeDb) {
return undefined;
}
return this.state.get(this.activeDb);
}
public hasStateForActiveDb(): boolean {
return !!this.getStateForActiveDb();
}
public anyDbsBeingModeled(): boolean {
return this.state.size > 0;
}
/**
* Returns the method for the given database item and method signature.
* Returns undefined if no method exists with that signature.
*/
public getMethod(
dbItem: DatabaseItem,
methodSignature: string,
): Method | undefined {
return this.getState(dbItem).methods.find(
(m) => m.signature === methodSignature,
);
}
/**
* Returns the methods for the given database item and method signatures.
* If the `methodSignatures` argument is not provided or is undefined, returns all methods.
*/
public getMethods(
dbItem: DatabaseItem,
methodSignatures?: string[],
): readonly Method[] {
const methods = this.getState(dbItem).methods;
if (!methodSignatures) {
return methods;
}
return methods.filter((method) =>
methodSignatures.includes(method.signature),
);
}
public setMethods(dbItem: DatabaseItem, methods: Method[]) {
const dbState = this.getState(dbItem);
const dbUri = dbItem.databaseUri.toString();
dbState.methods = [...methods];
this.onMethodsChangedEventEmitter.fire({
this.modelingEvents.fireMethodsChangedEvent(
methods,
dbUri,
isActiveDb: dbUri === this.activeDb,
});
dbUri === this.activeDb,
);
}
public setHideModeledMethods(
@@ -176,22 +164,55 @@ export class ModelingStore extends DisposableObject {
dbState.hideModeledMethods = hideModeledMethods;
this.onHideModeledMethodsChangedEventEmitter.fire({
this.modelingEvents.fireHideModeledMethodsChangedEvent(
hideModeledMethods,
isActiveDb: dbUri === this.activeDb,
});
dbUri === this.activeDb,
);
}
public setMode(dbItem: DatabaseItem, mode: Mode) {
const dbState = this.getState(dbItem);
const dbUri = dbItem.databaseUri.toString();
dbState.mode = mode;
this.modelingEvents.fireModeChangedEvent(mode, dbUri === this.activeDb);
}
public getMode(dbItem: DatabaseItem) {
return this.getState(dbItem).mode;
}
/**
* Returns the modeled methods for the given database item and method signatures.
* If the `methodSignatures` argument is not provided or is undefined, returns all modeled methods.
*/
public getModeledMethods(
dbItem: DatabaseItem,
methodSignatures?: string[],
): Readonly<Record<string, readonly ModeledMethod[]>> {
const modeledMethods = this.getState(dbItem).modeledMethods;
if (!methodSignatures) {
return modeledMethods;
}
return Object.fromEntries(
Object.entries(modeledMethods).filter(([key]) =>
methodSignatures.includes(key),
),
);
}
public addModeledMethods(
dbItem: DatabaseItem,
methods: Record<string, ModeledMethod>,
methods: Record<string, ModeledMethod[]>,
) {
this.changeModeledMethods(dbItem, (state) => {
const newModeledMethods = {
...methods,
// Keep all methods that are already modeled in some form in the state
...Object.fromEntries(
Object.entries(state.modeledMethods).filter(
([_, value]) => value.type !== "none",
Object.entries(state.modeledMethods).filter(([_, value]) =>
value.some((m) => m.type !== "none"),
),
),
};
@@ -201,17 +222,21 @@ export class ModelingStore extends DisposableObject {
public setModeledMethods(
dbItem: DatabaseItem,
methods: Record<string, ModeledMethod>,
methods: Record<string, ModeledMethod[]>,
) {
this.changeModeledMethods(dbItem, (state) => {
state.modeledMethods = { ...methods };
});
}
public updateModeledMethod(dbItem: DatabaseItem, method: ModeledMethod) {
public updateModeledMethods(
dbItem: DatabaseItem,
signature: string,
modeledMethods: ModeledMethod[],
) {
this.changeModeledMethods(dbItem, (state) => {
const newModeledMethods = { ...state.modeledMethods };
newModeledMethods[method.signature] = method;
newModeledMethods[signature] = modeledMethods;
state.modeledMethods = newModeledMethods;
});
}
@@ -255,23 +280,59 @@ export class ModelingStore extends DisposableObject {
});
}
/**
* Sets which method is considered to be selected. This method will be shown in the method modeling panel.
*
* The `Method` and `Usage` objects must have been retrieved from the modeling store, and not from
* a webview. This is because we rely on object referential identity so it must be the same object
* that is held internally by the modeling store.
*/
public setSelectedMethod(dbItem: DatabaseItem, method: Method, usage: Usage) {
const dbState = this.getState(dbItem);
dbState.selectedMethod = method;
dbState.selectedUsage = usage;
this.onSelectedMethodChangedEventEmitter.fire({
databaseItem: dbItem,
const modeledMethods = dbState.modeledMethods[method.signature] ?? [];
const isModified = dbState.modifiedMethodSignatures.has(method.signature);
const isInProgress = dbState.inProgressMethods.has(method.signature);
this.modelingEvents.fireSelectedMethodChangedEvent(
dbItem,
method,
usage,
modeledMethod: dbState.modeledMethods[method.signature],
isModified: dbState.modifiedMethodSignatures.has(method.signature),
modeledMethods,
isModified,
isInProgress,
);
}
public addInProgressMethods(
dbItem: DatabaseItem,
inProgressMethods: string[],
) {
this.changeInProgressMethods(dbItem, (state) => {
state.inProgressMethods = new Set([
...state.inProgressMethods,
...inProgressMethods,
]);
});
}
public getSelectedMethodDetails() {
const dbState = this.getStateForActiveDb();
public removeInProgressMethods(
dbItem: DatabaseItem,
methodSignatures: string[],
) {
this.changeInProgressMethods(dbItem, (state) => {
state.inProgressMethods = new Set(
Array.from(state.inProgressMethods).filter(
(s) => !methodSignatures.includes(s),
),
);
});
}
public getSelectedMethodDetails(): SelectedMethodDetails | undefined {
const dbState = this.getInternalStateForActiveDb();
if (!dbState) {
throw new Error("No active state found in modeling store");
}
@@ -282,16 +343,18 @@ export class ModelingStore extends DisposableObject {
}
return {
databaseItem: dbState.databaseItem,
method: selectedMethod,
usage: dbState.selectedUsage,
modeledMethod: dbState.modeledMethods[selectedMethod.signature],
modeledMethods: dbState.modeledMethods[selectedMethod.signature] ?? [],
isModified: dbState.modifiedMethodSignatures.has(
selectedMethod.signature,
),
isInProgress: dbState.inProgressMethods.has(selectedMethod.signature),
};
}
private getState(databaseItem: DatabaseItem): DbModelingState {
private getState(databaseItem: DatabaseItem): InternalDbModelingState {
if (!this.state.has(databaseItem.databaseUri.toString())) {
throw Error(
"Cannot get state for a database that has not been initialized",
@@ -303,31 +366,45 @@ export class ModelingStore extends DisposableObject {
private changeModifiedMethods(
dbItem: DatabaseItem,
updateState: (state: DbModelingState) => void,
updateState: (state: InternalDbModelingState) => void,
) {
const state = this.getState(dbItem);
updateState(state);
this.onModifiedMethodsChangedEventEmitter.fire({
modifiedMethods: state.modifiedMethodSignatures,
dbUri: dbItem.databaseUri.toString(),
isActiveDb: dbItem.databaseUri.toString() === this.activeDb,
});
this.modelingEvents.fireModifiedMethodsChangedEvent(
state.modifiedMethodSignatures,
dbItem.databaseUri.toString(),
dbItem.databaseUri.toString() === this.activeDb,
);
}
private changeModeledMethods(
dbItem: DatabaseItem,
updateState: (state: DbModelingState) => void,
updateState: (state: InternalDbModelingState) => void,
) {
const state = this.getState(dbItem);
updateState(state);
this.onModeledMethodsChangedEventEmitter.fire({
modeledMethods: state.modeledMethods,
dbUri: dbItem.databaseUri.toString(),
isActiveDb: dbItem.databaseUri.toString() === this.activeDb,
});
this.modelingEvents.fireModeledMethodsChangedEvent(
state.modeledMethods,
dbItem.databaseUri.toString(),
dbItem.databaseUri.toString() === this.activeDb,
);
}
private changeInProgressMethods(
dbItem: DatabaseItem,
updateState: (state: InternalDbModelingState) => void,
) {
const state = this.getState(dbItem);
updateState(state);
this.modelingEvents.fireInProgressMethodsChangedEvent(
dbItem.databaseUri.toString(),
state.inProgressMethods,
);
}
}

View File

@@ -1,153 +0,0 @@
import { ModeledMethod, ModeledMethodType, Provenance } from "./modeled-method";
import { DataTuple } from "./model-extension-file";
export type ExtensiblePredicateDefinition = {
extensiblePredicate: string;
generateMethodDefinition: (method: ModeledMethod) => DataTuple[];
readModeledMethod: (row: DataTuple[]) => ModeledMethod;
supportedKinds?: string[];
};
function readRowToMethod(row: DataTuple[]): string {
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
}
export const extensiblePredicateDefinitions: Record<
Exclude<ModeledMethodType, "none">,
ExtensiblePredicateDefinition
> = {
source: {
extensiblePredicate: "sourceModel",
// extensible predicate sourceModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.packageName,
method.typeName,
true,
method.methodName,
method.methodParameters,
"",
method.output,
method.kind,
method.provenance,
],
readModeledMethod: (row) => ({
type: "source",
input: "",
output: row[6] as string,
kind: row[7] as string,
provenance: row[8] as Provenance,
signature: readRowToMethod(row),
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
supportedKinds: ["local", "remote"],
},
sink: {
extensiblePredicate: "sinkModel",
// extensible predicate sinkModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.packageName,
method.typeName,
true,
method.methodName,
method.methodParameters,
"",
method.input,
method.kind,
method.provenance,
],
readModeledMethod: (row) => ({
type: "sink",
input: row[6] as string,
output: "",
kind: row[7] as string,
provenance: row[8] as Provenance,
signature: readRowToMethod(row),
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
supportedKinds: [
"code-injection",
"command-injection",
"file-content-store",
"html-injection",
"js-injection",
"ldap-injection",
"log-injection",
"path-injection",
"request-forgery",
"sql-injection",
"url-redirection",
],
},
summary: {
extensiblePredicate: "summaryModel",
// extensible predicate summaryModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.packageName,
method.typeName,
true,
method.methodName,
method.methodParameters,
"",
method.input,
method.output,
method.kind,
method.provenance,
],
readModeledMethod: (row) => ({
type: "summary",
input: row[6] as string,
output: row[7] as string,
kind: row[8] as string,
provenance: row[9] as Provenance,
signature: readRowToMethod(row),
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
supportedKinds: ["taint", "value"],
},
neutral: {
extensiblePredicate: "neutralModel",
// extensible predicate neutralModel(
// string package, string type, string name, string signature, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.packageName,
method.typeName,
method.methodName,
method.methodParameters,
method.kind,
method.provenance,
],
readModeledMethod: (row) => ({
type: "neutral",
input: "",
output: "",
kind: row[4] as string,
provenance: row[5] as Provenance,
signature: `${row[0]}.${row[1]}#${row[2]}${row[3]}`,
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[2] as string,
methodParameters: row[3] as string,
}),
supportedKinds: ["summary", "source", "sink"],
},
};

View File

@@ -1,9 +1,11 @@
import { fetchExternalApisQuery as csharpFetchExternalApisQuery } from "./csharp";
import { fetchExternalApisQuery as javaFetchExternalApisQuery } from "./java";
import { fetchExternalApisQuery as rubyFetchExternalApisQuery } from "./ruby";
import { Query } from "./query";
import { QueryLanguage } from "../../common/query-language";
export const fetchExternalApiQueries: Partial<Record<QueryLanguage, Query>> = {
[QueryLanguage.CSharp]: csharpFetchExternalApisQuery,
[QueryLanguage.Java]: javaFetchExternalApisQuery,
[QueryLanguage.Ruby]: rubyFetchExternalApisQuery,
};

View File

@@ -0,0 +1,404 @@
import { Query } from "./query";
export const fetchExternalApisQuery: Query = {
applicationModeQuery: `/**
* @name Fetch endpoints for use in the model editor (application mode)
* @description A list of 3rd party endpoints (methods and attributes) used in the codebase. Excludes test and generated code.
* @kind table
* @id rb/utils/modeleditor/application-mode-endpoints
* @tags modeleditor endpoints application-mode
*/
import ruby
select "todo", "todo", "todo", "todo", "todo", false, "todo", "todo", "todo", "todo"
`,
frameworkModeQuery: `/**
* @name Fetch endpoints for use in the model editor (framework mode)
* @description A list of endpoints accessible (methods and attributes) for consumers of the library. Excludes test and generated code.
* @kind table
* @id rb/utils/modeleditor/framework-mode-endpoints
* @tags modeleditor endpoints framework-mode
*/
import ruby
import FrameworkModeEndpointsQuery
import ModelEditor
from PublicEndpointFromSource endpoint, boolean supported, string type
where
supported = isSupported(endpoint) and
type = supportedType(endpoint)
select endpoint, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
endpoint.getParameterTypes(), supported, endpoint.getFile().getBaseName(), type
`,
dependencies: {
"FrameworkModeEndpointsQuery.qll": `private import ruby
private import ModelEditor
private import modeling.internal.Util as Util
/**
* A class of effectively public callables from source code.
*/
class PublicEndpointFromSource extends Endpoint {
PublicEndpointFromSource() {
this.getFile() instanceof Util::RelevantFile
}
override predicate isSource() { this instanceof SourceCallable }
override predicate isSink() { this instanceof SinkCallable }
}
`,
"ModelEditor.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
private import ruby
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.dataflow.internal.DataFlowPrivate
private import codeql.ruby.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific
private import modeling.internal.Util as Util
private import modeling.internal.Types
private import codeql.ruby.frameworks.core.Gem
/** Holds if the given callable is not worth supporting. */
private predicate isUninteresting(DataFlow::MethodNode c) {
c.getLocation().getFile().getRelativePath().regexpMatch(".*(test|spec).*")
}
/**
* A callable method or accessor from either the Ruby Standard Library, a 3rd party library, or from the source.
*/
class Endpoint extends DataFlow::MethodNode {
Endpoint() {
this.isPublic() and not isUninteresting(this)
}
File getFile() { result = this.getLocation().getFile() }
string getName() { result = this.getMethodName() }
/**
* Gets the namespace of this endpoint.
*/
bindingset[this]
string getNamespace() {
// Return the name of any gemspec file in the database.
// TODO: make this work for projects with multiple gems (and hence multiple gemspec files)
result = any(Gem::GemSpec g).getName()
}
/**
* Gets the unbound type name of this endpoint.
*/
bindingset[this]
string getTypeName() {
// result = nestedName(this.getDeclaringType().getUnboundDeclaration())
// result = any(DataFlow::ClassNode c | Types::methodReturnsType(this, c) | c).getQualifiedName()
result = Util::getAnAccessPathPrefixWithoutSuffix(this)
}
/**
* Gets the parameter types of this endpoint.
*/
bindingset[this]
string getParameterTypes() {
// For now, return the names of postional parameters. We don't always have type information, so we can't return type names.
// We don't yet handle keyword params, splat params or block params.
// result = "(" + parameterQualifiedTypeNamesToString(this) + ")"
result =
"(" +
concat(DataFlow::ParameterNode p, int i |
p = this.asCallable().getParameter(i)
|
p.getName(), "," order by i
) + ")"
}
/** Holds if this API has a supported summary. */
pragma[nomagic]
predicate hasSummary() {
// this instanceof SummarizedCallable
none()
}
/** Holds if this API is a known source. */
pragma[nomagic]
abstract predicate isSource();
/** Holds if this API is a known sink. */
pragma[nomagic]
abstract predicate isSink();
/** Holds if this API is a known neutral. */
pragma[nomagic]
predicate isNeutral() {
// this instanceof FlowSummaryImpl::Public::NeutralCallable
none()
}
/**
* Holds if this API is supported by existing CodeQL libraries, that is, it is either a
* recognized source, sink or neutral or it has a flow summary.
*/
predicate isSupported() {
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
}
}
boolean isSupported(Endpoint endpoint) {
if endpoint.isSupported() then result = true else result = false
}
string supportedType(Endpoint endpoint) {
endpoint.isSink() and result = "sink"
or
endpoint.isSource() and result = "source"
or
endpoint.hasSummary() and result = "summary"
or
endpoint.isNeutral() and result = "neutral"
or
not endpoint.isSupported() and result = ""
}
string methodClassification(Call method) {
result = "source"
}
/**
* A callable where there exists a MaD sink model that applies to it.
*/
class SinkCallable extends DataFlow::CallableNode {
SinkCallable() { sinkElement(this.asExpr().getExpr(), _, _, _) }
}
/**
* A callable where there exists a MaD source model that applies to it.
*/
class SourceCallable extends DataFlow::CallableNode {
SourceCallable() { sourceElement(this.asExpr().getExpr(), _, _, _) }
}`,
"modeling/internal/Util.qll": `private import ruby
// \`SomeClass#initialize\` methods are usually called indirectly via
// \`SomeClass.new\`, so we need to account for this when generating access paths
private string getNormalizedMethodName(DataFlow::MethodNode methodNode) {
exists(string actualMethodName | actualMethodName = methodNode.getMethodName() |
if actualMethodName = "initialize" then result = "new" else result = actualMethodName
)
}
private string getAccessPathSuffix(Ast::MethodBase method) {
if method instanceof Ast::SingletonMethod or method.getName() = "initialize"
then result = "!"
else result = ""
}
string getAnAccessPathPrefix(DataFlow::MethodNode methodNode) {
result =
getAnAccessPathPrefixWithoutSuffix(methodNode) +
getAccessPathSuffix(methodNode.asExpr().getExpr())
}
string getAnAccessPathPrefixWithoutSuffix(DataFlow::MethodNode methodNode) {
result =
methodNode
.asExpr()
.getExpr()
.getEnclosingModule()
.(Ast::ConstantWriteAccess)
.getAQualifiedName()
}
class RelevantFile extends File {
RelevantFile() { not this.getRelativePath().regexpMatch(".*/?test(case)?s?/.*") }
}
string getMethodPath(DataFlow::MethodNode methodNode) {
result = "Method[" + getNormalizedMethodName(methodNode) + "]"
}
private string getParameterPath(DataFlow::ParameterNode paramNode) {
exists(Ast::Parameter param, string paramSpec |
param = paramNode.asParameter() and
(
paramSpec = param.getPosition().toString()
or
paramSpec = param.(Ast::KeywordParameter).getName() + ":"
or
param instanceof Ast::BlockParameter and
paramSpec = "block"
)
|
result = "Parameter[" + paramSpec + "]"
)
}
string getMethodParameterPath(DataFlow::MethodNode methodNode, DataFlow::ParameterNode paramNode) {
result = getMethodPath(methodNode) + "." + getParameterPath(paramNode)
}
`,
"modeling/internal/Types.qll": `private import ruby
private import codeql.ruby.ApiGraphs
private import Util as Util
module Types {
private module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
// TODO: construction of type values not using a "new" call
source.(DataFlow::CallNode).getMethodName() = "new"
}
predicate isSink(DataFlow::Node sink) { sink = any(DataFlow::MethodNode m).getAReturnNode() }
}
private import DataFlow::Global<Config>
predicate methodReturnsType(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode) {
// ignore cases of initializing instance of self
not methodNode.getMethodName() = "initialize" and
exists(DataFlow::CallNode initCall |
flow(initCall, methodNode.getAReturnNode()) and
classNode.getAnImmediateReference().getAMethodCall() = initCall and
// constructed object does not have a type declared in test code
/*
* TODO: this may be too restrictive, e.g.
* - if a type is declared in both production and test code
* - if a built-in type is extended in test code
*/
forall(Ast::ModuleBase classDecl | classDecl = classNode.getADeclaration() |
classDecl.getLocation().getFile() instanceof Util::RelevantFile
)
)
}
// \`exprNode\` is an instance of \`classNode\`
private predicate exprHasType(DataFlow::ExprNode exprNode, DataFlow::ClassNode classNode) {
exists(DataFlow::MethodNode methodNode, DataFlow::CallNode callNode |
methodReturnsType(methodNode, classNode) and
callNode.getATarget() = methodNode
|
exprNode.getALocalSource() = callNode
)
or
exists(DataFlow::MethodNode containingMethod |
classNode.getInstanceMethod(containingMethod.getMethodName()) = containingMethod
|
exprNode.getALocalSource() = containingMethod.getSelfParameter()
)
}
// extensible predicate typeModel(string type1, string type2, string path);
// the method node in type2 constructs an instance of classNode
private predicate typeModelReturns(string type1, string type2, string path) {
exists(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode |
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
methodReturnsType(methodNode, classNode)
|
type1 = classNode.getQualifiedName() and
type2 = Util::getAnAccessPathPrefix(methodNode) and
path = Util::getMethodPath(methodNode) + ".ReturnValue"
)
}
predicate methodTakesParameterOfType(
DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode,
DataFlow::ParameterNode parameterNode
) {
exists(DataFlow::CallNode callToMethodNode, DataFlow::LocalSourceNode argumentNode |
callToMethodNode.getATarget() = methodNode and
// positional parameter
exists(int paramIndex |
argumentNode.flowsTo(callToMethodNode.getArgument(paramIndex)) and
parameterNode = methodNode.getParameter(paramIndex)
)
or
// keyword parameter
exists(string kwName |
argumentNode.flowsTo(callToMethodNode.getKeywordArgument(kwName)) and
parameterNode = methodNode.getKeywordParameter(kwName)
)
or
// block parameter
argumentNode.flowsTo(callToMethodNode.getBlock()) and
parameterNode = methodNode.getBlockParameter()
|
// parameter directly from new call
argumentNode.(DataFlow::CallNode).getMethodName() = "new" and
classNode.getAnImmediateReference().getAMethodCall() = argumentNode
or
// parameter from indirect new call
exists(DataFlow::ExprNode argExpr |
exprHasType(argExpr, classNode) and
argumentNode.(DataFlow::CallNode).getATarget() = argExpr
)
)
}
private predicate typeModelParameters(string type1, string type2, string path) {
exists(
DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode,
DataFlow::ParameterNode parameterNode
|
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
methodTakesParameterOfType(methodNode, classNode, parameterNode)
|
type1 = classNode.getQualifiedName() and
type2 = Util::getAnAccessPathPrefix(methodNode) and
path = Util::getMethodParameterPath(methodNode, parameterNode)
)
}
// TODO: non-positional params for block arg parameters
private predicate methodYieldsType(
DataFlow::CallableNode callableNode, int argIdx, DataFlow::ClassNode classNode
) {
exprHasType(callableNode.getABlockCall().getArgument(argIdx), classNode)
}
/*
* e.g. for
* \`\`\`rb
* class Foo
* def initialize
* // do some stuff...
* if block_given?
* yield self
* end
* end
*
* def do_something
* // do something else
* end
* end
*
* Foo.new do |foo| foo.do_something end
* \`\`\`
*
* the parameter foo to the block is an instance of Foo.
*/
private predicate typeModelBlockArgumentParameters(string type1, string type2, string path) {
exists(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode, int argIdx |
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
methodYieldsType(methodNode, argIdx, classNode)
|
type1 = classNode.getQualifiedName() and
type2 = Util::getAnAccessPathPrefix(methodNode) and
path = Util::getMethodPath(methodNode) + ".Argument[block].Parameter[" + argIdx + "]"
)
}
predicate typeModel(string type1, string type2, string path) {
typeModelReturns(type1, type2, path)
or
typeModelParameters(type1, type2, path)
or
typeModelBlockArgumentParameters(type1, type2, path)
}
}
`,
},
};

View File

@@ -1,34 +0,0 @@
/**
* A class that keeps track of which methods are in progress for each package.
*
* This class is immutable and therefore is safe to be used in a React useState hook.
*/
export class InProgressMethods {
// A map of in-progress method signatures for each package.
private readonly methodMap: ReadonlyMap<string, Set<string>>;
constructor(methodMap?: ReadonlyMap<string, Set<string>>) {
this.methodMap = methodMap ?? new Map<string, Set<string>>();
}
/**
* Sets the in-progress methods for the given package.
* Returns a new InProgressMethods instance.
*/
public setPackageMethods(
packageName: string,
methods: Set<string>,
): InProgressMethods {
const newMethodMap = new Map<string, Set<string>>(this.methodMap);
newMethodMap.set(packageName, methods);
return new InProgressMethods(newMethodMap);
}
public hasMethod(packageName: string, method: string): boolean {
const methods = this.methodMap.get(packageName);
if (methods) {
return methods.has(method);
}
return false;
}
}

View File

@@ -2,3 +2,5 @@ export enum Mode {
Application = "application",
Framework = "framework",
}
export const INITIAL_MODE = Mode.Application;

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