Compare commits

...

456 Commits

Author SHA1 Message Date
Shati Patel
6be797d9d2 Merge pull request #2612 from github/v1.8.8
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
Release prep for v1.8.8
2023-07-17 11:10:54 +01:00
shati-patel
3fa7590187 v1.8.8 2023-07-17 10:17:54 +01:00
Shati Patel
6cfc7d5ced Remove support for CodeQL CLI versions older than 2.9.4 (#2610) 2023-07-14 16:13:46 +00:00
Philip Ginsbach
7ea6cd871b Merge pull request #2609 from github/ginsbach/AdditionalAndDefaultKeywords
add `additional` and `default` keywords to the TextMate grammar
2023-07-14 16:23:50 +01:00
Shati Patel
5631d33b20 Change the data extension's webview icon (#2608) 2023-07-14 15:17:56 +00:00
Philip Ginsbach
e1e55d1d01 link to the PR from the changelog entry 2023-07-14 15:23:55 +01:00
Philip Ginsbach
0b18492946 add changelog entry for 'additional' and 'default' keywords 2023-07-14 14:57:46 +01:00
Philip Ginsbach
f3ad6ec105 recompile the TextMate grammar 2023-07-14 14:57:46 +01:00
Philip Ginsbach
d97e4d1ba1 add 'additional' and 'default' annotation keywords to grammar 2023-07-14 14:47:07 +01:00
Koen Vlaswinkel
ccda490ab4 Merge pull request #2566 from github/koesie10/library-version
Add library versions to data extensions editor
2023-07-14 15:43:42 +02:00
Robert
2392d7c7b6 Merge pull request #2606 from github/robertbrignull/data-modeled-method-signature
Add signature and related fields to ModeledMethod
2023-07-14 14:11:40 +01:00
Koen Vlaswinkel
4158df197c Improve library version matching regex 2023-07-14 14:13:23 +02:00
Robert
e2b211ad53 Merge branch 'main' into robertbrignull/data-modeled-method-signature
pick 2db42e3e Pull out createDataExtensionYamls into yaml.ts
pick 52f7cac0 Move saveModeledMethods to a separate file
pick ba27230e Move loadModeledMethods to a separate file
pick c512a11e Split out listModelFiles from loadModeledMethods
pick 752cf8ab Add some tests of listModelFiles
2023-07-14 11:05:14 +01:00
Koen Vlaswinkel
f91da95081 Add library versions to data extensions editor 2023-07-14 12:02:05 +02:00
Koen Vlaswinkel
10d9213dbe Merge pull request #2563 from github/koesie10/classification
Add call classification and supported kind to data extensions editor
2023-07-14 12:01:58 +02:00
Robert
bb110152f2 Fix expected values in tests 2023-07-14 10:55:43 +01:00
Koen Vlaswinkel
bd6c302360 Add tooltip to classifications container 2023-07-14 11:38:45 +02:00
Koen Vlaswinkel
1272ddd696 Decrease text size of test and generated tags 2023-07-14 11:36:00 +02:00
Koen Vlaswinkel
ca7ba89a68 Move method classifications to separate component 2023-07-14 11:32:34 +02:00
Andrew Eisenberg
39465d9ad9 Merge pull request #2607 from github/github-action/bump-cli 2023-07-13 14:45:20 -07:00
github-actions[bot]
b419b8d308 Bump CLI version from v2.13.5 to v2.14.0 for integration tests 2023-07-13 18:34:27 +00:00
Robert
169221305f Remove the ModeledMethodWithSignature type in favour of just ModeledMethod 2023-07-13 16:55:04 +01:00
Robert
97a331cf6a Remove the ModeledExternalApiUsage type in favour of just ModeledMethod 2023-07-13 16:55:04 +01:00
Robert
16f98491e7 Remove the ExternalApiUsageByType in favour of just ModeledMethod 2023-07-13 16:55:04 +01:00
Robert
111dfff7fb Add signature to ModeledMethod 2023-07-13 16:55:04 +01:00
Robert
f927ac9f1c Split out MethodSignature from ExternalApiUsage 2023-07-13 16:55:04 +01:00
Koen Vlaswinkel
6a0cae58e0 Remove call type for unmodelable methods
This removes the call type as shown for an unmodelable method. We still
need to decide how to show this information, so this may be added back
in later.
2023-07-13 16:44:43 +02:00
Koen Vlaswinkel
1efc276c24 Show labels for methods only classified in test and generated 2023-07-13 16:36:37 +02:00
Koen Vlaswinkel
94015a0ac2 Show type for supported methods 2023-07-13 16:33:34 +02:00
Koen Vlaswinkel
048552093b Add classification and type to types 2023-07-13 16:19:05 +02:00
Koen Vlaswinkel
0560f4fe76 Add classification and type to queries 2023-07-13 16:15:58 +02:00
Robert
31fdc794e5 Merge pull request #2600 from github/robertbrignull/data-supported
Show more clearly when a method is already modeled
2023-07-12 17:26:10 +01:00
Robert
e55800ae2d Tweak message 2023-07-12 16:37:44 +01:00
Robert
0f39d41e50 fix typo 2023-07-12 16:37:18 +01:00
Robert
31118a514f Merge pull request #2599 from github/nora/make-result-table-functional-component
Convert ResultTable to a functional component
2023-07-12 16:34:57 +01:00
Robert
fa5c24d837 Display an explanatory message on unmodelable methods 2023-07-12 14:56:15 +01:00
Robert
8ec08ef43f Remove ugly jumpToUsage prop 2023-07-12 14:36:40 +01:00
Robert
7d59224407 Pull out ModelableMethodRow as a separate component 2023-07-12 14:36:40 +01:00
Robert
aba574e423 Pull out UmmodelableMethodRow to a separate component 2023-07-12 14:36:39 +01:00
Robert
799b96e7f6 Convert modelTypeOptions to be a const 2023-07-12 14:35:42 +01:00
Nora
60f33e573e Convert ResultTable 2023-07-12 13:12:14 +00:00
Robert
5fa338e460 Return early if method cannot be modeled 2023-07-12 14:04:58 +01:00
Robert
8529c05396 Move boolean showXCell variables to be later and together 2023-07-12 14:02:39 +01:00
Robert
70b2e68ce7 Rename showModelTypeCell => methodCanBeModeled 2023-07-12 14:01:01 +01:00
Robert
8432f6cdfe Move predicate variable closer to where it's used 2023-07-12 13:50:31 +01:00
Robert
39d53f469f Merge pull request #2598 from github/robertbrignull/data-framework-model
Don't show model from source button in framework mode
2023-07-12 10:22:24 +01:00
Robert
c8ba8d6e1b Merge branch 'main' into robertbrignull/data-framework-model 2023-07-12 10:05:06 +01:00
Robert
28c6ab36ce Merge pull request #2597 from github/robertbrignull/data-view-state
Clear up how we pass around view state
2023-07-12 10:04:39 +01:00
Dave Bartolomeo
3e5985955d Merge pull request #2538 from github/dbartol/save-before-start
Save dirty documents before evaluating queries
2023-07-11 15:55:03 -04:00
Dave Bartolomeo
a4cff531be Remove more debugging code 2023-07-11 15:32:09 -04:00
Dave Bartolomeo
c18bb39f40 Remove more debugging code 2023-07-11 14:49:32 -04:00
Dave Bartolomeo
d93f2b67c8 Remove some debugging code 2023-07-11 14:42:57 -04:00
Robert
5b65e08fdf Don't show model from source button in framework mode 2023-07-11 17:32:12 +01:00
Robert
79a567b478 Avoid passing in both viewState and mode at the same time 2023-07-11 16:53:12 +01:00
Robert
5e84b5f055 Enforce that viewState is defined and show a loading screen otherwise 2023-07-11 16:47:44 +01:00
Robert
aa4df082bf Merge pull request #2594 from github/robertbrignull/data-unsaved-changes
Include in addModeledMethods whether the methods are unsaved or not
2023-07-11 16:26:44 +01:00
Robert
370872d005 Split addModeledMethods into that and loadModeledMethods 2023-07-11 15:23:30 +01:00
Robert
930103b3a8 Merge branch 'main' into robertbrignull/data-unsaved-changes 2023-07-11 15:11:24 +01:00
dependabot[bot]
6cf2f32705 Bump @octokit/plugin-retry from 3.0.9 to 4.1.6 in /extensions/ql-vscode (#2518)
Bumps [@octokit/plugin-retry](https://github.com/octokit/plugin-retry.js) from 3.0.9 to 4.1.6.
- [Release notes](https://github.com/octokit/plugin-retry.js/releases)
- [Commits](https://github.com/octokit/plugin-retry.js/compare/v3.0.9...v4.1.6)

---
updated-dependencies:
- dependency-name: "@octokit/plugin-retry"
  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>
Co-authored-by: Charis Kyriakou <charisk@users.noreply.github.com>
2023-07-11 13:02:04 +01:00
Robert
83a25006ec Merge pull request #2586 from github/dependabot/npm_and_yarn/extensions/ql-vscode/prettier-3.0.0
Bump prettier from 2.7.1 to 3.0.0 in /extensions/ql-vscode
2023-07-11 11:11:21 +01:00
Robert
38d0d0ee7d Merge pull request #2593 from github/dependabot/npm_and_yarn/extensions/ql-vscode/tough-cookie-4.1.3
Bump tough-cookie from 4.1.2 to 4.1.3 in /extensions/ql-vscode
2023-07-11 10:51:42 +01:00
Robert
296922c193 Merge pull request #2595 from github/robertbrignull/data-generate-from-source
Hook up generating from source in the new collapsible section
2023-07-11 09:54:07 +01:00
Robert
e5ae41328b Run: npm run format 2023-07-11 09:51:28 +01:00
Robert
0fc3adf29a Upgrade eslint-plugin-prettier to 5.0.0 2023-07-11 09:43:58 +01:00
dependabot[bot]
49954b5af0 Bump prettier from 2.7.1 to 3.0.0 in /extensions/ql-vscode
Bumps [prettier](https://github.com/prettier/prettier) from 2.7.1 to 3.0.0.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.7.1...3.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 15:19:00 +00:00
Nora
f493ba102b Merge pull request #2596 from github/nora/update-eslint
Update eslint
2023-07-10 17:17:01 +02:00
Nora
43aa06a248 Update eslint 2023-07-10 13:49:55 +00:00
Robert
c8fd00b983 Hook up generating from source 2023-07-10 13:03:28 +01:00
Robert
c25619332c Merge pull request #2569 from github/robertbrignull/convert-ResultsApp
Convert ResultsApp to a function component
2023-07-10 11:36:32 +01:00
Robert
f4b37c96e4 Merge branch 'main' into robertbrignull/convert-ResultsApp 2023-07-10 11:16:57 +01:00
Robert
f7239b073a Include in addModeledMethods whether the methods are unsaved or not 2023-07-10 10:48:33 +01:00
Robert
bf0032d8de Merge pull request #2592 from github/robertbrignull/data-supported-doc
Add documentation to ExternalApiUsage fields
2023-07-10 10:04:52 +01:00
dependabot[bot]
7eeec834ed Bump tough-cookie from 4.1.2 to 4.1.3 in /extensions/ql-vscode
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-09 22:27:45 +00:00
Robert
5eab1f8882 Add documentation to ExternalApiUsage fields 2023-07-07 17:10:06 +01:00
Robert
2525ae80b9 Merge pull request #2591 from github/robertbrignull/data-llm-section
Use AI to model just a single section
2023-07-07 16:16:42 +01:00
Robert
8423c73bdd Merge pull request #2590 from github/robertbrignull/data-button-features
Only show LLM / framework mode when the view state says to show the button
2023-07-07 16:09:10 +01:00
Robert
37980612ac Define separate no-op handler 2023-07-07 15:31:08 +01:00
Robert
c4912b1a65 Show framework mode button when available 2023-07-07 14:28:18 +01:00
Robert
ae3b6eccc3 Only show AI button when available 2023-07-07 14:28:01 +01:00
Robert
48ffca3103 Use AI to model just a single section 2023-07-07 12:37:10 +01:00
Robert
dae74e8772 Merge pull request #2588 from github/robertbrignull/data-save-single-model
Implement saving only one model at a time
2023-07-07 09:50:19 +01:00
Robert
e2dc0d6db5 Merge pull request #2589 from github/robertbrignull/data-title-buttons-background
Use better colours for title buttons
2023-07-07 09:26:03 +01:00
Robert
bf087d2114 Merge branch 'main' into robertbrignull/data-title-buttons-background 2023-07-07 08:41:58 +01:00
Shati Patel
8fab24f424 Merge pull request #2587 from github/shati-patel/unsaved-tag
Data extensions editor: Update styling of "unsaved" tag
2023-07-07 08:39:33 +01:00
Robert
7bda76347c Changes buttons to avoid custom styling 2023-07-06 17:02:01 +01:00
dependabot[bot]
5db2b90212 Bump zip-a-folder from 1.1.3 to 2.0.0 in /extensions/ql-vscode (#2517)
Bumps [zip-a-folder](https://github.com/maugenst/zip-a-folder) from 1.1.3 to 2.0.0.
- [Commits](https://github.com/maugenst/zip-a-folder/commits)

---
updated-dependencies:
- dependency-name: zip-a-folder
  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>
Co-authored-by: Charis Kyriakou <charisk@users.noreply.github.com>
2023-07-06 17:01:50 +01:00
shati-patel
2b13645b6f Move "Save" button slightly to the left 2023-07-06 16:32:34 +01:00
shati-patel
4acc6f9e41 Use native "VS Code Tag" instead of custom span 2023-07-06 16:31:29 +01:00
Robert
e67f93c7bc Implement saving only one model at a time 2023-07-06 16:28:29 +01:00
Robert
261f11e30e Merge pull request #2582 from github/robertbrignull/data-table
Update data extensions modelling table to new designs
2023-07-06 16:27:06 +01:00
Robert
a07e829bf1 Make view link not wrap 2023-07-06 15:25:39 +01:00
Robert
afc9635d43 Use --vscode styles 2023-07-06 15:25:27 +01:00
Robert
7a54b00d29 Merge pull request #2583 from github/robertbrignull/data-unsaved-changes
Show when there are unsaved changes to a model
2023-07-06 12:08:01 +01:00
Robert
903e8c6688 Convert dropdown to a full component 2023-07-06 11:46:57 +01:00
Robert
d5c4f33d6e Use a native select instead of VSCodeDropdown 2023-07-06 11:22:17 +01:00
Robert
7688542aa2 Merge branch 'main' into robertbrignull/data-unsaved-changes 2023-07-06 10:14:15 +01:00
Dave Bartolomeo
614a8d123c Merge pull request #2584 from github/github-action/bump-cli
Bump CLI Version to v2.13.5 for integration tests
2023-07-05 14:33:43 -04:00
github-actions[bot]
e3ff6ace08 Bump CLI version from v2.13.4 to v2.13.5 for integration tests 2023-07-05 15:31:44 +00:00
Robert
fc866acae3 Show when there are unsaved changes to a model 2023-07-05 12:16:40 +01:00
Robert
f75b358e6c Disable buttons instead of hiding them 2023-07-04 17:21:55 +01:00
Robert
e82bfb4153 Pull out booleans controlling visibility of each cell 2023-07-04 17:10:10 +01:00
Charis Kyriakou
7541b64ec8 Move some top level files (#2580)
* Move code-tour.ts to /code-tour

* Move qlpack-generator.ts to /local-queries

* Move query-status.ts to /query-history

* Move skeleton-query-wizard.ts to /local-queries
2023-07-04 16:08:26 +00:00
Robert
12e9de85c7 Merge pull request #2575 from github/robertbrignull/data-editor-story
Use more varied data for the number of usages
2023-07-04 17:01:20 +01:00
Shati Patel
5afdef1ec8 Merge pull request #2581 from github/shati-patel/data-ui-background
Data extensions UI: Change background colour of library container
2023-07-04 17:00:27 +01:00
Robert
870827085d Remove colouring of the methods 2023-07-04 16:57:13 +01:00
Robert
e384f2447c Convert data table to new column layout 2023-07-04 16:57:13 +01:00
Shati Patel
2c5b1da7b2 Merge pull request #2579 from github/shati-patel/bottom-bar
Data extensions UI: Add save button
2023-07-04 16:38:03 +01:00
shati-patel
c7295e66bc Change background colour of library container 2023-07-04 16:35:15 +01:00
Shati Patel
5e49bd5491 Merge pull request #2578 from github/data-ext-divider
Data extensions UI: Add divider between title and status containers
2023-07-04 16:32:28 +01:00
shati-patel
33cb206fed Add save button 2023-07-04 16:07:39 +01:00
shati-patel
882352fcbf Add divider between title and status containers 2023-07-04 16:06:47 +01:00
Robert
f8ad72233a Merge pull request #2574 from github/robertbrignull/data-title
Update data extensions modelling table title bar
2023-07-04 15:57:45 +01:00
Charis Kyriakou
31e94a877d Add code search functionality to test plan (#2577) 2023-07-04 14:47:49 +01:00
Charis Kyriakou
545286b8d9 Add ability to record and replay mock code search requests (#2576) 2023-07-04 14:47:30 +01:00
Robert
c178d251a1 Use more varied data for the number of usages 2023-07-04 11:20:06 +01:00
Robert
97219b9f63 Remove old status info 2023-07-03 16:01:35 +01:00
Robert
15be27c4c3 Add new buttons to title bar 2023-07-03 16:01:34 +01:00
Nora
a3921b7afa Merge pull request #2562 from github/nora/refactor-graph
Convert Graph to a functional component
2023-07-03 15:08:53 +02:00
Nora
9ba5701874 implement merge comments 2023-07-03 11:31:02 +00:00
Robert
4d8694e78b Merge pull request #2571 from github/robertbrignull/AlertTableHeader
Pull out AlertTableHeader to a separate component/file
2023-07-03 12:22:05 +01:00
Robert
e5d4545150 Remove unnecessary function 2023-07-03 10:55:27 +01:00
Robert
0a0b9e590b Merge pull request #2523 from github/robertbrignull/queries-loading
Display different message when queries are loading vs no queries found
2023-07-03 10:52:33 +01:00
Robert
f0af593b67 Simplify comparisons with undefined 2023-07-03 10:51:52 +01:00
Robert
08a8d6396c Remove column arg where it is always "alert-message" 2023-07-03 10:47:02 +01:00
Robert
c876867753 Rename to folder/file items 2023-07-03 09:52:09 +01:00
Robert
eec2f33cba Check for underfined before filtering 2023-07-03 09:44:07 +01:00
Robert
78e794ccdf Convert back to type import 2023-07-03 09:40:05 +01:00
Robert
6acfb8d151 Merge pull request #2570 from github/robertbrignull/AlertTable-rename
Rename PathTable => AlertTable
2023-07-03 09:35:10 +01:00
Dave Bartolomeo
e80a06c5c1 Try grabbing screenshot 2023-06-30 13:36:58 -04:00
Dave Bartolomeo
c70ec7159a Try different default for saveBeforeStart 2023-06-30 10:50:26 -04:00
Dave Bartolomeo
c25410ed5d Log config settings 2023-06-30 10:16:49 -04:00
Dave Bartolomeo
81b2407a47 Log content of untitled docs 2023-06-30 09:57:47 -04:00
Dave Bartolomeo
1f5b3919b0 Dump dirty documents 2023-06-30 09:37:28 -04:00
Dave Bartolomeo
b859bca25f Even more logging 2023-06-30 08:50:06 -04:00
Robert
bfae001b3c Pull out AlertTableHeader to a separate component/file 2023-06-30 10:07:11 +01:00
Robert
2c2b0ecd79 Rename PathTable => AlertTable 2023-06-30 10:01:34 +01:00
Dave Bartolomeo
53a51ab1c9 More logging 2023-06-29 15:33:34 -04:00
Dave Bartolomeo
9f077b0810 Run Jest in verbose mode 2023-06-29 15:14:48 -04:00
Dave Bartolomeo
bf36051054 Increase timeout 2023-06-29 14:52:19 -04:00
Dave Bartolomeo
dbd257e2c0 More logging 2023-06-29 14:39:30 -04:00
Dave Bartolomeo
9fd0697868 Fix format 2023-06-29 12:15:36 -04:00
Dave Bartolomeo
adf0ccb48b Disable workspace trust for CLI integration tests 2023-06-29 12:10:39 -04:00
Robert
3ff649a49a Convert ResultsApp to a function component 2023-06-29 14:49:15 +01:00
Robert
dc5826a848 Rearrange functions so they are defined earlier in the file from where they are called 2023-06-29 14:47:45 +01:00
Anders Starcke Henriksen
4e92688900 Merge pull request #2567 from github/version/bump-to-v1.8.8
Bump version to v1.8.8
2023-06-29 15:30:29 +02:00
github-actions[bot]
08544a4248 Bump version to v1.8.8 2023-06-29 12:22:33 +00:00
Anders Starcke Henriksen
c00adc01f1 Merge pull request #2565 from github/v1.8.7
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
v1.8.7
2023-06-29 13:04:42 +02:00
Anders Starcke Henriksen
65a3ba96c0 v1.8.7 2023-06-29 10:43:05 +02:00
Dave Bartolomeo
d27efb3a17 Merge remote-tracking branch 'origin/main' into dbartol/save-before-start 2023-06-28 20:07:12 -04:00
Dave Bartolomeo
8ff1db13f7 Fix bad diff 2023-06-28 19:55:43 -04:00
Alexander Eyers-Taylor
67d342f2ed Give visibility information to the ide-server. (#2482)
* Add version constraint for Check errors

* Refactor parts of the ideserver out of extension.ts

* Give visibility information to the ide-server.

This allows it to report errors on visible files
eagerly.
2023-06-28 18:21:55 +00:00
Robert
d1838ba0f7 Convert to match style from DB panel 2023-06-28 17:00:44 +01:00
Nora
98d1a24a43 Convert Graph to a functional component 2023-06-28 12:11:55 +00:00
Robert
66d233d669 Merge pull request #2561 from github/enable-a11y-linting-rules
Enable accessibility linting rules and comment out existing violations
2023-06-28 11:54:50 +01:00
Koen Vlaswinkel
a64f44bc41 Merge pull request #2559 from github/koesie10/framework-mode-refresh-button
Add a refresh button to the data extensions editor
2023-06-28 12:38:04 +02:00
Sam Partington
b8b15a53dc Enable accessibility linting rules and comment out existing violations
cf https://github.com/github/code-stats-collector/pull/390 /
https://github.com/github/code-scanning/issues/9347
2023-06-28 10:59:18 +01:00
Robert
6be9e5359c fix typo 2023-06-28 10:37:19 +01:00
Koen Vlaswinkel
266b1e5818 Add a refresh button to the data extensions editor
This adds a refresh button to the data extensions editor when the
framework mode feature flag is enabled. If you are using framework mode,
you can have multiple tabs of the data extensions editor open in which
you are modeling the library separately from the application. When you
save the library in framework mode, the application mode will not
refresh and show that these calls have been modeled. Rather than using
apply, which might also save all modeled methods, you can now use the
refresh button to refresh the external API usages and whether they are
supported.
2023-06-28 11:33:21 +02:00
Koen Vlaswinkel
001179056e Merge pull request #2558 from github/koesie10/exclude-test-methods-in-framework-mode
Exclude methods in tests in framework mode
2023-06-28 11:14:55 +02:00
Koen Vlaswinkel
bcbbb42b41 Exclude methods in tests in framework mode
This excludes methods defined in tests in framework mode, significantly
cutting down on the number of methods shown that would need to be
modeled.

For C#, this just checks that the file is not a test file, as defined
by the QL library.

For Java, this makes a copy of the internal
[`ModelExclusions.qll`](249f9f863d/java/ql/lib/semmle/code/java/dataflow/internal/ModelExclusions.qll)
file to avoid the use of internal modules. This module will tell us
whether a method is "interesting" to model or not. Not all of the checks
in this module need to happen for framework mode, but these checks might
be useful for telling a user whether a method is interesting to model
in application mode.
2023-06-28 10:21:26 +02:00
Koen Vlaswinkel
6465786411 Merge pull request #2557 from github/koesie10/jump-to-def-framework-mode
Add jump to definition in framework mode
2023-06-28 09:49:45 +02:00
Robert
d290b56649 Display custom text message when there are no queries found 2023-06-27 17:04:26 +01:00
Robert
cf49d5dcde Change welcome message to say we're searching for queries 2023-06-27 17:04:26 +01:00
Robert
94fe3e0020 Always trigger pathsChanged listener after initial refresh 2023-06-27 17:04:26 +01:00
Shati Patel
9314b3ba56 Merge pull request #2554 from github/shati-patel/run-all-context-menu
Queries panel: Run all queries in folder (context menu)
2023-06-27 16:51:40 +01:00
Nora
af366afcff Merge pull request #2556 from github/nora/release-run-from-tab
Release run query from file tab icon
2023-06-27 17:46:35 +02:00
Robert
6fe7b82397 Only return something from getPathData once discover has run at least once 2023-06-27 16:20:30 +01:00
Koen Vlaswinkel
1579859c9d Merge pull request #2555 from github/koesie10/nested-name-csharp
Add `nestedName` function for C#
2023-06-27 17:00:23 +02:00
Koen Vlaswinkel
5349a75bd0 Add jump to definition in framework mode
This makes the method name and parameters in framework mode a link to
the definition of the method. In framework mode, the `usages` contains
1 element, which is the location of the definition of the method.
Therefore, we can simply use `jumpToUsage` to jump to the definition.
2023-06-27 16:57:46 +02:00
Nora
46a32081d9 Release run query from file tab 2023-06-27 14:40:48 +00:00
Nora
82977519ce Merge pull request #2552 from github/nora/run-all-queries-panel
Queries Panel: run-all queries of a folders
2023-06-27 16:37:43 +02:00
Nora
32555cc4f2 Merge pull request #2548 from github/dependabot/npm_and_yarn/extensions/ql-vscode/semver-7.5.2
Bump semver from 7.3.7 to 7.5.2 in /extensions/ql-vscode
2023-06-27 16:33:16 +02:00
Koen Vlaswinkel
4b8cdf872a Merge pull request #2553 from github/koesie10/nested-name
Use `nestedName` for getting the name of an API
2023-06-27 16:17:43 +02:00
Koen Vlaswinkel
8af0ba7411 Add nestedName function for C#
Similar to https://github.com/github/vscode-codeql/pull/2553, this
changes the C# query to correctly report the name of nested types. I
couldn't find a `nestedName` method for C#, so this adds one in the
`AutomodelVsCode` library.

C# seems to use `+` as a separator for nested types, as reported by
`getQualifiedName()`:

```
GitHub.Nested.MyFirstClass+NestedClass
```

The `getApiName()` will now report:

```
GitHub.Nested#MyFirstClass+NestedClass.Test()
```
2023-06-27 16:14:42 +02:00
shati-patel
02b356cf86 Queries panel: Run all queries in folder (context menu)
Adds a command to run all queries in a certain folder.
This uses the existing `runQueries` command, which lets you run multiple queries against the selected local database.
We don't have a corresponding command for running multiple variant analysis queries, so I haven't implemented that.
2023-06-27 15:03:23 +01:00
Koen Vlaswinkel
3ad3644219 Merge pull request #2542 from github/koesie10/override-extension-directory
Allow overriding the data extensions editor extensions directory
2023-06-27 15:13:59 +02:00
Koen Vlaswinkel
77495df97d Merge pull request #2550 from github/koesie10/refactor-duplication-queries
Refactor data extensions editor queries to reduce duplication
2023-06-27 15:13:50 +02:00
Nora
a591c82b3c Add run all on folders 2023-06-27 15:08:30 +02:00
Shati Patel
ee68156574 Merge pull request #2547 from github/shati-patel/run-query-context-menu-remote
Run variant analysis from Queries Panel context menu
2023-06-27 13:38:25 +01:00
Koen Vlaswinkel
a053792d6e Use nestedName for getting the name of an API
This changes the Java `CallableMethod.getApiName()` to use `nestedName`
instead of `getSourceDeclaration`. `getSourceDeclaration` would return a
`RefType`, on which the `toString()` method returns its `getName`().
However, for nested types this wouldn't work and wouldn't include the
enclosing type. This fixes it by using `nestedName` which matches the
method that is also used for determining whether a type matches an
extensible predicate.
2023-06-27 14:38:19 +02:00
Alexander Eyers-Taylor
b0699ee524 Add quick eval count to the command palette (#2475)
* Add version constraint for quick-eval-count

* Add quick eval count context.

* Add support for running quick-eval-count from the command pallete

* Adjust name for quick-eval-count-queries

* Add changenote for quick-eval-count.

* QuickEval:Address review comments

* Fix rebase conflict in changelog
2023-06-27 11:28:28 +01:00
Koen Vlaswinkel
bd0e5604a8 Fix incorrect JAR name being displayed in application mode 2023-06-27 11:22:03 +02:00
Nora
2a332f90c4 Merge pull request #2551 from github/nora/fix-merge-conflict-mistake
Fix merge conflict error: Re-add icon
2023-06-27 10:46:00 +02:00
Shati Patel
7b73ff4231 Merge pull request #2546 from github/shati-patel/run-query-context-menu-local
Run local query from Queries Panel context menu
2023-06-26 17:08:42 +01:00
Shati Patel
0d0ae6449f Merge branch 'main' into shati-patel/run-query-context-menu-local 2023-06-26 16:46:24 +01:00
Nora
3c156b858c Re-add icon 2023-06-26 14:54:17 +00:00
Koen Vlaswinkel
7e8578a22c Refactor data extensions editor queries to reduce duplication
This refactors the data extensions editor queries to use a new
`AutomodelVsCode` module. This module is based on the `ExternalApi`
module, but is more general and can be used for retrieving public
methods from the source as well. The actual conditions are now in the
queries themselves.

This reduces the duplicated module in the framework mode query and will
mean that when we update the `ExternalApi` module, we will just have to
port it to the `AutomodelVsCode` module, and not to the `ExternalApi`
and a separate framework mode query.
2023-06-26 16:33:05 +02:00
Koen Vlaswinkel
aa4d3f4399 Merge pull request #2549 from github/koesie10/consistent-sorting
Sort methods sent to LLM the same way as the UI
2023-06-26 16:11:38 +02:00
Nora
75d2f76658 Merge pull request #2544 from github/nora/queries-panel-file-tab
Queries Panel: run local query from file tab
2023-06-26 16:09:03 +02:00
Robert
75cffd50b1 Merge pull request #2539 from github/robertbrignull/raw-results-react
Convert RawTable to a function component
2023-06-26 14:23:19 +01:00
Nora
10d8bbfe63 Add run icon to file tab 2023-06-26 14:48:48 +02:00
Koen Vlaswinkel
90c8391fea Sort methods sent to LLM the same way as the UI
This changes the sorting of the methods sent to LLM to match the order
shown in the data extensions editor. This will ensure that the methods
which are shown first in the data extensions editor are also modeled
first.
2023-06-26 14:26:31 +02:00
Koen Vlaswinkel
a8aee6a8e1 Extract sorting to separate functions 2023-06-26 14:08:46 +02:00
dependabot[bot]
d41e9ef163 Bump semver from 7.3.7 to 7.5.2 in /extensions/ql-vscode
Bumps [semver](https://github.com/npm/node-semver) from 7.3.7 to 7.5.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.3.7...v7.5.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 11:57:15 +00:00
Nora
13a5b7854f Merge pull request #2534 from github/nora/run-local-query-panel
Queries Panel: run local query from panel
2023-06-26 13:56:08 +02:00
shati-patel
3a3264302a Run variant analysis from Queries Panel context menu 2023-06-26 11:53:01 +01:00
shati-patel
9704b498fe Run local query from Queries Panel context menu 2023-06-26 10:51:18 +01:00
Nora
2b48991494 Rename command 2023-06-26 09:04:06 +00:00
Nora
ff41e50954 Show run button only when db is selected 2023-06-26 09:04:06 +00:00
Nora
24683f34de Adjust command lint tests 2023-06-26 09:04:06 +00:00
Nora
7db84b0276 Always show run button 2023-06-26 09:04:06 +00:00
Nora
655294db06 Use only one command 2023-06-26 09:04:06 +00:00
Nora
5845e9e59e Merge comments and mock command 2023-06-26 09:04:06 +00:00
Nora
c0c42d36b9 Use corret Uri method and extract feature flag guard 2023-06-26 09:04:06 +00:00
Nora
2898acd67f Hide new command behind feature flag 2023-06-26 09:04:06 +00:00
Robert
7409fe8a56 rename commands to match linter 2023-06-26 09:04:06 +00:00
Robert
f25d7baa56 Expose QueryServerCommandManager from app 2023-06-26 09:04:06 +00:00
Nora
3f1b619904 Create new runQuery command for the queries panel 2023-06-26 09:04:06 +00:00
Nora
12c0c57c25 hook up new command and make runQuery executable 2023-06-26 09:04:06 +00:00
Nora
c78db22599 Hide run button when no local database is selected 2023-06-26 09:04:06 +00:00
Nora
fea0c3ce46 Add icon 2023-06-26 09:04:06 +00:00
Robert
0e033b48d4 Avoid creating and throwing away Scroller each render 2023-06-26 10:03:15 +01:00
Koen Vlaswinkel
971d1461c8 Merge pull request #2543 from github/koesie10/exclude-anonymous-classes
Exclude methods from anonymous classes in framework mode
2023-06-26 10:00:04 +02:00
Koen Vlaswinkel
a76bd4627c Merge pull request #2535 from github/koesie10/framework-mode
Add initial implementation of framework mode
2023-06-23 13:57:09 +02:00
Koen Vlaswinkel
6e16f826fb Exclude methods from anonymous classes in framework mode 2023-06-23 13:45:06 +02:00
Koen Vlaswinkel
4f367119cb Fix data extensions editor story 2023-06-23 11:59:26 +02:00
Koen Vlaswinkel
01da0f1d34 Fix incorrect comment on method 2023-06-23 11:58:43 +02:00
Koen Vlaswinkel
aec5ff3902 Allow overriding the extensions directory
This will allow users to override the default directory in which
extension packs are created by using the
`codeQL.dataExtensions.extensionsDirectory` setting. This setting can be
overriden per language, so the user could create the following
configuration to set the extension pack setting for Java only:

```json
"[java]": {
  "codeQL.dataExtensions.extensionsDirectory": "/Users/user/github/vscode-codeql-starter/codeql-custom-queries-java",
}
```
2023-06-23 11:37:58 +02:00
Koen Vlaswinkel
f90d538743 Add feature flag for framework mode 2023-06-23 11:12:28 +02:00
Koen Vlaswinkel
72a91efde9 Add tests for framework mode YAML generation 2023-06-23 10:45:12 +02:00
Koen Vlaswinkel
8c36e572cb Create model filenames for framework mode 2023-06-23 10:04:37 +02:00
Koen Vlaswinkel
2351346440 Change data flow generator for framework mode 2023-06-23 09:53:53 +02:00
Koen Vlaswinkel
d26d886d09 Add grouping by package and remove usages for framework mode 2023-06-23 09:53:53 +02:00
Koen Vlaswinkel
48b78c1ac1 Change tests to include framework mode queries 2023-06-23 09:53:53 +02:00
Koen Vlaswinkel
526e7474a5 Add switching of mode 2023-06-23 09:53:52 +02:00
Koen Vlaswinkel
31e1bef548 Add framework mode queries 2023-06-23 09:53:01 +02:00
Koen Vlaswinkel
82cdf03d8c Merge pull request #2521 from github/koesie10/auto-create-model-files
Automatically create different model files per library
2023-06-23 09:52:39 +02:00
Dave Bartolomeo
d9a1aa8a23 Just kidding, _this_ time it's fixed for reals 2023-06-22 14:40:26 -04:00
Dave Bartolomeo
3705464766 Seriously, I think they'll pass for reals this time. 2023-06-22 14:32:25 -04:00
Dave Bartolomeo
ff2d67d930 Merge remote-tracking branch 'origin/main' into dbartol/save-before-start 2023-06-22 17:32:05 +00:00
Dave Bartolomeo
ec71f53e38 Fix more test failures 2023-06-22 13:31:12 -04:00
Robert
fa23441efb Merge pull request #2541 from github/robertbrignull/move-pure-4
Move tests out of pure directory
2023-06-22 17:08:58 +01:00
Dave Bartolomeo
6bdc095d27 Fix test failure 2023-06-22 11:45:54 -04:00
Robert
ecfa0ff5b9 Convert dataRows to a useMemo 2023-06-22 16:45:23 +01:00
Robert
3957d356f0 Import react hooks explicitly 2023-06-22 16:45:23 +01:00
Robert
f08ef1b742 Use useRef instead of useMemo for scroller 2023-06-22 16:45:21 +01:00
Robert
e29bfc83c8 Use useEffect instead of calling directly every time 2023-06-22 16:20:32 +01:00
Robert
8b95320ba8 Move tests out of pure directory 2023-06-22 15:19:47 +01:00
Robert
480bd48a8d Merge pull request #2533 from github/robertbrignull/assert-pure
Update assert-pure.ql now that the /pure directory has been removed
2023-06-22 15:19:25 +01:00
Robert
1499d909c8 Merge pull request #2537 from github/robertbrignull/move-pure-3
Move files from /pure to the /common (and other) directories (part 3 of 3)
2023-06-22 15:15:31 +01:00
Dave Bartolomeo
2397ead586 Fix test failure 2023-06-22 10:02:10 -04:00
Koen Vlaswinkel
b42457c50b Merge pull request #2540 from github/koesie10/update-external-api-qll
Update ExternalApi.qll dependencies
2023-06-22 15:21:10 +02:00
Robert
f5fef92f0f Move variant-analysis-filter-sort to shared directory 2023-06-22 13:42:12 +01:00
Robert
fc36eaab4b Move variant-analysis-filter-sort.ts to /variant-analysis 2023-06-22 13:37:13 +01:00
Robert
08fdddeefc Move result-keys.ts to /view/results 2023-06-22 13:37:13 +01:00
Robert
8e58854302 Move log-summary-parser.ts to /log-insights 2023-06-22 13:37:13 +01:00
Robert
1750594d11 Move new-messages.ts to /query-server 2023-06-22 13:37:10 +01:00
Koen Vlaswinkel
2e7c86d107 Update ExternalApi.qll dependencies 2023-06-22 14:33:28 +02:00
Robert
6143bd30d8 Convert RawTable to a functional component 2023-06-22 12:41:23 +01:00
Dave Bartolomeo
0bd0bf1944 _Correctly_ emulate VS Code's saveBeforeStart 2023-06-22 07:30:53 -04:00
Robert
cd69e5934b Merge pull request #2536 from github/robertbrignull/throw-instead-of-fail
Throw error instead of using fail method
2023-06-22 11:48:49 +01:00
Koen Vlaswinkel
669f4a6430 Add .model suffix to model filenames 2023-06-22 11:44:20 +02:00
Koen Vlaswinkel
b7b4302c1e Automatically create different model files per library
This will remove the user input for a model file and will instead create
1 model file per library (JAR/DLL). The model filename will be based on
the JAR/DLL name, but will remove the version number and the file
extension. It will also normalize the name.

These files will be created automatically, and the editor now also reads
in all files contained in an extension pack to read the modeled methods.
This could result in duplicates if the user has created a different file
to contain the same modeled methods, but this is an edge-case that we're
explicitly not handling.
2023-06-22 11:44:18 +02:00
Koen Vlaswinkel
100b557823 Merge pull request #2520 from github/koesie10/auto-name-extension-pack
Automatically name extension packs
2023-06-22 11:34:37 +02:00
Koen Vlaswinkel
7249f4c343 Add comment to explain heuristic 2023-06-22 10:08:11 +02:00
Koen Vlaswinkel
e4e849d14c Merge remote-tracking branch 'origin/main' into koesie10/auto-name-extension-pack 2023-06-22 10:01:51 +02:00
Dave Bartolomeo
b182d7afef Fix PR feedback 2023-06-21 17:20:32 -04:00
Dave Bartolomeo
1da96c5a55 Allow languageId: scopes in tests 2023-06-21 17:01:13 -04:00
Dave Bartolomeo
de38b1fd20 Stop overriding saveBeforeStart for ql language 2023-06-21 14:27:57 -04:00
Dave Bartolomeo
7a46bac078 Save dirty documents before evaluating queries 2023-06-21 18:12:42 +00:00
Robert
f8b0583c5f Move messages-shared.ts to /query-server 2023-06-21 16:31:57 +01:00
Robert
b0e6478bfe Move legacy-messages.ts to /query-server 2023-06-21 16:31:57 +01:00
Robert
0031c1acc0 Throw error instead of using fail method 2023-06-21 16:02:57 +01:00
Robert
0451dd8d1b Merge pull request #2532 from github/robertbrignull/move-pure-2
Move files from /pure to the /common directory (part 2 of 3)
2023-06-21 13:15:38 +01:00
Robert
8559d3baa0 Update assert-pure.ql now that the /pure directory has been removed 2023-06-21 12:33:32 +01:00
Robert
f12b62fa9d Move zip.ts to /common 2023-06-21 12:19:35 +01:00
Robert
90e94e04fc Move word.ts to /common 2023-06-21 12:19:35 +01:00
Robert
236a317fa0 Move time.ts to /common 2023-06-21 12:19:35 +01:00
Robert
1bf14e393f Move text-utils.ts to /common 2023-06-21 12:19:35 +01:00
Robert
655adfcd51 Move sarif-utils.ts to /common 2023-06-21 12:19:35 +01:00
Robert
b11a675004 Move ql.ts to /common 2023-06-21 12:19:35 +01:00
Robert
855cac628b Move number.ts to /common 2023-06-21 12:19:35 +01:00
Robert
5b168dfb7e Move location-link-utils.ts to /common 2023-06-21 12:19:35 +01:00
Robert
704ebf1ff6 Move interface-types.ts to /common 2023-06-21 12:19:35 +01:00
Robert
9903982bb1 Merge pull request #2530 from github/robertbrignull/move-pure-1
Move files from /pure to the /common directory (part 1 of 3)
2023-06-21 11:54:11 +01:00
Robert
d0df2966c5 Merge branch 'main' into robertbrignull/move-pure-1 2023-06-21 11:23:42 +01:00
Koen Vlaswinkel
7f9208f1e1 Merge pull request #2531 from github/koesie10/remove-common-index
Remove `common/index.ts` and vscode from `common/logging`
2023-06-21 12:11:19 +02:00
Robert
e92b676820 Merge branch 'main' into robertbrignull/move-pure-1 2023-06-21 10:37:39 +01:00
Robert
a054290c50 Merge pull request #2529 from github/robertbrignull/queries-panel-errors
Ensure errors for one path don't stop discovery of other paths
2023-06-21 10:09:45 +01:00
Koen Vlaswinkel
eeb867624e Remove common/index.ts and vscode from common/logging
The `logging` part of `common` was exported in `common/index.ts` and
could be imported by importing `common`. I don't think this makes a lot
of sense, so I removed it.

The `common/logging/index.ts` also contained exports of the
`common/logging/vscode` directory, which means that importing
`common/logging` automatically brings in the `vscode` module. This
removes that export, so now there are two separate imports needed for
importing the common part and the `vscode` specific part. This should
make it easier to keep them separate and be more explicit about what
you're importing.
2023-06-21 10:20:19 +02:00
Robert
2813576f07 Move helpers-pure.ts to /common 2023-06-21 09:10:26 +01:00
Robert
1ced7a90fd Move files.ts to /common 2023-06-21 09:06:38 +01:00
Robert
4cbd0b7fb8 Move errors.ts to /common 2023-06-21 09:03:16 +01:00
Robert
af97077095 Move distribution.ts to /common 2023-06-21 08:59:51 +01:00
Robert
a5aa0c4cf3 Move disposable-object.ts to /common 2023-06-21 08:56:59 +01:00
Koen Vlaswinkel
d092e69abf Add detection of root workspace directory using .git folder 2023-06-21 09:55:29 +02:00
Robert
7cab02de60 Move date.ts to /common 2023-06-21 08:53:51 +01:00
Robert
dc91028cee Move bqrs-utils.ts to /common 2023-06-21 08:50:39 +01:00
Robert
f228ec9645 Move bqrs-cli-types.ts to /common 2023-06-21 08:50:35 +01:00
Koen Vlaswinkel
f32a240e24 Exclude workspace folders in the system temp dir 2023-06-21 09:44:39 +02:00
Robert
7135d39aad Log error message 2023-06-21 08:24:22 +01:00
Shati Patel
c628454e25 Allow queries panel to be visible in non-dev mode too (#2528) 2023-06-20 16:46:31 +01:00
Robert
fa773a0029 Ensure errors for one path don't stop discovery of other paths 2023-06-20 16:21:20 +01:00
Koen Vlaswinkel
2c97ca95aa Merge pull request #2527 from github/koesie10/replace-faker-uuid
Replace deprecated faker.datatype.uuid by faker.string.uuid
2023-06-20 16:50:30 +02:00
Koen Vlaswinkel
d3a179744e Merge branch 'main' into koesie10/replace-faker-uuid 2023-06-20 16:22:51 +02:00
Koen Vlaswinkel
8fb1229c49 Merge pull request #2524 from github/koesie10/grouping-improvements
Improve grouping of libraries
2023-06-20 15:22:33 +02:00
Robert
23173bf441 Merge pull request #2525 from github/robertbrignull/sendRequest_progress
Remove ProgressCallback / CancellationToken arguments where they aren't used
2023-06-20 14:16:36 +01:00
Koen Vlaswinkel
1cc6aa5303 Merge branch 'main' into koesie10/auto-name-extension-pack 2023-06-20 15:12:15 +02:00
Koen Vlaswinkel
2800ccb74c Add extra sorting parameters for libraries 2023-06-20 14:24:56 +02:00
Robert
3685575c11 Merge branch 'main' into robertbrignull/sendRequest_progress 2023-06-20 13:24:30 +01:00
github-actions[bot]
c40be89636 Bump CLI version from v2.13.3 to v2.13.4 for integration tests (#2526)
Co-authored-by: github-actions[bot] <github-actions@github.com>
2023-06-20 11:28:25 +01:00
Robert
f99957435d Fix expected args in tests 2023-06-20 11:09:13 +01:00
Koen Vlaswinkel
ff491bb706 Replace deprecated faker.datatype.uuid by faker.string.uuid 2023-06-20 11:48:31 +02:00
Koen Vlaswinkel
cfc66a4e17 Store extension packs in .github/codeql/extensions 2023-06-20 11:16:23 +02:00
Robert
4d8506b3f5 Add back in manual progress update 2023-06-20 09:42:59 +01:00
Koen Vlaswinkel
ab6db71727 Move workspace folder functions to separate file 2023-06-20 10:06:52 +02:00
Koen Vlaswinkel
ddd97f08a3 Add chevron to show whether row is expanded 2023-06-20 09:32:57 +02:00
Koen Vlaswinkel
32d8968c56 Use pluralize function for pluralization 2023-06-20 09:32:57 +02:00
Koen Vlaswinkel
768c10734e Make libraries collapsible 2023-06-20 09:32:57 +02:00
Koen Vlaswinkel
a833f78151 Sort libraries by supported and usages 2023-06-20 09:32:57 +02:00
Robert
c93449ab9f Remove ProgressCallback / CancellationToken arguments where they aren't used 2023-06-19 17:13:40 +01:00
Robert
d8c3410641 Merge pull request #2490 from github/robertbrignull/resolve-queries-manual
Add manual discovery of queries and query packs
2023-06-19 14:40:50 +01:00
Robert
d2b69b1316 Normalize tmp path to ensure correct comparrison on windows 2023-06-19 14:13:09 +01:00
Koen Vlaswinkel
e83ad364f5 Merge pull request #2522 from github/koesie10/group-by-library
Group modeled methods by library in the data extensions editor
2023-06-19 15:01:19 +02:00
Koen Vlaswinkel
fe29a1a32a Add more comments 2023-06-19 13:54:17 +02:00
Koen Vlaswinkel
3323fd4e3b Add more tests for auto pack naming 2023-06-19 13:51:24 +02:00
Koen Vlaswinkel
3c60708b55 Separate pack naming and create interface 2023-06-19 13:47:27 +02:00
Koen Vlaswinkel
8980aabbfc Split flows for checking existing extension pack 2023-06-19 13:34:35 +02:00
Robert
a30ec907d0 Add tiny sleep to fix tests 2023-06-19 11:32:55 +01:00
Robert
96bb7058a2 Make note about NodeJS.ErrnoException 2023-06-19 11:09:08 +01:00
Koen Vlaswinkel
5dcadd2f1f Merge pull request #2519 from github/koesie10/add-library-to-data-extensions-editor
Add library to data extensions editor
2023-06-19 12:07:34 +02:00
Robert
1f18cc3f2c Use Readonly and ReadonlyArray 2023-06-19 10:58:26 +01:00
Robert
989ef8b681 Fix typo 2023-06-19 10:57:20 +01:00
Robert
70681253eb Only expose event to subclasses, instead of event emitter 2023-06-19 10:57:10 +01:00
Koen Vlaswinkel
bbc39b060f Remove library column 2023-06-19 11:52:09 +02:00
Koen Vlaswinkel
590e908886 Group modeled methods by library 2023-06-19 11:39:47 +02:00
Koen Vlaswinkel
487c0a66f4 Create ModeledMethodDataGrid component 2023-06-19 11:33:45 +02:00
Koen Vlaswinkel
23745ba93f Change jar to library for Java query 2023-06-19 11:25:40 +02:00
Robert
af62a92c5b Make pathData private to FilePathDiscovery 2023-06-16 16:40:11 +01:00
Robert
da92a67834 Introduce recomputeAllData to avoid mutating pathData from outside of FilePathDiscovery 2023-06-16 16:35:58 +01:00
Robert
c6a7e1fb3c Rename from path to pathData where appropriate 2023-06-16 16:23:39 +01:00
Robert
d626cea837 Use getOnDiskWorkspaceFolders instead of getOnDiskWorkspaceFoldersObjects when all we need is the path 2023-06-16 16:14:08 +01:00
Robert
bdea0c2c20 Use one lstat call instead of calling exists first 2023-06-16 16:12:29 +01:00
Robert
44327cac23 Use sets instead of custom expectArraysEqual 2023-06-16 15:23:32 +01:00
Koen Vlaswinkel
5d83ac84e3 Fix tests on Windows 2023-06-16 16:01:30 +02:00
Robert
3a0aaa0ae9 Use jest.SpiedFunction instead of jest.SpyInstance 2023-06-16 12:35:59 +01:00
Robert
18e7431a44 Make conditional easier to read 2023-06-16 12:33:21 +01:00
Koen Vlaswinkel
549884d507 Automatically name extension packs
This will change how extension packs are named in the data extensions
editor. Before, the user had to pick a workspace folder and a name for
the extension pack. Now, the workspace folder will be picked
automatically if we can detect it (i.e. it follows the naming structure
we expect), or the user will still need to select it. The extension pack
name is always auto-generated based on the database name and the
database language.

This adds a new `codeQL.dataExtensions.disableAutoNameExtensionPack`
setting to disable this behavior while we are still working on changing
how the data extensions editor works.
2023-06-16 13:31:31 +02:00
Robert
6504e46011 Invert condition to reduce nesting 2023-06-16 12:31:12 +01:00
Robert
ce6a21c65a Use pathExists instead of exists 2023-06-16 12:21:36 +01:00
Robert
fce27d02dc Use a plain Set intead of custom FilePathSet 2023-06-16 11:29:38 +01:00
Robert
f7a72c6d45 Use QueryLanguage where possible 2023-06-16 11:29:28 +01:00
Koen Vlaswinkel
55d1f14ac4 Add library to data extensions editor
This adds a new library column to the data extensions editor containing
the JAR or DLL file the method is defined in. This will be used to group
methods by library in the future. For now, it just shows in a column.
2023-06-16 12:01:30 +02:00
Robert
959c3fbcb8 Fix typos 2023-06-16 10:47:07 +01:00
Koen Vlaswinkel
0f9d127b4c Merge pull request #2513 from github/koesie10/show-telemetry-without-vscode
Make the `showAndLogExceptionWithTelemetry` function work without the `vscode` module
2023-06-16 09:39:12 +02:00
Andrew Eisenberg
f9a415c377 Merge pull request #2516 from github/aeisenberg/modernize
Modernize query packs
2023-06-15 08:01:22 -07:00
Koen Vlaswinkel
539284b902 Merge pull request #2511 from github/koesie10/show-and-log-without-vscode
Make the showAndLog family of functions usable without the `vscode` module
2023-06-15 09:29:18 +02:00
Andrew Eisenberg
244bc3bdab Modernize query packs
Remove legacy `libraryPathDependencies`. We are making some
changes internally that will cause legacy packs with lock
files to throw an error.
2023-06-14 20:56:45 +00:00
Robert
5cbb7b49d7 Update QueryDiscovery to use FilePathDiscovery and QueryPackDiscovery 2023-06-14 17:30:40 +01:00
Robert
a9d59aecb8 Add QueryPackDiscovery 2023-06-14 17:30:37 +01:00
Robert
17b5e000f8 Add FilePathDiscovery 2023-06-14 17:30:33 +01:00
Robert
790c33c661 Add silent arg to resolveLibraryPath 2023-06-14 16:09:42 +01:00
Robert
51b94e3fed Add FilePathSet 2023-06-14 16:09:42 +01:00
Robert
b0441956df Remove the update method from the Discovery class
See https://github.com/github/vscode-codeql/pull/2490#discussion_r1226437598
for more explanation. This will make the class more useful for future usecases
where we don't want the behaviour of only calling update when there isn't
another refresh scheduled. I also think it doesn't negatively affect other
users such as the query test discovery. The effect should be that we'll see
more updates to the UI. These updates will get overwritten quickly, but they
are all genuine snapshots of the filesystem at the point the discovery process
ran, so they aren't incorrect, or aren't more incorrect than continuing to show
the old state before any discovery ran.
2023-06-14 16:09:42 +01:00
Koen Vlaswinkel
8803433fa4 Merge pull request #2515 from github/version/bump-to-v1.8.7
Bump version to v1.8.7
2023-06-14 14:53:24 +02:00
github-actions[bot]
ab448e51d5 Bump version to v1.8.7 2023-06-14 12:20:00 +00:00
Koen Vlaswinkel
2905f5340a Merge pull request #2514 from github/v1.8.6
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
v1.8.6
2023-06-14 13:51:21 +02:00
Koen Vlaswinkel
170fce8815 v1.8.6 2023-06-14 13:17:03 +02:00
Koen Vlaswinkel
7a76e20841 Move showAndLogExceptionWithTelemetry out of vscode directory 2023-06-14 11:59:14 +02:00
Koen Vlaswinkel
d03d355513 Add telemetry as argument to showAndLogExceptionWithTelemetry 2023-06-14 11:19:50 +02:00
Koen Vlaswinkel
959728d1ca Add telemetry to App 2023-06-14 11:19:46 +02:00
Koen Vlaswinkel
fefb2f6694 Add AppTelemetry interface 2023-06-14 11:19:16 +02:00
Koen Vlaswinkel
08786055e3 Move telemetry to common/vscode 2023-06-14 11:19:10 +02:00
Koen Vlaswinkel
108d5268b0 Fix tests mocking showAndLog functions 2023-06-14 11:17:49 +02:00
Koen Vlaswinkel
fcbe3bea1e Use app logger instead of extLogger where possible
To increase the use of the `app` logger, this replaces the direct use of
`extLogger` by the `app.logger` where possible. This should not change
the behavior since the `extLogger` is the logger used by the `app`.
2023-06-14 11:17:37 +02:00
Koen Vlaswinkel
2b85690c68 Change logger on app to NotificationLogger 2023-06-14 11:17:18 +02:00
Koen Vlaswinkel
67c081921b Move show and log functions to common
This moves the `showAndLog` family of functions to the `common/logging`
directory. It explicitly moves the `showAndLogExceptionWithTelemetry`
function to the `common/vscode/logging.ts` file because it still has a
dependency on the `telemetryListener`, which depends on the `vscode`
module.
2023-06-14 11:17:17 +02:00
Koen Vlaswinkel
9ff2d568c8 Change showAndLog functions to take NotificationLogger 2023-06-14 11:16:52 +02:00
Koen Vlaswinkel
d54ee0c0e5 Merge pull request #2508 from github/koesie10/split-helpers-4
Split helpers.ts [Part 4]
2023-06-14 11:15:04 +02:00
Nora
fa7d85ea58 Merge pull request #2512 from github/nora/remove-code-search-flag
Remove code search feature flag
2023-06-14 10:41:19 +02:00
Koen Vlaswinkel
179942680e Merge pull request #2506 from github/koesie10/split-helpers-3
Move show and log functions out of helpers
2023-06-14 10:28:43 +02:00
Koen Vlaswinkel
f67f53dd68 Merge remote-tracking branch 'origin/main' into koesie10/split-helpers-3 2023-06-14 09:30:41 +02:00
Koen Vlaswinkel
c6c56284ff Merge pull request #2505 from github/koesie10/split-helpers-2
Split `helpers.ts` [Part 2]
2023-06-14 09:29:58 +02:00
Nora
afa2f426b8 Update Changelog 2023-06-13 14:55:14 +00:00
Nora
fd381640a0 Remove feature flag 2023-06-13 14:55:02 +00:00
Robert
019e3772ef Merge pull request #2510 from github/robertbrignull/use_contains_path
Use containsPath where possible
2023-06-13 10:19:01 +01:00
Nora
a03b3dca86 Merge pull request #2507 from github/nora/remove-1000-list-limit
Remove limit of 1000 repos per list
2023-06-13 09:57:16 +02:00
Robert
cad651d6bd Use containsPath where possible 2023-06-12 16:33:35 +01:00
Robert
400bde6e03 Merge pull request #2509 from github/robertbrignull/contains_path
Fix containsPath for paths that share a prefix
2023-06-12 16:17:39 +01:00
Robert
3a9fa42790 Merge branch 'main' into robertbrignull/contait push origin robertbrignull/contains_pathns_path 2023-06-12 15:39:47 +01:00
Robert
c920b7e49e Remove explicit check for windows 2023-06-12 15:16:43 +01:00
Koen Vlaswinkel
ccf38a98fb Move incorrectly named helpers.test.ts 2023-06-12 15:55:10 +02:00
Koen Vlaswinkel
00240e56f4 Rename helpers.ts to tmp-dir.ts 2023-06-12 15:55:10 +02:00
Koen Vlaswinkel
1096ed8bf5 Move upgradesTmpDir out of helpers 2023-06-12 15:55:10 +02:00
Koen Vlaswinkel
61ac19f715 Move createTimestampFile out of helpers 2023-06-12 15:55:09 +02:00
Koen Vlaswinkel
9a32556b4d Move query language functions out of helpers 2023-06-12 15:55:09 +02:00
Koen Vlaswinkel
2cd88cecde Move tryGetQueryMetadata out of helpers 2023-06-12 15:55:09 +02:00
Robert
6dbbd22c0a Merge pull request #2497 from github/robertbrignull/cleanup_workspace_folders
Remove workspaceFolders from app because it turned out not to be useful
2023-06-12 14:50:00 +01:00
Robert
aa4c459cdd Use relative instead of startsWith to handle paths with the same prefix 2023-06-12 14:41:31 +01:00
Robert
f7c1f06354 Add another test case for case insensitivity on windows 2023-06-12 14:41:31 +01:00
Robert
6e3d0147c9 Add test cases for paths with the same prefix 2023-06-12 14:41:31 +01:00
Robert
300503e1c9 Remove platform arg as it's never not the process.platform 2023-06-12 14:41:31 +01:00
Robert
bdd2319297 Update docs to mention about paths being equal 2023-06-12 14:41:21 +01:00
Nora
4c16888624 Remove limit of 1000 to repo lists 2023-06-12 12:46:40 +00:00
Koen Vlaswinkel
d71f210647 Merge pull request #2504 from github/koesie10/split-helpers
Split helpers.ts file
2023-06-12 14:42:32 +02:00
Koen Vlaswinkel
c16d363b08 Move show and log functions out of helpers 2023-06-12 14:24:04 +02:00
Robert
8b1e49c6c0 Use more descriptive names 2023-06-12 12:50:01 +01:00
Koen Vlaswinkel
50f958067c Move prepareCodeTour to separate file 2023-06-12 12:12:18 +02:00
Koen Vlaswinkel
7f3e9607aa Move dialog functions to separate file 2023-06-12 12:09:36 +02:00
Koen Vlaswinkel
0cfbf0cb2a Move workspace folder functions to separate file 2023-06-12 12:02:45 +02:00
Koen Vlaswinkel
bfead07592 Move walkDirectory to pure files file 2023-06-12 11:13:57 +02:00
Koen Vlaswinkel
8c98401efe Move isQueryLanguage to query language file 2023-06-12 11:11:47 +02:00
Koen Vlaswinkel
a4e4c67bf2 Move getInitialQueryContents to separate file 2023-06-12 11:10:23 +02:00
Koen Vlaswinkel
754fa675f9 Move db contents heuristics to separate file 2023-06-12 11:07:25 +02:00
Koen Vlaswinkel
b0c18b3300 Move unused languageToDbScheme 2023-06-12 11:01:42 +02:00
Koen Vlaswinkel
706c6b8a7a Move qlpacks helpers to separate file 2023-06-12 10:59:42 +02:00
Nora
fe21a21ca2 Merge pull request #2501 from github/nora/polish-code-search-copy
Code search: Use correct link and text
2023-06-12 07:44:25 +02:00
Koen Vlaswinkel
bca8e8fdb9 Merge pull request #2503 from github/koesie10/move-interface
Move interface.ts to local-queries/results-view.ts
2023-06-09 17:11:51 +02:00
Koen Vlaswinkel
5259456fe8 Merge pull request #2502 from github/koesie10/move-interface-utils
Move interface-utils to local-queries/webview
2023-06-09 17:11:39 +02:00
Robert
adc64c37c5 Merge pull request #2498 from github/robertbrignull/queries-panel-codeowners
Mark the queries panel as owned by secexp
2023-06-09 15:27:29 +01:00
Koen Vlaswinkel
2f1a3e95bf Move interface.ts to local-queries/results-view.ts 2023-06-09 16:24:32 +02:00
Koen Vlaswinkel
00b32376d5 Move interface-utils to local-queries/webview 2023-06-09 16:20:30 +02:00
Koen Vlaswinkel
dfef8104c8 Merge pull request #2485 from github/koesie10/move-location-utils
Move functions for resolving locations
2023-06-09 15:59:48 +02:00
Nora
f0a8f79c2e Use correct link and test 2023-06-09 13:25:39 +00:00
Charis Kyriakou
d485ff0015 Deal with no predictions gracefully (#2499) 2023-06-09 13:17:26 +01:00
Robert
7b5fb5b3aa Mark the queries panel as owned by secexp 2023-06-09 10:49:32 +01:00
Robert
eb938034fb Pass in just the environment instead of full app 2023-06-09 10:26:50 +01:00
Robert
a19c40bd66 Avoid using app.createEventEmitter 2023-06-09 10:24:07 +01:00
Koen Vlaswinkel
6b8169c479 Merge pull request #2494 from github/dependabot/npm_and_yarn/extensions/ql-vscode/eslint-plugin-jest-dom-5.0.1
Bump eslint-plugin-jest-dom from 4.0.2 to 5.0.1 in /extensions/ql-vscode
2023-06-09 11:19:26 +02:00
Koen Vlaswinkel
71ac6c73cd Merge pull request #2496 from github/dependabot/npm_and_yarn/extensions/ql-vscode/xml2js-and-vscode/vsce-and-azure/core-http-0.5.0
Bump xml2js, @vscode/vsce and @azure/core-http in /extensions/ql-vscode
2023-06-09 11:19:15 +02:00
Koen Vlaswinkel
8b3ca1035c Merge pull request #2495 from github/koesie10/remove-xml2js-types
Remove `@types/xml2js` dependency
2023-06-09 11:18:51 +02:00
Robert
f0cf4a0105 Remove onDidChangeWorkspaceFolders from app 2023-06-09 10:18:22 +01:00
Robert
1bd78649e7 Remove workspaceFolders from app 2023-06-09 09:57:23 +01:00
dependabot[bot]
f2ab949417 Bump xml2js, @vscode/vsce and @azure/core-http in /extensions/ql-vscode
Bumps [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) to 0.5.0 and updates ancestor dependencies [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js), [@vscode/vsce](https://github.com/Microsoft/vsce) and [@azure/core-http](https://github.com/Azure/azure-sdk-for-js). These dependencies need to be updated together.


Updates `xml2js` from 0.4.23 to 0.5.0
- [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/commits/0.5.0)

Updates `@vscode/vsce` from 2.15.0 to 2.19.0
- [Release notes](https://github.com/Microsoft/vsce/releases)
- [Commits](https://github.com/Microsoft/vsce/compare/v2.15.0...v2.19.0)

Updates `@azure/core-http` from 2.2.7 to 2.3.2
- [Release notes](https://github.com/Azure/azure-sdk-for-js/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/Changelog-for-next-generation.md)
- [Commits](https://github.com/Azure/azure-sdk-for-js/compare/@azure/core-http_2.2.7...@azure/core-http_2.3.2)

---
updated-dependencies:
- dependency-name: xml2js
  dependency-type: indirect
- dependency-name: "@vscode/vsce"
  dependency-type: direct:development
- dependency-name: "@azure/core-http"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-09 08:44:18 +00:00
Koen Vlaswinkel
7869225cf1 Remove @types/xml2js
We don't seem to be using this dependency anymore, so we can remove it.
2023-06-09 10:38:19 +02:00
Koen Vlaswinkel
95828cdc61 Add @testing-library/dom as devDependency 2023-06-09 10:35:05 +02:00
Andrew Eisenberg
afb490b64b Merge pull request #2441 from github/RasmusWL/codeql-executable-wording
Expand description for `codeQL.cli.executablePath`
2023-06-09 01:19:09 -07:00
dependabot[bot]
c3299f92c4 Bump eslint-plugin-jest-dom from 4.0.2 to 5.0.1 in /extensions/ql-vscode
Bumps [eslint-plugin-jest-dom](https://github.com/testing-library/eslint-plugin-jest-dom) from 4.0.2 to 5.0.1.
- [Release notes](https://github.com/testing-library/eslint-plugin-jest-dom/releases)
- [Commits](https://github.com/testing-library/eslint-plugin-jest-dom/compare/v4.0.2...v5.0.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest-dom
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-08 13:58:25 +00:00
Koen Vlaswinkel
dc9f648452 Merge pull request #2470 from github/dependabot/npm_and_yarn/extensions/ql-vscode/faker-js/faker-8.0.2
Bump @faker-js/faker from 7.5.0 to 8.0.2 in /extensions/ql-vscode
2023-06-08 14:52:15 +02:00
Shati Patel
ee11805060 Update changelog (MRVA -> variant analysis) (#2492) 2023-06-08 12:47:59 +00:00
Koen Vlaswinkel
a24f640dc0 Replace faker.datatype.hexadecimal by faker.string.hexadecimal 2023-06-08 13:47:03 +02:00
Koen Vlaswinkel
4d2a935e80 Merge pull request #2469 from github/dependabot/npm_and_yarn/extensions/ql-vscode/css-loader-6.8.1
Bump css-loader from 3.1.0 to 6.8.1 in /extensions/ql-vscode
2023-06-08 12:57:59 +02:00
Koen Vlaswinkel
bbffc16b64 Replace faker.random.word by faker.word.sample 2023-06-08 12:50:42 +02:00
Koen Vlaswinkel
a4f90b7197 Replace faker.datatype.number by faker.number.int 2023-06-08 12:48:48 +02:00
Charis Kyriakou
286018ccea Colour draft modeled methods with orange (#2491) 2023-06-08 11:33:51 +01:00
Koen Vlaswinkel
d2df162afd Switch to Webpack asset modules
css-loader 6 deprecates the use of `file-loader`. As a replacement,
we can use asset modules which generates very similar results. This is a
fairly simple change, and this makes fonts work again.

See: https://github.com/webpack-contrib/css-loader/releases/tag/v6.0.0
See: https://webpack.js.org/guides/asset-modules/
2023-06-08 11:38:20 +02:00
dependabot[bot]
a73c39a29a Bump css-loader from 3.1.0 to 6.8.1 in /extensions/ql-vscode
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 3.1.0 to 6.8.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v3.1.0...v6.8.1)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-08 09:31:26 +00:00
dependabot[bot]
5113b04b36 Bump @faker-js/faker from 7.5.0 to 8.0.2 in /extensions/ql-vscode
Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 7.5.0 to 8.0.2.
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v7.5.0...v8.0.2)

---
updated-dependencies:
- dependency-name: "@faker-js/faker"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-08 09:30:06 +00:00
Koen Vlaswinkel
8db5c6de65 Merge pull request #2488 from github/koesie10/fix-locale-lower-case
Add locale to `toLocaleLowerCase` calls
2023-06-08 11:14:07 +02:00
Koen Vlaswinkel
a46a8d06ec Merge pull request #2489 from github/koesie10/add-cli-tests-timeout
Add timeout to CLI tests
2023-06-08 09:45:30 +02:00
Koen Vlaswinkel
3569c77626 Add timeout to CLI tests
It seems like some CLI tests are hanging and only completing after 6
hours when they run into the default timeout. This updates the timeout
to 30 minutes. All CLI tests should complete in 30 minutes, so this
should ensure that they are cancelled when they are stuck.
2023-06-07 15:41:42 +02:00
Koen Vlaswinkel
0b22a6f34d Add locale to toLocaleLowerCase calls 2023-06-07 14:51:03 +02:00
Anders Starcke Henriksen
7c47a99805 Merge pull request #2486 from github/starcke/candidate-selection
Tweak candidate selection
2023-06-07 14:16:50 +02:00
Anders Starcke Henriksen
15c2a86725 Use Argument[this] instead of Argument[-1] 2023-06-07 13:14:31 +02:00
Anders Starcke Henriksen
e14b4c3040 Update extensions/ql-vscode/src/data-extensions-editor/auto-model.ts
Co-authored-by: Koen Vlaswinkel <koesie10@users.noreply.github.com>
2023-06-07 11:56:46 +02:00
Anders Starcke Henriksen
e3f192b76d Add new test. 2023-06-07 11:24:06 +02:00
Anders Starcke Henriksen
222c0d72bd Update tests. 2023-06-07 11:24:06 +02:00
Anders Starcke Henriksen
895c22ea85 Fix supported. 2023-06-07 11:24:02 +02:00
Nora
805d71286f Merge pull request #2487 from github/version/bump-to-v1.8.6
Bump version to v1.8.6
2023-06-07 10:18:17 +02:00
github-actions[bot]
2e01836f55 Bump version to v1.8.6 2023-06-06 15:16:08 +00:00
Koen Vlaswinkel
bca8885513 Merge pull request #2484 from github/koesie10/move-webview-html
Move Webview HTML generation out of `interface-utils.ts`
2023-06-06 17:14:09 +02:00
Anders Starcke Henriksen
76fb55f918 Tweak candidate selection:
- At most 6 usages.
- Send -1 for `this` argument.
- Do not send only some arguments for candidate or sample.
2023-06-06 16:49:50 +02:00
Koen Vlaswinkel
ba9f5e35cb Remove dependency on interface types from jumpToLocation 2023-06-06 11:27:57 +02:00
Koen Vlaswinkel
aa87fa8cda Move functions for resolving locations 2023-06-06 11:25:43 +02:00
Koen Vlaswinkel
461ff9bd21 Move Webview HTML generation out of interface-utils.ts
This moves the Webview HTML generation used by `AbstractWebview` out of
`interface-utils.ts` and into a new file `webview-html.ts` in the
`common/vscode` directory.
2023-06-06 11:12:05 +02:00
Rasmus Wriedt Larsen
c82ba1bdff Expand description for codeQL.cli.executablePath
To match what the code actually does.
2023-05-23 12:05:19 +02:00
382 changed files with 89114 additions and 8000 deletions

View File

@@ -2,9 +2,8 @@
* @name Unwanted dependency on vscode API
* @kind path-problem
* @problem.severity error
* @id vscode-codeql/assert-pure
* @description The modules stored under `pure` and tested in the `pure-tests`
* are intended to be "pure".
* @id vscode-codeql/assert-no-vscode-dependency
* @description The modules stored under `common` should not have dependencies on the VS Code API
*/
import javascript
@@ -13,12 +12,9 @@ class VSCodeImport extends ImportDeclaration {
VSCodeImport() { this.getImportedPath().getValue() = "vscode" }
}
class PureFile extends File {
PureFile() {
(
this.getRelativePath().regexpMatch(".*/src/pure/.*") or
this.getRelativePath().regexpMatch(".*/src/common/.*")
) and
class CommonFile extends File {
CommonFile() {
this.getRelativePath().regexpMatch(".*/src/common/.*") and
not this.getRelativePath().regexpMatch(".*/vscode/.*")
}
}
@@ -34,7 +30,8 @@ query predicate edges(AstNode a, AstNode b) {
from Module m, VSCodeImport v
where
m.getFile() instanceof PureFile and
m.getFile() instanceof CommonFile and
edges+(m, v)
select m, m, v,
"This module is not pure: it has a transitive dependency on the vscode API imported $@", v, "here"
"This module is in the 'common' directory but has a transitive dependency on the vscode API imported $@",
v, "here"

View File

@@ -1,3 +1,4 @@
name: vscode-codeql-custom-queries-javascript
version: 0.0.0
libraryPathDependencies: codeql-javascript
dependencies:
codeql/javascript-queries: "*"

View File

@@ -209,6 +209,7 @@ jobs:
name: CLI Test
runs-on: ${{ matrix.os }}
needs: [find-nightly, set-matrix]
timeout-minutes: 30
strategy:
matrix:
os: [ubuntu-latest, windows-latest]

View File

@@ -2,3 +2,4 @@
**/variant-analysis/ @github/code-scanning-secexp-reviewers
**/databases/ @github/code-scanning-secexp-reviewers
**/data-extensions-editor/ @github/code-scanning-secexp-reviewers
**/queries-panel/ @github/code-scanning-secexp-reviewers

View File

@@ -378,6 +378,7 @@ This requires running a MRVA query and seeing the results view.
1. Make changes via config file (ensure JSON schema is helping out)
1. Close and re-open VS Code (ensure lists are there)
1. Collapse/expand tree nodes
1. Create a new list, right click and select "Add repositories with GitHub Code Search". Enter the language 'python' and the query "UserMixin". This should show a rate limiting notification after a while but eventually populate the list with roughly 770 items.
Error cases that trigger an error notification:

View File

@@ -65,10 +65,6 @@ const baseConfig = {
"import/no-namespace": "off",
"import/no-unresolved": "off",
"import/no-webpack-loader-syntax": "off",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/click-events-have-key-events": "off",
"no-invalid-this": "off",
"no-fallthrough": "off",
"no-console": "off",

View File

@@ -1,5 +1,20 @@
# CodeQL for Visual Studio Code: Changelog
## 1.8.8 - 17 July 2023
- Remove support for CodeQL CLI versions older than 2.9.4. [#2610](https://github.com/github/vscode-codeql/pull/2610)
- Implement syntax highlighting for the `additional` and `default` keywords. [#2609](https://github.com/github/vscode-codeql/pull/2609)
## 1.8.7 - 29 June 2023
- Show a run button on the file tab for query files, that will start a local query. This button will only show when a local database is selected in the extension. [#2544](https://github.com/github/vscode-codeql/pull/2544)
- Add a `CodeQL: Quick Evaluation Count` command to generate the count summary statistics of the results set
without spending the time to compute locations and strings. [#2475](https://github.com/github/vscode-codeql/pull/2475)
## 1.8.6 - 14 June 2023
- Add repositories to a variant analysis list with GitHub Code Search. [#2439](https://github.com/github/vscode-codeql/pull/2439) and [#2476](https://github.com/github/vscode-codeql/pull/2476)
## 1.8.5 - 6 June 2023
- Add settings `codeQL.variantAnalysis.defaultResultsFilter` and `codeQL.variantAnalysis.defaultResultsSort` for configuring how variant analysis results are filtered and sorted in the results view. The default is to show all repositories, and to sort by the number of results. [#2392](https://github.com/github/vscode-codeql/pull/2392)
@@ -7,7 +22,7 @@
- Fix bug where the `CodeQL: Compare Query` command did not work for comparing quick-eval queries. [#2422](https://github.com/github/vscode-codeql/pull/2422)
- Update text of copy and export buttons in variant analysis results view to clarify that they only copy/export the selected/filtered results. [#2427](https://github.com/github/vscode-codeql/pull/2427)
- Add warning when using unsupported CodeQL CLI version. [#2428](https://github.com/github/vscode-codeql/pull/2428)
- Retry MRVA results download if connection times out. [#2440](https://github.com/github/vscode-codeql/pull/2440)
- Retry variant analysis results download if connection times out. [#2440](https://github.com/github/vscode-codeql/pull/2440)
## 1.8.4 - 3 May 2023

View File

@@ -62,18 +62,10 @@ export const config: webpack.Configuration = {
},
{
test: /\.(woff(2)?|ttf|eot)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "fonts/",
// We need this to make Webpack use the correct path for the fonts.
// Without this, the CSS file will use `url([object Module])`
esModule: false,
},
},
],
type: "asset/resource",
generator: {
filename: "fonts/[hash][ext][query]",
},
},
],
},

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 2h8v4c.341.035.677.112 1 .23V1H3v8.48l1-1.75V2zm2.14 8L5 8 4 9.75 3.29 11 1 15h8l-2.29-4-.57-1zm-3.42 4l1.72-3L5 10l.56 1 1.72 3H2.72zm6.836-6.41a3.5 3.5 0 1 1 3.888 5.82 3.5 3.5 0 0 1-3.888-5.82zm.555 4.989a2.5 2.5 0 1 0 2.778-4.157 2.5 2.5 0 0 0-2.778 4.157z" fill="#C5C5C5"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 2h8v4c.341.035.677.112 1 .23V1H3v8.48l1-1.75V2zm2.14 8L5 8 4 9.75 3.29 11 1 15h8l-2.29-4-.57-1zm-3.42 4l1.72-3L5 10l.56 1 1.72 3H2.72zm6.836-6.41a3.5 3.5 0 1 1 3.888 5.82 3.5 3.5 0 0 1-3.888-5.82zm.555 4.989a2.5 2.5 0 1 0 2.778-4.157 2.5 2.5 0 0 0-2.778 4.157z" fill="#424242"/></svg>

After

Width:  |  Height:  |  Size: 431 B

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.8.5",
"version": "1.8.8",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -71,6 +71,7 @@
"contributes": {
"configurationDefaults": {
"[ql]": {
"debug.saveBeforeStart": "nonUntitledEditorsInActiveGroup",
"editor.wordBasedSuggestions": false
},
"[dbscheme]": {
@@ -189,7 +190,7 @@
"scope": "machine-overridable",
"type": "string",
"default": "",
"markdownDescription": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. If empty, the extension will look for a CodeQL executable on your shell PATH, or if CodeQL is not on your PATH, download and manage its own CodeQL executable."
"markdownDescription": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. If empty, the extension will look for a CodeQL executable on your shell PATH, or if CodeQL is not on your PATH, download and manage its own CodeQL executable (note: if you later introduce CodeQL on your PATH, the extension will prefer a CodeQL executable it has downloaded itself)."
},
"codeQL.runningQueries.numberOfThreads": {
"type": "integer",
@@ -246,8 +247,8 @@
},
"codeQL.runningQueries.autoSave": {
"type": "boolean",
"default": false,
"description": "Enable automatically saving a modified query file when running a query."
"description": "Enable automatically saving a modified query file when running a query.",
"markdownDeprecationMessage": "This property is deprecated and no longer has any effect. To control automatic saving of documents before running queries, use the `debug.saveBeforeStart` setting."
},
"codeQL.runningQueries.maxQueries": {
"type": "integer",
@@ -457,6 +458,10 @@
"command": "codeQL.quickEval",
"title": "CodeQL: Quick Evaluation"
},
{
"command": "codeQL.quickEvalCount",
"title": "CodeQL: Quick Evaluation Count"
},
{
"command": "codeQL.quickEvalContextEditor",
"title": "CodeQL: Quick Evaluation"
@@ -501,6 +506,33 @@
"command": "codeQL.copyVersion",
"title": "CodeQL: Copy Version Information"
},
{
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
"title": "Run local query",
"icon": "$(run)"
},
{
"command": "codeQLQueries.runLocalQueriesFromPanel",
"title": "Run local queries",
"icon": "$(run-all)"
},
{
"command": "codeQL.runLocalQueryFromFileTab",
"title": "CodeQL: Run local query",
"icon": "$(run)"
},
{
"command": "codeQLQueries.runLocalQueryContextMenu",
"title": "Run against local database"
},
{
"command": "codeQLQueries.runLocalQueriesContextMenu",
"title": "Run against local database"
},
{
"command": "codeQLQueries.runVariantAnalysisContextMenu",
"title": "Run against variant analysis repositories"
},
{
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
"title": "Open database configuration file",
@@ -872,6 +904,13 @@
}
],
"menus": {
"editor/title": [
{
"command": "codeQL.runLocalQueryFromFileTab",
"group": "navigation",
"when": "resourceExtname == .ql && codeQL.currentDatabaseItem"
}
],
"view/title": [
{
"command": "codeQLDatabases.sortByName",
@@ -967,7 +1006,7 @@
},
{
"command": "codeQLVariantAnalysisRepositories.importFromCodeSearch",
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/ && config.codeQL.codeSearch",
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/",
"group": "2_qlContextMenu@1"
},
{
@@ -1095,6 +1134,31 @@
"group": "1_queryHistory@1",
"when": "viewItem == remoteResultsItem"
},
{
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
"group": "inline",
"when": "view == codeQLQueries && viewItem == queryFile && codeQL.currentDatabaseItem"
},
{
"command": "codeQLQueries.runLocalQueryContextMenu",
"group": "queriesPanel@1",
"when": "view == codeQLQueries && viewItem == queryFile && codeQL.currentDatabaseItem"
},
{
"command": "codeQLQueries.runLocalQueriesContextMenu",
"group": "queriesPanel@1",
"when": "view == codeQLQueries && viewItem == queryFolder && codeQL.currentDatabaseItem"
},
{
"command": "codeQLQueries.runVariantAnalysisContextMenu",
"group": "queriesPanel@1",
"when": "view == codeQLQueries && viewItem == queryFile"
},
{
"command": "codeQLQueries.runLocalQueriesFromPanel",
"group": "inline",
"when": "view == codeQLQueries && viewItem == queryFolder && codeQL.currentDatabaseItem"
},
{
"command": "codeQLTests.showOutputDifferences",
"group": "qltest@1",
@@ -1154,6 +1218,18 @@
"command": "codeQL.runQuery",
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
"when": "false"
},
{
"command": "codeQLQueries.runLocalQueriesFromPanel",
"when": "false"
},
{
"command": "codeQL.runLocalQueryFromFileTab",
"when": "false"
},
{
"command": "codeQL.runQueryContextEditor",
"when": "false"
@@ -1206,6 +1282,10 @@
"command": "codeQL.quickEval",
"when": "editorLangId == ql"
},
{
"command": "codeQL.quickEvalCount",
"when": "editorLangId == ql && codeql.supportsQuickEvalCount"
},
{
"command": "codeQL.quickEvalContextEditor",
"when": "false"
@@ -1274,6 +1354,18 @@
"command": "codeQL.openDataExtensionsEditor",
"when": "config.codeQL.canary && config.codeQL.dataExtensions.editor"
},
{
"command": "codeQLQueries.runLocalQueryContextMenu",
"when": "false"
},
{
"command": "codeQLQueries.runLocalQueriesContextMenu",
"when": "false"
},
{
"command": "codeQLQueries.runVariantAnalysisContextMenu",
"when": "false"
},
{
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
"when": "false"
@@ -1608,7 +1700,7 @@
},
{
"view": "codeQLQueries",
"contents": "This workspace doesn't contain any CodeQL queries at the moment."
"contents": "Looking for queries..."
},
{
"view": "codeQLDatabases",
@@ -1635,7 +1727,7 @@
"test:vscode-integration:activated-extension": "jest --projects test/vscode-tests/activated-extension",
"test:vscode-integration:no-workspace": "jest --projects test/vscode-tests/no-workspace",
"test:vscode-integration:minimal-workspace": "jest --projects test/vscode-tests/minimal-workspace",
"test:cli-integration": "jest --projects test/vscode-tests/cli-integration",
"test:cli-integration": "jest --projects test/vscode-tests/cli-integration --verbose",
"update-vscode": "node ./node_modules/vscode/bin/install",
"format": "prettier --write **/*.{ts,tsx} && eslint . --ext .ts,.tsx --fix",
"lint": "eslint . --ext .js,.ts,.tsx --max-warnings=0",
@@ -1649,7 +1741,7 @@
"prepare": "cd ../.. && husky install"
},
"dependencies": {
"@octokit/plugin-retry": "^3.0.9",
"@octokit/plugin-retry": "^4.1.6",
"@octokit/rest": "^19.0.4",
"@vscode/codicons": "^0.0.31",
"@vscode/debugadapter": "^1.59.0",
@@ -1673,7 +1765,7 @@
"path-browserify": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"semver": "~7.3.2",
"semver": "~7.5.2",
"source-map": "^0.7.4",
"source-map-support": "^0.5.21",
"stream": "^0.0.2",
@@ -1689,12 +1781,12 @@
"vscode-languageclient": "^8.0.2",
"vscode-test-adapter-api": "~1.7.0",
"vscode-test-adapter-util": "~0.7.0",
"zip-a-folder": "~1.1.3"
"zip-a-folder": "~2.0.0"
},
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@faker-js/faker": "^7.5.0",
"@faker-js/faker": "^8.0.2",
"@github/markdownlint-github": "^0.3.0",
"@octokit/plugin-throttling": "^5.0.1",
"@storybook/addon-actions": "^6.5.17-alpha.0",
@@ -1705,6 +1797,7 @@
"@storybook/manager-webpack5": "^6.5.17-alpha.0",
"@storybook/react": "^6.5.17-alpha.0",
"@storybook/testing-library": "^0.0.13",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
@@ -1738,23 +1831,22 @@
"@types/vscode": "^1.67.0",
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.18.0",
"@types/xml2js": "~0.4.4",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"@vscode/test-electron": "^2.2.0",
"@vscode/vsce": "^2.15.0",
"@vscode/vsce": "^2.19.0",
"ansi-colors": "^4.1.1",
"applicationinsights": "^2.3.5",
"cross-env": "^7.0.3",
"css-loader": "~3.1.0",
"css-loader": "~6.8.1",
"del": "^6.0.0",
"esbuild": "^0.15.15",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-etc": "^2.0.2",
"eslint-plugin-github": "^4.4.1",
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-jest-dom": "^5.0.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.6.4",
@@ -1775,7 +1867,7 @@
"mini-css-extract-plugin": "^2.6.1",
"npm-run-all": "^4.1.5",
"patch-package": "^7.0.0",
"prettier": "^2.7.1",
"prettier": "^3.0.0",
"tar-stream": "^3.0.0",
"through2": "^4.0.2",
"ts-jest": "^29.0.1",

View File

@@ -0,0 +1,55 @@
import { AppCommandManager } from "../common/commands";
import { Uri, workspace } from "vscode";
import { join } from "path";
import { pathExists } from "fs-extra";
import { isCodespacesTemplate } from "../config";
import { showBinaryChoiceDialog } from "../common/vscode/dialog";
import { extLogger } from "../common/logging/vscode";
/**
* Check if the current workspace is the CodeTour and open the workspace folder.
* Without this, we can't run the code tour correctly.
**/
export async function prepareCodeTour(
commandManager: AppCommandManager,
): Promise<void> {
if (workspace.workspaceFolders?.length) {
const currentFolder = workspace.workspaceFolders[0].uri.fsPath;
const tutorialWorkspacePath = join(
currentFolder,
"tutorial.code-workspace",
);
const toursFolderPath = join(currentFolder, ".tours");
/** We're opening the tutorial workspace, if we detect it.
* This will only happen if the following three conditions are met:
* - the .tours folder exists
* - the tutorial.code-workspace file exists
* - the CODESPACES_TEMPLATE setting doesn't exist (it's only set if the user has already opened
* the tutorial workspace so it's a good indicator that the user is in the folder but has ignored
* the prompt to open the workspace)
*/
if (
(await pathExists(tutorialWorkspacePath)) &&
(await pathExists(toursFolderPath)) &&
!isCodespacesTemplate()
) {
const answer = await showBinaryChoiceDialog(
"We've detected you're in the CodeQL Tour repo. We will need to open the workspace file to continue. Reload?",
);
if (!answer) {
return;
}
const tutorialWorkspaceUri = Uri.file(tutorialWorkspacePath);
void extLogger.log(
`In prepareCodeTour() method, going to open the tutorial workspace file: ${tutorialWorkspacePath}`,
);
await commandManager.execute("vscode.openFolder", tutorialWorkspaceUri);
}
}
}

View File

@@ -1,7 +1,7 @@
import * as semver from "semver";
import { runCodeQlCliCommand } from "./cli";
import { Logger } from "../common";
import { getErrorMessage } from "../pure/helpers-pure";
import { Logger } from "../common/logging";
import { getErrorMessage } from "../common/helpers-pure";
/**
* Get the version of a CodeQL CLI.

View File

@@ -11,7 +11,7 @@ import tk from "tree-kill";
import { promisify } from "util";
import { CancellationToken, Disposable, Uri } from "vscode";
import { BQRSInfo, DecodedBqrsChunk } from "../pure/bqrs-cli-types";
import { BQRSInfo, DecodedBqrsChunk } from "../common/bqrs-cli-types";
import { allowCanaryQueryServer, CliConfig } from "../config";
import {
DistributionProvider,
@@ -21,12 +21,13 @@ import {
assertNever,
getErrorMessage,
getErrorStack,
} from "../pure/helpers-pure";
import { QueryMetadata, SortDirection } from "../pure/interface-types";
import { BaseLogger, Logger, ProgressReporter } from "../common";
import { CompilationMessage } from "../pure/legacy-messages";
} from "../common/helpers-pure";
import { walkDirectory } from "../common/files";
import { QueryMetadata, SortDirection } from "../common/interface-types";
import { BaseLogger, Logger } from "../common/logging";
import { ProgressReporter } from "../common/logging/vscode";
import { CompilationMessage } from "../query-server/legacy-messages";
import { sarifParser } from "../common/sarif-parser";
import { walkDirectory } from "../helpers";
import { App } from "../common/app";
import { QueryLanguage } from "../common/query-language";
@@ -718,6 +719,7 @@ export class CodeQLCliServer implements Disposable {
async resolveLibraryPath(
workspaces: string[],
queryPath: string,
silent = false,
): Promise<QuerySetup> {
const subcommandArgs = [
"--query",
@@ -728,6 +730,7 @@ export class CodeQLCliServer implements Disposable {
["resolve", "library-path"],
subcommandArgs,
"Resolving library paths",
{ silent },
);
}
@@ -1480,6 +1483,13 @@ export class CodeQLCliServer implements Disposable {
CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG,
) >= 0,
);
await this.app.commands.execute(
"setContext",
"codeql.supportsQuickEvalCount",
newVersion.compare(
CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT,
) >= 0,
);
} catch (e) {
this._versionChangedListeners.forEach((listener) =>
listener(undefined),
@@ -1756,34 +1766,31 @@ async function logStream(stream: Readable, logger: BaseLogger): Promise<void> {
}
}
export function shouldDebugIdeServer() {
function isEnvTrue(name: string): boolean {
return (
"IDE_SERVER_JAVA_DEBUG" in process.env &&
process.env.IDE_SERVER_JAVA_DEBUG !== "0" &&
process.env.IDE_SERVER_JAVA_DEBUG?.toLocaleLowerCase() !== "false"
name in process.env &&
process.env[name] !== "0" &&
// Use en-US since we expect the value to be either "false" or "FALSE", not a localized version.
process.env[name]?.toLocaleLowerCase("en-US") !== "false"
);
}
export function shouldDebugIdeServer() {
return isEnvTrue("IDE_SERVER_JAVA_DEBUG");
}
export function shouldDebugQueryServer() {
return (
"QUERY_SERVER_JAVA_DEBUG" in process.env &&
process.env.QUERY_SERVER_JAVA_DEBUG !== "0" &&
process.env.QUERY_SERVER_JAVA_DEBUG?.toLocaleLowerCase() !== "false"
);
return isEnvTrue("QUERY_SERVER_JAVA_DEBUG");
}
export function shouldDebugCliServer() {
return (
"CLI_SERVER_JAVA_DEBUG" in process.env &&
process.env.CLI_SERVER_JAVA_DEBUG !== "0" &&
process.env.CLI_SERVER_JAVA_DEBUG?.toLocaleLowerCase() !== "false"
);
return isEnvTrue("CLI_SERVER_JAVA_DEBUG");
}
export class CliVersionConstraint {
// The oldest version of the CLI that we support. This is used to determine
// whether to show a warning about the CLI being too old on startup.
public static OLDEST_SUPPORTED_CLI_VERSION = new SemVer("2.7.6");
public static OLDEST_SUPPORTED_CLI_VERSION = new SemVer("2.9.4");
/**
* CLI version where building QLX packs for remote queries is supported.
@@ -1845,6 +1852,18 @@ export class CliVersionConstraint {
public static CLI_VERSION_GLOBAL_CACHE = new SemVer("2.12.4");
/**
* CLI version where the query server supports quick-eval count mode.
*/
public static CLI_VERSION_WITH_QUICK_EVAL_COUNT = new SemVer("2.13.3");
/**
* CLI version where the langauge server supports visisbility change notifications.
*/
public static CLI_VERSION_WITH_VISIBILITY_NOTIFICATIONS = new SemVer(
"2.14.0",
);
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1918,4 +1937,16 @@ export class CliVersionConstraint {
async usesGlobalCompilationCache() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_GLOBAL_CACHE);
}
async supportsVisibilityNotifications() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_VISIBILITY_NOTIFICATIONS,
);
}
async supportsQuickEvalCount() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT,
);
}
}

View File

@@ -6,8 +6,7 @@ import * as semver from "semver";
import { URL } from "url";
import { ExtensionContext, Event } from "vscode";
import { DistributionConfig } from "../config";
import { showAndLogErrorMessage, showAndLogWarningMessage } from "../helpers";
import { extLogger } from "../common";
import { extLogger } from "../common/logging/vscode";
import { getCodeQlCliVersion } from "./cli-version";
import {
ProgressCallback,
@@ -18,11 +17,15 @@ import {
deprecatedCodeQlLauncherName,
extractZipArchive,
getRequiredAssetName,
} from "../pure/distribution";
} from "../common/distribution";
import {
InvocationRateLimiter,
InvocationRateLimiterResultKind,
} from "../common/invocation-rate-limiter";
import {
showAndLogErrorMessage,
showAndLogWarningMessage,
} from "../common/logging";
/**
* distribution.ts
@@ -157,6 +160,7 @@ export class DistributionManager implements DistributionProvider {
if (this.config.customCodeQlPath) {
if (!(await pathExists(this.config.customCodeQlPath))) {
void showAndLogErrorMessage(
extLogger,
`The CodeQL executable path is specified as "${this.config.customCodeQlPath}" ` +
"by a configuration setting, but a CodeQL executable could not be found at that path. Please check " +
"that a CodeQL executable exists at the specified path or remove the setting.",
@@ -849,6 +853,7 @@ export async function getExecutableFromDirectory(
function warnDeprecatedLauncher() {
void showAndLogWarningMessage(
extLogger,
`The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` +
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`,
);
@@ -949,7 +954,10 @@ export interface GithubReleaseAsset {
}
export class GithubApiError extends Error {
constructor(public status: number, public body: string) {
constructor(
public status: number,
public body: string,
) {
super(`API call failed with status code ${status}, body: ${body}`);
}
}

View File

@@ -0,0 +1,80 @@
import { CodeQLCliServer } from "./cli";
import { Uri, window } from "vscode";
import { 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";
import { showAndLogErrorMessage } from "../common/logging";
/**
* Finds the language that a query targets.
* If it can't be autodetected, prompt the user to specify the language manually.
*/
export async function findLanguage(
cliServer: CodeQLCliServer,
queryUri: Uri | undefined,
): Promise<QueryLanguage | undefined> {
const uri = queryUri || window.activeTextEditor?.document.uri;
if (uri !== undefined) {
try {
const queryInfo = await cliServer.resolveQueryByLanguage(
getOnDiskWorkspaceFolders(),
uri,
);
const language = Object.keys(queryInfo.byLanguage)[0];
void extLogger.log(`Detected query language: ${language}`);
if (isQueryLanguage(language)) {
return language;
}
void extLogger.log(
"Query language is unsupported. Select language manually.",
);
} catch (e) {
void extLogger.log(
"Could not autodetect query language. Select language manually.",
);
}
}
// will be undefined if user cancels the quick pick.
return await askForLanguage(cliServer, false);
}
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) {
// This only happens if the user cancels the quick pick.
if (throwOnEmpty) {
throw new UserCancellationException("Cancelled.");
} else {
void showAndLogErrorMessage(
extLogger,
"Language not found. Language must be specified manually.",
);
}
return undefined;
}
if (!isQueryLanguage(language)) {
void showAndLogErrorMessage(
extLogger,
`Language '${language}' is not supported. Only languages ${Object.values(
QueryLanguage,
).join(", ")} are supported.`,
);
return undefined;
}
return language;
}

View File

@@ -0,0 +1,22 @@
import { CodeQLCliServer } from "./cli";
import { QueryMetadata } from "../common/interface-types";
import { extLogger } from "../common/logging/vscode";
/**
* Gets metadata for a query, if it exists.
* @param cliServer The CLI server.
* @param queryPath The path to the query.
* @returns A promise that resolves to the query metadata, if available.
*/
export async function tryGetQueryMetadata(
cliServer: CodeQLCliServer,
queryPath: string,
): Promise<QueryMetadata | undefined> {
try {
return await cliServer.resolveMetadata(queryPath);
} catch (e) {
// Ignore errors and provide no metadata.
void extLogger.log(`Couldn't resolve metadata for ${queryPath}: ${e}`);
return;
}
}

View File

@@ -1,26 +1,21 @@
import { Credentials } from "./authentication";
import { Disposable } from "../pure/disposable-object";
import { Disposable } from "./disposable-object";
import { AppEventEmitter } from "./events";
import { Logger } from "./logging";
import { NotificationLogger } from "./logging";
import { Memento } from "./memento";
import { AppCommandManager } from "./commands";
import type {
WorkspaceFolder,
Event,
WorkspaceFoldersChangeEvent,
} from "vscode";
import { AppTelemetry } from "./telemetry";
export interface App {
createEventEmitter<T>(): AppEventEmitter<T>;
readonly mode: AppMode;
readonly logger: Logger;
readonly logger: NotificationLogger;
readonly telemetry?: AppTelemetry;
readonly subscriptions: Disposable[];
readonly extensionPath: string;
readonly globalStoragePath: string;
readonly workspaceStoragePath?: string;
readonly workspaceState: Memento;
readonly workspaceFolders: readonly WorkspaceFolder[] | undefined;
readonly onDidChangeWorkspaceFolders: Event<WorkspaceFoldersChangeEvent>;
readonly credentials: Credentials;
readonly commands: AppCommandManager;
readonly environment: EnvironmentContext;

View File

@@ -4,7 +4,7 @@ import {
LineColumnLocation,
WholeFileLocation,
} from "./bqrs-cli-types";
import { createRemoteFileRef } from "./location-link-utils";
import { createRemoteFileRef } from "../common/location-link-utils";
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates

View File

@@ -4,7 +4,7 @@ import type { AstItem } from "../language-support";
import type { DbTreeViewItem } from "../databases/ui/db-tree-view-item";
import type { DatabaseItem } from "../databases/local-databases";
import type { QueryHistoryInfo } from "../query-history/query-history-info";
import type { RepositoriesFilterSortStateWithIds } from "../pure/variant-analysis-filter-sort";
import type { RepositoriesFilterSortStateWithIds } from "../variant-analysis/shared/variant-analysis-filter-sort";
import type { TestTreeNode } from "../query-testing/test-tree-node";
import type {
VariantAnalysis,
@@ -12,6 +12,7 @@ import type {
VariantAnalysisScannedRepositoryResult,
} from "../variant-analysis/shared/variant-analysis";
import type { QLDebugConfiguration } from "../debugger/debug-configuration";
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
// A command function matching the signature that VS Code calls when
// a command is invoked from a context menu on a TreeView with
@@ -129,8 +130,14 @@ export type LocalQueryCommands = {
"codeQL.runQueryOnMultipleDatabasesContextEditor": (
uri?: Uri,
) => Promise<void>;
"codeQLQueries.runLocalQueryFromQueriesPanel": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
"codeQLQueries.runLocalQueryContextMenu": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
"codeQLQueries.runLocalQueriesContextMenu": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
"codeQLQueries.runLocalQueriesFromPanel": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
"codeQL.runLocalQueryFromFileTab": (uri: Uri) => Promise<void>;
"codeQL.runQueries": ExplorerSelectionCommandFunction<Uri>;
"codeQL.quickEval": (uri: Uri) => Promise<void>;
"codeQL.quickEvalCount": (uri: Uri) => Promise<void>;
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
"codeQL.quickQuery": () => Promise<void>;
@@ -262,6 +269,7 @@ export type VariantAnalysisCommands = {
) => Promise<void>;
"codeQL.runVariantAnalysis": (uri?: Uri) => Promise<void>;
"codeQL.runVariantAnalysisContextEditor": (uri?: Uri) => Promise<void>;
"codeQLQueries.runVariantAnalysisContextMenu": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
};
export type DatabasePanelCommands = {

View File

@@ -1,5 +1,5 @@
import { DisposableObject } from "../pure/disposable-object";
import { getErrorMessage } from "../pure/helpers-pure";
import { DisposableObject } from "./disposable-object";
import { getErrorMessage } from "./helpers-pure";
import { Logger } from "./logging";
/**
@@ -7,11 +7,14 @@ import { Logger } from "./logging";
* files. This class automatically prevents more than one discovery operation from running at the
* same time.
*/
export abstract class Discovery<T> extends DisposableObject {
export abstract class Discovery extends DisposableObject {
private restartWhenFinished = false;
private currentDiscoveryPromise: Promise<void> | undefined;
constructor(private readonly name: string, private readonly logger: Logger) {
constructor(
protected readonly name: string,
private readonly logger: Logger,
) {
super();
}
@@ -64,14 +67,12 @@ export abstract class Discovery<T> extends DisposableObject {
* discovery.
*/
private async launchDiscovery(): Promise<void> {
let results: T | undefined;
try {
results = await this.discover();
await this.discover();
} catch (err) {
void this.logger.log(
`${this.name} failed. Reason: ${getErrorMessage(err)}`,
);
results = undefined;
}
if (this.restartWhenFinished) {
@@ -82,24 +83,11 @@ export abstract class Discovery<T> extends DisposableObject {
// succeeded or failed.
this.restartWhenFinished = false;
await this.launchDiscovery();
} else {
// If the discovery was successful, then update any listeners with the results.
if (results !== undefined) {
this.update(results);
}
}
}
/**
* Overridden by the derived class to spawn the actual discovery operation, returning the results.
*/
protected abstract discover(): Promise<T>;
/**
* Overridden by the derived class to atomically update the `Discovery` object with the results of
* the discovery operation, and to notify any listeners that the discovery results may have
* changed.
* @param results The discovery results returned by the `discover` function.
*/
protected abstract update(results: T): void;
protected abstract discover(): Promise<void>;
}

View File

@@ -1,4 +1,4 @@
import { Disposable } from "../pure/disposable-object";
import { Disposable } from "./disposable-object";
export interface AppEvent<T> {
(listener: (event: T) => void): Disposable;

View File

@@ -1,5 +1,6 @@
import { pathExists, stat, readdir } from "fs-extra";
import { join, resolve } from "path";
import { pathExists, stat, readdir, opendir } from "fs-extra";
import { isAbsolute, join, relative, resolve } from "path";
import { tmpdir as osTmpdir } from "os";
/**
* Recursively finds all .ql files in this set of Uris.
@@ -51,36 +52,32 @@ export async function getDirectoryNamesInsidePath(
return dirNames;
}
function normalizePath(path: string, platform: NodeJS.Platform): string {
export function normalizePath(path: string): string {
// On Windows, "C:/", "C:\", and "c:/" are all equivalent. We need
// to normalize the paths to ensure they all get resolved to the
// same format. On Windows, we also need to do the comparison
// case-insensitively.
path = resolve(path);
if (platform === "win32") {
if (process.platform === "win32") {
path = path.toLowerCase();
}
return path;
}
export function pathsEqual(
path1: string,
path2: string,
platform: NodeJS.Platform,
): boolean {
return normalizePath(path1, platform) === normalizePath(path2, platform);
export function pathsEqual(path1: string, path2: string): boolean {
return normalizePath(path1) === normalizePath(path2);
}
/**
* Returns true if path1 contains path2.
* Returns true if `parent` contains `child`, or if they are equal.
*/
export function containsPath(
path1: string,
path2: string,
platform: NodeJS.Platform,
): boolean {
return normalizePath(path2, platform).startsWith(
normalizePath(path1, platform),
export function containsPath(parent: string, child: string): boolean {
const relativePath = relative(parent, child);
return (
!relativePath.startsWith("..") &&
// On windows, if the two paths are in different drives, then the
// relative path will be an absolute path to the other drive.
!isAbsolute(relativePath)
);
}
@@ -88,3 +85,45 @@ export async function readDirFullPaths(path: string): Promise<string[]> {
const baseNames = await readdir(path);
return baseNames.map((baseName) => join(path, baseName));
}
/**
* Recursively walk a directory and return the full path to all files found.
* Symbolic links are ignored.
*
* @param dir the directory to walk
*
* @return An iterator of the full path to all files recursively found in the directory.
*/
export async function* walkDirectory(
dir: string,
): AsyncIterableIterator<string> {
const seenFiles = new Set<string>();
for await (const d of await opendir(dir)) {
const entry = join(dir, d.name);
seenFiles.add(entry);
if (d.isDirectory()) {
yield* walkDirectory(entry);
} else if (d.isFile()) {
yield entry;
}
}
}
/**
* Error thrown from methods from the `fs` module.
*
* In practice, any error matching this is likely an instance of `NodeJS.ErrnoException`.
* If desired in the future, we could model more fields or use `NodeJS.ErrnoException` directly.
*/
export interface IOError {
readonly code: string;
}
export function isIOError(e: any): e is IOError {
return e.code !== undefined && typeof e.code === "string";
}
// This function is a wrapper around `os.tmpdir()` to make it easier to mock in tests.
export function tmpdir(): string {
return osTmpdir();
}

View File

@@ -1,4 +1,4 @@
import { OWNER_REGEX, REPO_REGEX } from "../pure/helpers-pure";
import { OWNER_REGEX, REPO_REGEX } from "./helpers-pure";
/**
* Checks if a string is a valid GitHub NWO.

View File

@@ -1 +0,0 @@
export * from "./logging";

View File

@@ -5,7 +5,7 @@ import {
ResultSetSchema,
Column,
ResolvableLocationValue,
} from "./bqrs-cli-types";
} from "../common/bqrs-cli-types";
import {
VariantAnalysis,
VariantAnalysisScannedRepositoryResult,
@@ -14,12 +14,13 @@ import {
import {
RepositoriesFilterSortState,
RepositoriesFilterSortStateWithIds,
} from "./variant-analysis-filter-sort";
import { ErrorLike } from "./errors";
} from "../variant-analysis/shared/variant-analysis-filter-sort";
import { ErrorLike } from "../common/errors";
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
import { ExternalApiUsage } from "../data-extensions-editor/external-api-usage";
import { ModeledMethod } from "../data-extensions-editor/modeled-method";
import { DataExtensionEditorViewState } from "../data-extensions-editor/shared/view-state";
import { Mode } from "../data-extensions-editor/shared/mode";
/**
* This module contains types and code that are shared between
@@ -508,17 +509,19 @@ export interface ShowProgressMessage {
message: string;
}
export interface LoadModeledMethodsMessage {
t: "loadModeledMethods";
modeledMethods: Record<string, ModeledMethod>;
}
export interface AddModeledMethodsMessage {
t: "addModeledMethods";
modeledMethods: Record<string, ModeledMethod>;
}
/**
* If true, then any existing modeled methods set to "none" will be
* overwritten by the new modeled methods. Otherwise, the "none" modeled
* methods will not be overwritten, even if the new modeled methods
* contain a better model.
*/
overrideNone?: boolean;
export interface SwitchModeMessage {
t: "switchMode";
mode: Mode;
}
export interface JumpToUsageMessage {
@@ -530,8 +533,8 @@ export interface OpenExtensionPackMessage {
t: "openExtensionPack";
}
export interface OpenModelFileMessage {
t: "openModelFile";
export interface RefreshExternalApiUsages {
t: "refreshExternalApiUsages";
}
export interface SaveModeledMethods {
@@ -554,11 +557,13 @@ export type ToDataExtensionsEditorMessage =
| SetExtensionPackStateMessage
| SetExternalApiUsagesMessage
| ShowProgressMessage
| LoadModeledMethodsMessage
| AddModeledMethodsMessage;
export type FromDataExtensionsEditorMessage =
| ViewLoadedMsg
| OpenModelFileMessage
| SwitchModeMessage
| RefreshExternalApiUsages
| OpenExtensionPackMessage
| JumpToUsageMessage
| SaveModeledMethods

View File

@@ -1,4 +1,4 @@
export * from "./logger";
export * from "./notification-logger";
export * from "./notifications";
export * from "./tee-logger";
export * from "./vscode/loggers";
export * from "./vscode/output-channel-logger";

View File

@@ -0,0 +1,7 @@
import { Logger } from "./logger";
export interface NotificationLogger extends Logger {
showErrorMessage(message: string): Promise<void>;
showWarningMessage(message: string): Promise<void>;
showInformationMessage(message: string): Promise<void>;
}

View File

@@ -0,0 +1,116 @@
import { NotificationLogger } from "./notification-logger";
import { AppTelemetry } from "../telemetry";
import { RedactableError } from "../errors";
export interface ShowAndLogOptions {
/**
* An alternate message that is added to the log, but not displayed in the popup.
* This is useful for adding extra detail to the logs that would be too noisy for the popup.
*/
fullMessage?: string;
}
/**
* Show an error message and log it to the console
*
* @param logger The logger that will receive the message.
* @param message The message to show.
* @param options? See individual fields on `ShowAndLogOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogErrorMessage(
logger: NotificationLogger,
message: string,
options?: ShowAndLogOptions,
): Promise<void> {
return internalShowAndLog(
logger,
dropLinesExceptInitial(message),
logger.showErrorMessage,
{ fullMessage: message, ...options },
);
}
function dropLinesExceptInitial(message: string, n = 2) {
return message.toString().split(/\r?\n/).slice(0, n).join("\n");
}
/**
* Show a warning message and log it to the console
*
* @param logger The logger that will receive the message.
* @param message The message to show.
* @param options? See individual fields on `ShowAndLogOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogWarningMessage(
logger: NotificationLogger,
message: string,
options?: ShowAndLogOptions,
): Promise<void> {
return internalShowAndLog(
logger,
message,
logger.showWarningMessage,
options,
);
}
/**
* Show an information message and log it to the console
*
* @param logger The logger that will receive the message.
* @param message The message to show.
* @param options? See individual fields on `ShowAndLogOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogInformationMessage(
logger: NotificationLogger,
message: string,
options?: ShowAndLogOptions,
): Promise<void> {
return internalShowAndLog(
logger,
message,
logger.showInformationMessage,
options,
);
}
async function internalShowAndLog(
logger: NotificationLogger,
message: string,
fn: (message: string) => Promise<void>,
{ fullMessage }: ShowAndLogOptions = {},
): Promise<void> {
void logger.log(fullMessage || message);
await fn.bind(logger)(message);
}
interface ShowAndLogExceptionOptions extends ShowAndLogOptions {
/** Custom properties to include in the telemetry report. */
extraTelemetryProperties?: { [key: string]: string };
}
/**
* Show an error message, log it to the console, and emit redacted information as telemetry
*
* @param logger The logger that will receive the message.
* @param telemetry The telemetry instance to use for reporting.
* @param error The error to show. Only redacted information will be included in the telemetry.
* @param options See individual fields on `ShowAndLogExceptionOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogExceptionWithTelemetry(
logger: NotificationLogger,
telemetry: AppTelemetry | undefined,
error: RedactableError,
options: ShowAndLogExceptionOptions = {},
): Promise<void> {
telemetry?.sendError(error, options.extraTelemetryProperties);
return showAndLogErrorMessage(logger, error.fullMessage, options);
}

View File

@@ -1,6 +1,6 @@
import { appendFile, ensureFile } from "fs-extra";
import { isAbsolute } from "path";
import { getErrorMessage } from "../../pure/helpers-pure";
import { getErrorMessage } from "../helpers-pure";
import { Logger, LogOptions } from "./logger";
/**

View File

@@ -0,0 +1,2 @@
export * from "./loggers";
export * from "./output-channel-logger";

View File

@@ -1,11 +1,15 @@
import { window as Window, OutputChannel, Progress } from "vscode";
import { Logger, LogOptions } from "../logger";
import { DisposableObject } from "../../../pure/disposable-object";
import { DisposableObject } from "../../disposable-object";
import { NotificationLogger } from "../notification-logger";
/**
* A logger that writes messages to an output channel in the VS Code Output tab.
*/
export class OutputChannelLogger extends DisposableObject implements Logger {
export class OutputChannelLogger
extends DisposableObject
implements Logger, NotificationLogger
{
public readonly outputChannel: OutputChannel;
isCustomLogDirectory: boolean;
@@ -42,6 +46,30 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
show(preserveFocus?: boolean): void {
this.outputChannel.show(preserveFocus);
}
async showErrorMessage(message: string): Promise<void> {
await this.showMessage(message, Window.showErrorMessage);
}
async showInformationMessage(message: string): Promise<void> {
await this.showMessage(message, Window.showInformationMessage);
}
async showWarningMessage(message: string): Promise<void> {
await this.showMessage(message, Window.showWarningMessage);
}
private async showMessage(
message: string,
show: (message: string, ...items: string[]) => Thenable<string | undefined>,
): Promise<void> {
const label = "Show Log";
const result = await show(message, label);
if (result === label) {
this.show();
}
}
}
export type ProgressReporter = Progress<{ message: string }>;

View File

@@ -19,3 +19,11 @@ export const basename = (path: string): string => {
const index = path.lastIndexOf("\\");
return index === -1 ? path : path.slice(index + 1);
};
// Returns the extension of a path, including the leading dot.
export const extname = (path: string): string => {
const name = basename(path);
const index = name.lastIndexOf(".");
return index === -1 ? "" : name.slice(index);
};

View File

@@ -25,13 +25,17 @@ export const PACKS_BY_QUERY_LANGUAGE = {
[QueryLanguage.Ruby]: ["codeql/ruby-queries"],
};
export const dbSchemeToLanguage = {
"semmlecode.javascript.dbscheme": "javascript",
"semmlecode.cpp.dbscheme": "cpp",
"semmlecode.dbscheme": "java",
"semmlecode.python.dbscheme": "python",
"semmlecode.csharp.dbscheme": "csharp",
"go.dbscheme": "go",
"ruby.dbscheme": "ruby",
"swift.dbscheme": "swift",
export const dbSchemeToLanguage: Record<string, QueryLanguage> = {
"semmlecode.javascript.dbscheme": QueryLanguage.Javascript,
"semmlecode.cpp.dbscheme": QueryLanguage.Cpp,
"semmlecode.dbscheme": QueryLanguage.Java,
"semmlecode.python.dbscheme": QueryLanguage.Python,
"semmlecode.csharp.dbscheme": QueryLanguage.CSharp,
"go.dbscheme": QueryLanguage.Go,
"ruby.dbscheme": QueryLanguage.Ruby,
"swift.dbscheme": QueryLanguage.Swift,
};
export function isQueryLanguage(language: string): language is QueryLanguage {
return Object.values(QueryLanguage).includes(language as QueryLanguage);
}

View File

@@ -1,7 +1,7 @@
import * as Sarif from "sarif";
import { createReadStream } from "fs-extra";
import { connectTo } from "stream-json/Assembler";
import { getErrorMessage } from "../pure/helpers-pure";
import { getErrorMessage } from "./helpers-pure";
import { withParser } from "stream-json/filters/Pick";
const DUMMY_TOOL: Sarif.Tool = { driver: { name: "" } };

View File

@@ -1,6 +1,6 @@
import * as Sarif from "sarif";
import type { HighlightedRegion } from "../variant-analysis/shared/analysis-result";
import { ResolvableLocationValue } from "./bqrs-cli-types";
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
export interface SarifLink {
dest: number;

View File

@@ -0,0 +1,10 @@
import { RedactableError } from "./errors";
export interface AppTelemetry {
sendCommandUsage(name: string, executionTime: number, error?: Error): void;
sendUIInteraction(name: string): void;
sendError(
error: RedactableError,
extraProperties?: { [key: string]: string },
): void;
}

View File

@@ -9,13 +9,9 @@ import {
} from "vscode";
import { join } from "path";
import { DisposableObject, DisposeHandler } from "../../pure/disposable-object";
import { tmpDir } from "../../helpers";
import {
getHtmlForWebview,
WebviewMessage,
WebviewView,
} from "../../interface-utils";
import { DisposableObject, DisposeHandler } from "../disposable-object";
import { tmpDir } from "../../tmp-dir";
import { getHtmlForWebview, WebviewMessage, WebviewView } from "./webview-html";
export type WebviewPanelConfig = {
viewId: string;
@@ -23,6 +19,7 @@ export type WebviewPanelConfig = {
viewColumn: ViewColumn;
view: WebviewView;
preserveFocus?: boolean;
iconPath?: Uri | { dark: Uri; light: Uri };
additionalOptions?: WebviewPanelOptions & WebviewOptions;
allowWasmEval?: boolean;
};
@@ -90,6 +87,8 @@ export abstract class AbstractWebview<
);
this.panel = panel;
this.panel.iconPath = config.iconPath;
this.setupPanel(panel, config);
this.panelResolves.forEach((resolve) => resolve(panel));

View File

@@ -1,7 +1,7 @@
import { pathExists } from "fs-extra";
import * as unzipper from "unzipper";
import * as vscode from "vscode";
import { extLogger } from "..";
import { extLogger } from "../logging/vscode";
// All path operations in this file must be on paths *within* the zip
// archive.
@@ -14,7 +14,10 @@ export class File implements vscode.FileStat {
mtime: number;
size: number;
constructor(public name: string, public data: Uint8Array) {
constructor(
public name: string,
public data: Uint8Array,
) {
this.type = vscode.FileType.File;
this.ctime = Date.now();
this.mtime = Date.now();

View File

@@ -1,18 +1,20 @@
import { commands, Disposable } from "vscode";
import { CommandFunction, CommandManager } from "../../packages/commands";
import { extLogger, OutputChannelLogger } from "../logging";
import {
NotificationLogger,
showAndLogWarningMessage,
showAndLogExceptionWithTelemetry,
} from "../logging";
import { extLogger } from "../logging/vscode";
import {
asError,
getErrorMessage,
getErrorStack,
} from "../../pure/helpers-pure";
import { redactableError } from "../../pure/errors";
} from "../../common/helpers-pure";
import { redactableError } from "../../common/errors";
import { UserCancellationException } from "./progress";
import {
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
} from "../../helpers";
import { telemetryListener } from "../../telemetry";
import { telemetryListener } from "./telemetry";
import { AppTelemetry } from "../telemetry";
/**
* Create a command manager for VSCode, wrapping registerCommandWithErrorHandling
@@ -20,9 +22,12 @@ import { telemetryListener } from "../../telemetry";
*/
export function createVSCodeCommandManager<
Commands extends Record<string, CommandFunction>,
>(outputLogger?: OutputChannelLogger): CommandManager<Commands> {
>(
logger?: NotificationLogger,
telemetry?: AppTelemetry,
): CommandManager<Commands> {
return new CommandManager((commandId, task) => {
return registerCommandWithErrorHandling(commandId, task, outputLogger);
return registerCommandWithErrorHandling(commandId, task, logger, telemetry);
}, wrapExecuteCommand);
}
@@ -32,11 +37,14 @@ export function createVSCodeCommandManager<
* @param commandId The ID of the command to register.
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
* arguments to the command handler are passed on to the task.
* @param logger The logger to use for error reporting.
* @param telemetry The telemetry listener to use for error reporting.
*/
export function registerCommandWithErrorHandling(
commandId: string,
task: (...args: any[]) => Promise<any>,
outputLogger = extLogger,
logger: NotificationLogger = extLogger,
telemetry: AppTelemetry | undefined = telemetryListener,
): Disposable {
return commands.registerCommand(commandId, async (...args: any[]) => {
const startTime = Date.now();
@@ -52,11 +60,9 @@ export function registerCommandWithErrorHandling(
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
void outputLogger.log(errorMessage.fullMessage);
void logger.log(errorMessage.fullMessage);
} else {
void showAndLogWarningMessage(errorMessage.fullMessage, {
outputLogger,
});
void showAndLogWarningMessage(logger, errorMessage.fullMessage);
}
} else {
// Include the full stack in the error log only.
@@ -64,8 +70,7 @@ export function registerCommandWithErrorHandling(
const fullMessage = errorStack
? `${errorMessage.fullMessage}\n${errorStack}`
: errorMessage.fullMessage;
void showAndLogExceptionWithTelemetry(errorMessage, {
outputLogger,
void showAndLogExceptionWithTelemetry(logger, telemetry, errorMessage, {
fullMessage,
extraTelemetryProperties: {
command: commandId,

View File

@@ -0,0 +1,135 @@
import { env, Uri, window } from "vscode";
/**
* Opens a modal dialog for the user to make a yes/no choice.
*
* @param message The message to show.
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
* be closed even if the user does not make a choice.
* @param yesTitle The text in the box indicating the affirmative choice.
* @param noTitle The text in the box indicating the negative choice.
*
* @return
* `true` if the user clicks 'Yes',
* `false` if the user clicks 'No' or cancels the dialog,
* `undefined` if the dialog is closed without the user making a choice.
*/
export async function showBinaryChoiceDialog(
message: string,
modal = true,
yesTitle = "Yes",
noTitle = "No",
): Promise<boolean | undefined> {
const yesItem = { title: yesTitle, isCloseAffordance: false };
const noItem = { title: noTitle, isCloseAffordance: true };
const chosenItem = await window.showInformationMessage(
message,
{ modal },
yesItem,
noItem,
);
if (!chosenItem) {
return undefined;
}
return chosenItem?.title === yesItem.title;
}
/**
* Opens a modal dialog for the user to make a yes/no choice.
*
* @param message The message to show.
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
* be closed even if the user does not make a choice.
*
* @return
* `true` if the user clicks 'Yes',
* `false` if the user clicks 'No' or cancels the dialog,
* `undefined` if the dialog is closed without the user making a choice.
*/
export async function showBinaryChoiceWithUrlDialog(
message: string,
url: string,
): Promise<boolean | undefined> {
const urlItem = { title: "More Information", isCloseAffordance: false };
const yesItem = { title: "Yes", isCloseAffordance: false };
const noItem = { title: "No", isCloseAffordance: true };
let chosenItem;
// Keep the dialog open as long as the user is clicking the 'more information' option.
// To prevent an infinite loop, if the user clicks 'more information' 5 times, close the dialog and return cancelled
let count = 0;
do {
chosenItem = await window.showInformationMessage(
message,
{ modal: true },
urlItem,
yesItem,
noItem,
);
if (chosenItem === urlItem) {
await env.openExternal(Uri.parse(url, true));
}
count++;
} while (chosenItem === urlItem && count < 5);
if (!chosenItem || chosenItem.title === urlItem.title) {
return undefined;
}
return chosenItem.title === yesItem.title;
}
/**
* Show an information message with a customisable action.
* @param message The message to show.
* @param actionMessage The call to action message.
*
* @return `true` if the user clicks the action, `false` if the user cancels the dialog.
*/
export async function showInformationMessageWithAction(
message: string,
actionMessage: string,
): Promise<boolean> {
const actionItem = { title: actionMessage, isCloseAffordance: false };
const chosenItem = await window.showInformationMessage(message, actionItem);
return chosenItem === actionItem;
}
/**
* Opens a modal dialog for the user to make a choice between yes/no/never be asked again.
*
* @param message The message to show.
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
* be closed even if the user does not make a choice.
* @param yesTitle The text in the box indicating the affirmative choice.
* @param noTitle The text in the box indicating the negative choice.
* @param neverTitle The text in the box indicating the opt out choice.
*
* @return
* `Yes` if the user clicks 'Yes',
* `No` if the user clicks 'No' or cancels the dialog,
* `No, and never ask me again` if the user clicks 'No, and never ask me again',
* `undefined` if the dialog is closed without the user making a choice.
*/
export async function showNeverAskAgainDialog(
message: string,
modal = true,
yesTitle = "Yes",
noTitle = "No",
neverAskAgainTitle = "No, and never ask me again",
): Promise<string | undefined> {
const yesItem = { title: yesTitle, isCloseAffordance: true };
const noItem = { title: noTitle, isCloseAffordance: false };
const neverAskAgainItem = {
title: neverAskAgainTitle,
isCloseAffordance: false,
};
const chosenItem = await window.showInformationMessage(
message,
{ modal },
yesItem,
noItem,
neverAskAgainItem,
);
return chosenItem?.title;
}

View File

@@ -1,15 +1,15 @@
import { Uri, window } from "vscode";
import { AppCommandManager } from "../commands";
import {
showAndLogExceptionWithTelemetry,
showBinaryChoiceDialog,
} from "../../helpers";
import { redactableError } from "../../pure/errors";
import { showBinaryChoiceDialog } from "./dialog";
import { redactableError } from "../../common/errors";
import {
asError,
getErrorMessage,
getErrorStack,
} from "../../pure/helpers-pure";
} from "../../common/helpers-pure";
import { showAndLogExceptionWithTelemetry } from "../logging";
import { extLogger } from "../logging/vscode";
import { telemetryListener } from "./telemetry";
export async function tryOpenExternalFile(
commandManager: AppCommandManager,
@@ -36,6 +36,8 @@ the file in the file explorer and dragging it into the workspace.`,
await commandManager.execute("revealFileInOS", uri);
} catch (e) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError(
asError(e),
)`Failed to reveal file in OS: ${getErrorMessage(e)}`,
@@ -44,6 +46,8 @@ the file in the file explorer and dragging it into the workspace.`,
}
} else {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError(asError(e))`Could not open file ${fileLocation}`,
{
fullMessage: `${getErrorMessage(e)}\n${getErrorStack(e)}`,

View File

@@ -0,0 +1,272 @@
import { Discovery } from "../discovery";
import {
Event,
EventEmitter,
RelativePattern,
Uri,
WorkspaceFoldersChangeEvent,
workspace,
} from "vscode";
import { MultiFileSystemWatcher } from "./multi-file-system-watcher";
import { AppEventEmitter } from "../events";
import { extLogger } from "../logging/vscode";
import { lstat } from "fs-extra";
import { containsPath, isIOError } from "../files";
import {
getOnDiskWorkspaceFolders,
getOnDiskWorkspaceFoldersObjects,
} from "./workspace-folders";
import { getErrorMessage } from "../../common/helpers-pure";
interface PathData {
path: string;
}
/**
* Discovers and watches for changes to all files matching a given filter
* contained in the workspace. Also allows computing extra data about each
* file path, and only recomputing the data when the file changes.
*
* Scans the whole workspace on startup, and then watches for changes to files
* to do the minimum work to keep up with changes.
*
* Can configure which changes it watches for, which files are considered
* relevant, and what extra data to compute for each file.
*/
export abstract class FilePathDiscovery<T extends PathData> extends Discovery {
/**
* Has `discover` been called. This allows distinguishing between
* "no paths found" and not having scanned yet.
*/
private discoverHasCompletedOnce = false;
/** The set of known paths and associated data that we are tracking */
private pathData: T[] = [];
/** Event that fires whenever the contents of `pathData` changes */
private readonly onDidChangePathDataEmitter: AppEventEmitter<void>;
/**
* The set of file paths that may have changed on disk since the last time
* refresh was run. Whenever a watcher reports some change to a file we add
* it to this set, and then during the next refresh we will process all
* file paths from this set and update our internal state to match whatever
* we find on disk (i.e. the file exists, doesn't exist, computed data has
* changed).
*/
private readonly changedFilePaths = new Set<string>();
/**
* Watches for changes to files and directories in all workspace folders.
*/
private readonly watcher: MultiFileSystemWatcher = this.push(
new MultiFileSystemWatcher(),
);
/**
* @param name Name of the discovery operation, for logging purposes.
* @param fileWatchPattern Passed to `vscode.RelativePattern` to determine the files to watch for changes to.
*/
constructor(
name: string,
private readonly fileWatchPattern: string,
) {
super(name, extLogger);
this.onDidChangePathDataEmitter = this.push(new EventEmitter<void>());
this.push(
workspace.onDidChangeWorkspaceFolders(
this.workspaceFoldersChanged.bind(this),
),
);
this.push(this.watcher.onDidChange(this.fileChanged.bind(this)));
}
protected getPathData(): ReadonlyArray<Readonly<T>> | undefined {
if (!this.discoverHasCompletedOnce) {
return undefined;
}
return this.pathData;
}
protected get onDidChangePathData(): Event<void> {
return this.onDidChangePathDataEmitter.event;
}
/**
* Compute any extra data to be stored regarding the given path.
*/
protected abstract getDataForPath(path: string): Promise<T>;
/**
* Is the given path relevant to this discovery operation?
*/
protected abstract pathIsRelevant(path: string): boolean;
/**
* Should the given new data overwrite the existing data we have stored?
*/
protected abstract shouldOverwriteExistingData(
newData: T,
existingData: T,
): boolean;
/**
* Update the data for every path by calling `getDataForPath`.
*/
protected async recomputeAllData() {
this.pathData = await Promise.all(
this.pathData.map((p) => this.getDataForPath(p.path)),
);
this.onDidChangePathDataEmitter.fire();
}
/**
* Do the initial scan of the entire workspace and set up watchers for future changes.
*/
public async initialRefresh() {
getOnDiskWorkspaceFolders().forEach((workspaceFolder) => {
this.changedFilePaths.add(workspaceFolder);
});
this.updateWatchers();
await this.refresh();
this.onDidChangePathDataEmitter.fire();
}
private workspaceFoldersChanged(event: WorkspaceFoldersChangeEvent) {
event.added.forEach((workspaceFolder) => {
this.changedFilePaths.add(workspaceFolder.uri.fsPath);
});
event.removed.forEach((workspaceFolder) => {
this.changedFilePaths.add(workspaceFolder.uri.fsPath);
});
this.updateWatchers();
void this.refresh();
}
private updateWatchers() {
this.watcher.clear();
for (const workspaceFolder of getOnDiskWorkspaceFoldersObjects()) {
// Watch for changes to individual files
this.watcher.addWatch(
new RelativePattern(workspaceFolder, this.fileWatchPattern),
);
// need to explicitly watch for changes to directories themselves.
this.watcher.addWatch(new RelativePattern(workspaceFolder, "**/"));
}
}
private fileChanged(uri: Uri) {
this.changedFilePaths.add(uri.fsPath);
void this.refresh();
}
protected async discover() {
let pathsUpdated = false;
for (const path of this.changedFilePaths) {
try {
this.changedFilePaths.delete(path);
if (await this.handleChangedPath(path)) {
pathsUpdated = true;
}
} catch (e) {
// If we get an error while processing a path, just log it and continue.
// There aren't any network operations happening here or anything else
// that's likely to succeed on a retry, so don't bother adding it back
// to the changedFilePaths set.
void extLogger.log(
`${
this.name
} failed while processing path "${path}": ${getErrorMessage(e)}`,
);
}
}
this.discoverHasCompletedOnce = true;
if (pathsUpdated) {
this.onDidChangePathDataEmitter.fire();
}
}
private async handleChangedPath(path: string): Promise<boolean> {
try {
// If the path is not in the workspace then we don't want to be
// tracking or displaying it, so treat it as if it doesn't exist.
if (!this.pathIsInWorkspace(path)) {
return this.handleRemovedPath(path);
}
if ((await lstat(path)).isDirectory()) {
return await this.handleChangedDirectory(path);
} else {
return this.handleChangedFile(path);
}
} catch (e) {
if (isIOError(e) && e.code === "ENOENT") {
return this.handleRemovedPath(path);
}
throw e;
}
}
private pathIsInWorkspace(path: string): boolean {
return getOnDiskWorkspaceFolders().some((workspaceFolder) =>
containsPath(workspaceFolder, path),
);
}
private handleRemovedPath(path: string): boolean {
const oldLength = this.pathData.length;
this.pathData = this.pathData.filter(
(existingPathData) => !containsPath(path, existingPathData.path),
);
return this.pathData.length !== oldLength;
}
private async handleChangedDirectory(path: string): Promise<boolean> {
const newPaths = await workspace.findFiles(
new RelativePattern(path, this.fileWatchPattern),
);
let pathsUpdated = false;
for (const path of newPaths) {
if (await this.addOrUpdatePath(path.fsPath)) {
pathsUpdated = true;
}
}
return pathsUpdated;
}
private async handleChangedFile(path: string): Promise<boolean> {
if (this.pathIsRelevant(path)) {
return await this.addOrUpdatePath(path);
} else {
return false;
}
}
private async addOrUpdatePath(path: string): Promise<boolean> {
const data = await this.getDataForPath(path);
const existingPathDataIndex = this.pathData.findIndex(
(existingPathData) => existingPathData.path === path,
);
if (existingPathDataIndex !== -1) {
if (
this.shouldOverwriteExistingData(
data,
this.pathData[existingPathDataIndex],
)
) {
this.pathData.splice(existingPathDataIndex, 1, data);
return true;
} else {
return false;
}
} else {
this.pathData.push(data);
return true;
}
}
}

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from "../../pure/disposable-object";
import { DisposableObject } from "../disposable-object";
import { EventEmitter, Event, Uri, GlobPattern, workspace } from "vscode";
/**

View File

@@ -10,7 +10,10 @@ export class UserCancellationException extends Error {
* @param message The error message
* @param silent If silent is true, then this exception will avoid showing a warning message to the user.
*/
constructor(message?: string, public readonly silent = false) {
constructor(
message?: string,
public readonly silent = false,
) {
super(message);
}
}

View File

@@ -1,9 +1,9 @@
import { showAndLogErrorMessage } from "../../helpers";
import {
ExplorerSelectionCommandFunction,
TreeViewContextMultiSelectionCommandFunction,
TreeViewContextSingleSelectionCommandFunction,
} from "../commands";
import { showAndLogErrorMessage, NotificationLogger } from "../logging";
// A hack to match types that are not an array, which is useful to help avoid
// misusing createSingleSelectionCommand, e.g. where T accidentally gets instantiated
@@ -25,6 +25,7 @@ type SelectionCommand<T extends NotArray> = CreateSupertypeOf<
>;
export function createSingleSelectionCommand<T extends NotArray>(
logger: NotificationLogger,
f: (argument: T) => Promise<void>,
itemName: string,
): SelectionCommand<T> {
@@ -32,7 +33,10 @@ export function createSingleSelectionCommand<T extends NotArray>(
if (multiSelect === undefined || multiSelect.length === 1) {
return f(singleItem);
} else {
void showAndLogErrorMessage(`Please select a single ${itemName}.`);
void showAndLogErrorMessage(
logger,
`Please select a single ${itemName}.`,
);
return;
}
};

View File

@@ -13,13 +13,14 @@ import {
LOG_TELEMETRY,
isIntegrationTestMode,
isCanary,
} from "./config";
} from "../../config";
import * as appInsights from "applicationinsights";
import { extLogger } from "./common";
import { UserCancellationException } from "./common/vscode/progress";
import { showBinaryChoiceWithUrlDialog } from "./helpers";
import { RedactableError } from "./pure/errors";
import { extLogger } from "../logging/vscode";
import { UserCancellationException } from "./progress";
import { showBinaryChoiceWithUrlDialog } from "./dialog";
import { RedactableError } from "../errors";
import { SemVer } from "semver";
import { AppTelemetry } from "../telemetry";
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
const key = "REPLACE-APP-INSIGHTS-KEY";
@@ -54,7 +55,10 @@ const baseDataPropertiesToRemove = [
const NOT_SET_CLI_VERSION = "not-set";
export class TelemetryListener extends ConfigListener {
export class ExtensionTelemetryListener
extends ConfigListener
implements AppTelemetry
{
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
private reporter?: TelemetryReporter;
@@ -152,7 +156,7 @@ export class TelemetryListener extends ConfigListener {
void this.reporter?.dispose();
}
sendCommandUsage(name: string, executionTime: number, error?: Error) {
sendCommandUsage(name: string, executionTime: number, error?: Error): void {
if (!this.reporter) {
return;
}
@@ -174,7 +178,7 @@ export class TelemetryListener extends ConfigListener {
);
}
sendUIInteraction(name: string) {
sendUIInteraction(name: string): void {
if (!this.reporter) {
return;
}
@@ -193,7 +197,7 @@ export class TelemetryListener extends ConfigListener {
sendError(
error: RedactableError,
extraProperties?: { [key: string]: string },
) {
): void {
if (!this.reporter) {
return;
}
@@ -272,16 +276,16 @@ export class TelemetryListener extends ConfigListener {
/**
* The global Telemetry instance
*/
export let telemetryListener: TelemetryListener | undefined;
export let telemetryListener: ExtensionTelemetryListener | undefined;
export async function initializeTelemetry(
extension: Extension<any>,
ctx: ExtensionContext,
): Promise<TelemetryListener> {
): Promise<ExtensionTelemetryListener> {
if (telemetryListener !== undefined) {
throw new Error("Telemetry is already initialized");
}
telemetryListener = new TelemetryListener(
telemetryListener = new ExtensionTelemetryListener(
extension.id,
extension.packageJSON.version,
key,

View File

@@ -1,14 +1,17 @@
import * as vscode from "vscode";
import { VSCodeCredentials } from "./authentication";
import { Disposable } from "../../pure/disposable-object";
import { Disposable } from "../disposable-object";
import { App, AppMode, EnvironmentContext } from "../app";
import { AppEventEmitter } from "../events";
import { extLogger, Logger, queryServerLogger } from "../logging";
import { NotificationLogger } from "../logging";
import { extLogger, queryServerLogger } from "../logging/vscode";
import { Memento } from "../memento";
import { VSCodeAppEventEmitter } from "./events";
import { AppCommandManager, QueryServerCommandManager } from "../commands";
import { createVSCodeCommandManager } from "./commands";
import { AppEnvironmentContext } from "./environment-context";
import { AppTelemetry } from "../telemetry";
import { telemetryListener } from "./telemetry";
export class ExtensionApp implements App {
public readonly credentials: VSCodeCredentials;
@@ -40,14 +43,6 @@ export class ExtensionApp implements App {
return this.extensionContext.workspaceState;
}
public get workspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined {
return vscode.workspace.workspaceFolders;
}
public get onDidChangeWorkspaceFolders(): vscode.Event<vscode.WorkspaceFoldersChangeEvent> {
return vscode.workspace.onDidChangeWorkspaceFolders;
}
public get subscriptions(): Disposable[] {
return this.extensionContext.subscriptions;
}
@@ -63,10 +58,14 @@ export class ExtensionApp implements App {
}
}
public get logger(): Logger {
public get logger(): NotificationLogger {
return extLogger;
}
public get telemetry(): AppTelemetry | undefined {
return telemetryListener;
}
public createEventEmitter<T>(): AppEventEmitter<T> {
return new VSCodeAppEventEmitter<T>();
}

View File

@@ -0,0 +1,101 @@
import { ExtensionContext, Uri, Webview } from "vscode";
import { randomBytes } from "crypto";
import { EOL } from "os";
export type WebviewView =
| "results"
| "compare"
| "variant-analysis"
| "data-flow-paths"
| "data-extensions-editor";
export interface WebviewMessage {
t: string;
}
/**
* Returns HTML to populate the given webview.
* Uses a content security policy that only loads the given script.
*/
export function getHtmlForWebview(
ctx: ExtensionContext,
webview: Webview,
view: WebviewView,
{
allowInlineStyles,
allowWasmEval,
}: {
allowInlineStyles?: boolean;
allowWasmEval?: boolean;
} = {
allowInlineStyles: false,
allowWasmEval: false,
},
): string {
const scriptUriOnDisk = Uri.file(ctx.asAbsolutePath("out/webview.js"));
const stylesheetUrisOnDisk = [
Uri.file(ctx.asAbsolutePath("out/webview.css")),
];
// Convert the on-disk URIs into webview URIs.
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
const stylesheetWebviewUris = stylesheetUrisOnDisk.map(
(stylesheetUriOnDisk) => webview.asWebviewUri(stylesheetUriOnDisk),
);
// Use a nonce in the content security policy to uniquely identify the above resources.
const nonce = getNonce();
const stylesheetsHtmlLines = allowInlineStyles
? stylesheetWebviewUris.map((uri) => createStylesLinkWithoutNonce(uri))
: stylesheetWebviewUris.map((uri) => createStylesLinkWithNonce(nonce, uri));
const styleSrc = allowInlineStyles
? `${webview.cspSource} vscode-file: 'unsafe-inline'`
: `'nonce-${nonce}'`;
const fontSrc = webview.cspSource;
/*
* Content security policy:
* default-src: allow nothing by default.
* script-src:
* - allow the given script, using the nonce.
* - 'wasm-unsafe-eval: allow loading WebAssembly modules if necessary.
* style-src: allow only the given stylesheet, using the nonce.
* connect-src: only allow fetch calls to webview resource URIs
* (this is used to load BQRS result files).
*/
return `
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'${
allowWasmEval ? " 'wasm-unsafe-eval'" : ""
}; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${
webview.cspSource
};">
${stylesheetsHtmlLines.join(` ${EOL}`)}
</head>
<body>
<div id=root data-view="${view}">
</div>
<script nonce="${nonce}" src="${scriptWebviewUri}">
</script>
</body>
</html>`;
}
/** Gets a nonce string created with 128 bits of entropy. */
function getNonce(): string {
return randomBytes(16).toString("base64");
}
function createStylesLinkWithNonce(nonce: string, uri: Uri): string {
return `<link nonce="${nonce}" rel="stylesheet" href="${uri}">`;
}
function createStylesLinkWithoutNonce(uri: Uri): string {
return `<link rel="stylesheet" href="${uri}">`;
}

View File

@@ -0,0 +1,64 @@
import { dirname, join } from "path";
import { workspace, WorkspaceFolder } from "vscode";
/** Returns true if the specified workspace folder is on the file system. */
export function isWorkspaceFolderOnDisk(
workspaceFolder: WorkspaceFolder,
): boolean {
return workspaceFolder.uri.scheme === "file";
}
/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFoldersObjects() {
const workspaceFolders = workspace.workspaceFolders ?? [];
return workspaceFolders.filter(isWorkspaceFolderOnDisk);
}
/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFolders() {
return getOnDiskWorkspaceFoldersObjects().map((folder) => folder.uri.fsPath);
}
/** Check if folder is already present in workspace */
export function isFolderAlreadyInWorkspace(folderName: string) {
const workspaceFolders = workspace.workspaceFolders || [];
return !!workspaceFolders.find(
(workspaceFolder) => workspaceFolder.name === folderName,
);
}
/**
* Returns the path of the first folder in the workspace.
* This is used to decide where to create skeleton QL packs.
*
* If the first folder is a QL pack, then the parent folder is returned.
* This is because the vscode-codeql-starter repo contains a ql pack in
* the first folder.
*
* This is a temporary workaround until we can retire the
* vscode-codeql-starter repo.
*/
export function getFirstWorkspaceFolder() {
const workspaceFolders = getOnDiskWorkspaceFolders();
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error("No workspace folders found");
}
const firstFolderFsPath = workspaceFolders[0];
// For the vscode-codeql-starter repo, the first folder will be a ql pack
// so we need to get the parent folder
if (
firstFolderFsPath.includes(
join("vscode-codeql-starter", "codeql-custom-queries"),
)
) {
// return the parent folder
return dirname(firstFolderFsPath);
} else {
// if the first folder is not a ql pack, then we are in a normal workspace
return firstFolderFsPath;
}
}

View File

@@ -7,8 +7,9 @@ export function pluralize(
numItems: number | undefined,
singular: string,
plural: string,
numberFormatter: (value: number) => string = (value) => value.toString(),
): string {
return numItems !== undefined
? `${numItems} ${numItems === 1 ? singular : plural}`
? `${numberFormatter(numItems)} ${numItems === 1 ? singular : plural}`
: "";
}

View File

@@ -4,27 +4,27 @@ import {
FromCompareViewMessage,
ToCompareViewMessage,
QueryCompareResult,
} from "../pure/interface-types";
import { Logger } from "../common";
} from "../common/interface-types";
import { Logger, showAndLogExceptionWithTelemetry } from "../common/logging";
import { extLogger } from "../common/logging/vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { DatabaseManager } from "../databases/local-databases";
import { jumpToLocation } from "../interface-utils";
import { jumpToLocation } from "../databases/local-databases/locations";
import {
transformBqrsResultSet,
RawResultSet,
BQRSInfo,
} from "../pure/bqrs-cli-types";
} from "../common/bqrs-cli-types";
import resultsDiff from "./resultsDiff";
import { CompletedLocalQueryInfo } from "../query-results";
import { assertNever, getErrorMessage } from "../pure/helpers-pure";
import { assertNever, getErrorMessage } from "../common/helpers-pure";
import { HistoryItemLabelProvider } from "../query-history/history-item-label-provider";
import {
AbstractWebview,
WebviewPanelConfig,
} from "../common/vscode/abstract-webview";
import { telemetryListener } from "../telemetry";
import { redactableError } from "../pure/errors";
import { showAndLogExceptionWithTelemetry } from "../helpers";
import { telemetryListener } from "../common/vscode/telemetry";
import { redactableError } from "../common/errors";
interface ComparePair {
from: CompletedLocalQueryInfo;
@@ -130,7 +130,12 @@ export class CompareView extends AbstractWebview<
break;
case "viewSourceFile":
await jumpToLocation(msg, this.databaseManager, this.logger);
await jumpToLocation(
msg.databaseUri,
msg.loc,
this.databaseManager,
this.logger,
);
break;
case "openQuery":
@@ -146,6 +151,8 @@ export class CompareView extends AbstractWebview<
case "unhandledError":
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError(
msg.error,
)`Unhandled error in result comparison view: ${msg.error.message}`,

View File

@@ -1,5 +1,5 @@
import { RawResultSet } from "../pure/bqrs-cli-types";
import { QueryCompareResult } from "../pure/interface-types";
import { RawResultSet } from "../common/bqrs-cli-types";
import { QueryCompareResult } from "../common/interface-types";
/**
* Compare the rows of two queries. Use deep equality to determine if

View File

@@ -1,19 +1,20 @@
import { DisposableObject } from "./pure/disposable-object";
import { DisposableObject } from "./common/disposable-object";
import {
workspace,
Event,
EventEmitter,
ConfigurationChangeEvent,
ConfigurationTarget,
ConfigurationScope,
} from "vscode";
import { DistributionManager } from "./codeql-cli/distribution";
import { extLogger } from "./common";
import { ONE_DAY_IN_MS } from "./pure/time";
import { extLogger } from "./common/logging/vscode";
import { ONE_DAY_IN_MS } from "./common/time";
import {
FilterKey,
SortKey,
defaultFilterSortState,
} from "./pure/variant-analysis-filter-sort";
} from "./variant-analysis/shared/variant-analysis-filter-sort";
export const ALL_SETTINGS: Setting[] = [];
@@ -44,12 +45,12 @@ export class Setting {
}
}
getValue<T>(): T {
getValue<T>(scope?: ConfigurationScope | null): T {
if (this.parent === undefined) {
throw new Error("Cannot get the value of a root setting.");
}
return workspace
.getConfiguration(this.parent.qualifiedName)
.getConfiguration(this.parent.qualifiedName, scope)
.get<T>(this.name)!;
}
@@ -69,6 +70,12 @@ export interface InspectionResult<T> {
workspaceFolderValue?: T;
}
const VSCODE_DEBUG_SETTING = new Setting("debug", undefined);
export const VSCODE_SAVE_BEFORE_START_SETTING = new Setting(
"saveBeforeStart",
VSCODE_DEBUG_SETTING,
);
const ROOT_SETTING = new Setting("codeQL");
// Global configuration
@@ -160,10 +167,6 @@ export const NUMBER_OF_TEST_THREADS_SETTING = new Setting(
RUNNING_TESTS_SETTING,
);
export const MAX_QUERIES = new Setting("maxQueries", RUNNING_QUERIES_SETTING);
export const AUTOSAVE_SETTING = new Setting(
"autoSave",
RUNNING_QUERIES_SETTING,
);
export const PAGE_SIZE = new Setting("pageSize", RESULTS_DISPLAY_SETTING);
const CUSTOM_LOG_DIRECTORY_SETTING = new Setting(
"customLogDirectory",
@@ -692,7 +695,7 @@ const AUTOGENERATE_QL_PACKS = new Setting(
);
const AutogenerateQLPacksValues = ["ask", "never"] as const;
type AutogenerateQLPacks = typeof AutogenerateQLPacksValues[number];
type AutogenerateQLPacks = (typeof AutogenerateQLPacksValues)[number];
export function getAutogenerateQlPacks(): AutogenerateQLPacks {
const value = AUTOGENERATE_QL_PACKS.getValue<AutogenerateQLPacks>();
@@ -714,7 +717,30 @@ export function showQueriesPanel(): boolean {
const DATA_EXTENSIONS = new Setting("dataExtensions", ROOT_SETTING);
const LLM_GENERATION = new Setting("llmGeneration", DATA_EXTENSIONS);
const FRAMEWORK_MODE = new Setting("frameworkMode", DATA_EXTENSIONS);
const DISABLE_AUTO_NAME_EXTENSION_PACK = new Setting(
"disableAutoNameExtensionPack",
DATA_EXTENSIONS,
);
const EXTENSIONS_DIRECTORY = new Setting(
"extensionsDirectory",
DATA_EXTENSIONS,
);
export function showLlmGeneration(): boolean {
return !!LLM_GENERATION.getValue<boolean>();
}
export function enableFrameworkMode(): boolean {
return !!FRAMEWORK_MODE.getValue<boolean>();
}
export function disableAutoNameExtensionPack(): boolean {
return !!DISABLE_AUTO_NAME_EXTENSION_PACK.getValue<boolean>();
}
export function getExtensionsDirectory(languageId: string): string | undefined {
return EXTENSIONS_DIRECTORY.getValue<string>({
languageId,
});
}

View File

@@ -34,7 +34,7 @@ export interface ModelRequest {
export interface ModelResponse {
language: string;
predicted: Method[];
predicted?: Method[];
}
export async function autoModel(

View File

@@ -32,7 +32,7 @@ export async function getAutoModelUsages({
// This will re-run the query that was already run when opening the data extensions editor. This
// might be unnecessary, but this makes it really easy to get the path to the BQRS file which we
// need to interpret the results.
const queryResult = await runQuery({
const queryResult = await runQuery("applicationModeQuery", {
cliServer,
queryRunner,
queryStorageDir,

View File

@@ -7,12 +7,21 @@ import {
ModelRequest,
} from "./auto-model-api";
import type { UsageSnippetsBySignature } from "./auto-model-usages-query";
import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
import { Mode } from "./shared/mode";
// Soft limit on the number of candidates to send to the model.
// Note that the model may return fewer than this number of candidates.
const candidateLimit = 20;
// Soft limit on the number of samples to send to the model.
const sampleLimit = 100;
export function createAutoModelRequest(
language: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
usages: UsageSnippetsBySignature,
mode: Mode,
): ModelRequest {
const request: ModelRequest = {
language,
@@ -20,11 +29,14 @@ export function createAutoModelRequest(
candidates: [],
};
// Sort by number of usages so we always send the most used methods first
externalApiUsages = [...externalApiUsages];
externalApiUsages.sort((a, b) => b.usages.length - a.usages.length);
// Sort the same way as the UI so we send the first ones listed in the UI first
const grouped = groupMethods(externalApiUsages, mode);
const sortedGroupNames = sortGroupNames(grouped);
const sortedExternalApiUsages = sortedGroupNames.flatMap((name) =>
sortMethods(grouped[name]),
);
for (const externalApiUsage of externalApiUsages) {
for (const externalApiUsage of sortedExternalApiUsages) {
const modeledMethod: ModeledMethod = modeledMethods[
externalApiUsage.signature
] ?? {
@@ -40,11 +52,15 @@ export function createAutoModelRequest(
? 0
: externalApiUsage.methodParameters.split(",").length;
const candidates: Method[] = [];
const samples: Method[] = [];
for (
let argumentIndex = 0;
let argumentIndex = -1; // Start at -1 which means `this` as in `this.method()`
argumentIndex < numberOfArguments;
argumentIndex++
) {
const argumentInput: string =
argumentIndex === -1 ? "Argument[this]" : `Argument[${argumentIndex}]`;
const method: Method = {
package: externalApiUsage.packageName,
type: externalApiUsage.typeName,
@@ -54,21 +70,35 @@ export function createAutoModelRequest(
modeledMethod.type === "none"
? undefined
: toMethodClassification(modeledMethod),
usages: usagesForMethod.slice(0, 10),
input: `Argument[${argumentIndex}]`,
usages: usagesForMethod.slice(0, 6), // At most 6 usages per argument
input: argumentInput,
};
// A method that is supported is modeled outside of the model file, so it is not a candidate.
// We also do not want it as a sample because we do not know the classification.
if (modeledMethod.type === "none" && externalApiUsage.supported) {
continue;
}
// Candidates are methods that are not currently modeled
if (modeledMethod.type === "none") {
request.candidates.push(method);
candidates.push(method);
} else {
request.samples.push(method);
samples.push(method);
}
}
// If there is room for at least one candidate, add all candidates.
// This ensures that we send all arguments for a method together.
// NOTE: this might go above the candidate limit, but that's okay.
if (request.candidates.length < candidateLimit) {
request.candidates.push(...candidates);
}
// Same for samples
if (request.samples.length < sampleLimit) {
request.samples.push(...samples);
}
}
request.candidates = request.candidates.slice(0, 20);
request.samples = request.samples.slice(0, 100);
return request;
}
@@ -110,6 +140,12 @@ export function parsePredictedClassifications(
input: "",
output: "",
provenance: "ai-generated",
signature,
// predictedBySignature[signature] always has at least element
packageName: predictedMethods[0].package,
typeName: predictedMethods[0].type,
methodName: predictedMethods[0].name,
methodParameters: predictedMethods[0].signature,
};
continue;
}
@@ -127,6 +163,11 @@ export function parsePredictedClassifications(
input: sink.input ?? "",
output: sink.output ?? "",
provenance: "ai-generated",
signature,
packageName: sink.package,
typeName: sink.type,
methodName: sink.name,
methodParameters: sink.signature,
};
}

View File

@@ -1,5 +1,11 @@
import { DecodedBqrsChunk } from "../pure/bqrs-cli-types";
import { Call, ExternalApiUsage } from "./external-api-usage";
import { DecodedBqrsChunk } from "../common/bqrs-cli-types";
import {
Call,
CallClassification,
ExternalApiUsage,
} from "./external-api-usage";
import { ModeledMethodType } from "./modeled-method";
import { parseLibraryFilename } from "./library";
export function decodeBqrsToExternalApiUsages(
chunk: DecodedBqrsChunk,
@@ -10,6 +16,10 @@ export function decodeBqrsToExternalApiUsages(
const usage = tuple[0] as Call;
const signature = tuple[1] as string;
const supported = (tuple[2] as string) === "true";
let library = tuple[4] as string;
let libraryVersion: string | undefined = tuple[5] as string;
const type = tuple[6] as ModeledMethodType;
const classification = tuple[8] as CallClassification;
const [packageWithType, methodDeclaration] = signature.split("#");
@@ -29,33 +39,42 @@ export function decodeBqrsToExternalApiUsages(
methodDeclaration.indexOf("("),
);
// 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
// for Java.
if (library.endsWith(".jar") || libraryVersion === "") {
const { name, version } = parseLibraryFilename(library);
library = name;
if (version) {
libraryVersion = version;
}
}
if (libraryVersion === "") {
libraryVersion = undefined;
}
if (!methodsByApiName.has(signature)) {
methodsByApiName.set(signature, {
library,
libraryVersion,
signature,
packageName,
typeName,
methodName,
methodParameters,
supported,
supportedType: type,
usages: [],
});
}
const method = methodsByApiName.get(signature)!;
method.usages.push(usage);
method.usages.push({
...usage,
classification,
});
});
const externalApiUsages = Array.from(methodsByApiName.values());
externalApiUsages.sort((a, b) => {
// Sort first by supported, putting unmodeled methods first.
if (a.supported && !b.supported) {
return 1;
}
if (!a.supported && b.supported) {
return -1;
}
// Then sort by number of usages descending
return b.usages.length - a.usages.length;
});
return externalApiUsages;
return Array.from(methodsByApiName.values());
}

View File

@@ -7,9 +7,9 @@ import { DatabaseManager } from "../databases/local-databases";
import { ensureDir } from "fs-extra";
import { join } from "path";
import { App } from "../common/app";
import { showAndLogErrorMessage } from "../helpers";
import { withProgress } from "../common/vscode/progress";
import { pickExtensionPackModelFile } from "./extension-pack-picker";
import { pickExtensionPack } from "./extension-pack-picker";
import { showAndLogErrorMessage } from "../common/logging";
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
@@ -56,12 +56,13 @@ export class DataExtensionsEditorModule {
"codeQL.openDataExtensionsEditor": async () => {
const db = this.databaseManager.currentDatabaseItem;
if (!db) {
void showAndLogErrorMessage("No database selected");
void showAndLogErrorMessage(this.app.logger, "No database selected");
return;
}
if (!SUPPORTED_LANGUAGES.includes(db.language)) {
void showAndLogErrorMessage(
this.app.logger,
`The data extensions editor is not supported for ${db.language} databases.`,
);
return;
@@ -71,14 +72,16 @@ export class DataExtensionsEditorModule {
async (progress, token) => {
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;
}
const modelFile = await pickExtensionPackModelFile(
const modelFile = await pickExtensionPack(
this.cliServer,
db,
this.app.logger,
progress,
token,
);

View File

@@ -4,8 +4,8 @@ import {
Uri,
ViewColumn,
window,
workspace,
} from "vscode";
import { join } from "path";
import { RequestError } from "@octokit/request-error";
import {
AbstractWebview,
@@ -14,38 +14,43 @@ import {
import {
FromDataExtensionsEditorMessage,
ToDataExtensionsEditorMessage,
} from "../pure/interface-types";
} from "../common/interface-types";
import { ProgressUpdate } from "../common/vscode/progress";
import { QueryRunner } from "../query-server";
import {
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { extLogger } from "../common";
import { outputFile, pathExists, readFile } from "fs-extra";
showAndLogErrorMessage,
} from "../common/logging";
import { outputFile, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
import { asError, assertNever, getErrorMessage } from "../common/helpers-pure";
import { generateFlowModel } from "./generate-flow-model";
import { promptImportGithubDatabase } from "../databases/database-fetcher";
import { App } from "../common/app";
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
import { showResolvableLocation } from "../interface-utils";
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
import { showResolvableLocation } from "../databases/local-databases/locations";
import { decodeBqrsToExternalApiUsages } from "./bqrs";
import { redactableError } from "../pure/errors";
import { redactableError } from "../common/errors";
import { readQueryResults, runQuery } from "./external-api-usage-query";
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
import {
createDataExtensionYamlsForApplicationMode,
createDataExtensionYamlsForFrameworkMode,
loadDataExtensionYaml,
} from "./yaml";
import { ExternalApiUsage } from "./external-api-usage";
import { ModeledMethod } from "./modeled-method";
import { ExtensionPackModelFile } from "./shared/extension-pack";
import { ExtensionPack } from "./shared/extension-pack";
import { autoModel, ModelRequest, ModelResponse } from "./auto-model-api";
import {
createAutoModelRequest,
parsePredictedClassifications,
} from "./auto-model";
import { showLlmGeneration } from "../config";
import { enableFrameworkMode, showLlmGeneration } from "../config";
import { getAutoModelUsages } from "./auto-model-usages-query";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { Mode } from "./shared/mode";
export class DataExtensionsEditorView extends AbstractWebview<
ToDataExtensionsEditorMessage,
@@ -59,7 +64,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
private readonly queryRunner: QueryRunner,
private readonly queryStorageDir: string,
private readonly databaseItem: DatabaseItem,
private readonly modelFile: ExtensionPackModelFile,
private readonly extensionPack: ExtensionPack,
private mode: Mode = Mode.Application,
) {
super(ctx);
}
@@ -78,6 +84,14 @@ export class DataExtensionsEditorView extends AbstractWebview<
viewColumn: ViewColumn.Active,
preserveFocus: true,
view: "data-extensions-editor",
iconPath: {
dark: Uri.file(
join(this.ctx.extensionPath, "media/dark/symbol-misc.svg"),
),
light: Uri.file(
join(this.ctx.extensionPath, "media/light/symbol-misc.svg"),
),
},
};
}
@@ -96,14 +110,12 @@ export class DataExtensionsEditorView extends AbstractWebview<
case "openExtensionPack":
await this.app.commands.execute(
"revealInExplorer",
Uri.file(this.modelFile.extensionPack.path),
Uri.file(this.extensionPack.path),
);
break;
case "openModelFile":
await window.showTextDocument(
await workspace.openTextDocument(this.modelFile.filename),
);
case "refreshExternalApiUsages":
await this.loadExternalApiUsages();
break;
case "jumpToUsage":
@@ -128,6 +140,12 @@ export class DataExtensionsEditorView extends AbstractWebview<
msg.modeledMethods,
);
break;
case "switchMode":
this.mode = msg.mode;
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);
break;
default:
assertNever(msg);
@@ -148,9 +166,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
await this.postMessage({
t: "setDataExtensionEditorViewState",
viewState: {
extensionPackModelFile: this.modelFile,
modelFileExists: await pathExists(this.modelFile.filename),
extensionPack: this.extensionPack,
enableFrameworkMode: enableFrameworkMode(),
showLlmButton: showLlmGeneration(),
mode: this.mode,
},
});
}
@@ -167,10 +186,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
"Original file of this result is not in the database's source archive.",
);
} else {
void extLogger.log(`Unable to handleMsgFromView: ${e.message}`);
void this.app.logger.log(`Unable to handleMsgFromView: ${e.message}`);
}
} else {
void extLogger.log(`Unable to handleMsgFromView: ${e}`);
void this.app.logger.log(`Unable to handleMsgFromView: ${e}`);
}
}
}
@@ -179,49 +198,80 @@ export class DataExtensionsEditorView extends AbstractWebview<
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
): Promise<void> {
const yaml = createDataExtensionYaml(
this.databaseItem.language,
externalApiUsages,
modeledMethods,
);
let yamls: Record<string, string>;
switch (this.mode) {
case Mode.Application:
yamls = createDataExtensionYamlsForApplicationMode(
this.databaseItem.language,
externalApiUsages,
modeledMethods,
);
break;
case Mode.Framework:
yamls = createDataExtensionYamlsForFrameworkMode(
this.databaseItem.name,
this.databaseItem.language,
externalApiUsages,
modeledMethods,
);
break;
default:
assertNever(this.mode);
}
await outputFile(this.modelFile.filename, yaml);
for (const [filename, yaml] of Object.entries(yamls)) {
await outputFile(join(this.extensionPack.path, filename), yaml);
}
void extLogger.log(
`Saved data extension YAML to ${this.modelFile.filename}`,
);
void this.app.logger.log(`Saved data extension YAML`);
}
protected async loadExistingModeledMethods(): Promise<void> {
try {
if (!(await pathExists(this.modelFile.filename))) {
return;
const extensions = await this.cliServer.resolveExtensions(
this.extensionPack.path,
getOnDiskWorkspaceFolders(),
);
const modelFiles = new Set<string>();
if (this.extensionPack.path in extensions.data) {
for (const extension of extensions.data[this.extensionPack.path]) {
modelFiles.add(extension.file);
}
}
const yaml = await readFile(this.modelFile.filename, "utf8");
const existingModeledMethods: Record<string, ModeledMethod> = {};
const data = loadYaml(yaml, {
filename: this.modelFile.filename,
});
for (const modelFile of modelFiles) {
const yaml = await readFile(modelFile, "utf8");
const existingModeledMethods = loadDataExtensionYaml(data);
const data = loadYaml(yaml, {
filename: modelFile,
});
if (!existingModeledMethods) {
void showAndLogErrorMessage(
`Failed to parse data extension YAML ${this.modelFile.filename}.`,
);
return;
const modeledMethods = loadDataExtensionYaml(data);
if (!modeledMethods) {
void showAndLogErrorMessage(
this.app.logger,
`Failed to parse data extension YAML ${modelFile}.`,
);
continue;
}
for (const [key, value] of Object.entries(modeledMethods)) {
existingModeledMethods[key] = value;
}
}
await this.postMessage({
t: "addModeledMethods",
t: "loadModeledMethods",
modeledMethods: existingModeledMethods,
});
} catch (e: unknown) {
void showAndLogErrorMessage(
`Unable to read data extension YAML ${
this.modelFile.filename
}: ${getErrorMessage(e)}`,
this.app.logger,
`Unable to read data extension YAML: ${getErrorMessage(e)}`,
);
}
}
@@ -230,16 +280,21 @@ export class DataExtensionsEditorView extends AbstractWebview<
const cancellationTokenSource = new CancellationTokenSource();
try {
const queryResult = await runQuery({
cliServer: this.cliServer,
queryRunner: this.queryRunner,
databaseItem: this.databaseItem,
queryStorageDir: this.queryStorageDir,
progress: (progressUpdate: ProgressUpdate) => {
void this.showProgress(progressUpdate, 1500);
const queryResult = await runQuery(
this.mode === Mode.Framework
? "frameworkModeQuery"
: "applicationModeQuery",
{
cliServer: this.cliServer,
queryRunner: this.queryRunner,
databaseItem: this.databaseItem,
queryStorageDir: this.queryStorageDir,
progress: (progressUpdate: ProgressUpdate) => {
void this.showProgress(progressUpdate, 1500);
},
token: cancellationTokenSource.token,
},
token: cancellationTokenSource.token,
});
);
if (!queryResult) {
await this.clearProgress();
return;
@@ -276,6 +331,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
await this.clearProgress();
} catch (err) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(err),
)`Failed to load external API usages: ${getErrorMessage(err)}`,
@@ -286,31 +343,36 @@ export class DataExtensionsEditorView extends AbstractWebview<
protected async generateModeledMethods(): Promise<void> {
const tokenSource = new CancellationTokenSource();
const selectedDatabase = this.databaseManager.currentDatabaseItem;
let addedDatabase: DatabaseItem | undefined;
// The external API methods are in the library source code, so we need to ask
// the user to import the library database. We need to have the database
// imported to the query server, so we need to register it to our workspace.
const database = await promptImportGithubDatabase(
this.app.commands,
this.databaseManager,
this.app.workspaceStoragePath ?? this.app.globalStoragePath,
this.app.credentials,
(update) => this.showProgress(update),
tokenSource.token,
this.cliServer,
);
if (!database) {
await this.clearProgress();
void extLogger.log("No database chosen");
// 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) {
const selectedDatabase = this.databaseManager.currentDatabaseItem;
return;
// The external API methods are in the library source code, so we need to ask
// the user to import the library database. We need to have the database
// imported to the query server, so we need to register it to our workspace.
addedDatabase = await promptImportGithubDatabase(
this.app.commands,
this.databaseManager,
this.app.workspaceStoragePath ?? this.app.globalStoragePath,
this.app.credentials,
(update) => this.showProgress(update),
this.cliServer,
);
if (!addedDatabase) {
await this.clearProgress();
void this.app.logger.log("No database chosen");
return;
}
// The library database was set as the current database by importing it,
// but we need to set it back to the originally selected database.
await this.databaseManager.setCurrentDatabaseItem(selectedDatabase);
}
// The library database was set as the current database by importing it,
// but we need to set it back to the originally selected database.
await this.databaseManager.setCurrentDatabaseItem(selectedDatabase);
await this.showProgress({
step: 0,
maxStep: 4000,
@@ -322,18 +384,17 @@ export class DataExtensionsEditorView extends AbstractWebview<
cliServer: this.cliServer,
queryRunner: this.queryRunner,
queryStorageDir: this.queryStorageDir,
databaseItem: database,
onResults: async (results) => {
databaseItem: addedDatabase ?? this.databaseItem,
onResults: async (modeledMethods) => {
const modeledMethodsByName: Record<string, ModeledMethod> = {};
for (const result of results) {
modeledMethodsByName[result.signature] = result.modeledMethod;
for (const modeledMethod of modeledMethods) {
modeledMethodsByName[modeledMethod.signature] = modeledMethod;
}
await this.postMessage({
t: "addModeledMethods",
modeledMethods: modeledMethodsByName,
overrideNone: true,
});
},
progress: (update) => this.showProgress(update),
@@ -341,24 +402,24 @@ export class DataExtensionsEditorView extends AbstractWebview<
});
} catch (e: unknown) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(e),
)`Failed to generate flow model: ${getErrorMessage(e)}`,
);
}
// After the flow model has been generated, we can remove the temporary database
// which we used for generating the flow model.
await this.databaseManager.removeDatabaseItem(
() =>
this.showProgress({
step: 3900,
maxStep: 4000,
message: "Removing temporary database",
}),
tokenSource.token,
database,
);
if (addedDatabase) {
// After the flow model has been generated, we can remove the temporary database
// which we used for generating the flow model.
await this.showProgress({
step: 3900,
maxStep: 4000,
message: "Removing temporary database",
});
await this.databaseManager.removeDatabaseItem(addedDatabase);
}
await this.clearProgress();
}
@@ -394,6 +455,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
externalApiUsages,
modeledMethods,
usages,
this.mode,
);
await this.showProgress({
@@ -414,7 +476,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
});
const predictedModeledMethods = parsePredictedClassifications(
response.predicted,
response.predicted || [],
);
await this.showProgress({
@@ -426,7 +488,6 @@ export class DataExtensionsEditorView extends AbstractWebview<
await this.postMessage({
t: "addModeledMethods",
modeledMethods: predictedModeledMethods,
overrideNone: true,
});
await this.clearProgress();
@@ -474,6 +535,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
if (e instanceof RequestError && e.status === 429) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(e)`Rate limit hit, please try again soon.`,
);
return null;

View File

@@ -0,0 +1,93 @@
const packNamePartRegex = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
const packNameRegex = new RegExp(
`^(?<scope>${packNamePartRegex.source})/(?<name>${packNamePartRegex.source})$`,
);
const packNameLength = 128;
export interface ExtensionPackName {
scope: string;
name: string;
}
export function formatPackName(packName: ExtensionPackName): string {
return `${packName.scope}/${packName.name}`;
}
export function autoNameExtensionPack(
name: string,
language: string,
): ExtensionPackName | undefined {
let packName = `${name}-${language}`;
if (!packName.includes("/")) {
packName = `pack/${packName}`;
}
const parts = packName.split("/");
const sanitizedParts = parts.map((part) => sanitizeExtensionPackName(part));
// If the scope is empty (e.g. if the given name is "-/b"), then we need to still set a scope
if (sanitizedParts[0].length === 0) {
sanitizedParts[0] = "pack";
}
return {
scope: sanitizedParts[0],
// This will ensure there's only 1 slash
name: sanitizedParts.slice(1).join("-"),
};
}
export function sanitizeExtensionPackName(name: string) {
// Lowercase everything
name = name.toLowerCase();
// Replace all spaces, dots, and underscores with hyphens
name = name.replaceAll(/[\s._]+/g, "-");
// Replace all characters which are not allowed by empty strings
name = name.replaceAll(/[^a-z0-9-]/g, "");
// Remove any leading or trailing hyphens
name = name.replaceAll(/^-|-$/g, "");
// Remove any duplicate hyphens
name = name.replaceAll(/-{2,}/g, "-");
return name;
}
export function parsePackName(packName: string): ExtensionPackName | undefined {
const matches = packNameRegex.exec(packName);
if (!matches?.groups) {
return;
}
const scope = matches.groups.scope;
const name = matches.groups.name;
return {
scope,
name,
};
}
export function validatePackName(name: string): string | undefined {
if (!name) {
return "Pack name must not be empty";
}
if (name.length > packNameLength) {
return `Pack name must be no longer than ${packNameLength} characters`;
}
const matches = packNameRegex.exec(name);
if (!matches?.groups) {
if (!name.includes("/")) {
return "Invalid package name: a pack name must contain a slash to separate the scope from the pack name";
}
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
}
return undefined;
}

View File

@@ -1,64 +1,37 @@
import { join, relative, resolve, sep } from "path";
import { join } from "path";
import { outputFile, pathExists, readFile } from "fs-extra";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { minimatch } from "minimatch";
import { CancellationToken, window } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import {
getOnDiskWorkspaceFolders,
getOnDiskWorkspaceFoldersObjects,
showAndLogErrorMessage,
} from "../helpers";
import { CancellationToken, Uri, window } from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "../codeql-cli/cli";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { ProgressCallback } from "../common/vscode/progress";
import { DatabaseItem } from "../databases/local-databases";
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";
import { getErrorMessage } from "../pure/helpers-pure";
import { ExtensionPack, ExtensionPackModelFile } from "./shared/extension-pack";
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 {
disableAutoNameExtensionPack,
getExtensionsDirectory,
} from "../config";
import {
autoNameExtensionPack,
ExtensionPackName,
formatPackName,
parsePackName,
validatePackName,
} from "./extension-pack-name";
import {
askForWorkspaceFolder,
autoPickExtensionsDirectory,
} from "./extensions-workspace-folder";
const maxStep = 3;
const packNamePartRegex = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
const packNameRegex = new RegExp(
`^(?<scope>${packNamePartRegex.source})/(?<name>${packNamePartRegex.source})$`,
);
const packNameLength = 128;
export async function pickExtensionPackModelFile(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
databaseItem: Pick<DatabaseItem, "name" | "language">,
progress: ProgressCallback,
token: CancellationToken,
): Promise<ExtensionPackModelFile | undefined> {
const extensionPack = await pickExtensionPack(
cliServer,
databaseItem,
progress,
token,
);
if (!extensionPack) {
return undefined;
}
const modelFile = await pickModelFile(
cliServer,
databaseItem,
extensionPack,
progress,
token,
);
if (!modelFile) {
return;
}
return {
filename: modelFile,
extensionPack,
};
}
async function pickExtensionPack(
export async function pickExtensionPack(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
databaseItem: Pick<DatabaseItem, "name" | "language">,
logger: NotificationLogger,
progress: ProgressCallback,
token: CancellationToken,
): Promise<ExtensionPack | undefined> {
@@ -75,6 +48,21 @@ async function pickExtensionPack(
true,
);
if (!disableAutoNameExtensionPack()) {
progress({
message: "Creating extension pack...",
step: 2,
maxStep,
});
return autoCreateExtensionPack(
databaseItem.name,
databaseItem.language,
extensionPacksInfo,
logger,
);
}
if (Object.keys(extensionPacksInfo).length === 0) {
return pickNewExtensionPack(databaseItem, token);
}
@@ -84,6 +72,7 @@ async function pickExtensionPack(
Object.entries(extensionPacksInfo).map(async ([name, paths]) => {
if (paths.length !== 1) {
void showAndLogErrorMessage(
logger,
`Extension pack ${name} resolves to multiple paths`,
{
fullMessage: `Extension pack ${name} resolves to multiple paths: ${paths.join(
@@ -101,11 +90,15 @@ async function pickExtensionPack(
try {
extensionPack = await readExtensionPack(path);
} catch (e: unknown) {
void showAndLogErrorMessage(`Could not read extension pack ${name}`, {
fullMessage: `Could not read extension pack ${name} at ${path}: ${getErrorMessage(
e,
)}`,
});
void showAndLogErrorMessage(
logger,
`Could not read extension pack ${name}`,
{
fullMessage: `Could not read extension pack ${name} at ${path}: ${getErrorMessage(
e,
)}`,
},
);
return undefined;
}
@@ -163,118 +156,39 @@ async function pickExtensionPack(
return extensionPackOption.extensionPack;
}
async function pickModelFile(
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
databaseItem: Pick<DatabaseItem, "name">,
extensionPack: ExtensionPack,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
// Find the existing model files in the extension pack
const additionalPacks = getOnDiskWorkspaceFolders();
const extensions = await cliServer.resolveExtensions(
extensionPack.path,
additionalPacks,
);
const modelFiles = new Set<string>();
if (extensionPack.path in extensions.data) {
for (const extension of extensions.data[extensionPack.path]) {
modelFiles.add(extension.file);
}
}
if (modelFiles.size === 0) {
return pickNewModelFile(databaseItem, extensionPack, token);
}
const fileOptions: Array<{ label: string; file: string | null }> = [];
for (const file of modelFiles) {
fileOptions.push({
label: relative(extensionPack.path, file).replaceAll(sep, "/"),
file,
});
}
fileOptions.push({
label: "Create new model file",
file: null,
});
progress({
message: "Choosing model file...",
step: 3,
maxStep,
});
const fileOption = await window.showQuickPick(
fileOptions,
{
title: "Select model file to use",
},
token,
);
if (!fileOption) {
return undefined;
}
if (fileOption.file) {
return fileOption.file;
}
return pickNewModelFile(databaseItem, extensionPack, token);
}
async function pickNewExtensionPack(
databaseItem: Pick<DatabaseItem, "name" | "language">,
token: CancellationToken,
): Promise<ExtensionPack | undefined> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
const workspaceFolderOptions = workspaceFolders.map((folder) => ({
label: folder.name,
detail: folder.uri.fsPath,
path: folder.uri.fsPath,
}));
// We're not using window.showWorkspaceFolderPick because that also includes the database source folders while
// we only want to include on-disk workspace folders.
const workspaceFolder = await window.showQuickPick(workspaceFolderOptions, {
title: "Select workspace folder to create extension pack in",
});
const workspaceFolder = await askForWorkspaceFolder();
if (!workspaceFolder) {
return undefined;
}
let examplePackName = `${databaseItem.name}-extensions`;
if (!examplePackName.includes("/")) {
examplePackName = `pack/${examplePackName}`;
}
const examplePackName = autoNameExtensionPack(
databaseItem.name,
databaseItem.language,
);
const packName = await window.showInputBox(
const name = await window.showInputBox(
{
title: "Create new extension pack",
prompt: "Enter name of extension pack",
placeHolder: `e.g. ${examplePackName}`,
placeHolder: examplePackName
? `e.g. ${formatPackName(examplePackName)}`
: "",
validateInput: async (value: string): Promise<string | undefined> => {
if (!value) {
return "Pack name must not be empty";
const message = validatePackName(value);
if (message) {
return message;
}
if (value.length > packNameLength) {
return `Pack name must be no longer than ${packNameLength} characters`;
const packName = parsePackName(value);
if (!packName) {
return "Invalid pack name";
}
const matches = packNameRegex.exec(value);
if (!matches?.groups) {
if (!value.includes("/")) {
return "Invalid package name: a pack name must contain a slash to separate the scope from the pack name";
}
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
}
const packPath = join(workspaceFolder.path, matches.groups.name);
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
if (await pathExists(packPath)) {
return `A pack already exists at ${packPath}`;
}
@@ -284,31 +198,127 @@ async function pickNewExtensionPack(
},
token,
);
if (!name) {
return undefined;
}
const packName = parsePackName(name);
if (!packName) {
return undefined;
}
const matches = packNameRegex.exec(packName);
if (!matches?.groups) {
return;
}
const name = matches.groups.name;
const packPath = join(workspaceFolder.path, name);
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
if (await pathExists(packPath)) {
return undefined;
}
return writeExtensionPack(packPath, packName, databaseItem.language);
}
async function autoCreateExtensionPack(
name: string,
language: string,
extensionPacksInfo: QlpacksInfo,
logger: NotificationLogger,
): Promise<ExtensionPack | undefined> {
// Get the `codeQL.dataExtensions.extensionsDirectory` setting for the language
const userExtensionsDirectory = getExtensionsDirectory(language);
// If the setting is not set, automatically pick a suitable directory
const extensionsDirectory = userExtensionsDirectory
? Uri.file(userExtensionsDirectory)
: await autoPickExtensionsDirectory();
if (!extensionsDirectory) {
return undefined;
}
// Generate the name of the extension pack
const packName = autoNameExtensionPack(name, language);
if (!packName) {
void showAndLogErrorMessage(
logger,
`Could not automatically name extension pack for database ${name}`,
);
return undefined;
}
// Find any existing locations of this extension pack
const existingExtensionPackPaths =
extensionPacksInfo[formatPackName(packName)];
// If there is already an extension pack with this name, use it if it is valid
if (existingExtensionPackPaths?.length === 1) {
let extensionPack: ExtensionPack;
try {
extensionPack = await readExtensionPack(existingExtensionPackPaths[0]);
} catch (e: unknown) {
void showAndLogErrorMessage(
logger,
`Could not read extension pack ${formatPackName(packName)}`,
{
fullMessage: `Could not read extension pack ${formatPackName(
packName,
)} at ${existingExtensionPackPaths[0]}: ${getErrorMessage(e)}`,
},
);
return undefined;
}
return extensionPack;
}
// If there is already an existing extension pack with this name, but it resolves
// to multiple paths, then we can't use it
if (existingExtensionPackPaths?.length > 1) {
void showAndLogErrorMessage(
logger,
`Extension pack ${formatPackName(packName)} resolves to multiple paths`,
{
fullMessage: `Extension pack ${formatPackName(
packName,
)} resolves to multiple paths: ${existingExtensionPackPaths.join(
", ",
)}`,
},
);
return undefined;
}
const packPath = join(extensionsDirectory.fsPath, packName.name);
if (await pathExists(packPath)) {
void showAndLogErrorMessage(
logger,
`Directory ${packPath} already exists for extension pack ${formatPackName(
packName,
)}`,
);
return undefined;
}
return writeExtensionPack(packPath, packName, language);
}
async function writeExtensionPack(
packPath: string,
packName: ExtensionPackName,
language: string,
): Promise<ExtensionPack> {
const packYamlPath = join(packPath, "codeql-pack.yml");
const extensionPack: ExtensionPack = {
path: packPath,
yamlPath: packYamlPath,
name: packName,
name: formatPackName(packName),
version: "0.0.0",
extensionTargets: {
[`codeql/${databaseItem.language}-all`]: "*",
[`codeql/${language}-all`]: "*",
},
dataExtensions: ["models/**/*.yml"],
};
@@ -327,53 +337,6 @@ async function pickNewExtensionPack(
return extensionPack;
}
async function pickNewModelFile(
databaseItem: Pick<DatabaseItem, "name">,
extensionPack: ExtensionPack,
token: CancellationToken,
) {
const filename = await window.showInputBox(
{
title: "Enter the name of the new model file",
value: `models/${databaseItem.name.replaceAll("/", ".")}.model.yml`,
validateInput: async (value: string): Promise<string | undefined> => {
if (value === "") {
return "File name must not be empty";
}
const path = resolve(extensionPack.path, value);
if (await pathExists(path)) {
return "File already exists";
}
const notInExtensionPack = relative(
extensionPack.path,
path,
).startsWith("..");
if (notInExtensionPack) {
return "File must be in the extension pack";
}
const matchesPattern = extensionPack.dataExtensions.some((pattern) =>
minimatch(value, pattern, { matchBase: true }),
);
if (!matchesPattern) {
return `File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`;
}
return undefined;
},
},
token,
);
if (!filename) {
return undefined;
}
return resolve(extensionPack.path, filename);
}
async function readExtensionPack(path: string): Promise<ExtensionPack> {
const qlpackPath = await getQlPackPath(path);
if (!qlpackPath) {

View File

@@ -0,0 +1,224 @@
import { FileType, Uri, window, workspace, WorkspaceFolder } from "vscode";
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
import { extLogger } from "../common/logging/vscode";
import { tmpdir } from "../common/files";
/**
* Returns the ancestors of this path in order from furthest to closest (i.e. root of filesystem to parent directory)
*/
function getAncestors(uri: Uri): Uri[] {
const ancestors: Uri[] = [];
let current = uri;
while (current.fsPath !== Uri.joinPath(current, "..").fsPath) {
ancestors.push(current);
current = Uri.joinPath(current, "..");
}
// The ancestors are now in order from closest to furthest, so reverse them
ancestors.reverse();
return ancestors;
}
async function getRootWorkspaceDirectory(): Promise<Uri | undefined> {
// If there is a valid workspace file, just use its directory as the directory for the extensions
const workspaceFile = workspace.workspaceFile;
if (workspaceFile?.scheme === "file") {
return Uri.joinPath(workspaceFile, "..");
}
const allWorkspaceFolders = getOnDiskWorkspaceFoldersObjects();
// Get the system temp directory and convert it to a URI so it's normalized
const systemTmpdir = Uri.file(tmpdir());
const workspaceFolders = allWorkspaceFolders.filter((folder) => {
// Never use a workspace folder that is in the system temp directory
return !folder.uri.fsPath.startsWith(systemTmpdir.fsPath);
});
// Find the common root directory of all workspace folders by finding the longest common prefix
const commonRoot = workspaceFolders.reduce((commonRoot, folder) => {
const folderUri = folder.uri;
const ancestors = getAncestors(folderUri);
const minLength = Math.min(commonRoot.length, ancestors.length);
let commonLength = 0;
for (let i = 0; i < minLength; i++) {
if (commonRoot[i].fsPath === ancestors[i].fsPath) {
commonLength++;
} else {
break;
}
}
return commonRoot.slice(0, commonLength);
}, getAncestors(workspaceFolders[0].uri));
if (commonRoot.length === 0) {
return await findGitFolder(workspaceFolders);
}
// The path closest to the workspace folders is the last element of the common root
const commonRootUri = commonRoot[commonRoot.length - 1];
// If we are at the root of the filesystem, we can't go up any further and there's something
// wrong, so just return undefined
if (commonRootUri.fsPath === Uri.joinPath(commonRootUri, "..").fsPath) {
return await findGitFolder(workspaceFolders);
}
return commonRootUri;
}
async function findGitFolder(
workspaceFolders: WorkspaceFolder[],
): Promise<Uri | undefined> {
// Go through all workspace folders one-by-one and try to find the closest .git folder for each one
const folders = await Promise.all(
workspaceFolders.map(async (folder) => {
const ancestors = getAncestors(folder.uri);
// Reverse the ancestors so we're going from closest to furthest
ancestors.reverse();
const gitFoldersExists = await Promise.all(
ancestors.map(async (uri) => {
const gitFolder = Uri.joinPath(uri, ".git");
try {
const stat = await workspace.fs.stat(gitFolder);
// Check whether it's a directory
return (stat.type & FileType.Directory) !== 0;
} catch (e) {
return false;
}
}),
);
// Find the first ancestor that has a .git folder
const ancestorIndex = gitFoldersExists.findIndex((exists) => exists);
if (ancestorIndex === -1) {
return undefined;
}
return [ancestorIndex, ancestors[ancestorIndex]];
}),
);
const validFolders = folders.filter(
(folder): folder is [number, Uri] => folder !== undefined,
);
if (validFolders.length === 0) {
return undefined;
}
// Find the .git folder which is closest to a workspace folder
const closestFolder = validFolders.reduce((closestFolder, folder) => {
if (folder[0] < closestFolder[0]) {
return folder;
}
return closestFolder;
}, validFolders[0]);
return closestFolder?.[1];
}
/**
* Finds a suitable directory for extension packs to be created in. This will
* always be a path ending in `.github/codeql/extensions`. The parent directory
* will be determined heuristically based on the on-disk workspace folders.
*
* The heuristic is as follows (`.github/codeql/extensions` is added automatically unless
* otherwise specified):
* 1. If there is only 1 workspace folder, use that folder
* 2. If there is a workspace folder for which the path ends in `.github/codeql/extensions`, use that folder
* - If there are multiple such folders, use the first one
* - Does not append `.github/codeql/extensions` to the path
* 3. If there is a workspace file (`<basename>.code-workspace`), use the directory containing that file
* 4. If there is a common root directory for all workspace folders, use that directory
* - Workspace folders in the system temp directory are ignored
* - If the common root directory is the root of the filesystem, then it's not used
* 5. If there is a .git directory in any workspace folder, use the directory containing that .git directory
* 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> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
// If there's only 1 workspace folder, use the `.github/codeql/extensions` directory in that folder
if (workspaceFolders.length === 1) {
return Uri.joinPath(
workspaceFolders[0].uri,
".github",
"codeql",
"extensions",
);
}
// Now try to find a workspace folder for which the path ends in `.github/codeql/extensions`
const workspaceFolderForExtensions = workspaceFolders.find((folder) =>
// Using path instead of fsPath because path always uses forward slashes
folder.uri.path.endsWith(".github/codeql/extensions"),
);
if (workspaceFolderForExtensions) {
return workspaceFolderForExtensions.uri;
}
// 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");
return undefined;
}
// We'll create a new workspace folder for the extensions in the root workspace directory
// at `.github/codeql/extensions`
const extensionsUri = Uri.joinPath(
rootDirectory,
".github",
"codeql",
"extensions",
);
if (
!workspace.updateWorkspaceFolders(
workspace.workspaceFolders?.length ?? 0,
0,
{
name: "CodeQL Extension Packs",
uri: extensionsUri,
},
)
) {
void extLogger.log(
`Failed to add workspace folder for extensions at ${extensionsUri.fsPath}`,
);
return undefined;
}
return extensionsUri;
}
export async function askForWorkspaceFolder(): Promise<
WorkspaceFolder | undefined
> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
const workspaceFolderOptions = workspaceFolders.map((folder) => ({
label: folder.name,
detail: folder.uri.fsPath,
folder,
}));
// We're not using window.showWorkspaceFolderPick because that also includes the database source folders while
// we only want to include on-disk workspace folders.
const workspaceFolder = await window.showQuickPick(workspaceFolderOptions, {
title: "Select workspace folder to create extension pack in",
});
if (!workspaceFolder) {
return undefined;
}
return workspaceFolder.folder;
}

View File

@@ -2,20 +2,20 @@ import { CoreCompletedQuery, QueryRunner } from "../query-server";
import { dir } from "tmp-promise";
import { writeFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
import {
getOnDiskWorkspaceFolders,
isQueryLanguage,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { TeeLogger } from "../common";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { extLogger } from "../common/logging/vscode";
import { showAndLogExceptionWithTelemetry, TeeLogger } from "../common/logging";
import { isQueryLanguage } from "../common/query-language";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { DatabaseItem } from "../databases/local-databases";
import { ProgressCallback } from "../common/vscode/progress";
import { fetchExternalApiQueries } from "./queries";
import { QueryResultType } from "../pure/new-messages";
import { QueryResultType } from "../query-server/new-messages";
import { join } from "path";
import { redactableError } from "../pure/errors";
import { redactableError } from "../common/errors";
import { telemetryListener } from "../common/vscode/telemetry";
import { Query } from "./queries/query";
export type RunQueryOptions = {
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">;
@@ -27,14 +27,17 @@ export type RunQueryOptions = {
token: CancellationToken;
};
export async function runQuery({
cliServer,
queryRunner,
databaseItem,
queryStorageDir,
progress,
token,
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
export async function runQuery(
queryName: keyof Omit<Query, "dependencies">,
{
cliServer,
queryRunner,
databaseItem,
queryStorageDir,
progress,
token,
}: RunQueryOptions,
): Promise<CoreCompletedQuery | 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.
@@ -43,6 +46,8 @@ export async function runQuery({
if (!isQueryLanguage(databaseItem.language)) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`Unsupported database language ${databaseItem.language}`,
);
return;
@@ -51,6 +56,8 @@ export async function runQuery({
const query = fetchExternalApiQueries[databaseItem.language];
if (!query) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`No external API usage query found for language ${databaseItem.language}`,
);
return;
@@ -58,7 +65,7 @@ export async function runQuery({
const queryDir = (await dir({ unsafeCleanup: true })).path;
const queryFile = join(queryDir, "FetchExternalApis.ql");
await writeFile(queryFile, query.mainQuery, "utf8");
await writeFile(queryFile, query[queryName], "utf8");
if (query.dependencies) {
for (const [filename, contents] of Object.entries(query.dependencies)) {
@@ -106,6 +113,8 @@ export async function runQuery({
if (completedQuery.resultType !== QueryResultType.SUCCESS) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`External API usage query failed: ${
completedQuery.message ?? "No message"
}`,
@@ -128,6 +137,8 @@ export async function readQueryResults({
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;

View File

@@ -1,19 +1,54 @@
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
import { ModeledMethodType } from "./modeled-method";
export type Call = {
label: string;
url: ResolvableLocationValue;
};
export type ExternalApiUsage = {
export enum CallClassification {
Unknown = "unknown",
Source = "source",
Test = "test",
Generated = "generated",
}
export type Usage = Call & {
classification: CallClassification;
};
export interface MethodSignature {
/**
* Contains the full method signature, e.g. `org.sql2o.Connection#createQuery(String)`
* Contains the version of the library if it can be determined by CodeQL, e.g. `4.2.2.2`
*/
libraryVersion?: string;
/**
* A unique signature that can be used to identify this external API usage.
*
* The signature contains the package name, type name, method name, and method parameters
* in the form "packageName.typeName#methodName(methodParameters)".
* e.g. `org.sql2o.Connection#createQuery(String)`
*/
signature: string;
packageName: string;
typeName: string;
methodName: string;
/**
* The method parameters, including enclosing parentheses, e.g. `(String, String)`
*/
methodParameters: string;
}
export interface ExternalApiUsage 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;
/**
* Is this method already supported by CodeQL standard libraries.
* If so, there is no need for the user to model it themselves.
*/
supported: boolean;
usages: Call[];
};
supportedType: ModeledMethodType;
usages: Usage[];
}

View File

@@ -3,23 +3,19 @@ import { DatabaseItem } from "../databases/local-databases";
import { basename } from "path";
import { QueryRunner } from "../query-server";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { TeeLogger } from "../common";
import { showAndLogExceptionWithTelemetry, TeeLogger } from "../common/logging";
import { extLogger } from "../common/logging/vscode";
import { extensiblePredicateDefinitions } from "./predicates";
import { ProgressCallback } from "../common/vscode/progress";
import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import {
ModeledMethodType,
ModeledMethodWithSignature,
} from "./modeled-method";
import { redactableError } from "../pure/errors";
import { QueryResultType } from "../pure/new-messages";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
import { redactableError } from "../common/errors";
import { QueryResultType } from "../query-server/new-messages";
import { file } from "tmp-promise";
import { writeFile } from "fs-extra";
import { dump } from "js-yaml";
import { qlpackOfDatabase } from "../language-support";
import { telemetryListener } from "../common/vscode/telemetry";
type FlowModelOptions = {
cliServer: CodeQLCliServer;
@@ -28,7 +24,7 @@ type FlowModelOptions = {
databaseItem: DatabaseItem;
progress: ProgressCallback;
token: CancellationToken;
onResults: (results: ModeledMethodWithSignature[]) => void | Promise<void>;
onResults: (results: ModeledMethod[]) => void | Promise<void>;
};
async function resolveQueries(
@@ -80,9 +76,11 @@ async function getModeledMethodsFromFlow(
progress,
token,
}: Omit<FlowModelOptions, "onResults">,
): Promise<ModeledMethodWithSignature[]> {
): Promise<ModeledMethod[]> {
if (queryPath === undefined) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`Failed to find ${type} query`,
);
return [];
@@ -117,6 +115,8 @@ async function getModeledMethodsFromFlow(
);
if (queryResult.resultType !== QueryResultType.SUCCESS) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`Failed to run ${basename(queryPath)} query: ${
queryResult.message ?? "No message"
}`,
@@ -129,6 +129,8 @@ async function getModeledMethodsFromFlow(
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)}`,

View File

@@ -0,0 +1,58 @@
import { basename, extname } from "../common/path";
// From the semver package using
// const { re, t } = require("semver/internal/re");
// console.log(re[t.LOOSE]);
// Modifications:
// - Added version named group which does not capture the v prefix
// - Removed the ^ and $ anchors
// - Made the minor and patch versions optional
// - Added a hyphen to the start of the version
// - Added a dot as a valid separator between the version and the label
// - Made the patch version optional even if a label is given
// This will match any semver string at the end of a larger string
const semverRegex =
/-[v=\s]*(?<version>([0-9]+)(\.([0-9]+)(?:(\.([0-9]+))?(?:[-.]?((?:[0-9]+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:[0-9]+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?)?)?)/g;
export interface Library {
name: string;
version?: string;
}
export function parseLibraryFilename(filename: string): Library {
let libraryName = basename(filename);
const extension = extname(libraryName);
libraryName = libraryName.slice(0, -extension.length);
let libraryVersion: string | undefined;
let match: RegExpMatchArray | null = null;
// Reset the regex
semverRegex.lastIndex = 0;
// Find the last occurence of the regex within the library name
// eslint-disable-next-line no-constant-condition
while (true) {
const currentMatch = semverRegex.exec(libraryName);
if (currentMatch === null) {
break;
}
match = currentMatch;
}
if (match?.groups) {
libraryVersion = match.groups?.version;
// Remove everything after the start of the match
libraryName = libraryName.slice(0, match.index);
}
// Remove any leading or trailing hyphens or dots
libraryName = libraryName.replaceAll(/^[.-]+|[.-]+$/g, "");
return {
name: libraryName,
version: libraryVersion,
};
}

View File

@@ -1,3 +1,5 @@
import { MethodSignature } from "./external-api-usage";
export type ModeledMethodType =
| "none"
| "source"
@@ -17,15 +19,10 @@ export type Provenance =
// Entered by the user in the editor manually
| "manual";
export type ModeledMethod = {
export interface ModeledMethod extends MethodSignature {
type: ModeledMethodType;
input: string;
output: string;
kind: string;
provenance: Provenance;
};
export type ModeledMethodWithSignature = {
signature: string;
modeledMethod: ModeledMethod;
};
}

View File

@@ -1,20 +1,9 @@
import { ExternalApiUsage } from "./external-api-usage";
import {
ModeledMethod,
ModeledMethodType,
ModeledMethodWithSignature,
Provenance,
} from "./modeled-method";
export type ExternalApiUsageByType = {
externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod;
};
import { ModeledMethod, ModeledMethodType, Provenance } from "./modeled-method";
export type ExtensiblePredicateDefinition = {
extensiblePredicate: string;
generateMethodDefinition: (method: ExternalApiUsageByType) => Tuple[];
readModeledMethod: (row: Tuple[]) => ModeledMethodWithSignature;
generateMethodDefinition: (method: ModeledMethod) => Tuple[];
readModeledMethod: (row: Tuple[]) => ModeledMethod;
supportedKinds?: string[];
};
@@ -36,25 +25,27 @@ export const extensiblePredicateDefinitions: Record<
// string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
method.packageName,
method.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
method.methodName,
method.methodParameters,
"",
method.modeledMethod.output,
method.modeledMethod.kind,
method.modeledMethod.provenance,
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),
modeledMethod: {
type: "source",
input: "",
output: row[6] as string,
kind: row[7] as string,
provenance: row[8] as Provenance,
},
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
supportedKinds: ["remote"],
},
@@ -65,25 +56,27 @@ export const extensiblePredicateDefinitions: Record<
// string input, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
method.packageName,
method.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
method.methodName,
method.methodParameters,
"",
method.modeledMethod.input,
method.modeledMethod.kind,
method.modeledMethod.provenance,
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),
modeledMethod: {
type: "sink",
input: row[6] as string,
output: "",
kind: row[7] as string,
provenance: row[8] as Provenance,
},
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
supportedKinds: ["sql", "xss", "logging"],
},
@@ -94,26 +87,28 @@ export const extensiblePredicateDefinitions: Record<
// string input, string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
method.packageName,
method.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
method.methodName,
method.methodParameters,
"",
method.modeledMethod.input,
method.modeledMethod.output,
method.modeledMethod.kind,
method.modeledMethod.provenance,
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),
modeledMethod: {
type: "summary",
input: row[6] as string,
output: row[7] as string,
kind: row[8] as string,
provenance: row[9] as Provenance,
},
packageName: row[0] as string,
typeName: row[1] as string,
methodName: row[3] as string,
methodParameters: row[4] as string,
}),
supportedKinds: ["taint", "value"],
},
@@ -123,22 +118,24 @@ export const extensiblePredicateDefinitions: Record<
// string package, string type, string name, string signature, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
method.modeledMethod.kind,
method.modeledMethod.provenance,
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]}`,
modeledMethod: {
type: "neutral",
input: "",
output: "",
kind: row[4] as string,
provenance: row[5] as Provenance,
},
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,7 +1,7 @@
import { Query } from "./query";
export const fetchExternalApisQuery: Query = {
mainQuery: `/**
applicationModeQuery: `/**
* @name Usage of APIs coming from external libraries
* @description A list of 3rd party APIs used in the codebase.
* @tags telemetry
@@ -9,27 +9,57 @@ export const fetchExternalApisQuery: Query = {
* @id cs/telemetry/fetch-external-apis
*/
import csharp
import ExternalApi
private import csharp
private import AutomodelVsCode
class ExternalApi extends CallableMethod {
ExternalApi() {
this.isUnboundDeclaration() and
this.fromLibrary() and
this.(Modifiable).isEffectivelyPublic()
}
}
private Call aUsage(ExternalApi api) { result.getTarget().getUnboundDeclaration() = api }
private boolean isSupported(ExternalApi api) {
api.isSupported() and result = true
or
not api.isSupported() and
result = false
}
from ExternalApi api, string apiName, boolean supported, Call usage
from
ExternalApi api, string apiName, boolean supported, Call usage, string type, string classification
where
apiName = api.getApiName() and
supported = isSupported(api) and
usage = aUsage(api)
select usage, apiName, supported.toString(), "supported"
usage = aUsage(api) and
type = supportedType(api) and
classification = methodClassification(usage)
select usage, apiName, supported.toString(), "supported", api.dllName(), api.dllVersion(), type,
"type", classification, "classification"
`,
frameworkModeQuery: `/**
* @name Public methods
* @description A list of APIs callable by consumers. Excludes test and generated code.
* @tags telemetry
* @kind problem
* @id cs/telemetry/fetch-public-methods
*/
private import csharp
private import dotnet
private import semmle.code.csharp.frameworks.Test
private import AutomodelVsCode
class PublicMethod extends CallableMethod {
PublicMethod() { this.fromSource() and not this.getFile() instanceof TestFile }
}
from PublicMethod publicMethod, string apiName, boolean supported, string type
where
apiName = publicMethod.getApiName() and
supported = isSupported(publicMethod) and
type = supportedType(publicMethod)
select publicMethod, apiName, supported.toString(), "supported",
publicMethod.getFile().getBaseName(), "library", type, "type", "unknown", "classification"
`,
dependencies: {
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
"AutomodelVsCode.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
private import csharp
private import dotnet
@@ -41,6 +71,7 @@ private import semmle.code.csharp.dataflow.internal.DataFlowPrivate
private import semmle.code.csharp.dataflow.internal.DataFlowDispatch as DataFlowDispatch
private import semmle.code.csharp.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
private import semmle.code.csharp.dataflow.internal.TaintTrackingPrivate
private import semmle.code.csharp.frameworks.Test
private import semmle.code.csharp.security.dataflow.flowsources.Remote
pragma[nomagic]
@@ -59,22 +90,31 @@ class TestLibrary extends RefType {
}
/** Holds if the given callable is not worth supporting. */
private predicate isUninteresting(DotNet::Callable c) {
private predicate isUninteresting(DotNet::Declaration c) {
c.getDeclaringType() instanceof TestLibrary or
c.(Constructor).isParameterless()
c.(Constructor).isParameterless() or
c.getDeclaringType() instanceof AnonymousClass
}
/**
* An external API from either the C# Standard Library or a 3rd party library.
* An callable method from either the C# Standard Library, a 3rd party library, or from the source.
*/
class ExternalApi extends DotNet::Callable {
ExternalApi() {
this.isUnboundDeclaration() and
this.fromLibrary() and
class CallableMethod extends DotNet::Declaration {
CallableMethod() {
this.(Modifiable).isEffectivelyPublic() and
not isUninteresting(this)
}
/**
* Gets the unbound type, name and parameter types of this API.
*/
bindingset[this]
private string getSignature() {
result =
nestedName(this.getDeclaringType().getUnboundDeclaration()) + "#" + this.getName() + "(" +
parameterQualifiedTypeNamesToString(this) + ")"
}
/**
* Gets the namespace of this API.
*/
@@ -85,8 +125,23 @@ class ExternalApi extends DotNet::Callable {
* Gets the namespace and signature of this API.
*/
bindingset[this]
string getApiName() { result = this.getNamespace() + "." + this.getDeclaringType().getUnboundDeclaration() + "#" + this.getName() + "(" +
parameterQualifiedTypeNamesToString(this) + ")" }
string getApiName() { result = this.getNamespace() + "." + this.getSignature() }
private string getDllName() { result = this.getLocation().(Assembly).getName() }
private string getDllVersion() { result = this.getLocation().(Assembly).getVersion().toString() }
string dllName() {
result = this.getDllName()
or
not exists(this.getDllName()) and result = this.getFile().getBaseName()
}
string dllVersion() {
result = this.getDllVersion()
or
not exists(this.getDllVersion()) and result = ""
}
/** Gets a node that is an input to a call to this API. */
private ArgumentNode getAnInput() {
@@ -140,47 +195,45 @@ class ExternalApi extends DotNet::Callable {
}
}
/**
* Gets the limit for the number of results produced by a telemetry query.
*/
int resultLimit() { result = 1000 }
boolean isSupported(CallableMethod callableMethod) {
callableMethod.isSupported() and result = true
or
not callableMethod.isSupported() and
result = false
}
string supportedType(CallableMethod method) {
method.isSink() and result = "sink"
or
method.isSource() and result = "source"
or
method.hasSummary() and result = "summary"
or
method.isNeutral() and result = "neutral"
or
not method.isSupported() and result = ""
}
string methodClassification(Call method) {
method.getFile() instanceof TestFile and result = "test"
or
not method.getFile() instanceof TestFile and
result = "source"
}
/**
* Holds if it is relevant to count usages of "api".
* Gets the nested name of the declaration.
*
* If the declaration is not a nested type, the result is the same as \`getName()\`.
* Otherwise the name of the nested type is prefixed with a \`+\` and appended to
* the name of the enclosing type, which might be a nested type as well.
*/
signature predicate relevantApi(ExternalApi api);
/**
* Given a predicate to count relevant API usages, this module provides a predicate
* for restricting the number or returned results based on a certain limit.
*/
module Results<relevantApi/1 getRelevantUsages> {
private int getUsages(string apiName) {
result =
strictcount(Call c, ExternalApi api |
c.getTarget().getUnboundDeclaration() = api and
apiName = api.getApiName() and
getRelevantUsages(api)
)
}
private int getOrder(string apiName) {
apiName =
rank[result](string name, int usages |
usages = getUsages(name)
|
name order by usages desc, name
)
}
/**
* Holds if there exists an API with "apiName" that is being used "usages" times
* and if it is in the top results (guarded by resultLimit).
*/
predicate restrict(string apiName, int usages) {
usages = getUsages(apiName) and
getOrder(apiName) <= resultLimit()
}
private string nestedName(Declaration declaration) {
not exists(declaration.getDeclaringType().getUnboundDeclaration()) and
result = declaration.getName()
or
nestedName(declaration.getDeclaringType().getUnboundDeclaration()) + "+" + declaration.getName() =
result
}
`,
},

View File

@@ -1,7 +1,7 @@
import { Query } from "./query";
export const fetchExternalApisQuery: Query = {
mainQuery: `/**
applicationModeQuery: `/**
* @name Usage of APIs coming from external libraries
* @description A list of 3rd party APIs used in the codebase. Excludes test and generated code.
* @tags telemetry
@@ -10,28 +10,50 @@ export const fetchExternalApisQuery: Query = {
*/
import java
import ExternalApi
import AutomodelVsCode
private Call aUsage(ExternalApi api) {
result.getCallee().getSourceDeclaration() = api and
not result.getFile() instanceof GeneratedFile
class ExternalApi extends CallableMethod {
ExternalApi() { not this.fromSource() }
}
private boolean isSupported(ExternalApi api) {
api.isSupported() and result = true
or
not api.isSupported() and result = false
}
private Call aUsage(ExternalApi api) { result.getCallee().getSourceDeclaration() = api }
from ExternalApi api, string apiName, boolean supported, Call usage
from
ExternalApi externalApi, string apiName, boolean supported, Call usage, string type,
string classification
where
apiName = api.getApiName() and
supported = isSupported(api) and
usage = aUsage(api)
select usage, apiName, supported.toString(), "supported"
apiName = externalApi.getApiName() and
supported = isSupported(externalApi) and
usage = aUsage(externalApi) and
type = supportedType(externalApi) and
classification = methodClassification(usage)
select usage, apiName, supported.toString(), "supported", externalApi.jarContainer(),
externalApi.jarVersion(), type, "type", classification, "classification"
`,
frameworkModeQuery: `/**
* @name Public methods
* @description A list of APIs callable by consumers. Excludes test and generated code.
* @tags telemetry
* @kind problem
* @id java/telemetry/fetch-public-methods
*/
import java
import AutomodelVsCode
class PublicMethodFromSource extends CallableMethod, ModelApi { }
from PublicMethodFromSource publicMethod, string apiName, boolean supported, string type
where
apiName = publicMethod.getApiName() and
supported = isSupported(publicMethod) and
type = supportedType(publicMethod)
select publicMethod, apiName, supported.toString(), "supported",
publicMethod.getCompilationUnit().getParentContainer().getBaseName(), "library", type, "type",
"unknown", "classification"
`,
dependencies: {
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
"AutomodelVsCode.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
private import java
private import semmle.code.java.dataflow.DataFlow
@@ -41,57 +63,55 @@ private import semmle.code.java.dataflow.FlowSummary
private import semmle.code.java.dataflow.internal.DataFlowPrivate
private import semmle.code.java.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
private import semmle.code.java.dataflow.TaintTracking
private import semmle.code.java.dataflow.internal.ModelExclusions
pragma[nomagic]
private predicate isTestPackage(Package p) {
p.getName()
.matches([
"org.junit%", "junit.%", "org.mockito%", "org.assertj%",
"com.github.tomakehurst.wiremock%", "org.hamcrest%", "org.springframework.test.%",
"org.springframework.mock.%", "org.springframework.boot.test.%", "reactor.test%",
"org.xmlunit%", "org.testcontainers.%", "org.opentest4j%", "org.mockserver%",
"org.powermock%", "org.skyscreamer.jsonassert%", "org.rnorth.visibleassertions",
"org.openqa.selenium%", "com.gargoylesoftware.htmlunit%", "org.jboss.arquillian.testng%",
"org.testng%"
])
}
/**
* A test library.
*/
private class TestLibrary extends RefType {
TestLibrary() { isTestPackage(this.getPackage()) }
}
private string containerAsJar(Container container) {
if container instanceof JarFile then result = container.getBaseName() else result = "rt.jar"
}
/** Holds if the given callable is not worth supporting. */
/** Holds if the given callable/method is not worth supporting. */
private predicate isUninteresting(Callable c) {
c.getDeclaringType() instanceof TestLibrary or
c.(Constructor).isParameterless()
c.(Constructor).isParameterless() or
c.getDeclaringType() instanceof AnonymousClass
}
/**
* An external API from either the Standard Library or a 3rd party library.
* A callable method from either the Standard Library, a 3rd party library or from the source.
*/
class ExternalApi extends Callable {
ExternalApi() { not this.fromSource() and not isUninteresting(this) }
class CallableMethod extends Callable {
CallableMethod() { not isUninteresting(this) }
/**
* Gets information about the external API in the form expected by the MaD modeling framework.
*/
string getApiName() {
result =
this.getDeclaringType().getPackage() + "." + this.getDeclaringType().getSourceDeclaration() +
"#" + this.getName() + paramsString(this)
this.getDeclaringType().getPackage() + "." + this.getDeclaringType().nestedName() + "#" +
this.getName() + paramsString(this)
}
private string getJarName() {
result = this.getCompilationUnit().getParentContainer*().(JarFile).getBaseName()
}
private string getJarVersion() {
result = this.getCompilationUnit().getParentContainer*().(JarFile).getSpecificationVersion()
}
/**
* Gets the jar file containing this API. Normalizes the Java Runtime to "rt.jar" despite the presence of modules.
*/
string jarContainer() { result = containerAsJar(this.getCompilationUnit().getParentContainer*()) }
string jarContainer() {
result = this.getJarName()
or
not exists(this.getJarName()) and result = "rt.jar"
}
/**
* Gets the version of the JAR file containing this API. Empty if no version is found in the JAR.
*/
string jarVersion() {
result = this.getJarVersion()
or
not exists(this.getJarVersion()) and result = ""
}
/** Gets a node that is an input to a call to this API. */
private DataFlow::Node getAnInput() {
@@ -138,50 +158,107 @@ class ExternalApi extends Callable {
}
}
/** DEPRECATED: Alias for ExternalApi */
deprecated class ExternalAPI = ExternalApi;
boolean isSupported(CallableMethod method) {
method.isSupported() and result = true
or
not method.isSupported() and result = false
}
string supportedType(CallableMethod method) {
method.isSink() and result = "sink"
or
method.isSource() and result = "source"
or
method.hasSummary() and result = "summary"
or
method.isNeutral() and result = "neutral"
or
not method.isSupported() and result = ""
}
string methodClassification(Call method) {
isInTestFile(method.getLocation().getFile()) and result = "test"
or
method.getFile() instanceof GeneratedFile and result = "generated"
or
not isInTestFile(method.getLocation().getFile()) and
not method.getFile() instanceof GeneratedFile and
result = "source"
}
// The below is a copy of https://github.com/github/codeql/blob/249f9f863db1e94e3c46ca85b49fb0ec32f8ca92/java/ql/lib/semmle/code/java/dataflow/internal/ModelExclusions.qll
// to avoid the use of internal modules.
/** Holds if the given package \`p\` is a test package. */
pragma[nomagic]
private predicate isTestPackage(Package p) {
p.getName()
.matches([
"org.junit%", "junit.%", "org.mockito%", "org.assertj%",
"com.github.tomakehurst.wiremock%", "org.hamcrest%", "org.springframework.test.%",
"org.springframework.mock.%", "org.springframework.boot.test.%", "reactor.test%",
"org.xmlunit%", "org.testcontainers.%", "org.opentest4j%", "org.mockserver%",
"org.powermock%", "org.skyscreamer.jsonassert%", "org.rnorth.visibleassertions",
"org.openqa.selenium%", "com.gargoylesoftware.htmlunit%", "org.jboss.arquillian.testng%",
"org.testng%"
])
}
/**
* Gets the limit for the number of results produced by a telemetry query.
* A test library.
*/
int resultLimit() { result = 1000 }
class TestLibrary extends RefType {
TestLibrary() { isTestPackage(this.getPackage()) }
}
/** Holds if the given file is a test file. */
private predicate isInTestFile(File file) {
file.getAbsolutePath().matches(["%/test/%", "%/guava-tests/%", "%/guava-testlib/%"]) and
not file.getAbsolutePath().matches("%/ql/test/%") // allows our test cases to work
}
/** Holds if the given compilation unit's package is a JDK internal. */
private predicate isJdkInternal(CompilationUnit cu) {
cu.getPackage().getName().matches("org.graalvm%") or
cu.getPackage().getName().matches("com.sun%") or
cu.getPackage().getName().matches("sun%") or
cu.getPackage().getName().matches("jdk%") or
cu.getPackage().getName().matches("java2d%") or
cu.getPackage().getName().matches("build.tools%") or
cu.getPackage().getName().matches("propertiesparser%") or
cu.getPackage().getName().matches("org.jcp%") or
cu.getPackage().getName().matches("org.w3c%") or
cu.getPackage().getName().matches("org.ietf.jgss%") or
cu.getPackage().getName().matches("org.xml.sax%") or
cu.getPackage().getName().matches("com.oracle%") or
cu.getPackage().getName().matches("org.omg%") or
cu.getPackage().getName().matches("org.relaxng%") or
cu.getPackage().getName() = "compileproperties" or
cu.getPackage().getName() = "transparentruler" or
cu.getPackage().getName() = "genstubs" or
cu.getPackage().getName() = "netscape.javascript" or
cu.getPackage().getName() = ""
}
/** Holds if the given callable is not worth modeling. */
predicate isUninterestingForModels(Callable c) {
isInTestFile(c.getCompilationUnit().getFile()) or
isJdkInternal(c.getCompilationUnit()) or
c instanceof MainMethod or
c instanceof StaticInitializer or
exists(FunctionalExpr funcExpr | c = funcExpr.asMethod()) or
c.getDeclaringType() instanceof TestLibrary or
c.(Constructor).isParameterless()
}
/**
* Holds if it is relevant to count usages of \`api\`.
* A class that represents all callables for which we might be
* interested in having a MaD model.
*/
signature predicate relevantApi(ExternalApi api);
/**
* Given a predicate to count relevant API usages, this module provides a predicate
* for restricting the number or returned results based on a certain limit.
*/
module Results<relevantApi/1 getRelevantUsages> {
private int getUsages(string apiName) {
result =
strictcount(Call c, ExternalApi api |
c.getCallee().getSourceDeclaration() = api and
not c.getFile() instanceof GeneratedFile and
apiName = api.getApiName() and
getRelevantUsages(api)
)
}
private int getOrder(string apiInfo) {
apiInfo =
rank[result](string info, int usages |
usages = getUsages(info)
|
info order by usages desc, info
)
}
/**
* Holds if there exists an API with \`apiName\` that is being used \`usages\` times
* and if it is in the top results (guarded by resultLimit).
*/
predicate restrict(string apiName, int usages) {
usages = getUsages(apiName) and
getOrder(apiName) <= resultLimit()
class ModelApi extends SrcCallable {
ModelApi() {
this.fromSource() and
this.isEffectivelyPublic() and
not isUninterestingForModels(this)
}
}
`,

View File

@@ -1,14 +1,37 @@
export type Query = {
/**
* The main query.
* The application query.
*
* It should select all usages of external APIs, and return the following result pattern:
* - usage: the usage of the external API. This is an entity.
* - apiName: the name of the external API. This is a string.
* - supported: whether the external API is supported by the extension. This should be a string representation of a boolean to satify the result pattern for a problem query.
* - supported: whether the external API is modeled. This should be a string representation of a boolean to satify the result pattern for a problem query.
* - "supported": a string literal. This is required to make the query a valid problem query.
* - libraryName: the name of the library that contains the external API. This is a string and usually the basename of a file.
* - libraryVersion: the version of the library that contains the external API. This is a string and can be empty if the version cannot be determined.
* - type: the modeled kind of the method, either "sink", "source", "summary", or "neutral"
* - "type": a string literal. This is required to make the query a valid problem query.
* - classification: the classification of the use of the method, either "source", "test", "generated", or "unknown"
* - "classification: a string literal. This is required to make the query a valid problem query.
*/
mainQuery: string;
applicationModeQuery: string;
/**
* The framework query.
*
* It should select all methods that are callable by applications, which is usually all public methods (and constructors).
* The result pattern should be as follows:
* - method: the method that is callable by applications. This is an entity.
* - apiName: the name of the external API. This is a string.
* - supported: whether this method is modeled. This should be a string representation of a boolean to satify the result pattern for a problem query.
* - "supported": a string literal. This is required to make the query a valid problem query.
* - libraryName: an arbitrary string. This is required to make it match the structure of the application query.
* - libraryVersion: an arbitrary string. This is required to make it match the structure of the application query.
* - type: the modeled kind of the method, either "sink", "source", "summary", or "neutral"
* - "type": a string literal. This is required to make the query a valid problem query.
* - "unknown": a string literal. This is required to make it match the structure of the application query.
* - "classification: a string literal. This is required to make the query a valid problem query.
*/
frameworkModeQuery: string;
dependencies?: {
[filename: string]: string;
};

View File

@@ -8,8 +8,3 @@ export interface ExtensionPack {
extensionTargets: Record<string, string>;
dataExtensions: string[];
}
export interface ExtensionPackModelFile {
filename: string;
extensionPack: ExtensionPack;
}

View File

@@ -0,0 +1,4 @@
export enum Mode {
Application = "application",
Framework = "framework",
}

View File

@@ -1,4 +1,4 @@
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
import { ExternalApiUsage } from "../external-api-usage";
export function calculateModeledPercentage(
externalApiUsages: Array<Pick<ExternalApiUsage, "supported">>,

View File

@@ -0,0 +1,88 @@
import { ExternalApiUsage } from "../external-api-usage";
import { Mode } from "./mode";
import { calculateModeledPercentage } from "./modeled-percentage";
export function groupMethods(
externalApiUsages: ExternalApiUsage[],
mode: Mode,
): Record<string, ExternalApiUsage[]> {
const groupedByLibrary: Record<string, ExternalApiUsage[]> = {};
for (const externalApiUsage of externalApiUsages) {
// Group by package if using framework mode
const key =
mode === Mode.Framework
? externalApiUsage.packageName
: externalApiUsage.library;
groupedByLibrary[key] ??= [];
groupedByLibrary[key].push(externalApiUsage);
}
return groupedByLibrary;
}
export function sortGroupNames(
methods: Record<string, ExternalApiUsage[]>,
): string[] {
return Object.keys(methods).sort((a, b) =>
compareGroups(methods[a], a, methods[b], b),
);
}
export function sortMethods(
externalApiUsages: ExternalApiUsage[],
): ExternalApiUsage[] {
const sortedExternalApiUsages = [...externalApiUsages];
sortedExternalApiUsages.sort((a, b) => compareMethod(a, b));
return sortedExternalApiUsages;
}
function compareGroups(
a: ExternalApiUsage[],
aName: string,
b: ExternalApiUsage[],
bName: string,
): number {
const supportedPercentageA = calculateModeledPercentage(a);
const supportedPercentageB = calculateModeledPercentage(b);
// Sort first by supported percentage ascending
if (supportedPercentageA > supportedPercentageB) {
return 1;
}
if (supportedPercentageA < supportedPercentageB) {
return -1;
}
const numberOfUsagesA = a.reduce((acc, curr) => acc + curr.usages.length, 0);
const numberOfUsagesB = b.reduce((acc, curr) => acc + curr.usages.length, 0);
// If the number of usages is equal, sort by number of methods descending
if (numberOfUsagesA === numberOfUsagesB) {
const numberOfMethodsA = a.length;
const numberOfMethodsB = b.length;
// If the number of methods is equal, sort by library name ascending
if (numberOfMethodsA === numberOfMethodsB) {
return aName.localeCompare(bName);
}
return numberOfMethodsB - numberOfMethodsA;
}
// Then sort by number of usages descending
return numberOfUsagesB - numberOfUsagesA;
}
function compareMethod(a: ExternalApiUsage, b: ExternalApiUsage): number {
// Sort first by supported, putting unmodeled methods first.
if (a.supported && !b.supported) {
return 1;
}
if (!a.supported && b.supported) {
return -1;
}
// Then sort by number of usages descending
return b.usages.length - a.usages.length;
}

View File

@@ -1,7 +1,9 @@
import { ExtensionPackModelFile } from "./extension-pack";
import { ExtensionPack } from "./extension-pack";
import { Mode } from "./mode";
export interface DataExtensionEditorViewState {
extensionPackModelFile: ExtensionPackModelFile;
modelFileExists: boolean;
extensionPack: ExtensionPack;
enableFrameworkMode: boolean;
showLlmButton: boolean;
mode: Mode;
}

View File

@@ -1,31 +1,20 @@
import Ajv from "ajv";
import { ExternalApiUsage } from "./external-api-usage";
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
import {
ModeledMethod,
ModeledMethodType,
ModeledMethodWithSignature,
} from "./modeled-method";
import { extensiblePredicateDefinitions } from "./predicates";
ExtensiblePredicateDefinition,
extensiblePredicateDefinitions,
} from "./predicates";
import * as dataSchemaJson from "./data-schema.json";
import { sanitizeExtensionPackName } from "./extension-pack-name";
const ajv = new Ajv({ allErrors: true });
const dataSchemaValidate = ajv.compile(dataSchemaJson);
type ExternalApiUsageByType = {
externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod;
};
type ExtensiblePredicateDefinition = {
extensiblePredicate: string;
generateMethodDefinition: (method: ExternalApiUsageByType) => any[];
readModeledMethod: (row: any[]) => ModeledMethodWithSignature;
};
function createDataProperty(
methods: ExternalApiUsageByType[],
methods: ModeledMethod[],
definition: ExtensiblePredicateDefinition,
) {
if (methods.length === 0) {
@@ -44,12 +33,11 @@ function createDataProperty(
export function createDataExtensionYaml(
language: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
modeledMethods: ModeledMethod[],
) {
const methodsByType: Record<
Exclude<ModeledMethodType, "none">,
ExternalApiUsageByType[]
ModeledMethod[]
> = {
source: [],
sink: [],
@@ -57,14 +45,9 @@ export function createDataExtensionYaml(
neutral: [],
};
for (const externalApiUsage of externalApiUsages) {
const modeledMethod = modeledMethods[externalApiUsage.signature];
for (const modeledMethod of modeledMethods) {
if (modeledMethod?.type && modeledMethod.type !== "none") {
methodsByType[modeledMethod.type].push({
externalApiUsage,
modeledMethod,
});
methodsByType[modeledMethod.type].push(modeledMethod);
}
}
@@ -83,6 +66,88 @@ export function createDataExtensionYaml(
${extensions.join("\n")}`;
}
export function createDataExtensionYamlsForApplicationMode(
language: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
): Record<string, string> {
const methodsByLibraryFilename: Record<string, ModeledMethod[]> = {};
for (const externalApiUsage of externalApiUsages) {
const modeledMethod = modeledMethods[externalApiUsage.signature];
if (!modeledMethod) {
continue;
}
const filename = createFilenameForLibrary(externalApiUsage.library);
methodsByLibraryFilename[filename] =
methodsByLibraryFilename[filename] || [];
methodsByLibraryFilename[filename].push(modeledMethod);
}
const result: Record<string, string> = {};
for (const [filename, methods] of Object.entries(methodsByLibraryFilename)) {
result[filename] = createDataExtensionYaml(language, methods);
}
return result;
}
export function createDataExtensionYamlsForFrameworkMode(
databaseName: string,
language: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
prefix = "models/",
suffix = ".model",
): Record<string, string> {
const parts = databaseName.split("/");
const libraryName = parts
.slice(1)
.map((part) => sanitizeExtensionPackName(part))
.join("-");
const methods = externalApiUsages
.map((externalApiUsage) => modeledMethods[externalApiUsage.signature])
.filter((modeledMethod) => modeledMethod !== undefined);
return {
[`${prefix}${libraryName}${suffix}.yml`]: createDataExtensionYaml(
language,
methods,
),
};
}
export function createFilenameForLibrary(
library: string,
prefix = "models/",
suffix = ".model",
) {
let libraryName = library;
// Lowercase everything
libraryName = libraryName.toLowerCase();
// Replace all spaces and underscores with hyphens
libraryName = libraryName.replaceAll(/[\s_]+/g, "-");
// Replace all characters which are not allowed by empty strings
libraryName = libraryName.replaceAll(/[^a-z0-9.-]/g, "");
// Remove any leading or trailing hyphens or dots
libraryName = libraryName.replaceAll(/^[.-]+|[.-]+$/g, "");
// Remove any duplicate hyphens
libraryName = libraryName.replaceAll(/-{2,}/g, "-");
// Remove any duplicate dots
libraryName = libraryName.replaceAll(/\.{2,}/g, ".");
return `${prefix}${libraryName}${suffix}.yml`;
}
export function loadDataExtensionYaml(
data: any,
): Record<string, ModeledMethod> | undefined {
@@ -116,14 +181,11 @@ export function loadDataExtensionYaml(
}
for (const row of data) {
const result = definition.readModeledMethod(row);
if (!result) {
const modeledMethod = definition.readModeledMethod(row);
if (!modeledMethod) {
continue;
}
const { signature, modeledMethod } = result;
modeledMethods[signature] = modeledMethod;
modeledMethods[modeledMethod.signature] = modeledMethod;
}
}

View File

@@ -2,8 +2,11 @@ import { retry } from "@octokit/plugin-retry";
import { throttling } from "@octokit/plugin-throttling";
import { Octokit } from "@octokit/rest";
import { Progress, CancellationToken } from "vscode";
import { showAndLogWarningMessage } from "../helpers";
import { Credentials } from "../common/authentication";
import {
NotificationLogger,
showAndLogWarningMessage,
} from "../common/logging";
export async function getCodeSearchRepositories(
query: string,
@@ -13,9 +16,10 @@ export async function getCodeSearchRepositories(
}>,
token: CancellationToken,
credentials: Credentials,
logger: NotificationLogger,
): Promise<string[]> {
let nwos: string[] = [];
const octokit = await provideOctokitWithThrottling(credentials);
const octokit = await provideOctokitWithThrottling(credentials, logger);
for await (const response of octokit.paginate.iterator(
octokit.rest.search.code,
@@ -43,6 +47,7 @@ export async function getCodeSearchRepositories(
async function provideOctokitWithThrottling(
credentials: Credentials,
logger: NotificationLogger,
): Promise<Octokit> {
const MyOctokit = Octokit.plugin(throttling);
const auth = await credentials.getAccessToken();
@@ -53,6 +58,7 @@ async function provideOctokitWithThrottling(
throttle: {
onRateLimit: (retryAfter: number, options: any): boolean => {
void showAndLogWarningMessage(
logger,
`Rate Limit detected for request ${options.method} ${options.url}. Retrying after ${retryAfter} seconds!`,
);
@@ -60,6 +66,7 @@ async function provideOctokitWithThrottling(
},
onSecondaryRateLimit: (_retryAfter: number, options: any): void => {
void showAndLogWarningMessage(
logger,
`Secondary Rate Limit detected for request ${options.method} ${options.url}`,
);
},

View File

@@ -18,7 +18,10 @@ import {
SelectedDbItemKind,
} from "./db-config";
import * as chokidar from "chokidar";
import { DisposableObject, DisposeHandler } from "../../pure/disposable-object";
import {
DisposableObject,
DisposeHandler,
} from "../../common/disposable-object";
import { DbConfigValidator } from "./db-config-validator";
import { App } from "../../common/app";
import { AppEvent, AppEventEmitter } from "../../common/events";
@@ -147,14 +150,10 @@ export class DbConfigStore extends DisposableObject {
await this.writeConfig(config);
}
/**
* Adds a list of remote repositories to an existing repository list and removes duplicates.
* @returns a list of repositories that were not added because the list reached 1000 entries.
*/
public async addRemoteReposToList(
repoNwoList: string[],
parentList: string,
): Promise<string[]> {
): Promise<void> {
if (!this.config) {
throw Error("Cannot add variant analysis repos if config is not loaded");
}
@@ -172,21 +171,15 @@ export class DbConfigStore extends DisposableObject {
...new Set([...parent.repositories, ...repoNwoList]),
];
parent.repositories = newRepositoriesList.slice(0, 1000);
const truncatedRepositories = newRepositoriesList.slice(1000);
parent.repositories = newRepositoriesList;
await this.writeConfig(config);
return truncatedRepositories;
}
/**
* Adds one remote repository
* @returns either nothing, or, if a parentList is given AND the number of repos on that list reaches 1000 returns the repo that was not added.
*/
public async addRemoteRepo(
repoNwo: string,
parentList?: string,
): Promise<string[]> {
): Promise<void> {
if (!this.config) {
throw Error("Cannot add variant analysis repo if config is not loaded");
}
@@ -201,7 +194,6 @@ export class DbConfigStore extends DisposableObject {
);
}
const truncatedRepositories = [];
const config = cloneDbConfig(this.config);
if (parentList) {
const parent = config.databases.variantAnalysis.repositoryLists.find(
@@ -210,15 +202,12 @@ export class DbConfigStore extends DisposableObject {
if (!parent) {
throw Error(`Cannot find parent list '${parentList}'`);
} else {
const newRepositories = [...parent.repositories, repoNwo];
parent.repositories = newRepositories.slice(0, 1000);
truncatedRepositories.push(...newRepositories.slice(1000));
parent.repositories = [...parent.repositories, repoNwo];
}
} else {
config.databases.variantAnalysis.repositories.push(repoNwo);
}
await this.writeConfig(config);
return truncatedRepositories;
}
public async addRemoteOwner(owner: string): Promise<void> {

View File

@@ -2,7 +2,7 @@ import { readJsonSync } from "fs-extra";
import { resolve } from "path";
import Ajv, { ValidateFunction } from "ajv";
import { clearLocalDbConfig, DbConfig } from "./db-config";
import { findDuplicateStrings } from "../../pure/text-utils";
import { findDuplicateStrings } from "../../common/text-utils";
import {
DbConfigValidationError,
DbConfigValidationErrorKind,

View File

@@ -1,7 +1,7 @@
import fetch, { Response } from "node-fetch";
import { zip } from "zip-a-folder";
import { Open } from "unzipper";
import { Uri, CancellationToken, window, InputBoxOptions } from "vscode";
import { Uri, window, InputBoxOptions } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import {
ensureDir,
@@ -17,13 +17,13 @@ import * as Octokit from "@octokit/rest";
import { retry } from "@octokit/plugin-retry";
import { DatabaseManager, DatabaseItem } from "./local-databases";
import { showAndLogInformationMessage, tmpDir } from "../helpers";
import { tmpDir } from "../tmp-dir";
import {
reportStreamProgress,
ProgressCallback,
} from "../common/vscode/progress";
import { extLogger } from "../common";
import { getErrorMessage } from "../pure/helpers-pure";
import { extLogger } from "../common/logging/vscode";
import { getErrorMessage } from "../common/helpers-pure";
import {
getNwoFromGitHubUrl,
isValidGitHubNwo,
@@ -31,6 +31,7 @@ import {
import { Credentials } from "../common/authentication";
import { AppCommandManager } from "../common/commands";
import { ALLOW_HTTP_SETTING } from "../config";
import { showAndLogInformationMessage } from "../common/logging";
/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
@@ -43,7 +44,6 @@ export async function promptImportInternetDatabase(
databaseManager: DatabaseManager,
storagePath: string,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer,
): Promise<DatabaseItem | undefined> {
const databaseUrl = await window.showInputBox({
@@ -62,13 +62,13 @@ export async function promptImportInternetDatabase(
storagePath,
undefined,
progress,
token,
cli,
);
if (item) {
await commandManager.execute("codeQLDatabases.focus");
void showAndLogInformationMessage(
extLogger,
"Database downloaded and imported successfully.",
);
}
@@ -84,7 +84,6 @@ export async function promptImportInternetDatabase(
* @param storagePath where to store the unzipped database.
* @param credentials the credentials to use to authenticate with GitHub
* @param progress the progress callback
* @param token the cancellation token
* @param cli the CodeQL CLI server
*/
export async function promptImportGithubDatabase(
@@ -93,7 +92,6 @@ export async function promptImportGithubDatabase(
storagePath: string,
credentials: Credentials | undefined,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer,
): Promise<DatabaseItem | undefined> {
const githubRepo = await askForGitHubRepo(progress);
@@ -107,13 +105,13 @@ export async function promptImportGithubDatabase(
storagePath,
credentials,
progress,
token,
cli,
);
if (databaseItem) {
await commandManager.execute("codeQLDatabases.focus");
void showAndLogInformationMessage(
extLogger,
"Database downloaded and imported successfully.",
);
return databaseItem;
@@ -154,7 +152,6 @@ export async function askForGitHubRepo(
* @param storagePath where to store the unzipped database.
* @param credentials the credentials to use to authenticate with GitHub
* @param progress the progress callback
* @param token the cancellation token
* @param cli the CodeQL CLI server
* @param language the language to download. If undefined, the user will be prompted to choose a language.
**/
@@ -164,7 +161,6 @@ export async function downloadGitHubDatabase(
storagePath: string,
credentials: Credentials | undefined,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer,
language?: string,
): Promise<DatabaseItem | undefined> {
@@ -210,7 +206,6 @@ export async function downloadGitHubDatabase(
storagePath,
`${owner}/${name}`,
progress,
token,
cli,
);
}
@@ -228,7 +223,6 @@ export async function importArchiveDatabase(
databaseManager: DatabaseManager,
storagePath: string,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer,
): Promise<DatabaseItem | undefined> {
try {
@@ -239,12 +233,12 @@ export async function importArchiveDatabase(
storagePath,
undefined,
progress,
token,
cli,
);
if (item) {
await commandManager.execute("codeQLDatabases.focus");
void showAndLogInformationMessage(
extLogger,
"Database unzipped and imported successfully.",
);
}
@@ -271,7 +265,6 @@ export async function importArchiveDatabase(
* @param storagePath where to store the unzipped database.
* @param nameOverride a name for the database that overrides the default
* @param progress callback to send progress messages to
* @param token cancellation token
*/
async function databaseArchiveFetcher(
databaseUrl: string,
@@ -280,7 +273,6 @@ async function databaseArchiveFetcher(
storagePath: string,
nameOverride: string | undefined,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer,
): Promise<DatabaseItem> {
progress({
@@ -323,8 +315,6 @@ async function databaseArchiveFetcher(
const makeSelected = true;
const item = await databaseManager.openDatabase(
progress,
token,
Uri.file(dbPath),
makeSelected,
nameOverride,

View File

@@ -1,7 +1,7 @@
import { App } from "../common/app";
import { AppEvent, AppEventEmitter } from "../common/events";
import { ValueResult } from "../common/value-result";
import { DisposableObject } from "../pure/disposable-object";
import { DisposableObject } from "../common/disposable-object";
import { DbConfigStore } from "./config/db-config-store";
import {
DbItem,
@@ -101,15 +101,15 @@ export class DbManager extends DisposableObject {
public async addNewRemoteRepo(
nwo: string,
parentList?: string,
): Promise<string[]> {
return await this.dbConfigStore.addRemoteRepo(nwo, parentList);
): Promise<void> {
await this.dbConfigStore.addRemoteRepo(nwo, parentList);
}
public async addNewRemoteReposToList(
nwoList: string[],
parentList: string,
): Promise<string[]> {
return await this.dbConfigStore.addRemoteReposToList(nwoList, parentList);
): Promise<void> {
await this.dbConfigStore.addRemoteReposToList(nwoList, parentList);
}
public async addNewRemoteOwner(owner: string): Promise<void> {

View File

@@ -1,7 +1,7 @@
import { window } from "vscode";
import { App } from "../common/app";
import { extLogger } from "../common";
import { DisposableObject } from "../pure/disposable-object";
import { extLogger } from "../common/logging/vscode";
import { DisposableObject } from "../common/disposable-object";
import { DbConfigStore } from "./config/db-config-store";
import { DbManager } from "./db-manager";
import { DbPanel } from "./ui/db-panel";

View File

@@ -1,5 +1,5 @@
import { join, basename, dirname as path_dirname } from "path";
import { DisposableObject } from "../pure/disposable-object";
import { DisposableObject } from "../common/disposable-object";
import {
Event,
EventEmitter,
@@ -31,20 +31,21 @@ import {
import {
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder,
showAndLogErrorMessage,
} from "./local-databases/db-contents-heuristics";
import {
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { extLogger } from "../common";
showAndLogErrorMessage,
} from "../common/logging";
import {
importArchiveDatabase,
promptImportGithubDatabase,
promptImportInternetDatabase,
} from "./database-fetcher";
import { asError, asyncFilter, getErrorMessage } from "../pure/helpers-pure";
import { asError, asyncFilter, getErrorMessage } from "../common/helpers-pure";
import { QueryRunner } from "../query-server";
import { isCanary } from "../config";
import { App } from "../common/app";
import { redactableError } from "../pure/errors";
import { redactableError } from "../common/errors";
import { LocalDatabasesCommands } from "../common/commands";
import {
createMultiSelectionCommand,
@@ -251,6 +252,7 @@ export class DatabaseUI extends DisposableObject {
this.handleUpgradeDatabase.bind(this),
),
"codeQLDatabases.renameDatabase": createSingleSelectionCommand(
this.app.logger,
this.handleRenameDatabase.bind(this),
"database",
),
@@ -279,6 +281,8 @@ export class DatabaseUI extends DisposableObject {
await this.chooseAndSetDatabase(true, { progress, token });
} catch (e) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(e),
)`Failed to choose and set database: ${getErrorMessage(e)}`,
@@ -310,7 +314,7 @@ export class DatabaseUI extends DisposableObject {
private async handleSetDefaultTourDatabase(): Promise<void> {
return withProgress(
async (progress, token) => {
async () => {
try {
if (!workspace.workspaceFolders?.length) {
throw new Error("No workspace folder is open.");
@@ -328,8 +332,6 @@ export class DatabaseUI extends DisposableObject {
const isTutorialDatabase = true;
await this.databaseManager.openDatabase(
progress,
token,
uri,
makeSelected,
nameOverride,
@@ -371,14 +373,16 @@ export class DatabaseUI extends DisposableObject {
// Public because it's used in tests
public async handleRemoveOrphanedDatabases(): Promise<void> {
void extLogger.log("Removing orphaned databases from workspace storage.");
void this.app.logger.log(
"Removing orphaned databases from workspace storage.",
);
let dbDirs = undefined;
if (
!(await pathExists(this.storagePath)) ||
!(await stat(this.storagePath)).isDirectory()
) {
void extLogger.log(
void this.app.logger.log(
"Missing or invalid storage directory. Not trying to remove orphaned databases.",
);
return;
@@ -403,7 +407,7 @@ export class DatabaseUI extends DisposableObject {
dbDirs = await asyncFilter(dbDirs, isLikelyDatabaseRoot);
if (!dbDirs.length) {
void extLogger.log("No orphaned databases found.");
void this.app.logger.log("No orphaned databases found.");
return;
}
@@ -412,10 +416,12 @@ export class DatabaseUI extends DisposableObject {
await Promise.all(
dbDirs.map(async (dbDir) => {
try {
void extLogger.log(`Deleting orphaned database '${dbDir}'.`);
void this.app.logger.log(`Deleting orphaned database '${dbDir}'.`);
await remove(dbDir);
} catch (e) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(e),
)`Failed to delete orphaned database: ${getErrorMessage(e)}`,
@@ -428,6 +434,7 @@ export class DatabaseUI extends DisposableObject {
if (failures.length) {
const dirname = path_dirname(failures[0]);
void showAndLogErrorMessage(
this.app.logger,
`Failed to delete unused databases (${failures.join(
", ",
)}).\nTo delete unused databases, please remove them manually from the storage folder ${dirname}.`,
@@ -443,6 +450,8 @@ export class DatabaseUI extends DisposableObject {
await this.chooseAndSetDatabase(false, { progress, token });
} catch (e: unknown) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(e),
)`Failed to choose and set database: ${getErrorMessage(e)}`,
@@ -474,13 +483,12 @@ export class DatabaseUI extends DisposableObject {
private async handleChooseDatabaseInternet(): Promise<void> {
return withProgress(
async (progress, token) => {
async (progress) => {
await promptImportInternetDatabase(
this.app.commands,
this.databaseManager,
this.storagePath,
progress,
token,
this.queryServer?.cliServer,
);
},
@@ -492,7 +500,7 @@ export class DatabaseUI extends DisposableObject {
private async handleChooseDatabaseGithub(): Promise<void> {
return withProgress(
async (progress, token) => {
async (progress) => {
const credentials = isCanary() ? this.app.credentials : undefined;
await promptImportGithubDatabase(
@@ -501,7 +509,6 @@ export class DatabaseUI extends DisposableObject {
this.storagePath,
credentials,
progress,
token,
this.queryServer?.cliServer,
);
},
@@ -597,14 +604,13 @@ export class DatabaseUI extends DisposableObject {
private async handleClearCache(): Promise<void> {
return withProgress(
async (progress, token) => {
async (_progress, token) => {
if (
this.queryServer !== undefined &&
this.databaseManager.currentDatabaseItem !== undefined
) {
await this.queryServer.clearCacheInDatabase(
this.databaseManager.currentDatabaseItem,
progress,
token,
);
}
@@ -622,7 +628,7 @@ export class DatabaseUI extends DisposableObject {
private async handleSetCurrentDatabase(uri: Uri): Promise<void> {
return withProgress(
async (progress, token) => {
async (progress) => {
try {
// Assume user has selected an archive if the file has a .zip extension
if (uri.path.endsWith(".zip")) {
@@ -632,11 +638,10 @@ export class DatabaseUI extends DisposableObject {
this.databaseManager,
this.storagePath,
progress,
token,
this.queryServer?.cliServer,
);
} else {
await this.databaseManager.openDatabase(progress, token, uri);
await this.databaseManager.openDatabase(uri);
}
} catch (e) {
// rethrow and let this be handled by default error handling.
@@ -657,10 +662,10 @@ export class DatabaseUI extends DisposableObject {
databaseItems: DatabaseItem[],
): Promise<void> {
return withProgress(
async (progress, token) => {
async () => {
await Promise.all(
databaseItems.map((dbItem) =>
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
this.databaseManager.removeDatabaseItem(dbItem),
),
);
},
@@ -747,15 +752,11 @@ export class DatabaseUI extends DisposableObject {
return await withInheritedProgress(
progress,
async (progress, token) => {
async (progress) => {
if (byFolder) {
const fixedUri = await this.fixDbUri(uri);
// we are selecting a database folder
return await this.databaseManager.openDatabase(
progress,
token,
fixedUri,
);
return await this.databaseManager.openDatabase(fixedUri);
} else {
// we are selecting a database archive. Must unzip into a workspace-controlled area
// before importing.
@@ -765,7 +766,6 @@ export class DatabaseUI extends DisposableObject {
this.databaseManager,
this.storagePath,
progress,
token,
this.queryServer?.cliServer,
);
}

View File

@@ -2,7 +2,7 @@
import * as cli from "../../codeql-cli/cli";
import vscode from "vscode";
import { FullDatabaseOptions } from "./database-options";
import { basename, dirname, extname, join, relative } from "path";
import { basename, dirname, extname, join } from "path";
import {
decodeSourceArchiveUri,
encodeArchiveBasePath,
@@ -10,9 +10,9 @@ import {
zipArchiveScheme,
} from "../../common/vscode/archive-filesystem-provider";
import { DatabaseItem, PersistedDatabaseItem } from "./database-item";
import { isLikelyDatabaseRoot } from "../../helpers";
import { isLikelyDatabaseRoot } from "./db-contents-heuristics";
import { stat } from "fs-extra";
import { pathsEqual } from "../../pure/files";
import { containsPath, pathsEqual } from "../../common/files";
import { DatabaseContents } from "./database-contents";
export class DatabaseItemImpl implements DatabaseItem {
@@ -199,7 +199,7 @@ export class DatabaseItemImpl implements DatabaseItem {
try {
const stats = await stat(testPath);
if (stats.isDirectory()) {
return !relative(testPath, databasePath).startsWith("..");
return containsPath(testPath, databasePath);
} else {
// database for /one/two/three/test.ql is at /one/two/three/three.testproj
const testdir = dirname(testPath);
@@ -207,7 +207,6 @@ export class DatabaseItemImpl implements DatabaseItem {
return pathsEqual(
databasePath,
join(testdir, `${testdirbase}.testproj`),
process.platform,
);
}
} catch {

View File

@@ -1,6 +1,7 @@
import vscode, { ExtensionContext } from "vscode";
import { extLogger, Logger } from "../../common";
import { DisposableObject } from "../../pure/disposable-object";
import { Logger, showAndLogExceptionWithTelemetry } from "../../common/logging";
import { extLogger } from "../../common/logging/vscode";
import { DisposableObject } from "../../common/disposable-object";
import { App } from "../../common/app";
import { QueryRunner } from "../../query-server";
import * as cli from "../../codeql-cli/cli";
@@ -13,22 +14,22 @@ import {
import { join } from "path";
import { FullDatabaseOptions } from "./database-options";
import { DatabaseItemImpl } from "./database-item-impl";
import { showNeverAskAgainDialog } from "../../common/vscode/dialog";
import {
getFirstWorkspaceFolder,
isFolderAlreadyInWorkspace,
isQueryLanguage,
showAndLogExceptionWithTelemetry,
showNeverAskAgainDialog,
} from "../../helpers";
} from "../../common/vscode/workspace-folders";
import { isQueryLanguage } from "../../common/query-language";
import { existsSync } from "fs";
import { QlPackGenerator } from "../../qlpack-generator";
import { asError, getErrorMessage } from "../../pure/helpers-pure";
import { QlPackGenerator } from "../../local-queries/qlpack-generator";
import { asError, getErrorMessage } from "../../common/helpers-pure";
import { DatabaseItem, PersistedDatabaseItem } from "./database-item";
import { redactableError } from "../../pure/errors";
import { redactableError } from "../../common/errors";
import { remove } from "fs-extra";
import { containsPath } from "../../pure/files";
import { containsPath } from "../../common/files";
import { DatabaseChangedEvent, DatabaseEventKind } from "./database-events";
import { DatabaseResolver } from "./database-resolver";
import { telemetryListener } from "../../common/vscode/telemetry";
/**
* The name of the key in the workspaceState dictionary in which we
@@ -103,8 +104,6 @@ export class DatabaseManager extends DisposableObject {
* databases.
*/
public async openDatabase(
progress: ProgressCallback,
token: vscode.CancellationToken,
uri: vscode.Uri,
makeSelected = true,
displayName?: string,
@@ -114,9 +113,7 @@ export class DatabaseManager extends DisposableObject {
return await this.addExistingDatabaseItem(
databaseItem,
progress,
makeSelected,
token,
isTutorialDatabase,
);
}
@@ -129,9 +126,7 @@ export class DatabaseManager extends DisposableObject {
*/
private async addExistingDatabaseItem(
databaseItem: DatabaseItemImpl,
progress: ProgressCallback,
makeSelected: boolean,
token: vscode.CancellationToken,
isTutorialDatabase?: boolean,
): Promise<DatabaseItem> {
const existingItem = this.findDatabaseItem(databaseItem.databaseUri);
@@ -142,7 +137,7 @@ export class DatabaseManager extends DisposableObject {
return existingItem;
}
await this.addDatabaseItem(progress, token, databaseItem);
await this.addDatabaseItem(databaseItem);
if (makeSelected) {
await this.setCurrentDatabaseItem(databaseItem);
}
@@ -259,14 +254,11 @@ export class DatabaseManager extends DisposableObject {
}
}
private async reregisterDatabases(
progress: ProgressCallback,
token: vscode.CancellationToken,
) {
private async reregisterDatabases(progress: ProgressCallback) {
let completed = 0;
await Promise.all(
this._databaseItems.map(async (databaseItem) => {
await this.registerDatabase(progress, token, databaseItem);
await this.registerDatabase(databaseItem);
completed++;
progress({
maxStep: this._databaseItems.length,
@@ -323,8 +315,6 @@ export class DatabaseManager extends DisposableObject {
}
private async createDatabaseItemFromPersistedState(
progress: ProgressCallback,
token: vscode.CancellationToken,
state: PersistedDatabaseItem,
): Promise<DatabaseItemImpl> {
let displayName: string | undefined = undefined;
@@ -355,12 +345,12 @@ export class DatabaseManager extends DisposableObject {
// Avoid persisting the database state after adding since that should happen only after
// all databases have been added.
await this.addDatabaseItem(progress, token, item, false);
await this.addDatabaseItem(item, false);
return item;
}
public async loadPersistedState(): Promise<void> {
return withProgress(async (progress, token) => {
return withProgress(async (progress) => {
const currentDatabaseUri =
this.ctx.workspaceState.get<string>(CURRENT_DB);
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(
@@ -387,13 +377,11 @@ export class DatabaseManager extends DisposableObject {
});
const databaseItem = await this.createDatabaseItemFromPersistedState(
progress,
token,
database,
);
try {
await this.refreshDatabase(databaseItem);
await this.registerDatabase(progress, token, databaseItem);
await this.registerDatabase(databaseItem);
if (currentDatabaseUri === database.uri) {
await this.setCurrentDatabaseItem(databaseItem, true);
}
@@ -412,6 +400,8 @@ export class DatabaseManager extends DisposableObject {
} catch (e) {
// database list had an unexpected type - nothing to be done?
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError(
asError(e),
)`Database list loading failed: ${getErrorMessage(e)}`,
@@ -486,8 +476,6 @@ export class DatabaseManager extends DisposableObject {
}
private async addDatabaseItem(
progress: ProgressCallback,
token: vscode.CancellationToken,
item: DatabaseItemImpl,
updatePersistedState = true,
) {
@@ -501,7 +489,7 @@ export class DatabaseManager extends DisposableObject {
// Database items reconstituted from persisted state
// will not have their contents yet.
if (item.contents?.datasetUri) {
await this.registerDatabase(progress, token, item);
await this.registerDatabase(item);
}
// note that we use undefined as the item in order to reset the entire tree
this._onDidChangeDatabaseItem.fire({
@@ -520,11 +508,7 @@ export class DatabaseManager extends DisposableObject {
});
}
public async removeDatabaseItem(
progress: ProgressCallback,
token: vscode.CancellationToken,
item: DatabaseItem,
) {
public async removeDatabaseItem(item: DatabaseItem) {
if (this._currentDatabaseItem === item) {
this._currentDatabaseItem = undefined;
}
@@ -546,7 +530,7 @@ export class DatabaseManager extends DisposableObject {
}
// Remove this database item from the allow-list
await this.deregisterDatabase(progress, token, item);
await this.deregisterDatabase(item);
// Delete folder from file system only if it is controlled by the extension
if (this.isExtensionControlledLocation(item.databaseUri)) {
@@ -569,22 +553,15 @@ export class DatabaseManager extends DisposableObject {
});
}
public async removeAllDatabases(
progress: ProgressCallback,
token: vscode.CancellationToken,
) {
public async removeAllDatabases() {
for (const item of this.databaseItems) {
await this.removeDatabaseItem(progress, token, item);
await this.removeDatabaseItem(item);
}
}
private async deregisterDatabase(
progress: ProgressCallback,
token: vscode.CancellationToken,
dbItem: DatabaseItem,
) {
private async deregisterDatabase(dbItem: DatabaseItem) {
try {
await this.qs.deregisterDatabase(progress, token, dbItem);
await this.qs.deregisterDatabase(dbItem);
} catch (e) {
const message = getErrorMessage(e);
if (message === "Connection is disposed.") {
@@ -597,12 +574,8 @@ export class DatabaseManager extends DisposableObject {
throw e;
}
}
private async registerDatabase(
progress: ProgressCallback,
token: vscode.CancellationToken,
dbItem: DatabaseItem,
) {
await this.qs.registerDatabase(progress, token, dbItem);
private async registerDatabase(dbItem: DatabaseItem) {
await this.qs.registerDatabase(dbItem);
}
/**
@@ -652,7 +625,7 @@ export class DatabaseManager extends DisposableObject {
private isExtensionControlledLocation(uri: vscode.Uri) {
const storageUri = this.ctx.storageUri || this.ctx.globalStorageUri;
if (storageUri) {
return containsPath(storageUri.fsPath, uri.fsPath, process.platform);
return containsPath(storageUri.fsPath, uri.fsPath);
}
return false;
}

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