Compare commits
382 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53df2bcd87 | ||
|
|
0835b140e7 | ||
|
|
0e62d2635c | ||
|
|
78284cbc7a | ||
|
|
0d7002273a | ||
|
|
7041dd7698 | ||
|
|
a3d41a2afe | ||
|
|
0cbdadb271 | ||
|
|
5105187dbd | ||
|
|
918362f39e | ||
|
|
0b5d2d86cf | ||
|
|
3a035708c5 | ||
|
|
84211c63bb | ||
|
|
11218522e7 | ||
|
|
0bdd441767 | ||
|
|
667bf19f46 | ||
|
|
c459d0ff65 | ||
|
|
039b28235d | ||
|
|
76db520ce7 | ||
|
|
0e1afcee64 | ||
|
|
62c9e51c25 | ||
|
|
2b47d3d192 | ||
|
|
4a62d05af6 | ||
|
|
e77cf28192 | ||
|
|
59e23f35e2 | ||
|
|
33d55b1f0a | ||
|
|
e7ddb4406a | ||
|
|
9a12a12065 | ||
|
|
073440914d | ||
|
|
c6a9f23e7e | ||
|
|
d531bc642d | ||
|
|
a6625334f0 | ||
|
|
77bb9780ec | ||
|
|
bb88c148aa | ||
|
|
6df7ea3ddc | ||
|
|
2beaf9a88c | ||
|
|
a7dbae02e0 | ||
|
|
28ac019929 | ||
|
|
2e2ab11e4f | ||
|
|
edf3cad6e4 | ||
|
|
246c347b04 | ||
|
|
e332b26f29 | ||
|
|
22f6ac7974 | ||
|
|
e45f4bd0d9 | ||
|
|
f4d74c7d3f | ||
|
|
7ba58b6298 | ||
|
|
2b59c041b8 | ||
|
|
a434fbffbc | ||
|
|
649f69234e | ||
|
|
4ee86c15ad | ||
|
|
fcfd4f37a6 | ||
|
|
6561bb0543 | ||
|
|
319a54c32f | ||
|
|
385b0e8d1a | ||
|
|
4dcfa8b679 | ||
|
|
47509a922a | ||
|
|
7eab7e4e48 | ||
|
|
f794a19d96 | ||
|
|
e98611fd21 | ||
|
|
461cf15a47 | ||
|
|
54e1b29940 | ||
|
|
623890adc9 | ||
|
|
e7f75ab928 | ||
|
|
df02fecf3c | ||
|
|
a861346b10 | ||
|
|
86b2157552 | ||
|
|
47fa163cb9 | ||
|
|
c78f01758a | ||
|
|
353e22d6e8 | ||
|
|
599d31e5ac | ||
|
|
64df792eda | ||
|
|
feebf7c3fd | ||
|
|
27c4bd8349 | ||
|
|
d4cbfbb70e | ||
|
|
153424ae5a | ||
|
|
edd2aa5e8f | ||
|
|
0265353370 | ||
|
|
623df4c2ee | ||
|
|
8b0825ab3c | ||
|
|
c620939895 | ||
|
|
e4a290fe37 | ||
|
|
3230cc9166 | ||
|
|
4dae9b87d4 | ||
|
|
5c368cec8b | ||
|
|
b76369330d | ||
|
|
323c5368ba | ||
|
|
c3e18910fc | ||
|
|
603c799717 | ||
|
|
aa045835fd | ||
|
|
850c04a289 | ||
|
|
4d19955740 | ||
|
|
b623f92c4a | ||
|
|
08342f1960 | ||
|
|
af334a98e3 | ||
|
|
9e26c29ddb | ||
|
|
705a7975c5 | ||
|
|
6be98f3f8d | ||
|
|
3b6263fb07 | ||
|
|
29aa4a3f29 | ||
|
|
3a494dff36 | ||
|
|
a01a76cb73 | ||
|
|
a29112e045 | ||
|
|
ee1bf8896e | ||
|
|
2410d2bfdd | ||
|
|
0e5551b650 | ||
|
|
951bd13881 | ||
|
|
08944a292c | ||
|
|
cf0057ecd9 | ||
|
|
1d691c2b7f | ||
|
|
20c63921f7 | ||
|
|
9c275130a5 | ||
|
|
b1df4a4f0a | ||
|
|
08a4457e39 | ||
|
|
db3d24236c | ||
|
|
9d40d9a703 | ||
|
|
0fabcda49b | ||
|
|
2b0bd840e6 | ||
|
|
d499ff3cf4 | ||
|
|
1806108166 | ||
|
|
02af432d5f | ||
|
|
76ec9e2e50 | ||
|
|
5ba64a1db3 | ||
|
|
f87b1c46de | ||
|
|
8eef4eb4f2 | ||
|
|
252c7a20c4 | ||
|
|
86d7d8345c | ||
|
|
a704cd7bae | ||
|
|
e75eccb416 | ||
|
|
17947fb7c2 | ||
|
|
c00207cccc | ||
|
|
801df7b14a | ||
|
|
a6c7af09d4 | ||
|
|
96668928aa | ||
|
|
a6c97077fb | ||
|
|
6d7fbfc4f8 | ||
|
|
c5175e040e | ||
|
|
55af9bc47c | ||
|
|
ada62ffd33 | ||
|
|
e10e3adc14 | ||
|
|
16af4c49ea | ||
|
|
c970c3bc19 | ||
|
|
3699f0b3b3 | ||
|
|
ccbe4ee974 | ||
|
|
486180d149 | ||
|
|
9362447338 | ||
|
|
ff34079247 | ||
|
|
7cd99cb000 | ||
|
|
21fd0cfd29 | ||
|
|
db3337cc1b | ||
|
|
a525499c2a | ||
|
|
6d2859cee5 | ||
|
|
ac9128735f | ||
|
|
2c0838e393 | ||
|
|
b294e16c44 | ||
|
|
f678f49bc9 | ||
|
|
99dabb0779 | ||
|
|
add7a25578 | ||
|
|
487a753ede | ||
|
|
79ea901a52 | ||
|
|
2872a2dcaf | ||
|
|
8b8bacb718 | ||
|
|
83e38c811f | ||
|
|
8107bf7cb1 | ||
|
|
625b3a5c37 | ||
|
|
0fa3cf5d8e | ||
|
|
558b9329c5 | ||
|
|
e15b7681db | ||
|
|
6539417d2d | ||
|
|
095d56ee37 | ||
|
|
7578697e92 | ||
|
|
6e06e7934b | ||
|
|
58249e3efe | ||
|
|
75540b449f | ||
|
|
04dfc4e647 | ||
|
|
3ba1712be0 | ||
|
|
e8f68c1b5f | ||
|
|
a9edb36242 | ||
|
|
40b79f2e61 | ||
|
|
3489c26ef6 | ||
|
|
6b522819fd | ||
|
|
15b8d2bdb4 | ||
|
|
213a03555a | ||
|
|
0a9a9792ad | ||
|
|
c4fe868826 | ||
|
|
4cf67ef799 | ||
|
|
c43d0fa805 | ||
|
|
0a27c0538d | ||
|
|
e798663bbb | ||
|
|
169a425e0b | ||
|
|
66fdabf4c8 | ||
|
|
f249b36660 | ||
|
|
1e6b7a6619 | ||
|
|
552a4f6eb3 | ||
|
|
2b17979b6c | ||
|
|
3999ae3728 | ||
|
|
493e8d915e | ||
|
|
dc632d5c3d | ||
|
|
ae2d6ce16e | ||
|
|
27a7474f2b | ||
|
|
8b1c52886a | ||
|
|
f9ddb4080c | ||
|
|
3afa15a0ce | ||
|
|
f22714777c | ||
|
|
469f65a392 | ||
|
|
9b10a09727 | ||
|
|
3c10e87529 | ||
|
|
a1ea1f8135 | ||
|
|
ffc90a0c30 | ||
|
|
e34f4ed485 | ||
|
|
7c1b6e2ff6 | ||
|
|
e719df711a | ||
|
|
6accba66fe | ||
|
|
a657df4468 | ||
|
|
6aab4b4090 | ||
|
|
c7e5922bd5 | ||
|
|
bb25874299 | ||
|
|
d5c78fd67b | ||
|
|
6c5f160eee | ||
|
|
ecbc458106 | ||
|
|
93652fc75f | ||
|
|
859eca0195 | ||
|
|
905eaf66aa | ||
|
|
7af8b7a274 | ||
|
|
a6b6b5a7d6 | ||
|
|
9aff9891d3 | ||
|
|
681a15ce45 | ||
|
|
f82b51f7c5 | ||
|
|
5b7124683a | ||
|
|
031b5076db | ||
|
|
f1533dde2d | ||
|
|
e9b67dd90c | ||
|
|
7bfe0df901 | ||
|
|
b1debee244 | ||
|
|
3b00d74f47 | ||
|
|
4efd3f8fe8 | ||
|
|
95c512e3e8 | ||
|
|
7501f9b81e | ||
|
|
a98b998e5f | ||
|
|
e03d106bc2 | ||
|
|
3c63df2221 | ||
|
|
c6996771ab | ||
|
|
e475036721 | ||
|
|
42192fa922 | ||
|
|
947084d792 | ||
|
|
6c1cd71743 | ||
|
|
c4b890597d | ||
|
|
93251f8d57 | ||
|
|
452329b07a | ||
|
|
1afee02e78 | ||
|
|
cbb1de4faf | ||
|
|
9e92c6c304 | ||
|
|
7864844ddd | ||
|
|
c77a300f24 | ||
|
|
f5fbd7f4cc | ||
|
|
405292ecd4 | ||
|
|
3be7eb9e15 | ||
|
|
c972a5c0de | ||
|
|
5ae67fecda | ||
|
|
d33c26798d | ||
|
|
9392fb75c8 | ||
|
|
e0509f684b | ||
|
|
c55e87c64b | ||
|
|
bc01d73ba5 | ||
|
|
db55e9cd42 | ||
|
|
19890b8591 | ||
|
|
df1c12f2ba | ||
|
|
bd67afe799 | ||
|
|
7a2876faad | ||
|
|
4c9ce2d537 | ||
|
|
868ffd79a5 | ||
|
|
eb3900f642 | ||
|
|
3934ba7e69 | ||
|
|
3e259f14c9 | ||
|
|
4323aad254 | ||
|
|
cd7c26f2ff | ||
|
|
6e9280b97e | ||
|
|
e43adb6424 | ||
|
|
54435d78cf | ||
|
|
3a1431ca31 | ||
|
|
73f161cdac | ||
|
|
ef008a1659 | ||
|
|
dc33784dbc | ||
|
|
c4396b764c | ||
|
|
4e096400db | ||
|
|
f99177ac21 | ||
|
|
f95bf6889b | ||
|
|
ed48f4ac76 | ||
|
|
d2f4f33bea | ||
|
|
a68d5df13b | ||
|
|
495f632ae2 | ||
|
|
929f54333b | ||
|
|
4f1a92d09c | ||
|
|
91866971dd | ||
|
|
9b15b35274 | ||
|
|
b3544b461a | ||
|
|
53fccdfb2e | ||
|
|
606bfd7f87 | ||
|
|
7d088b749b | ||
|
|
315021ef35 | ||
|
|
1dc70fe625 | ||
|
|
36f6531fc4 | ||
|
|
4227ff6338 | ||
|
|
ca96cdf879 | ||
|
|
aad1fee787 | ||
|
|
fc735cb83b | ||
|
|
48399a9aeb | ||
|
|
c514575bc8 | ||
|
|
651bc51ed6 | ||
|
|
dea6426c0b | ||
|
|
96a8bea50a | ||
|
|
297260af88 | ||
|
|
26450e9236 | ||
|
|
8d74933c70 | ||
|
|
8136328ad6 | ||
|
|
d3df14f860 | ||
|
|
31e233cc59 | ||
|
|
23641a01d9 | ||
|
|
209edf86e7 | ||
|
|
3d410c9ce2 | ||
|
|
ef38ffaa6d | ||
|
|
f195e93d91 | ||
|
|
26c301603d | ||
|
|
b9367095cf | ||
|
|
3d415d2852 | ||
|
|
177c770f4d | ||
|
|
d4033615c8 | ||
|
|
9ec6590548 | ||
|
|
5fc9a73e20 | ||
|
|
af5547fb77 | ||
|
|
1b0f0f4566 | ||
|
|
1c81eb1b07 | ||
|
|
a506276635 | ||
|
|
f3e72a0ab8 | ||
|
|
c7a9337ac0 | ||
|
|
c7bd343f54 | ||
|
|
9c76ba35f1 | ||
|
|
90093fb9f5 | ||
|
|
f183703b34 | ||
|
|
49a3b534ae | ||
|
|
d83ca35f0c | ||
|
|
8bc8ffe1a1 | ||
|
|
ea2454742c | ||
|
|
d970b51a7c | ||
|
|
69f5d2c134 | ||
|
|
4d4a72bddb | ||
|
|
ac9355edd1 | ||
|
|
2a9c8ef9dd | ||
|
|
48f0b92d67 | ||
|
|
57dcf58518 | ||
|
|
43efc9cc4c | ||
|
|
17e0291bb5 | ||
|
|
499060d549 | ||
|
|
a359fd7054 | ||
|
|
d3a9426411 | ||
|
|
72faf86678 | ||
|
|
7f20921984 | ||
|
|
d7565fb849 | ||
|
|
9175449323 | ||
|
|
c8487c9431 | ||
|
|
93fc90fcd0 | ||
|
|
0a705a6bfa | ||
|
|
b424157ab0 | ||
|
|
8c13124dfd | ||
|
|
cbf0f279b9 | ||
|
|
a5b22fd0b9 | ||
|
|
c6736e8576 | ||
|
|
acf7270e39 | ||
|
|
3ae55e92b8 | ||
|
|
c3fa83f301 | ||
|
|
9018cac91a | ||
|
|
aec742a508 | ||
|
|
64f6a770c3 | ||
|
|
7a2b61432c | ||
|
|
b04ecc959a | ||
|
|
3ee16f88f2 | ||
|
|
ca079e085d | ||
|
|
c93dd7a25b | ||
|
|
e379320015 | ||
|
|
bbacb147d2 | ||
|
|
58d38ff867 | ||
|
|
0897cdd96d | ||
|
|
709c49542e |
2
.github/workflows/bump-cli.yml
vendored
2
.github/workflows/bump-cli.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Bump CLI
|
||||
|
||||
8
.github/workflows/cli-test.yml
vendored
8
.github/workflows/cli-test.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Set the variables
|
||||
id: set-variables
|
||||
run: echo "cli-versions=$(cat ./extensions/ql-vscode/supported_cli_versions.json | jq -rc)" >> $GITHUB_OUTPUT
|
||||
@@ -58,11 +58,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.17.1'
|
||||
node-version-file: extensions/ql-vscode/.nvmrc
|
||||
cache: 'npm'
|
||||
cache-dependency-path: extensions/ql-vscode/package-lock.json
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout QL
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: github/codeql
|
||||
ref: ${{ steps.choose-ref.outputs.ref }}
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@main
|
||||
|
||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@@ -11,6 +11,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
||||
62
.github/workflows/main.yml
vendored
62
.github/workflows/main.yml
vendored
@@ -16,13 +16,13 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.17.1'
|
||||
node-version-file: extensions/ql-vscode/.nvmrc
|
||||
cache: 'npm'
|
||||
cache-dependency-path: extensions/ql-vscode/package-lock.json
|
||||
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.17.1'
|
||||
node-version-file: extensions/ql-vscode/.nvmrc
|
||||
cache: 'npm'
|
||||
cache-dependency-path: extensions/ql-vscode/package-lock.json
|
||||
|
||||
@@ -81,6 +81,8 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
working-directory: extensions/ql-vscode
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
run: |
|
||||
npm run lint
|
||||
|
||||
@@ -99,6 +101,42 @@ jobs:
|
||||
run: |
|
||||
npm run find-deadcode
|
||||
|
||||
generated:
|
||||
name: Check generated code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: extensions/ql-vscode/.nvmrc
|
||||
cache: 'npm'
|
||||
cache-dependency-path: extensions/ql-vscode/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: extensions/ql-vscode
|
||||
run: |
|
||||
npm ci
|
||||
shell: bash
|
||||
|
||||
- name: Check that repo is clean
|
||||
run: |
|
||||
git diff --exit-code
|
||||
git diff --exit-code --cached
|
||||
|
||||
- name: Generate code
|
||||
working-directory: extensions/ql-vscode
|
||||
run: |
|
||||
npm run generate
|
||||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
git diff --exit-code
|
||||
git diff --exit-code --cached
|
||||
|
||||
unit-test:
|
||||
name: Unit Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -107,13 +145,13 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.17.1'
|
||||
node-version-file: extensions/ql-vscode/.nvmrc
|
||||
cache: 'npm'
|
||||
cache-dependency-path: extensions/ql-vscode/package-lock.json
|
||||
|
||||
@@ -141,13 +179,13 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.17.1'
|
||||
node-version-file: extensions/ql-vscode/.nvmrc
|
||||
cache: 'npm'
|
||||
cache-dependency-path: extensions/ql-vscode/package-lock.json
|
||||
|
||||
@@ -187,7 +225,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Set the variable
|
||||
id: set-variable
|
||||
run: |
|
||||
@@ -211,11 +249,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.17.1'
|
||||
node-version-file: extensions/ql-vscode/.nvmrc
|
||||
cache: 'npm'
|
||||
cache-dependency-path: extensions/ql-vscode/package-lock.json
|
||||
|
||||
@@ -232,7 +270,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Checkout QL
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: github/codeql
|
||||
ref: 'codeql-cli/${{ needs.get-latest-cli-version.outputs.cli-version }}'
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.17.1'
|
||||
node-version-file: extensions/ql-vscode/.nvmrc
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
**/* @github/codeql-vscode-reviewers
|
||||
**/variant-analysis/ @github/code-scanning-secexp-reviewers
|
||||
**/databases/ @github/code-scanning-secexp-reviewers
|
||||
**/method-modeling/ @github/code-scanning-secexp-reviewers
|
||||
**/model-editor/ @github/code-scanning-secexp-reviewers
|
||||
**/queries-panel/ @github/code-scanning-secexp-reviewers
|
||||
|
||||
BIN
docs/images/about-vscode.png
Normal file
BIN
docs/images/about-vscode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
30
docs/node-version.md
Normal file
30
docs/node-version.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Node version
|
||||
|
||||
The CodeQL for VS Code extension defines the version of Node.js that it is intended to run with. This Node.js version is used when running most CI and unit tests.
|
||||
|
||||
When running in production (i.e. as an extension for a VS Code application) it will use the Node.js version provided by VS Code. This can mean a different Node.js version is used by different users with different versions of VS Code.
|
||||
We should make sure the CodeQL for VS Code extension works with the Node.js version supplied by all versions of VS Code that we support.
|
||||
|
||||
## Checking the version of Node.js supplied by VS Code
|
||||
|
||||
You can find this info by seleting "About Visual Studio Code" from the top menu.
|
||||
|
||||

|
||||
|
||||
## Updating the Node.js version
|
||||
|
||||
The following files will need to be updated:
|
||||
|
||||
- `extensions/ql-vscode/.nvmrc` - this will enable nvm to automatically switch to the correct Node
|
||||
version when you're in the project folder. It will also change the Node version the GitHub Actions
|
||||
workflows use.
|
||||
- `extensions/ql-vscode/package.json` - the "engines.node: '[VERSION]'" setting
|
||||
- `extensions/ql-vscode/package.json` - the "@types/node: '[VERSION]'" dependency
|
||||
|
||||
Then run `npm install` to update the `extensions/ql-vscode/package-lock.json` file.
|
||||
|
||||
## Node.js version used in tests
|
||||
|
||||
Unit tests will use whatever version of Node.js is installed locally. In CI this will be the version specified in the workflow.
|
||||
|
||||
Integration tests download a copy of VS Code and then will use whatever version of Node.js is provided by VS Code. Our integration tests are currently pinned to an older version of VS Code. See [VS Code version used in tests](./vscode-version.md#vs-code-version-used-in-tests) for more information.
|
||||
@@ -11,10 +11,7 @@
|
||||
* New telemetry events are added.
|
||||
* Deprecation or removal of commands.
|
||||
* Accumulation of many changes, none of which are individually big enough to warrant a minor bump, but which together are. This does not include changes which are purely internal to the extension, such as refactoring, or which are only available behind a feature flag.
|
||||
1. Double-check that the node version we're using matches the one used for VS Code. You can find this info by seleting "About Visual Studio Code" from the top menu. If it doesn't match, you will then need to update the node version in the following files:
|
||||
* `.nvmrc` - this will enable `nvm` to automatically switch to the correct node version when you're in the project folder
|
||||
* `.github/workflows/main.yml` - all the "node-version: '[VERSION]'" settings
|
||||
* `.github/workflows/release.yml` - the "node-version: '[VERSION]'" setting
|
||||
1. Double-check that the node version we're using matches the one used for VS Code. See the [Node.js version instructions](./node-version.md) for more information.
|
||||
1. Double-check that the extension `package.json` and `package-lock.json` have the version you intend to release. If you are doing a patch release (as opposed to minor or major version) this should already be correct.
|
||||
1. Create a PR for this release:
|
||||
* This PR will contain any missing bits from steps 1, 2 and 3. Most of the time, this will just be updating `CHANGELOG.md` with today's date.
|
||||
|
||||
@@ -145,8 +145,6 @@ Run one of the above MRVAs, but cancel it from within VS Code:
|
||||
|
||||
### CodeQL Model Editor
|
||||
|
||||
Note the tests here require the feature flag: `codeQL.model.editor`
|
||||
|
||||
#### Test Case 1: Opening the model editor
|
||||
|
||||
1. Download the `sofastack/sofa-jraft` java database from GitHub.
|
||||
@@ -175,6 +173,8 @@ Note that this test requires the feature flag: `codeQL.model.llmGeneration`
|
||||
|
||||
#### Test Case 4: Model as dependency
|
||||
|
||||
Note that this test requires the feature flag: `codeQL.model.flowGeneration`
|
||||
|
||||
1. Click "Model as dependency"
|
||||
- Check that grouping are now per package (e.g. `com.alipay.sofa.rraft.option` or `com.google.protobuf`)
|
||||
2. Click "Generate".
|
||||
|
||||
33
docs/vscode-version.md
Normal file
33
docs/vscode-version.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# VS Code version
|
||||
|
||||
The CodeQL for VS Code extension specifies the versions of VS Code that it is compatible with. VS Code will only offer to install and upgrade the extension when this version range is satisfied.
|
||||
|
||||
## Where is the VS Code version specified
|
||||
|
||||
1. Hard limit in [`package.json`](https://github.com/github/vscode-codeql/blob/606bfd7f877d9fffe4ff83b78015ab15f8840b12/extensions/ql-vscode/package.json#L16)
|
||||
|
||||
This is the value that VS Code understands and respects. If a user does not meet this version requirement then VS Code will not offer to install the CodeQL for VS Code extension, and if the extension is already installed then it will silently refuse to upgrade the extension.
|
||||
|
||||
1. Soft limit in [`extension.ts`](https://github.com/github/vscode-codeql/blob/606bfd7f877d9fffe4ff83b78015ab15f8840b12/extensions/ql-vscode/src/extension.ts#L307)
|
||||
|
||||
This value is used internally by the CodeQL for VS Code extension and is used to provide a warning to users without blocking them from installing or upgrading. If the extension detects that this version range is not met it will output a warning message to the user prompting them to upgrade their VS Code version to ge the latest features of CodeQL.
|
||||
|
||||
## When to update the VS Code version
|
||||
|
||||
Generally we should aim to support as wide a range of VS Code versions as we can, so unless there is a reason to do so we do not update the minimum VS Code version requirement.
|
||||
Reasons for updating the minimum VS Code version include:
|
||||
|
||||
- A new feature is included in VS Code. We may want to ensure that it is available to use so we do not have to provide an alternative code path.
|
||||
- A breaking change has happened in VS Code, and it is not possible to support both new and old versions.
|
||||
|
||||
Also consider what percentage of our users are using each VS Code version. This information is available in our telemetry.
|
||||
|
||||
## How to update the VS Code version
|
||||
|
||||
To provide a good experience to users, it is recommented to update the `MIN_VERSION` in `extension.ts` first and release, and then update the `vscode` version in `package.json` and release again. By stagging this update across two releases it gives users on older VS Code versions a chance to upgrade before it silently refuses to upgrade them.
|
||||
|
||||
## VS Code version used in tests
|
||||
|
||||
Our integration tests are currently pinned to use an older version of VS Code due to <https://github.com/github/vscode-codeql/issues/2402>.
|
||||
This version is specified in [`jest-runner-vscode.config.base.js`](https://github.com/github/vscode-codeql/blob/d93f2b67c84e79737b0ce4bb74e31558b5f5166e/extensions/ql-vscode/test/vscode-tests/jest-runner-vscode.config.base.js#L17).
|
||||
Until this is resolved this will limit us updating our minimum supported version of VS Code.
|
||||
@@ -1 +1 @@
|
||||
v16.17.1
|
||||
v18.15.0
|
||||
|
||||
@@ -5,7 +5,15 @@ import { action } from "@storybook/addon-actions";
|
||||
// Allow all stories/components to use Codicons
|
||||
import "@vscode/codicons/dist/codicon.css";
|
||||
|
||||
(window as any).acquireVsCodeApi = () => ({
|
||||
import type { VsCodeApi } from "../src/view/vscode-api";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
acquireVsCodeApi: () => VsCodeApi;
|
||||
}
|
||||
}
|
||||
|
||||
window.acquireVsCodeApi = () => ({
|
||||
postMessage: action("post-vscode-message"),
|
||||
setState: action("set-vscode-state"),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.9.2 - 12 October 2023
|
||||
|
||||
- Fix a bug where the query to Find Definitions in database source files would not be cancelled appropriately. [#2885](https://github.com/github/vscode-codeql/pull/2885)
|
||||
- It is now possible to show the language of query history items using the `%l` specifier in the `codeQL.queryHistory.format` setting. Note that this only works for queries run after this upgrade, and older items will show `unknown` as a language. [#2892](https://github.com/github/vscode-codeql/pull/2892)
|
||||
- Increase the required version of VS Code to 1.82.0. [#2877](https://github.com/github/vscode-codeql/pull/2877)
|
||||
- Fix a bug where the query server was restarted twice after configuration changes. [#2884](https://github.com/github/vscode-codeql/pull/2884).
|
||||
- Add support for the `telemetry.telemetryLevel` setting. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code). [#2824](https://github.com/github/vscode-codeql/pull/2824).
|
||||
- Fix syntax highlighting directly after import statements with instantiation arguments. [#2792](https://github.com/github/vscode-codeql/pull/2792)
|
||||
- The `debug.saveBeforeStart` setting is now respected when running variant analyses. [#2950](https://github.com/github/vscode-codeql/pull/2950)
|
||||
- The 'open database' button of the model editor was renamed to 'open source'. Also, it's now only available if the source archive is available as a workspace folder. [#2945](https://github.com/github/vscode-codeql/pull/2945)
|
||||
|
||||
## 1.9.1 - 29 September 2023
|
||||
|
||||
- Add warning when using a VS Code version older than 1.82.0. [#2854](https://github.com/github/vscode-codeql/pull/2854)
|
||||
- Fix a bug when parsing large evaluation log summaries. [#2858](https://github.com/github/vscode-codeql/pull/2858)
|
||||
- Right-align and format numbers in raw result tables. [#2864](https://github.com/github/vscode-codeql/pull/2864)
|
||||
- Remove rate limit warning notifications when using Code Search to add repositories to a variant analysis list. [#2812](https://github.com/github/vscode-codeql/pull/2812)
|
||||
|
||||
## 1.9.0 - 19 September 2023
|
||||
|
||||
- Release the [CodeQL model editor](https://codeql.github.com/docs/codeql/codeql-for-visual-studio-code/using-the-codeql-model-editor) to create CodeQL model packs for Java frameworks. Open the editor using the "CodeQL: Open CodeQL Model Editor (Beta)" command. [#2823](https://github.com/github/vscode-codeql/pull/2823)
|
||||
|
||||
## 1.8.12 - 11 September 2023
|
||||
|
||||
- Fix a bug where variant analysis queries would fail for queries in the `codeql/java-queries` query pack. [#2786](https://github.com/github/vscode-codeql/pull/2786)
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "fs-extra";
|
||||
import { resolve, join } from "path";
|
||||
import { isDevBuild } from "./dev";
|
||||
import type * as packageJsonType from "../package.json";
|
||||
|
||||
export interface DeployedPackage {
|
||||
distPath: string;
|
||||
@@ -46,12 +47,10 @@ async function copyPackage(
|
||||
);
|
||||
}
|
||||
|
||||
export async function deployPackage(
|
||||
packageJsonPath: string,
|
||||
): Promise<DeployedPackage> {
|
||||
export async function deployPackage(): Promise<DeployedPackage> {
|
||||
try {
|
||||
const packageJson: any = JSON.parse(
|
||||
await readFile(packageJsonPath, "utf8"),
|
||||
const packageJson: typeof packageJsonType = JSON.parse(
|
||||
await readFile(resolve(__dirname, "../package.json"), "utf8"),
|
||||
);
|
||||
|
||||
const distDir = join(__dirname, "../../../dist");
|
||||
|
||||
@@ -3,9 +3,7 @@ import { deployPackage } from "./deploy";
|
||||
import { spawn } from "child-process-promise";
|
||||
|
||||
export async function packageExtension(): Promise<void> {
|
||||
const deployedPackage = await deployPackage(
|
||||
resolve(__dirname, "../package.json"),
|
||||
);
|
||||
const deployedPackage = await deployPackage();
|
||||
console.log(
|
||||
`Packaging extension '${deployedPackage.name}@${deployedPackage.version}'...`,
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
"rootDir": ".."
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" fill="none"
|
||||
viewBox="0 0 432 432" style="enable-background:new 0 0 432 432;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="234.24,9.067 183.893,59.413 284.587,59.413" fill="#C5C5C5"/>
|
||||
<polygon points="301.44,304.32 427.947,120.853 427.947,93.973 250.88,93.973 250.88,128.107 376.32,128.107 250.027,310.72
|
||||
250.027,338.24 432,338.24 432,304.32" fill="#C5C5C5"/>
|
||||
<polygon points="234.24,422.933 283.947,373.227 184.533,373.227" fill="#C5C5C5"/>
|
||||
<path d="M226.773,338.24L130.987,93.76H96L0,338.24h39.253l19.627-52.267h109.013l19.627,52.267H226.773z M71.893,250.987
|
||||
L113.28,140.48l41.387,110.507H71.893z" fill="#C5C5C5"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 953 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 2L6 3V6H7V3H14V5.45306L14.2071 5.29286L15 6.08576V3L14 2H7ZM8 4H10V6H8V4ZM5 9H3V11H5V9ZM2 7L1 8V13L2 14H9L10 13V8L9 7H2ZM2 13V8H9V13H2ZM8 10H6V12H8V10ZM13 4H12V7.86388L10.818 6.68192L10.1109 7.38903L12.1465 9.42454L12.8536 9.42454L14.889 7.38908L14.1819 6.68197L13 7.86388V4Z" fill="#C5C5C5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 449 B |
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 432 432" style="enable-background:new 0 0 432 432;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="234.24,9.067 183.893,59.413 284.587,59.413 "/>
|
||||
<polygon points="301.44,304.32 427.947,120.853 427.947,93.973 250.88,93.973 250.88,128.107 376.32,128.107 250.027,310.72
|
||||
250.027,338.24 432,338.24 432,304.32 "/>
|
||||
<polygon points="234.24,422.933 283.947,373.227 184.533,373.227 "/>
|
||||
<path d="M226.773,338.24L130.987,93.76H96L0,338.24h39.253l19.627-52.267h109.013l19.627,52.267H226.773z M71.893,250.987
|
||||
L113.28,140.48l41.387,110.507H71.893z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 894 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 2L6 3V6H7V3H14V5.45306L14.2071 5.29286L15 6.08576V3L14 2H7ZM8 4H10V6H8V4ZM5 9H3V11H5V9ZM2 7L1 8V13L2 14H9L10 13V8L9 7H2ZM2 13V8H9V13H2ZM8 10H6V12H8V10ZM13 4H12V7.86388L10.818 6.68192L10.1109 7.38903L12.1465 9.42454L12.8536 9.42454L14.889 7.38908L14.1819 6.68197L13 7.86388V4Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 449 B |
2530
extensions/ql-vscode/package-lock.json
generated
2530
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.8.12",
|
||||
"version": "1.9.2",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -13,8 +13,8 @@
|
||||
"url": "https://github.com/github/vscode-codeql"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.67.0",
|
||||
"node": "^16.17.1",
|
||||
"vscode": "^1.82.0",
|
||||
"node": "^18.15.0",
|
||||
"npm": ">=7.20.6"
|
||||
},
|
||||
"categories": [
|
||||
@@ -110,6 +110,10 @@
|
||||
"string"
|
||||
],
|
||||
"description": "Names of extension packs to include in the evaluation. These are resolved from the locations specified in `additionalPacks`."
|
||||
},
|
||||
"additionalRunQueryArgs": {
|
||||
"type": "object",
|
||||
"description": "**Internal use only**. Additional arguments to pass to the `runQuery` command of the query server, without validation."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,13 +450,20 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"scope": "application",
|
||||
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)"
|
||||
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the one of the global telemetry settings (`#telemetry.enableTelemetry#` or `#telemetry.telemetryLevel#`) must be enabled for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)",
|
||||
"tags": [
|
||||
"telemetry",
|
||||
"usesOnlineServices"
|
||||
]
|
||||
},
|
||||
"codeQL.telemetry.logTelemetry": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"scope": "application",
|
||||
"description": "Specifies whether or not to write telemetry events to the extension log."
|
||||
"description": "Specifies whether or not to write telemetry events to the extension log.",
|
||||
"tags": [
|
||||
"telemetry"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,6 +764,78 @@
|
||||
"command": "codeQLDatabases.addDatabaseSource",
|
||||
"title": "Add Database Source to Workspace"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayAllLanguages",
|
||||
"title": "All languages"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayAllLanguagesSelected",
|
||||
"title": "All languages (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCpp",
|
||||
"title": "C/C++"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCppSelected",
|
||||
"title": "C/C++ (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCsharp",
|
||||
"title": "C#"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCsharpSelected",
|
||||
"title": "C# (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayGo",
|
||||
"title": "Go"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayGoSelected",
|
||||
"title": "Go (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJava",
|
||||
"title": "Java/Kotlin"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavaSelected",
|
||||
"title": "Java/Kotlin (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavascript",
|
||||
"title": "JavaScript/TypeScript"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavascriptSelected",
|
||||
"title": "JavaScript/TypeScript (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayPython",
|
||||
"title": "Python"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayPythonSelected",
|
||||
"title": "Python (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayRuby",
|
||||
"title": "Ruby"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayRubySelected",
|
||||
"title": "Ruby (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displaySwift",
|
||||
"title": "Swift"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displaySwiftSelected",
|
||||
"title": "Swift (selected)"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseFolder",
|
||||
"title": "CodeQL: Choose Database from Folder"
|
||||
@@ -771,19 +854,11 @@
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"title": "Sort by Name",
|
||||
"icon": {
|
||||
"light": "media/light/sort-alpha.svg",
|
||||
"dark": "media/dark/sort-alpha.svg"
|
||||
}
|
||||
"title": "Sort by Name"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByDateAdded",
|
||||
"title": "Sort by Date Added",
|
||||
"icon": {
|
||||
"light": "media/light/sort-date.svg",
|
||||
"dark": "media/dark/sort-date.svg"
|
||||
}
|
||||
"title": "Sort by Date Added"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.checkForUpdatesToCLI",
|
||||
@@ -981,16 +1056,6 @@
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByDateAdded",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseFolder",
|
||||
"when": "view == codeQLDatabases",
|
||||
@@ -1011,6 +1076,21 @@
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "1_databases@0"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByDateAdded",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "1_databases@1"
|
||||
},
|
||||
{
|
||||
"submenu": "codeQLDatabases.languages",
|
||||
"when": "view == codeQLDatabases && config.codeQL.canary && config.codeQL.showLanguageFilter",
|
||||
"group": "2_databases@0"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.createQuery",
|
||||
"when": "view == codeQLQueries",
|
||||
@@ -1429,8 +1509,7 @@
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openModelEditor",
|
||||
"when": "config.codeQL.canary && config.codeQL.model.editor"
|
||||
"command": "codeQL.openModelEditor"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueryContextMenu",
|
||||
@@ -1532,6 +1611,78 @@
|
||||
"command": "codeQLDatabases.upgradeDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayAllLanguages",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayAllLanguagesSelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCpp",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCppSelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCsharp",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCsharpSelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayGo",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayGoSelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJava",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavaSelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavascript",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavascriptSelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayPython",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayPythonSelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayRuby",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayRubySelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displaySwift",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displaySwiftSelected",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQueryContextMenu",
|
||||
"when": "false"
|
||||
@@ -1726,8 +1877,88 @@
|
||||
"command": "codeQL.gotoQLContextEditor",
|
||||
"when": "editorLangId == ql-summary && config.codeQL.canary"
|
||||
}
|
||||
],
|
||||
"codeQLDatabases.languages": [
|
||||
{
|
||||
"command": "codeQLDatabases.displayAllLanguages",
|
||||
"when": "codeQLDatabases.languageFilter"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayAllLanguagesSelected",
|
||||
"when": "!codeQLDatabases.languageFilter"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCpp",
|
||||
"when": "codeQLDatabases.languageFilter != cpp"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCppSelected",
|
||||
"when": "codeQLDatabases.languageFilter == cpp"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCsharp",
|
||||
"when": "codeQLDatabases.languageFilter != csharp"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayCsharpSelected",
|
||||
"when": "codeQLDatabases.languageFilter == csharp"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayGo",
|
||||
"when": "codeQLDatabases.languageFilter != go"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayGoSelected",
|
||||
"when": "codeQLDatabases.languageFilter == go"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJava",
|
||||
"when": "codeQLDatabases.languageFilter != java"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavaSelected",
|
||||
"when": "codeQLDatabases.languageFilter == java"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavascript",
|
||||
"when": "codeQLDatabases.languageFilter != javascript"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayJavascriptSelected",
|
||||
"when": "codeQLDatabases.languageFilter == javascript"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayPython",
|
||||
"when": "codeQLDatabases.languageFilter != python"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayPythonSelected",
|
||||
"when": "codeQLDatabases.languageFilter == python"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayRuby",
|
||||
"when": "codeQLDatabases.languageFilter != ruby"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displayRubySelected",
|
||||
"when": "codeQLDatabases.languageFilter == ruby"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displaySwift",
|
||||
"when": "codeQLDatabases.languageFilter != swift"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.displaySwiftSelected",
|
||||
"when": "codeQLDatabases.languageFilter == swift"
|
||||
}
|
||||
]
|
||||
},
|
||||
"submenus": [
|
||||
{
|
||||
"id": "codeQLDatabases.languages",
|
||||
"label": "Languages"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
@@ -1771,6 +2002,12 @@
|
||||
"id": "codeQLEvalLogViewer",
|
||||
"name": "Evaluator Log Viewer",
|
||||
"when": "config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"id": "codeQLMethodModeling",
|
||||
"type": "webview",
|
||||
"name": "CodeQL Method Modeling",
|
||||
"when": "config.codeQL.canary"
|
||||
}
|
||||
],
|
||||
"codeql-methods-usage": [
|
||||
@@ -1779,14 +2016,6 @@
|
||||
"name": "CodeQL Methods Usage",
|
||||
"when": "config.codeQL.canary && codeql.modelEditorOpen"
|
||||
}
|
||||
],
|
||||
"explorer": [
|
||||
{
|
||||
"type": "webview",
|
||||
"id": "codeQLMethodModeling",
|
||||
"name": "CodeQL Method Modeling",
|
||||
"when": "config.codeQL.canary && config.codeQL.model.methodModelingView && codeql.modelEditorOpen"
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewsWelcome": [
|
||||
@@ -1843,13 +2072,15 @@
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"lint:scenarios": "ts-node scripts/lint-scenarios.ts",
|
||||
"generate": "npm-run-all -p generate:*",
|
||||
"generate:schemas": "ts-node scripts/generate-schemas.ts",
|
||||
"check-types": "find . -type f -name \"tsconfig.json\" -not -path \"./node_modules/*\" | sed -r 's|/[^/]+$||' | sort | uniq | xargs -I {} sh -c \"echo Checking types in {} && cd {} && npx tsc --noEmit\"",
|
||||
"postinstall": "patch-package",
|
||||
"prepare": "cd ../.. && husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/plugin-retry": "^4.1.6",
|
||||
"@octokit/rest": "^19.0.4",
|
||||
"@octokit/plugin-retry": "^6.0.1",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@vscode/codicons": "^0.0.31",
|
||||
"@vscode/debugadapter": "^1.59.0",
|
||||
"@vscode/debugprotocol": "^1.59.0",
|
||||
@@ -1863,10 +2094,10 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"immutable": "^4.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"msw": "^1.2.0",
|
||||
"nanoid": "^3.2.0",
|
||||
"msw": "^0.0.0-fetch.rc-20",
|
||||
"nanoid": "^5.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"p-queue": "^6.0.0",
|
||||
"p-queue": "^7.4.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"semver": "^7.5.2",
|
||||
@@ -1883,7 +2114,7 @@
|
||||
"vscode-languageclient": "^8.0.2",
|
||||
"vscode-test-adapter-api": "^1.7.0",
|
||||
"vscode-test-adapter-util": "^0.7.0",
|
||||
"zip-a-folder": "^2.0.0"
|
||||
"zip-a-folder": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
@@ -1893,7 +2124,7 @@
|
||||
"@babel/preset-typescript": "^7.21.4",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@github/markdownlint-github": "^0.3.0",
|
||||
"@octokit/plugin-throttling": "^5.0.1",
|
||||
"@octokit/plugin-throttling": "^8.0.0",
|
||||
"@storybook/addon-actions": "^7.1.0",
|
||||
"@storybook/addon-essentials": "^7.1.0",
|
||||
"@storybook/addon-interactions": "^7.1.0",
|
||||
@@ -1917,9 +2148,9 @@
|
||||
"@types/gulp": "^4.0.9",
|
||||
"@types/gulp-replace": "^1.1.0",
|
||||
"@types/jest": "^29.0.2",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/js-yaml": "^4.0.6",
|
||||
"@types/nanoid": "^3.0.0",
|
||||
"@types/node": "^16.11.25",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/node-fetch": "^2.5.2",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
@@ -1931,7 +2162,7 @@
|
||||
"@types/through2": "^2.0.36",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"@types/unzipper": "^0.10.1",
|
||||
"@types/vscode": "^1.67.0",
|
||||
"@types/vscode": "^1.82.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/webpack-env": "^1.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
@@ -1959,7 +2190,6 @@
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-esbuild": "^0.10.5",
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"husky": "^8.0.0",
|
||||
"jest": "^29.0.3",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
import { pathExists, readJson, writeJson } from "fs-extra";
|
||||
import { resolve, relative } from "path";
|
||||
|
||||
import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
|
||||
import { Octokit } from "@octokit/core";
|
||||
import { type RestEndpointMethodTypes } from "@octokit/rest";
|
||||
import { throttling } from "@octokit/plugin-throttling";
|
||||
|
||||
import { getFiles } from "./util/files";
|
||||
@@ -22,6 +23,7 @@ import type { GitHubApiRequest } from "../src/common/mock-gh-api/gh-api-request"
|
||||
import { isGetVariantAnalysisRequest } from "../src/common/mock-gh-api/gh-api-request";
|
||||
import { VariantAnalysis } from "../src/variant-analysis/gh-api/variant-analysis";
|
||||
import { RepositoryWithMetadata } from "../src/variant-analysis/gh-api/repository";
|
||||
import { AppOctokit } from "../src/common/octokit";
|
||||
|
||||
const extensionDirectory = resolve(__dirname, "..");
|
||||
const scenariosDirectory = resolve(
|
||||
@@ -31,7 +33,7 @@ const scenariosDirectory = resolve(
|
||||
|
||||
// Make sure we don't run into rate limits by automatically waiting until we can
|
||||
// make another request.
|
||||
const MyOctokit = Octokit.plugin(throttling);
|
||||
const MyOctokit = AppOctokit.plugin(throttling);
|
||||
|
||||
const auth = process.env.GITHUB_TOKEN;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { exit } from "process";
|
||||
function ignoreFile(file: string): boolean {
|
||||
return (
|
||||
containsPath("gulpfile.ts", file) ||
|
||||
containsPath(".storybook", file) ||
|
||||
containsPath(join("src", "stories"), file) ||
|
||||
pathsEqual(
|
||||
join("test", "vscode-tests", "jest-runner-installed-extensions.ts"),
|
||||
|
||||
72
extensions/ql-vscode/scripts/generate-schemas.ts
Normal file
72
extensions/ql-vscode/scripts/generate-schemas.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { createGenerator } from "ts-json-schema-generator";
|
||||
import { join, resolve } from "path";
|
||||
import { outputFile } from "fs-extra";
|
||||
import { format, resolveConfig } from "prettier";
|
||||
|
||||
const extensionDirectory = resolve(__dirname, "..");
|
||||
|
||||
const schemas = [
|
||||
{
|
||||
path: join(
|
||||
extensionDirectory,
|
||||
"src",
|
||||
"model-editor",
|
||||
"extension-pack-metadata.ts",
|
||||
),
|
||||
type: "ExtensionPackMetadata",
|
||||
schemaPath: join(
|
||||
extensionDirectory,
|
||||
"src",
|
||||
"model-editor",
|
||||
"extension-pack-metadata.schema.json",
|
||||
),
|
||||
},
|
||||
{
|
||||
path: join(
|
||||
extensionDirectory,
|
||||
"src",
|
||||
"model-editor",
|
||||
"model-extension-file.ts",
|
||||
),
|
||||
type: "ModelExtensionFile",
|
||||
schemaPath: join(
|
||||
extensionDirectory,
|
||||
"src",
|
||||
"model-editor",
|
||||
"model-extension-file.schema.json",
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
async function generateSchema(
|
||||
schemaDefinition: (typeof schemas)[number],
|
||||
): Promise<void> {
|
||||
const schema = createGenerator({
|
||||
path: schemaDefinition.path,
|
||||
tsconfig: resolve(extensionDirectory, "tsconfig.json"),
|
||||
type: schemaDefinition.type,
|
||||
skipTypeCheck: true,
|
||||
topRef: true,
|
||||
additionalProperties: true,
|
||||
}).createSchema(schemaDefinition.type);
|
||||
|
||||
const schemaJson = JSON.stringify(schema, null, 2);
|
||||
|
||||
const prettierOptions = await resolveConfig(schemaDefinition.schemaPath);
|
||||
|
||||
const formattedSchemaJson = await format(schemaJson, {
|
||||
...prettierOptions,
|
||||
filepath: schemaDefinition.schemaPath,
|
||||
});
|
||||
|
||||
await outputFile(schemaDefinition.schemaPath, formattedSchemaJson);
|
||||
}
|
||||
|
||||
async function generateSchemas() {
|
||||
await Promise.all(schemas.map(generateSchema));
|
||||
}
|
||||
|
||||
generateSchemas().catch((e: unknown) => {
|
||||
console.error(e);
|
||||
process.exit(2);
|
||||
});
|
||||
@@ -6,7 +6,6 @@ import { dirname, join, delimiter } from "path";
|
||||
import * as sarif from "sarif";
|
||||
import { SemVer } from "semver";
|
||||
import { Readable } from "stream";
|
||||
import { StringDecoder } from "string_decoder";
|
||||
import tk from "tree-kill";
|
||||
import { promisify } from "util";
|
||||
import { CancellationToken, Disposable, Uri } from "vscode";
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
} from "./distribution";
|
||||
import {
|
||||
assertNever,
|
||||
getChildProcessErrorMessage,
|
||||
getErrorMessage,
|
||||
getErrorStack,
|
||||
} from "../common/helpers-pure";
|
||||
@@ -30,6 +30,7 @@ import { CompilationMessage } from "../query-server/legacy-messages";
|
||||
import { sarifParser } from "../common/sarif-parser";
|
||||
import { App } from "../common/app";
|
||||
import { QueryLanguage } from "../common/query-language";
|
||||
import { LINE_ENDINGS, splitStreamAtSeparators } from "../common/split-stream";
|
||||
|
||||
/**
|
||||
* The version of the SARIF format that we are using.
|
||||
@@ -547,9 +548,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
yield JSON.parse(event) as EventType;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Parsing output of ${description} failed: ${
|
||||
(err as any).stderr || getErrorMessage(err)
|
||||
}`,
|
||||
`Parsing output of ${description} failed: ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -647,9 +646,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
return JSON.parse(result) as OutputType;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Parsing output of ${description} failed: ${
|
||||
(err as any).stderr || getErrorMessage(err)
|
||||
}`,
|
||||
`Parsing output of ${description} failed: ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1647,125 +1644,18 @@ export async function runCodeQlCliCommand(
|
||||
return result.stdout;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`${description} failed: ${(err as any).stderr || getErrorMessage(err)}`,
|
||||
`${description} failed: ${getChildProcessErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer to hold state used when splitting a text stream into lines.
|
||||
*/
|
||||
class SplitBuffer {
|
||||
private readonly decoder = new StringDecoder("utf8");
|
||||
private readonly maxSeparatorLength: number;
|
||||
private buffer = "";
|
||||
private searchIndex = 0;
|
||||
|
||||
constructor(private readonly separators: readonly string[]) {
|
||||
this.maxSeparatorLength = separators
|
||||
.map((s) => s.length)
|
||||
.reduce((a, b) => Math.max(a, b), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append new text data to the buffer.
|
||||
* @param chunk The chunk of data to append.
|
||||
*/
|
||||
public addChunk(chunk: Buffer): void {
|
||||
this.buffer += this.decoder.write(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that the end of the input stream has been reached.
|
||||
*/
|
||||
public end(): void {
|
||||
this.buffer += this.decoder.end();
|
||||
this.buffer += this.separators[0]; // Append a separator to the end to ensure the last line is returned.
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of startsWith that isn't overriden by a broken version of ms-python.
|
||||
*
|
||||
* The definition comes from
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
* which is CC0/public domain
|
||||
*
|
||||
* See https://github.com/github/vscode-codeql/issues/802 for more context as to why we need it.
|
||||
*/
|
||||
private static startsWith(
|
||||
s: string,
|
||||
searchString: string,
|
||||
position: number,
|
||||
): boolean {
|
||||
const pos = position > 0 ? position | 0 : 0;
|
||||
return s.substring(pos, pos + searchString.length) === searchString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the next full line from the buffer, if one is available.
|
||||
* @returns The text of the next available full line (without the separator), or `undefined` if no
|
||||
* line is available.
|
||||
*/
|
||||
public getNextLine(): string | undefined {
|
||||
while (this.searchIndex <= this.buffer.length - this.maxSeparatorLength) {
|
||||
for (const separator of this.separators) {
|
||||
if (SplitBuffer.startsWith(this.buffer, separator, this.searchIndex)) {
|
||||
const line = this.buffer.slice(0, this.searchIndex);
|
||||
this.buffer = this.buffer.slice(this.searchIndex + separator.length);
|
||||
this.searchIndex = 0;
|
||||
return line;
|
||||
}
|
||||
}
|
||||
this.searchIndex++;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a text stream into lines based on a list of valid line separators.
|
||||
* @param stream The text stream to split. This stream will be fully consumed.
|
||||
* @param separators The list of strings that act as line separators.
|
||||
* @returns A sequence of lines (not including separators).
|
||||
*/
|
||||
async function* splitStreamAtSeparators(
|
||||
stream: Readable,
|
||||
separators: string[],
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
const buffer = new SplitBuffer(separators);
|
||||
for await (const chunk of stream) {
|
||||
buffer.addChunk(chunk);
|
||||
let line: string | undefined;
|
||||
do {
|
||||
line = buffer.getNextLine();
|
||||
if (line !== undefined) {
|
||||
yield line;
|
||||
}
|
||||
} while (line !== undefined);
|
||||
}
|
||||
buffer.end();
|
||||
let line: string | undefined;
|
||||
do {
|
||||
line = buffer.getNextLine();
|
||||
if (line !== undefined) {
|
||||
yield line;
|
||||
}
|
||||
} while (line !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard line endings for splitting human-readable text.
|
||||
*/
|
||||
const lineEndings = ["\r\n", "\r", "\n"];
|
||||
|
||||
/**
|
||||
* Log a text stream to a `Logger` interface.
|
||||
* @param stream The stream to log.
|
||||
* @param logger The logger that will consume the stream output.
|
||||
*/
|
||||
async function logStream(stream: Readable, logger: BaseLogger): Promise<void> {
|
||||
for await (const line of splitStreamAtSeparators(stream, lineEndings)) {
|
||||
for await (const line of splitStreamAtSeparators(stream, LINE_ENDINGS)) {
|
||||
// Await the result of log here in order to ensure the logs are written in the correct order.
|
||||
await logger.log(line);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
} from "../variant-analysis/shared/variant-analysis";
|
||||
import type { QLDebugConfiguration } from "../debugger/debug-configuration";
|
||||
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
|
||||
import type { Usage } from "../model-editor/method";
|
||||
|
||||
// A command function matching the signature that VS Code calls when
|
||||
// a command is invoked from a context menu on a TreeView with
|
||||
@@ -219,6 +218,24 @@ export type LocalDatabasesCommands = {
|
||||
"codeQLDatabases.chooseDatabaseGithub": () => Promise<void>;
|
||||
"codeQLDatabases.sortByName": () => Promise<void>;
|
||||
"codeQLDatabases.sortByDateAdded": () => Promise<void>;
|
||||
"codeQLDatabases.displayAllLanguages": () => Promise<void>;
|
||||
"codeQLDatabases.displayCpp": () => Promise<void>;
|
||||
"codeQLDatabases.displayCsharp": () => Promise<void>;
|
||||
"codeQLDatabases.displayGo": () => Promise<void>;
|
||||
"codeQLDatabases.displayJava": () => Promise<void>;
|
||||
"codeQLDatabases.displayJavascript": () => Promise<void>;
|
||||
"codeQLDatabases.displayPython": () => Promise<void>;
|
||||
"codeQLDatabases.displayRuby": () => Promise<void>;
|
||||
"codeQLDatabases.displaySwift": () => Promise<void>;
|
||||
"codeQLDatabases.displayAllLanguagesSelected": () => Promise<void>;
|
||||
"codeQLDatabases.displayCppSelected": () => Promise<void>;
|
||||
"codeQLDatabases.displayCsharpSelected": () => Promise<void>;
|
||||
"codeQLDatabases.displayGoSelected": () => Promise<void>;
|
||||
"codeQLDatabases.displayJavaSelected": () => Promise<void>;
|
||||
"codeQLDatabases.displayJavascriptSelected": () => Promise<void>;
|
||||
"codeQLDatabases.displayPythonSelected": () => Promise<void>;
|
||||
"codeQLDatabases.displayRubySelected": () => Promise<void>;
|
||||
"codeQLDatabases.displaySwiftSelected": () => Promise<void>;
|
||||
|
||||
// Database panel context menu
|
||||
"codeQLDatabases.setCurrentDatabase": (
|
||||
@@ -305,8 +322,9 @@ export type PackagingCommands = {
|
||||
|
||||
export type ModelEditorCommands = {
|
||||
"codeQL.openModelEditor": () => Promise<void>;
|
||||
"codeQLModelEditor.jumpToUsageLocation": (
|
||||
usage: Usage,
|
||||
"codeQL.openModelEditorFromModelingPanel": () => Promise<void>;
|
||||
"codeQLModelEditor.jumpToMethod": (
|
||||
methodSignature: string,
|
||||
databaseItem: DatabaseItem,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -9,10 +9,16 @@ export type DisposeHandler = (disposable: Disposable) => void;
|
||||
/**
|
||||
* Base class to make it easier to implement a `Disposable` that owns other disposable object.
|
||||
*/
|
||||
export abstract class DisposableObject implements Disposable {
|
||||
export class DisposableObject implements Disposable {
|
||||
private disposables: Disposable[] = [];
|
||||
private tracked?: Set<Disposable> = undefined;
|
||||
|
||||
constructor(...dispoables: Disposable[]) {
|
||||
for (const d of dispoables) {
|
||||
this.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `obj` to a list of objects to dispose when `this` is disposed. Objects added by `push` are
|
||||
* disposed in reverse order of being added.
|
||||
|
||||
@@ -67,3 +67,26 @@ export function asError(e: unknown): Error {
|
||||
|
||||
return e instanceof Error ? e : new Error(String(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error message when the error may have come from a method from the `child_process` module.
|
||||
*/
|
||||
export function getChildProcessErrorMessage(e: unknown): string {
|
||||
return isChildProcessError(e) ? e.stderr : getErrorMessage(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown from methods from the `child_process` module.
|
||||
*/
|
||||
interface ChildProcessError {
|
||||
readonly stderr: string;
|
||||
}
|
||||
|
||||
function isChildProcessError(e: unknown): e is ChildProcessError {
|
||||
return (
|
||||
typeof e === "object" &&
|
||||
e !== null &&
|
||||
"stderr" in e &&
|
||||
typeof e.stderr === "string"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,10 +17,14 @@ import {
|
||||
} from "../variant-analysis/shared/variant-analysis-filter-sort";
|
||||
import { ErrorLike } from "../common/errors";
|
||||
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
|
||||
import { Method, Usage } from "../model-editor/method";
|
||||
import { Method } from "../model-editor/method";
|
||||
import { ModeledMethod } from "../model-editor/modeled-method";
|
||||
import { ModelEditorViewState } from "../model-editor/shared/view-state";
|
||||
import {
|
||||
MethodModelingPanelViewState,
|
||||
ModelEditorViewState,
|
||||
} from "../model-editor/shared/view-state";
|
||||
import { Mode } from "../model-editor/shared/mode";
|
||||
import { QueryLanguage } from "./query-language";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -51,6 +55,7 @@ export const RAW_RESULTS_LIMIT = 10000;
|
||||
export interface DatabaseInfo {
|
||||
name: string;
|
||||
databaseUri: string;
|
||||
language?: QueryLanguage;
|
||||
}
|
||||
|
||||
/** Arbitrary query metadata */
|
||||
@@ -500,14 +505,14 @@ interface SetMethodsMessage {
|
||||
methods: Method[];
|
||||
}
|
||||
|
||||
interface LoadModeledMethodsMessage {
|
||||
t: "loadModeledMethods";
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
interface SetModeledMethodsMessage {
|
||||
t: "setModeledMethods";
|
||||
methods: Record<string, ModeledMethod[]>;
|
||||
}
|
||||
|
||||
interface AddModeledMethodsMessage {
|
||||
t: "addModeledMethods";
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
interface SetModifiedMethodsMessage {
|
||||
t: "setModifiedMethods";
|
||||
methodSignatures: string[];
|
||||
}
|
||||
|
||||
interface SetInProgressMethodsMessage {
|
||||
@@ -521,10 +526,9 @@ interface SwitchModeMessage {
|
||||
mode: Mode;
|
||||
}
|
||||
|
||||
interface JumpToUsageMessage {
|
||||
t: "jumpToUsage";
|
||||
method: Method;
|
||||
usage: Usage;
|
||||
interface JumpToMethodMessage {
|
||||
t: "jumpToMethod";
|
||||
methodSignature: string;
|
||||
}
|
||||
|
||||
interface OpenDatabaseMessage {
|
||||
@@ -541,8 +545,7 @@ interface RefreshMethods {
|
||||
|
||||
interface SaveModeledMethods {
|
||||
t: "saveModeledMethods";
|
||||
methods: Method[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
methodSignatures?: string[];
|
||||
}
|
||||
|
||||
interface GenerateMethodMessage {
|
||||
@@ -552,8 +555,7 @@ interface GenerateMethodMessage {
|
||||
interface GenerateMethodsFromLlmMessage {
|
||||
t: "generateMethodsFromLlm";
|
||||
packageName: string;
|
||||
methods: Method[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
methodSignatures: string[];
|
||||
}
|
||||
|
||||
interface StopGeneratingMethodsFromLlmMessage {
|
||||
@@ -565,39 +567,96 @@ interface ModelDependencyMessage {
|
||||
t: "modelDependency";
|
||||
}
|
||||
|
||||
interface HideModeledApisMessage {
|
||||
t: "hideModeledApis";
|
||||
hideModeledApis: boolean;
|
||||
interface HideModeledMethodsMessage {
|
||||
t: "hideModeledMethods";
|
||||
hideModeledMethods: boolean;
|
||||
}
|
||||
|
||||
interface SetModeledMethodMessage {
|
||||
t: "setModeledMethod";
|
||||
method: ModeledMethod;
|
||||
}
|
||||
|
||||
interface SetMultipleModeledMethodsMessage {
|
||||
t: "setMultipleModeledMethods";
|
||||
methodSignature: string;
|
||||
modeledMethods: ModeledMethod[];
|
||||
}
|
||||
|
||||
interface SetInModelingModeMessage {
|
||||
t: "setInModelingMode";
|
||||
inModelingMode: boolean;
|
||||
}
|
||||
|
||||
interface RevealMethodMessage {
|
||||
t: "revealMethod";
|
||||
methodSignature: string;
|
||||
}
|
||||
|
||||
export type ToModelEditorMessage =
|
||||
| SetExtensionPackStateMessage
|
||||
| SetMethodsMessage
|
||||
| LoadModeledMethodsMessage
|
||||
| AddModeledMethodsMessage
|
||||
| SetInProgressMethodsMessage;
|
||||
| SetModeledMethodsMessage
|
||||
| SetModifiedMethodsMessage
|
||||
| SetInProgressMethodsMessage
|
||||
| RevealMethodMessage;
|
||||
|
||||
export type FromModelEditorMessage =
|
||||
| ViewLoadedMsg
|
||||
| CommonFromViewMessages
|
||||
| SwitchModeMessage
|
||||
| RefreshMethods
|
||||
| OpenDatabaseMessage
|
||||
| OpenExtensionPackMessage
|
||||
| JumpToUsageMessage
|
||||
| JumpToMethodMessage
|
||||
| SaveModeledMethods
|
||||
| GenerateMethodMessage
|
||||
| GenerateMethodsFromLlmMessage
|
||||
| StopGeneratingMethodsFromLlmMessage
|
||||
| ModelDependencyMessage
|
||||
| HideModeledApisMessage;
|
||||
| HideModeledMethodsMessage
|
||||
| SetModeledMethodMessage;
|
||||
|
||||
export type FromMethodModelingMessage =
|
||||
| TelemetryMessage
|
||||
| UnhandledErrorMessage;
|
||||
|
||||
interface SetMethodMessage {
|
||||
t: "setMethod";
|
||||
interface RevealInEditorMessage {
|
||||
t: "revealInModelEditor";
|
||||
method: Method;
|
||||
}
|
||||
|
||||
export type ToMethodModelingMessage = SetMethodMessage;
|
||||
interface StartModelingMessage {
|
||||
t: "startModeling";
|
||||
}
|
||||
|
||||
export type FromMethodModelingMessage =
|
||||
| CommonFromViewMessages
|
||||
| SetModeledMethodMessage
|
||||
| RevealInEditorMessage
|
||||
| StartModelingMessage;
|
||||
|
||||
interface SetMethodModelingPanelViewStateMessage {
|
||||
t: "setMethodModelingPanelViewState";
|
||||
viewState: MethodModelingPanelViewState;
|
||||
}
|
||||
|
||||
interface SetMethodMessage {
|
||||
t: "setMethod";
|
||||
method: Method | undefined;
|
||||
}
|
||||
|
||||
interface SetMethodModifiedMessage {
|
||||
t: "setMethodModified";
|
||||
isModified: boolean;
|
||||
}
|
||||
|
||||
interface SetSelectedMethodMessage {
|
||||
t: "setSelectedMethod";
|
||||
method: Method;
|
||||
modeledMethods: ModeledMethod[];
|
||||
isModified: boolean;
|
||||
}
|
||||
|
||||
export type ToMethodModelingMessage =
|
||||
| SetMethodModelingPanelViewStateMessage
|
||||
| SetMethodMessage
|
||||
| SetMultipleModeledMethodsMessage
|
||||
| SetMethodModifiedMessage
|
||||
| SetSelectedMethodMessage
|
||||
| SetInModelingModeMessage;
|
||||
|
||||
@@ -17,7 +17,7 @@ export enum RequestKind {
|
||||
AutoModel = "autoModel",
|
||||
}
|
||||
|
||||
interface BasicErorResponse {
|
||||
export interface BasicErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ interface GetRepoRequest {
|
||||
};
|
||||
response: {
|
||||
status: number;
|
||||
body: Repository | BasicErorResponse | undefined;
|
||||
body: Repository | BasicErrorResponse | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ interface SubmitVariantAnalysisRequest {
|
||||
};
|
||||
response: {
|
||||
status: number;
|
||||
body?: VariantAnalysis | BasicErorResponse;
|
||||
body?: VariantAnalysis | BasicErrorResponse;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ interface GetVariantAnalysisRequest {
|
||||
};
|
||||
response: {
|
||||
status: number;
|
||||
body?: VariantAnalysis | BasicErorResponse;
|
||||
body?: VariantAnalysis | BasicErrorResponse;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ interface GetVariantAnalysisRepoRequest {
|
||||
};
|
||||
response: {
|
||||
status: number;
|
||||
body?: VariantAnalysisRepoTask | BasicErorResponse;
|
||||
body?: VariantAnalysisRepoTask | BasicErrorResponse;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,6 +74,13 @@ export interface GetVariantAnalysisRepoResultRequest {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CodeSearchResponse {
|
||||
total_count: number;
|
||||
items: Array<{
|
||||
repository: Repository;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface CodeSearchRequest {
|
||||
request: {
|
||||
kind: RequestKind.CodeSearch;
|
||||
@@ -81,16 +88,14 @@ interface CodeSearchRequest {
|
||||
};
|
||||
response: {
|
||||
status: number;
|
||||
body?: {
|
||||
total_count?: number;
|
||||
items?: Array<{
|
||||
repository: Repository;
|
||||
}>;
|
||||
};
|
||||
message?: string;
|
||||
body?: CodeSearchResponse | BasicErrorResponse;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AutoModelResponse {
|
||||
models: string;
|
||||
}
|
||||
|
||||
interface AutoModelRequest {
|
||||
request: {
|
||||
kind: RequestKind.AutoModel;
|
||||
@@ -100,10 +105,7 @@ interface AutoModelRequest {
|
||||
};
|
||||
response: {
|
||||
status: number;
|
||||
body?: {
|
||||
models: string;
|
||||
};
|
||||
message?: string;
|
||||
body?: AutoModelResponse | BasicErrorResponse;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { ensureDir, writeFile } from "fs-extra";
|
||||
import { join } from "path";
|
||||
|
||||
import { MockedRequest } from "msw";
|
||||
import { SetupServer } from "msw/node";
|
||||
import { IsomorphicResponse } from "@mswjs/interceptors";
|
||||
|
||||
import { Headers } from "headers-polyfill";
|
||||
import fetch from "node-fetch";
|
||||
import { SetupServer } from "msw/node";
|
||||
|
||||
import { DisposableObject } from "../disposable-object";
|
||||
import { gzipDecode } from "../zlib";
|
||||
|
||||
import {
|
||||
AutoModelResponse,
|
||||
BasicErrorResponse,
|
||||
CodeSearchResponse,
|
||||
GetVariantAnalysisRepoResultRequest,
|
||||
GitHubApiRequest,
|
||||
RequestKind,
|
||||
} from "./gh-api-request";
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisRepoTask,
|
||||
} from "../../variant-analysis/gh-api/variant-analysis";
|
||||
import { Repository } from "../../variant-analysis/gh-api/repository";
|
||||
|
||||
export class Recorder extends DisposableObject {
|
||||
private readonly allRequests = new Map<string, MockedRequest>();
|
||||
private currentRecordedScenario: GitHubApiRequest[] = [];
|
||||
|
||||
private _isRecording = false;
|
||||
|
||||
constructor(private readonly server: SetupServer) {
|
||||
super();
|
||||
this.onRequestStart = this.onRequestStart.bind(this);
|
||||
this.onResponseBypass = this.onResponseBypass.bind(this);
|
||||
}
|
||||
|
||||
@@ -45,7 +48,6 @@ export class Recorder extends DisposableObject {
|
||||
|
||||
this.clear();
|
||||
|
||||
this.server.events.on("request:start", this.onRequestStart);
|
||||
this.server.events.on("response:bypass", this.onResponseBypass);
|
||||
}
|
||||
|
||||
@@ -56,13 +58,11 @@ export class Recorder extends DisposableObject {
|
||||
|
||||
this._isRecording = false;
|
||||
|
||||
this.server.events.removeListener("request:start", this.onRequestStart);
|
||||
this.server.events.removeListener("response:bypass", this.onResponseBypass);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.currentRecordedScenario = [];
|
||||
this.allRequests.clear();
|
||||
}
|
||||
|
||||
public async save(scenariosPath: string, name: string): Promise<string> {
|
||||
@@ -91,7 +91,7 @@ export class Recorder extends DisposableObject {
|
||||
|
||||
let bodyFileLink = undefined;
|
||||
if (writtenRequest.response.body) {
|
||||
await writeFile(bodyFilePath, writtenRequest.response.body || "");
|
||||
await writeFile(bodyFilePath, writtenRequest.response.body);
|
||||
bodyFileLink = `file:${bodyFileName}`;
|
||||
}
|
||||
|
||||
@@ -112,33 +112,18 @@ export class Recorder extends DisposableObject {
|
||||
return scenarioDirectory;
|
||||
}
|
||||
|
||||
private onRequestStart(request: MockedRequest): void {
|
||||
private async onResponseBypass(
|
||||
response: Response,
|
||||
request: Request,
|
||||
_requestId: string,
|
||||
): Promise<void> {
|
||||
if (request.headers.has("x-vscode-codeql-msw-bypass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.allRequests.set(request.id, request);
|
||||
}
|
||||
|
||||
private async onResponseBypass(
|
||||
response: IsomorphicResponse,
|
||||
requestId: string,
|
||||
): Promise<void> {
|
||||
const request = this.allRequests.get(requestId);
|
||||
this.allRequests.delete(requestId);
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.body === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gitHubApiRequest = await createGitHubApiRequest(
|
||||
request.url.toString(),
|
||||
response.status,
|
||||
response.body,
|
||||
response.headers,
|
||||
request.url,
|
||||
response,
|
||||
);
|
||||
if (!gitHubApiRequest) {
|
||||
return;
|
||||
@@ -150,14 +135,14 @@ export class Recorder extends DisposableObject {
|
||||
|
||||
async function createGitHubApiRequest(
|
||||
url: string,
|
||||
status: number,
|
||||
body: string,
|
||||
headers: Headers,
|
||||
response: Response,
|
||||
): Promise<GitHubApiRequest | undefined> {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const status = response.status;
|
||||
|
||||
if (url.match(/\/repos\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_.]+$/)) {
|
||||
return {
|
||||
request: {
|
||||
@@ -165,7 +150,9 @@ async function createGitHubApiRequest(
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
body: await jsonResponseBody<
|
||||
Repository | BasicErrorResponse | undefined
|
||||
>(response),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -179,7 +166,9 @@ async function createGitHubApiRequest(
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
body: await jsonResponseBody<
|
||||
VariantAnalysis | BasicErrorResponse | undefined
|
||||
>(response),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -195,7 +184,9 @@ async function createGitHubApiRequest(
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
body: await jsonResponseBody<
|
||||
VariantAnalysis | BasicErrorResponse | undefined
|
||||
>(response),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -211,7 +202,9 @@ async function createGitHubApiRequest(
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
body: await jsonResponseBody<
|
||||
VariantAnalysisRepoTask | BasicErrorResponse | undefined
|
||||
>(response),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -238,9 +231,10 @@ async function createGitHubApiRequest(
|
||||
repositoryId: parseInt(repoDownloadMatch.groups.repositoryId, 10),
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
status: response.status,
|
||||
body: responseBuffer,
|
||||
contentType: headers.get("content-type") ?? "application/octet-stream",
|
||||
contentType:
|
||||
response.headers.get("content-type") ?? "application/octet-stream",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -254,7 +248,9 @@ async function createGitHubApiRequest(
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
body: await jsonResponseBody<
|
||||
CodeSearchResponse | BasicErrorResponse | undefined
|
||||
>(response),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -269,7 +265,9 @@ async function createGitHubApiRequest(
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
body: await jsonResponseBody<
|
||||
BasicErrorResponse | AutoModelResponse | undefined
|
||||
>(response),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -277,6 +275,26 @@ async function createGitHubApiRequest(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function responseBody(response: Response): Promise<Uint8Array> {
|
||||
const body = await response.arrayBuffer();
|
||||
const view = new Uint8Array(body);
|
||||
|
||||
if (view[0] === 0x1f && view[1] === 0x8b) {
|
||||
// Response body is gzipped, so we need to un-gzip it.
|
||||
|
||||
return await gzipDecode(view);
|
||||
} else {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
async function jsonResponseBody<T>(response: Response): Promise<T> {
|
||||
const body = await responseBody(response);
|
||||
const text = new TextDecoder("utf-8").decode(body);
|
||||
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
function shouldWriteBodyToFile(
|
||||
request: GitHubApiRequest,
|
||||
): request is GetVariantAnalysisRepoResultRequest {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from "path";
|
||||
import { readdir, readJson, readFile } from "fs-extra";
|
||||
import { DefaultBodyType, MockedRequest, rest, RestHandler } from "msw";
|
||||
import { RequestHandler, rest } from "msw";
|
||||
import {
|
||||
GitHubApiRequest,
|
||||
isAutoModelRequest,
|
||||
@@ -14,7 +14,19 @@ import {
|
||||
|
||||
const baseUrl = "https://api.github.com";
|
||||
|
||||
type RequestHandler = RestHandler<MockedRequest<DefaultBodyType>>;
|
||||
const jsonResponse = <T>(
|
||||
body: T,
|
||||
init?: ResponseInit,
|
||||
contentType = "application/json",
|
||||
): Response => {
|
||||
return new Response(JSON.stringify(body), {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export async function createRequestHandlers(
|
||||
scenarioDirPath: string,
|
||||
@@ -82,11 +94,10 @@ function createGetRepoRequestHandler(
|
||||
|
||||
const getRepoRequest = getRepoRequests[0];
|
||||
|
||||
return rest.get(`${baseUrl}/repos/:owner/:name`, (_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(getRepoRequest.response.status),
|
||||
ctx.json(getRepoRequest.response.body),
|
||||
);
|
||||
return rest.get(`${baseUrl}/repos/:owner/:name`, () => {
|
||||
return jsonResponse(getRepoRequest.response.body, {
|
||||
status: getRepoRequest.response.status,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,11 +116,10 @@ function createSubmitVariantAnalysisRequestHandler(
|
||||
|
||||
return rest.post(
|
||||
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses`,
|
||||
(_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(getRepoRequest.response.status),
|
||||
ctx.json(getRepoRequest.response.body),
|
||||
);
|
||||
() => {
|
||||
return jsonResponse(getRepoRequest.response.body, {
|
||||
status: getRepoRequest.response.status,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -127,7 +137,7 @@ function createGetVariantAnalysisRequestHandler(
|
||||
// request, so keep an index of the request and return the appropriate response.
|
||||
return rest.get(
|
||||
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId`,
|
||||
(_req, res, ctx) => {
|
||||
() => {
|
||||
const request = getVariantAnalysisRequests[requestIndex];
|
||||
|
||||
if (requestIndex < getVariantAnalysisRequests.length - 1) {
|
||||
@@ -135,10 +145,9 @@ function createGetVariantAnalysisRequestHandler(
|
||||
requestIndex++;
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.status(request.response.status),
|
||||
ctx.json(request.response.body),
|
||||
);
|
||||
return jsonResponse(request.response.body, {
|
||||
status: request.response.status,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -152,18 +161,17 @@ function createGetVariantAnalysisRepoRequestHandler(
|
||||
|
||||
return rest.get(
|
||||
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId/repositories/:repoId`,
|
||||
(req, res, ctx) => {
|
||||
({ request, params }) => {
|
||||
const scenarioRequest = getVariantAnalysisRepoRequests.find(
|
||||
(r) => r.request.repositoryId.toString() === req.params.repoId,
|
||||
(r) => r.request.repositoryId.toString() === params.repoId,
|
||||
);
|
||||
if (!scenarioRequest) {
|
||||
throw Error(`No scenario request found for ${req.url}`);
|
||||
throw Error(`No scenario request found for ${request.url}`);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.status(scenarioRequest.response.status),
|
||||
ctx.json(scenarioRequest.response.body),
|
||||
);
|
||||
return jsonResponse(scenarioRequest.response.body, {
|
||||
status: scenarioRequest.response.status,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -177,22 +185,23 @@ function createGetVariantAnalysisRepoResultRequestHandler(
|
||||
|
||||
return rest.get(
|
||||
"https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/:variantAnalysisId/:repoId/*",
|
||||
(req, res, ctx) => {
|
||||
({ request, params }) => {
|
||||
const scenarioRequest = getVariantAnalysisRepoResultRequests.find(
|
||||
(r) => r.request.repositoryId.toString() === req.params.repoId,
|
||||
(r) => r.request.repositoryId.toString() === params.repoId,
|
||||
);
|
||||
if (!scenarioRequest) {
|
||||
throw Error(`No scenario request found for ${req.url}`);
|
||||
throw Error(`No scenario request found for ${request.url}`);
|
||||
}
|
||||
|
||||
if (scenarioRequest.response.body) {
|
||||
return res(
|
||||
ctx.status(scenarioRequest.response.status),
|
||||
ctx.set("Content-Type", scenarioRequest.response.contentType),
|
||||
ctx.body(scenarioRequest.response.body),
|
||||
);
|
||||
return new Response(scenarioRequest.response.body, {
|
||||
status: scenarioRequest.response.status,
|
||||
headers: {
|
||||
"Content-Type": scenarioRequest.response.contentType,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res(ctx.status(scenarioRequest.response.status));
|
||||
return new Response(null, { status: scenarioRequest.response.status });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -207,7 +216,7 @@ function createCodeSearchRequestHandler(
|
||||
// During a code search, there are multiple request to get pages of results. We
|
||||
// need to return different responses for each request, so keep an index of the
|
||||
// request and return the appropriate response.
|
||||
return rest.get(`${baseUrl}/search/code?q=*`, (_req, res, ctx) => {
|
||||
return rest.get(`${baseUrl}/search/code`, () => {
|
||||
const request = codeSearchRequests[requestIndex];
|
||||
|
||||
if (requestIndex < codeSearchRequests.length - 1) {
|
||||
@@ -215,10 +224,9 @@ function createCodeSearchRequestHandler(
|
||||
requestIndex++;
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.status(request.response.status),
|
||||
ctx.json(request.response.body),
|
||||
);
|
||||
return jsonResponse(request.response.body, {
|
||||
status: request.response.status,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,7 +241,7 @@ function createAutoModelRequestHandler(
|
||||
// so keep an index of the request and return the appropriate response.
|
||||
return rest.post(
|
||||
`${baseUrl}/repos/github/codeql/code-scanning/codeql/auto-model`,
|
||||
(_req, res, ctx) => {
|
||||
() => {
|
||||
const request = autoModelRequests[requestIndex];
|
||||
|
||||
if (requestIndex < autoModelRequests.length - 1) {
|
||||
@@ -241,10 +249,9 @@ function createAutoModelRequestHandler(
|
||||
requestIndex++;
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.status(request.response.status),
|
||||
ctx.json(request.response.body),
|
||||
);
|
||||
return jsonResponse(request.response.body, {
|
||||
status: request.response.status,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
10
extensions/ql-vscode/src/common/octokit.ts
Normal file
10
extensions/ql-vscode/src/common/octokit.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as Octokit from "@octokit/rest";
|
||||
import { retry } from "@octokit/plugin-retry";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
export const AppOctokit = Octokit.Octokit.defaults({
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
retry,
|
||||
});
|
||||
@@ -40,10 +40,7 @@ export const PACKS_BY_QUERY_LANGUAGE = {
|
||||
],
|
||||
[QueryLanguage.Go]: ["codeql/go-queries"],
|
||||
[QueryLanguage.Java]: ["codeql/java-queries"],
|
||||
[QueryLanguage.Javascript]: [
|
||||
"codeql/javascript-queries",
|
||||
"codeql/javascript-experimental-atm-queries",
|
||||
],
|
||||
[QueryLanguage.Javascript]: ["codeql/javascript-queries"],
|
||||
[QueryLanguage.Python]: ["codeql/python-queries"],
|
||||
[QueryLanguage.Ruby]: ["codeql/ruby-queries"],
|
||||
};
|
||||
@@ -62,3 +59,9 @@ export const dbSchemeToLanguage: Record<string, QueryLanguage> = {
|
||||
export function isQueryLanguage(language: string): language is QueryLanguage {
|
||||
return Object.values(QueryLanguage).includes(language as QueryLanguage);
|
||||
}
|
||||
|
||||
export function tryGetQueryLanguage(
|
||||
language: string,
|
||||
): QueryLanguage | undefined {
|
||||
return isQueryLanguage(language) ? language : undefined;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,7 @@ export async function sarifParser(
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Parsing output of interpretation failed: ${
|
||||
(e as any).stderr || getErrorMessage(e)
|
||||
}`,
|
||||
`Parsing output of interpretation failed: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
125
extensions/ql-vscode/src/common/split-stream.ts
Normal file
125
extensions/ql-vscode/src/common/split-stream.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Readable } from "stream";
|
||||
import { StringDecoder } from "string_decoder";
|
||||
|
||||
/**
|
||||
* Buffer to hold state used when splitting a text stream into lines.
|
||||
*/
|
||||
export class SplitBuffer {
|
||||
private readonly decoder = new StringDecoder("utf8");
|
||||
private readonly maxSeparatorLength: number;
|
||||
private buffer = "";
|
||||
private searchIndex = 0;
|
||||
private ended = false;
|
||||
|
||||
constructor(private readonly separators: readonly string[]) {
|
||||
this.maxSeparatorLength = separators
|
||||
.map((s) => s.length)
|
||||
.reduce((a, b) => Math.max(a, b), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append new text data to the buffer.
|
||||
* @param chunk The chunk of data to append.
|
||||
*/
|
||||
public addChunk(chunk: Buffer): void {
|
||||
this.buffer += this.decoder.write(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that the end of the input stream has been reached.
|
||||
*/
|
||||
public end(): void {
|
||||
this.buffer += this.decoder.end();
|
||||
this.ended = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of startsWith that isn't overriden by a broken version of ms-python.
|
||||
*
|
||||
* The definition comes from
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
* which is CC0/public domain
|
||||
*
|
||||
* See https://github.com/github/vscode-codeql/issues/802 for more context as to why we need it.
|
||||
*/
|
||||
private static startsWith(
|
||||
s: string,
|
||||
searchString: string,
|
||||
position: number,
|
||||
): boolean {
|
||||
const pos = position > 0 ? position | 0 : 0;
|
||||
return s.substring(pos, pos + searchString.length) === searchString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the next full line from the buffer, if one is available.
|
||||
* @returns The text of the next available full line (without the separator), or `undefined` if no
|
||||
* line is available.
|
||||
*/
|
||||
public getNextLine(): string | undefined {
|
||||
// If we haven't received all of the input yet, don't search too close to the end of the buffer,
|
||||
// or we could match a separator that's split across two chunks. For example, we could see "\r"
|
||||
// at the end of the buffer and match that, even though we were about to receive a "\n" right
|
||||
// after it.
|
||||
const maxSearchIndex = this.ended
|
||||
? this.buffer.length - 1
|
||||
: this.buffer.length - this.maxSeparatorLength;
|
||||
while (this.searchIndex <= maxSearchIndex) {
|
||||
for (const separator of this.separators) {
|
||||
if (SplitBuffer.startsWith(this.buffer, separator, this.searchIndex)) {
|
||||
const line = this.buffer.slice(0, this.searchIndex);
|
||||
this.buffer = this.buffer.slice(this.searchIndex + separator.length);
|
||||
this.searchIndex = 0;
|
||||
return line;
|
||||
}
|
||||
}
|
||||
this.searchIndex++;
|
||||
}
|
||||
|
||||
if (this.ended && this.buffer.length > 0) {
|
||||
// If we still have some text left in the buffer, return it as the last line.
|
||||
const line = this.buffer;
|
||||
this.buffer = "";
|
||||
this.searchIndex = 0;
|
||||
return line;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a text stream into lines based on a list of valid line separators.
|
||||
* @param stream The text stream to split. This stream will be fully consumed.
|
||||
* @param separators The list of strings that act as line separators.
|
||||
* @returns A sequence of lines (not including separators).
|
||||
*/
|
||||
export async function* splitStreamAtSeparators(
|
||||
stream: Readable,
|
||||
separators: string[],
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
const buffer = new SplitBuffer(separators);
|
||||
for await (const chunk of stream) {
|
||||
buffer.addChunk(chunk);
|
||||
let line: string | undefined;
|
||||
do {
|
||||
line = buffer.getNextLine();
|
||||
if (line !== undefined) {
|
||||
yield line;
|
||||
}
|
||||
} while (line !== undefined);
|
||||
}
|
||||
buffer.end();
|
||||
let line: string | undefined;
|
||||
do {
|
||||
line = buffer.getNextLine();
|
||||
if (line !== undefined) {
|
||||
yield line;
|
||||
}
|
||||
} while (line !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard line endings for splitting human-readable text.
|
||||
*/
|
||||
export const LINE_ENDINGS = ["\r\n", "\r", "\n"];
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as vscode from "vscode";
|
||||
import { Uri, WebviewViewProvider } from "vscode";
|
||||
import { WebviewKind, WebviewMessage, getHtmlForWebview } from "./webview-html";
|
||||
import { Disposable } from "../disposable-object";
|
||||
import { App } from "../app";
|
||||
|
||||
export abstract class AbstractWebviewViewProvider<
|
||||
ToMessage extends WebviewMessage,
|
||||
FromMessage extends WebviewMessage,
|
||||
> implements WebviewViewProvider
|
||||
{
|
||||
protected webviewView: vscode.WebviewView | undefined = undefined;
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
protected readonly app: App,
|
||||
private readonly webviewKind: WebviewKind,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* This is called when a view first becomes visible. This may happen when the view is
|
||||
* first loaded or when the user hides and then shows a view again.
|
||||
*/
|
||||
public resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
_context: vscode.WebviewViewResolveContext,
|
||||
_token: vscode.CancellationToken,
|
||||
) {
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [Uri.file(this.app.extensionPath)],
|
||||
};
|
||||
|
||||
const html = getHtmlForWebview(
|
||||
this.app,
|
||||
webviewView.webview,
|
||||
this.webviewKind,
|
||||
{
|
||||
allowInlineStyles: true,
|
||||
allowWasmEval: false,
|
||||
},
|
||||
);
|
||||
|
||||
webviewView.webview.html = html;
|
||||
|
||||
this.webviewView = webviewView;
|
||||
|
||||
webviewView.webview.onDidReceiveMessage(async (msg) => this.onMessage(msg));
|
||||
webviewView.onDidDispose(() => this.dispose());
|
||||
}
|
||||
|
||||
protected get isShowingView() {
|
||||
return this.webviewView?.visible ?? false;
|
||||
}
|
||||
|
||||
protected async postMessage(msg: ToMessage): Promise<void> {
|
||||
await this.webviewView?.webview.postMessage(msg);
|
||||
}
|
||||
|
||||
protected dispose() {
|
||||
while (this.disposables.length > 0) {
|
||||
const disposable = this.disposables.pop()!;
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
this.webviewView = undefined;
|
||||
}
|
||||
|
||||
protected push<T extends Disposable>(obj: T): T {
|
||||
if (obj !== undefined) {
|
||||
this.disposables.push(obj);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
protected abstract onMessage(msg: FromMessage): Promise<void>;
|
||||
|
||||
/**
|
||||
* This is called when a view first becomes visible. This may happen when the view is
|
||||
* first loaded or when the user hides and then shows a view again.
|
||||
*/
|
||||
protected onWebViewLoaded(): void {
|
||||
// Do nothing by default.
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
WebviewPanel,
|
||||
ExtensionContext,
|
||||
window as Window,
|
||||
ViewColumn,
|
||||
Uri,
|
||||
@@ -9,7 +8,8 @@ import {
|
||||
} from "vscode";
|
||||
import { join } from "path";
|
||||
|
||||
import { DisposableObject, DisposeHandler } from "../disposable-object";
|
||||
import { App } from "../app";
|
||||
import { Disposable } from "../disposable-object";
|
||||
import { tmpDir } from "../../tmp-dir";
|
||||
import { getHtmlForWebview, WebviewMessage, WebviewKind } from "./webview-html";
|
||||
|
||||
@@ -27,16 +27,16 @@ export type WebviewPanelConfig = {
|
||||
export abstract class AbstractWebview<
|
||||
ToMessage extends WebviewMessage,
|
||||
FromMessage extends WebviewMessage,
|
||||
> extends DisposableObject {
|
||||
> {
|
||||
protected panel: WebviewPanel | undefined;
|
||||
protected panelLoaded = false;
|
||||
protected panelLoadedCallBacks: Array<() => void> = [];
|
||||
|
||||
private panelResolves?: Array<(panel: WebviewPanel) => void>;
|
||||
|
||||
constructor(protected readonly ctx: ExtensionContext) {
|
||||
super();
|
||||
}
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(protected readonly app: App) {}
|
||||
|
||||
public async restoreView(panel: WebviewPanel): Promise<void> {
|
||||
this.panel = panel;
|
||||
@@ -50,8 +50,6 @@ export abstract class AbstractWebview<
|
||||
|
||||
protected async getPanel(): Promise<WebviewPanel> {
|
||||
if (this.panel === undefined) {
|
||||
const { ctx } = this;
|
||||
|
||||
// This is an async method, so in theory this method can be called concurrently. To ensure that we don't
|
||||
// create two panels, we use a promise that resolves when the panel is created. This way, if the panel is
|
||||
// being created, the promise will resolve when it is done.
|
||||
@@ -81,7 +79,7 @@ export abstract class AbstractWebview<
|
||||
localResourceRoots: [
|
||||
...(config.additionalOptions?.localResourceRoots ?? []),
|
||||
Uri.file(tmpDir.name),
|
||||
Uri.file(join(ctx.extensionPath, "out")),
|
||||
Uri.file(join(this.app.extensionPath, "out")),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -99,19 +97,16 @@ export abstract class AbstractWebview<
|
||||
|
||||
protected setupPanel(panel: WebviewPanel, config: WebviewPanelConfig): void {
|
||||
this.push(
|
||||
panel.onDidDispose(
|
||||
() => {
|
||||
this.panel = undefined;
|
||||
this.panelLoaded = false;
|
||||
this.onPanelDispose();
|
||||
},
|
||||
null,
|
||||
this.ctx.subscriptions,
|
||||
),
|
||||
panel.onDidDispose(() => {
|
||||
this.panel = undefined;
|
||||
this.panelLoaded = false;
|
||||
this.onPanelDispose();
|
||||
this.disposeAll();
|
||||
}, null),
|
||||
);
|
||||
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
this.ctx,
|
||||
this.app,
|
||||
panel.webview,
|
||||
config.view,
|
||||
{
|
||||
@@ -123,7 +118,6 @@ export abstract class AbstractWebview<
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.onMessage(e),
|
||||
undefined,
|
||||
this.ctx.subscriptions,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -157,8 +151,27 @@ export abstract class AbstractWebview<
|
||||
return panel.webview.postMessage(msg);
|
||||
}
|
||||
|
||||
public dispose(disposeHandler?: DisposeHandler) {
|
||||
public dispose() {
|
||||
this.panel?.dispose();
|
||||
super.dispose(disposeHandler);
|
||||
this.disposeAll();
|
||||
}
|
||||
|
||||
private disposeAll() {
|
||||
while (this.disposables.length > 0) {
|
||||
const disposable = this.disposables.pop()!;
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `obj` to a list of objects to dispose when the panel is disposed. Objects added by `push` are
|
||||
* disposed in reverse order of being added.
|
||||
* @param obj The object to take ownership of.
|
||||
*/
|
||||
protected push<T extends Disposable>(obj: T): T {
|
||||
if (obj !== undefined) {
|
||||
this.disposables.push(obj);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as Octokit from "@octokit/rest";
|
||||
import { retry } from "@octokit/plugin-retry";
|
||||
import { Credentials } from "../authentication";
|
||||
import { AppOctokit } from "../octokit";
|
||||
|
||||
export const GITHUB_AUTH_PROVIDER_ID = "github";
|
||||
|
||||
@@ -32,9 +32,8 @@ export class VSCodeCredentials implements Credentials {
|
||||
|
||||
const accessToken = await this.getAccessToken();
|
||||
|
||||
return new Octokit.Octokit({
|
||||
return new AppOctokit({
|
||||
auth: accessToken,
|
||||
retry,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { CancellationToken, Disposable } from "vscode";
|
||||
import { DisposableObject } from "../disposable-object";
|
||||
|
||||
/**
|
||||
* A cancellation token that cancels when any of its constituent
|
||||
* cancellation tokens are cancelled.
|
||||
*/
|
||||
export class MultiCancellationToken implements CancellationToken {
|
||||
private readonly tokens: CancellationToken[];
|
||||
|
||||
constructor(...tokens: CancellationToken[]) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
get isCancellationRequested(): boolean {
|
||||
return this.tokens.some((t) => t.isCancellationRequested);
|
||||
}
|
||||
|
||||
onCancellationRequested<T>(listener: (e: T) => any): Disposable {
|
||||
return new DisposableObject(
|
||||
...this.tokens.map((t) => t.onCancellationRequested(listener)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
Extension,
|
||||
ExtensionContext,
|
||||
ConfigurationChangeEvent,
|
||||
env,
|
||||
} from "vscode";
|
||||
import TelemetryReporter from "vscode-extension-telemetry";
|
||||
import {
|
||||
ConfigListener,
|
||||
CANARY_FEATURES,
|
||||
ENABLE_TELEMETRY,
|
||||
GLOBAL_ENABLE_TELEMETRY,
|
||||
LOG_TELEMETRY,
|
||||
isIntegrationTestMode,
|
||||
isCanary,
|
||||
@@ -59,8 +59,6 @@ export class ExtensionTelemetryListener
|
||||
extends ConfigListener
|
||||
implements AppTelemetry
|
||||
{
|
||||
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
|
||||
|
||||
private reporter?: TelemetryReporter;
|
||||
|
||||
private cliVersionStr = NOT_SET_CLI_VERSION;
|
||||
@@ -72,6 +70,10 @@ export class ExtensionTelemetryListener
|
||||
private readonly ctx: ExtensionContext,
|
||||
) {
|
||||
super();
|
||||
|
||||
env.onDidChangeTelemetryEnabled(async () => {
|
||||
await this.initialize();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,10 +93,7 @@ export class ExtensionTelemetryListener
|
||||
async handleDidChangeConfiguration(
|
||||
e: ConfigurationChangeEvent,
|
||||
): Promise<void> {
|
||||
if (
|
||||
e.affectsConfiguration("codeQL.telemetry.enableTelemetry") ||
|
||||
e.affectsConfiguration("telemetry.enableTelemetry")
|
||||
) {
|
||||
if (e.affectsConfiguration(ENABLE_TELEMETRY.qualifiedName)) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
@@ -102,7 +101,7 @@ export class ExtensionTelemetryListener
|
||||
// Re-request if codeQL.canary is being set to `true` and telemetry
|
||||
// is not currently enabled.
|
||||
if (
|
||||
e.affectsConfiguration("codeQL.canary") &&
|
||||
e.affectsConfiguration(CANARY_FEATURES.qualifiedName) &&
|
||||
CANARY_FEATURES.getValue() &&
|
||||
!ENABLE_TELEMETRY.getValue()
|
||||
) {
|
||||
@@ -212,7 +211,7 @@ export class ExtensionTelemetryListener
|
||||
properties.stack = error.stack;
|
||||
}
|
||||
|
||||
this.reporter.sendTelemetryEvent("error", properties, {});
|
||||
this.reporter.sendTelemetryErrorEvent("error", properties, {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,7 +223,7 @@ export class ExtensionTelemetryListener
|
||||
// if global telemetry is disabled, avoid showing the dialog or making any changes
|
||||
let result = undefined;
|
||||
if (
|
||||
GLOBAL_ENABLE_TELEMETRY.getValue() &&
|
||||
env.isTelemetryEnabled &&
|
||||
// Avoid showing the dialog if we are in integration test mode.
|
||||
!isIntegrationTestMode()
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ExtensionContext, Uri, Webview } from "vscode";
|
||||
import { Uri, Webview } from "vscode";
|
||||
import { randomBytes } from "crypto";
|
||||
import { EOL } from "os";
|
||||
import { App } from "../app";
|
||||
|
||||
export type WebviewKind =
|
||||
| "results"
|
||||
@@ -19,7 +20,7 @@ export interface WebviewMessage {
|
||||
* Uses a content security policy that only loads the given script.
|
||||
*/
|
||||
export function getHtmlForWebview(
|
||||
ctx: ExtensionContext,
|
||||
app: App,
|
||||
webview: Webview,
|
||||
view: WebviewKind,
|
||||
{
|
||||
@@ -33,10 +34,13 @@ export function getHtmlForWebview(
|
||||
allowWasmEval: false,
|
||||
},
|
||||
): string {
|
||||
const scriptUriOnDisk = Uri.file(ctx.asAbsolutePath("out/webview.js"));
|
||||
const scriptUriOnDisk = Uri.joinPath(
|
||||
Uri.file(app.extensionPath),
|
||||
"out/webview.js",
|
||||
);
|
||||
|
||||
const stylesheetUrisOnDisk = [
|
||||
Uri.file(ctx.asAbsolutePath("out/webview.css")),
|
||||
Uri.joinPath(Uri.file(app.extensionPath), "out/webview.css"),
|
||||
];
|
||||
|
||||
// Convert the on-disk URIs into webview URIs.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExtensionContext, ViewColumn } from "vscode";
|
||||
import { ViewColumn } from "vscode";
|
||||
|
||||
import {
|
||||
FromCompareViewMessage,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "../common/vscode/abstract-webview";
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { redactableError } from "../common/errors";
|
||||
import { App } from "../common/app";
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedLocalQueryInfo;
|
||||
@@ -38,7 +39,7 @@ export class CompareView extends AbstractWebview<
|
||||
private comparePair: ComparePair | undefined;
|
||||
|
||||
constructor(
|
||||
ctx: ExtensionContext,
|
||||
app: App,
|
||||
private databaseManager: DatabaseManager,
|
||||
private cliServer: CodeQLCliServer,
|
||||
private logger: Logger,
|
||||
@@ -47,7 +48,7 @@ export class CompareView extends AbstractWebview<
|
||||
item: CompletedLocalQueryInfo,
|
||||
) => Promise<void>,
|
||||
) {
|
||||
super(ctx);
|
||||
super(app);
|
||||
}
|
||||
|
||||
async showResults(
|
||||
|
||||
@@ -72,15 +72,8 @@ export const VSCODE_SAVE_BEFORE_START_SETTING = new Setting(
|
||||
|
||||
const ROOT_SETTING = new Setting("codeQL");
|
||||
|
||||
// Global configuration
|
||||
// Telemetry configuration
|
||||
const TELEMETRY_SETTING = new Setting("telemetry", ROOT_SETTING);
|
||||
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
|
||||
const CONTEXTUAL_QUERIES_SETTINGS = new Setting(
|
||||
"contextualQueries",
|
||||
ROOT_SETTING,
|
||||
);
|
||||
const GLOBAL_TELEMETRY_SETTING = new Setting("telemetry");
|
||||
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
|
||||
|
||||
export const LOG_TELEMETRY = new Setting("logTelemetry", TELEMETRY_SETTING);
|
||||
export const ENABLE_TELEMETRY = new Setting(
|
||||
@@ -88,11 +81,6 @@ export const ENABLE_TELEMETRY = new Setting(
|
||||
TELEMETRY_SETTING,
|
||||
);
|
||||
|
||||
export const GLOBAL_ENABLE_TELEMETRY = new Setting(
|
||||
"enableTelemetry",
|
||||
GLOBAL_TELEMETRY_SETTING,
|
||||
);
|
||||
|
||||
// Distribution configuration
|
||||
const DISTRIBUTION_SETTING = new Setting("cli", ROOT_SETTING);
|
||||
export const CUSTOM_CODEQL_PATH_SETTING = new Setting(
|
||||
@@ -475,6 +463,7 @@ export function allowCanaryQueryServer() {
|
||||
return value === undefined ? true : !!value;
|
||||
}
|
||||
|
||||
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
|
||||
export const JOIN_ORDER_WARNING_THRESHOLD = new Setting(
|
||||
"joinOrderWarningThreshold",
|
||||
LOG_INSIGHTS_SETTING,
|
||||
@@ -484,6 +473,7 @@ export function joinOrderWarningThreshold(): number {
|
||||
return JOIN_ORDER_WARNING_THRESHOLD.getValue<number>();
|
||||
}
|
||||
|
||||
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
|
||||
/**
|
||||
* Hidden setting: Avoids caching in the AST viewer if the user is also a canary user.
|
||||
*/
|
||||
@@ -492,6 +482,10 @@ export const NO_CACHE_AST_VIEWER = new Setting(
|
||||
AST_VIEWER_SETTING,
|
||||
);
|
||||
|
||||
const CONTEXTUAL_QUERIES_SETTINGS = new Setting(
|
||||
"contextualQueries",
|
||||
ROOT_SETTING,
|
||||
);
|
||||
/**
|
||||
* Hidden setting: Avoids caching in jump to def and find refs contextual queries if the user is also a canary user.
|
||||
*/
|
||||
@@ -709,17 +703,35 @@ const MODEL_SETTING = new Setting("model", ROOT_SETTING);
|
||||
const FLOW_GENERATION = new Setting("flowGeneration", MODEL_SETTING);
|
||||
const LLM_GENERATION = new Setting("llmGeneration", MODEL_SETTING);
|
||||
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
|
||||
const SHOW_MULTIPLE_MODELS = new Setting("showMultipleModels", MODEL_SETTING);
|
||||
|
||||
export function showFlowGeneration(): boolean {
|
||||
return !!FLOW_GENERATION.getValue<boolean>();
|
||||
export interface ModelConfig {
|
||||
flowGeneration: boolean;
|
||||
llmGeneration: boolean;
|
||||
getExtensionsDirectory(languageId: string): string | undefined;
|
||||
showMultipleModels: boolean;
|
||||
}
|
||||
|
||||
export function showLlmGeneration(): boolean {
|
||||
return !!LLM_GENERATION.getValue<boolean>();
|
||||
}
|
||||
export class ModelConfigListener extends ConfigListener implements ModelConfig {
|
||||
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
|
||||
this.handleDidChangeConfigurationForRelevantSettings([MODEL_SETTING], e);
|
||||
}
|
||||
|
||||
export function getExtensionsDirectory(languageId: string): string | undefined {
|
||||
return EXTENSIONS_DIRECTORY.getValue<string>({
|
||||
languageId,
|
||||
});
|
||||
public get flowGeneration(): boolean {
|
||||
return !!FLOW_GENERATION.getValue<boolean>();
|
||||
}
|
||||
|
||||
public get llmGeneration(): boolean {
|
||||
return !!LLM_GENERATION.getValue<boolean>();
|
||||
}
|
||||
|
||||
public getExtensionsDirectory(languageId: string): string | undefined {
|
||||
return EXTENSIONS_DIRECTORY.getValue<string>({
|
||||
languageId,
|
||||
});
|
||||
}
|
||||
|
||||
public get showMultipleModels(): boolean {
|
||||
return !!SHOW_MULTIPLE_MODELS.getValue<boolean>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { retry } from "@octokit/plugin-retry";
|
||||
import { throttling } from "@octokit/plugin-throttling";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { Progress, CancellationToken } from "vscode";
|
||||
import { Credentials } from "../common/authentication";
|
||||
import {
|
||||
NotificationLogger,
|
||||
showAndLogWarningMessage,
|
||||
} from "../common/logging";
|
||||
import { BaseLogger } from "../common/logging";
|
||||
import { AppOctokit } from "../common/octokit";
|
||||
|
||||
export async function getCodeSearchRepositories(
|
||||
query: string,
|
||||
@@ -16,7 +13,7 @@ export async function getCodeSearchRepositories(
|
||||
}>,
|
||||
token: CancellationToken,
|
||||
credentials: Credentials,
|
||||
logger: NotificationLogger,
|
||||
logger: BaseLogger,
|
||||
): Promise<string[]> {
|
||||
let nwos: string[] = [];
|
||||
const octokit = await provideOctokitWithThrottling(credentials, logger);
|
||||
@@ -47,26 +44,23 @@ export async function getCodeSearchRepositories(
|
||||
|
||||
async function provideOctokitWithThrottling(
|
||||
credentials: Credentials,
|
||||
logger: NotificationLogger,
|
||||
logger: BaseLogger,
|
||||
): Promise<Octokit> {
|
||||
const MyOctokit = Octokit.plugin(throttling);
|
||||
const MyOctokit = AppOctokit.plugin(throttling);
|
||||
const auth = await credentials.getAccessToken();
|
||||
|
||||
const octokit = new MyOctokit({
|
||||
auth,
|
||||
retry,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: any): boolean => {
|
||||
void showAndLogWarningMessage(
|
||||
logger,
|
||||
void logger.log(
|
||||
`Rate Limit detected for request ${options.method} ${options.url}. Retrying after ${retryAfter} seconds!`,
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
onSecondaryRateLimit: (_retryAfter: number, options: any): void => {
|
||||
void showAndLogWarningMessage(
|
||||
logger,
|
||||
void logger.log(
|
||||
`Secondary Rate Limit detected for request ${options.method} ${options.url}`,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "fs-extra";
|
||||
import { basename, join } from "path";
|
||||
import * as Octokit from "@octokit/rest";
|
||||
import { retry } from "@octokit/plugin-retry";
|
||||
|
||||
import { DatabaseManager, DatabaseItem } from "./local-databases";
|
||||
import { tmpDir } from "../tmp-dir";
|
||||
@@ -32,6 +31,7 @@ import { Credentials } from "../common/authentication";
|
||||
import { AppCommandManager } from "../common/commands";
|
||||
import { allowHttp } from "../config";
|
||||
import { showAndLogInformationMessage } from "../common/logging";
|
||||
import { AppOctokit } from "../common/octokit";
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
|
||||
@@ -87,6 +87,7 @@ export async function promptImportInternetDatabase(
|
||||
* @param cli the CodeQL CLI server
|
||||
* @param language the language to download. If undefined, the user will be prompted to choose a language.
|
||||
* @param makeSelected make the new database selected in the databases panel (default: true)
|
||||
* @param addSourceArchiveFolder whether to add a workspace folder containing the source archive to the workspace
|
||||
*/
|
||||
export async function promptImportGithubDatabase(
|
||||
commandManager: AppCommandManager,
|
||||
@@ -97,6 +98,7 @@ export async function promptImportGithubDatabase(
|
||||
cli?: CodeQLCliServer,
|
||||
language?: string,
|
||||
makeSelected = true,
|
||||
addSourceArchiveFolder = true,
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
const githubRepo = await askForGitHubRepo(progress);
|
||||
if (!githubRepo) {
|
||||
@@ -112,6 +114,7 @@ export async function promptImportGithubDatabase(
|
||||
cli,
|
||||
language,
|
||||
makeSelected,
|
||||
addSourceArchiveFolder,
|
||||
);
|
||||
|
||||
if (databaseItem) {
|
||||
@@ -163,6 +166,7 @@ export async function askForGitHubRepo(
|
||||
* @param cli the CodeQL CLI server
|
||||
* @param language the language to download. If undefined, the user will be prompted to choose a language.
|
||||
* @param makeSelected make the new database selected in the databases panel (default: true)
|
||||
* @param addSourceArchiveFolder whether to add a workspace folder containing the source archive to the workspace
|
||||
**/
|
||||
export async function downloadGitHubDatabase(
|
||||
githubRepo: string,
|
||||
@@ -173,6 +177,7 @@ export async function downloadGitHubDatabase(
|
||||
cli?: CodeQLCliServer,
|
||||
language?: string,
|
||||
makeSelected = true,
|
||||
addSourceArchiveFolder = true,
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
const nwo = getNwoFromGitHubUrl(githubRepo) || githubRepo;
|
||||
if (!isValidGitHubNwo(nwo)) {
|
||||
@@ -181,7 +186,7 @@ export async function downloadGitHubDatabase(
|
||||
|
||||
const octokit = credentials
|
||||
? await credentials.getOctokit()
|
||||
: new Octokit.Octokit({ retry });
|
||||
: new AppOctokit();
|
||||
|
||||
const result = await convertGithubNwoToDatabaseUrl(
|
||||
nwo,
|
||||
@@ -218,6 +223,7 @@ export async function downloadGitHubDatabase(
|
||||
progress,
|
||||
cli,
|
||||
makeSelected,
|
||||
addSourceArchiveFolder,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -277,6 +283,7 @@ export async function importArchiveDatabase(
|
||||
* @param nameOverride a name for the database that overrides the default
|
||||
* @param progress callback to send progress messages to
|
||||
* @param makeSelected make the new database selected in the databases panel (default: true)
|
||||
* @param addSourceArchiveFolder whether to add a workspace folder containing the source archive to the workspace
|
||||
*/
|
||||
async function databaseArchiveFetcher(
|
||||
databaseUrl: string,
|
||||
@@ -287,6 +294,7 @@ async function databaseArchiveFetcher(
|
||||
progress: ProgressCallback,
|
||||
cli?: CodeQLCliServer,
|
||||
makeSelected = true,
|
||||
addSourceArchiveFolder = true,
|
||||
): Promise<DatabaseItem> {
|
||||
progress({
|
||||
message: "Getting database",
|
||||
@@ -329,6 +337,9 @@ async function databaseArchiveFetcher(
|
||||
Uri.file(dbPath),
|
||||
makeSelected,
|
||||
nameOverride,
|
||||
{
|
||||
addSourceArchiveFolder,
|
||||
},
|
||||
);
|
||||
return item;
|
||||
} else {
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
createMultiSelectionCommand,
|
||||
createSingleSelectionCommand,
|
||||
} from "../common/vscode/selection-commands";
|
||||
import { QueryLanguage, tryGetQueryLanguage } from "../common/query-language";
|
||||
import { LanguageContextStore } from "../language-context-store";
|
||||
|
||||
enum SortOrder {
|
||||
NameAsc = "NameAsc",
|
||||
@@ -73,7 +75,10 @@ class DatabaseTreeDataProvider
|
||||
);
|
||||
private currentDatabaseItem: DatabaseItem | undefined;
|
||||
|
||||
constructor(private databaseManager: DatabaseManager) {
|
||||
constructor(
|
||||
private databaseManager: DatabaseManager,
|
||||
private languageContext: LanguageContextStore,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.currentDatabaseItem = databaseManager.currentDatabaseItem;
|
||||
@@ -88,6 +93,11 @@ class DatabaseTreeDataProvider
|
||||
this.handleDidChangeCurrentDatabaseItem.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
this.languageContext.onLanguageContextChanged(async () => {
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public get onDidChangeTreeData(): Event<DatabaseItem | undefined> {
|
||||
@@ -131,7 +141,15 @@ class DatabaseTreeDataProvider
|
||||
|
||||
public getChildren(element?: DatabaseItem): ProviderResult<DatabaseItem[]> {
|
||||
if (element === undefined) {
|
||||
return this.databaseManager.databaseItems.slice(0).sort((db1, db2) => {
|
||||
// Filter items by language
|
||||
const displayItems = this.databaseManager.databaseItems.filter((item) => {
|
||||
return this.languageContext.shouldInclude(
|
||||
tryGetQueryLanguage(item.language),
|
||||
);
|
||||
});
|
||||
|
||||
// Sort items
|
||||
return displayItems.slice(0).sort((db1, db2) => {
|
||||
switch (this.sortOrder) {
|
||||
case SortOrder.NameAsc:
|
||||
return db1.name.localeCompare(db2.name, env.language);
|
||||
@@ -200,6 +218,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
public constructor(
|
||||
private app: App,
|
||||
private databaseManager: DatabaseManager,
|
||||
private languageContext: LanguageContextStore,
|
||||
private readonly queryServer: QueryRunner | undefined,
|
||||
private readonly storagePath: string,
|
||||
readonly extensionPath: string,
|
||||
@@ -207,7 +226,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
super();
|
||||
|
||||
this.treeDataProvider = this.push(
|
||||
new DatabaseTreeDataProvider(databaseManager),
|
||||
new DatabaseTreeDataProvider(databaseManager, languageContext),
|
||||
);
|
||||
this.push(
|
||||
window.createTreeView("codeQLDatabases", {
|
||||
@@ -245,6 +264,60 @@ export class DatabaseUI extends DisposableObject {
|
||||
this.handleMakeCurrentDatabase.bind(this),
|
||||
"codeQLDatabases.sortByName": this.handleSortByName.bind(this),
|
||||
"codeQLDatabases.sortByDateAdded": this.handleSortByDateAdded.bind(this),
|
||||
"codeQLDatabases.displayAllLanguages":
|
||||
this.handleClearLanguageFilter.bind(this),
|
||||
"codeQLDatabases.displayCpp": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.Cpp,
|
||||
),
|
||||
"codeQLDatabases.displayCsharp": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.CSharp,
|
||||
),
|
||||
"codeQLDatabases.displayGo": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.Go,
|
||||
),
|
||||
"codeQLDatabases.displayJava": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.Java,
|
||||
),
|
||||
"codeQLDatabases.displayJavascript": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.Javascript,
|
||||
),
|
||||
"codeQLDatabases.displayPython": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.Python,
|
||||
),
|
||||
"codeQLDatabases.displayRuby": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.Ruby,
|
||||
),
|
||||
"codeQLDatabases.displaySwift": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.Swift,
|
||||
),
|
||||
"codeQLDatabases.displayAllLanguagesSelected":
|
||||
this.handleClearLanguageFilter.bind(this),
|
||||
"codeQLDatabases.displayCppSelected":
|
||||
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Cpp),
|
||||
"codeQLDatabases.displayCsharpSelected":
|
||||
this.handleChangeLanguageFilter.bind(this, QueryLanguage.CSharp),
|
||||
"codeQLDatabases.displayGoSelected": this.handleChangeLanguageFilter.bind(
|
||||
this,
|
||||
QueryLanguage.Go,
|
||||
),
|
||||
"codeQLDatabases.displayJavaSelected":
|
||||
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Java),
|
||||
"codeQLDatabases.displayJavascriptSelected":
|
||||
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Javascript),
|
||||
"codeQLDatabases.displayPythonSelected":
|
||||
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Python),
|
||||
"codeQLDatabases.displayRubySelected":
|
||||
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Ruby),
|
||||
"codeQLDatabases.displaySwiftSelected":
|
||||
this.handleChangeLanguageFilter.bind(this, QueryLanguage.Swift),
|
||||
"codeQLDatabases.removeDatabase": createMultiSelectionCommand(
|
||||
this.handleRemoveDatabase.bind(this),
|
||||
),
|
||||
@@ -329,13 +402,14 @@ export class DatabaseUI extends DisposableObject {
|
||||
if (databaseItem === undefined) {
|
||||
const makeSelected = true;
|
||||
const nameOverride = "CodeQL Tutorial Database";
|
||||
const isTutorialDatabase = true;
|
||||
|
||||
await this.databaseManager.openDatabase(
|
||||
uri,
|
||||
makeSelected,
|
||||
nameOverride,
|
||||
isTutorialDatabase,
|
||||
{
|
||||
isTutorialDatabase: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
await this.handleTourDependencies();
|
||||
@@ -534,6 +608,14 @@ export class DatabaseUI extends DisposableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleClearLanguageFilter() {
|
||||
await this.languageContext.clearLanguageContext();
|
||||
}
|
||||
|
||||
private async handleChangeLanguageFilter(languageFilter: QueryLanguage) {
|
||||
await this.languageContext.setLanguageContext(languageFilter);
|
||||
}
|
||||
|
||||
private async handleUpgradeCurrentDatabase(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
|
||||
@@ -167,6 +167,15 @@ export class DatabaseItemImpl implements DatabaseItem {
|
||||
return encodeArchiveBasePath(sourceArchive.fsPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the database's source archive is in the workspace.
|
||||
*/
|
||||
public hasSourceArchiveInExplorer(): boolean {
|
||||
return (vscode.workspace.workspaceFolders || []).some((folder) =>
|
||||
this.belongsToSourceArchiveExplorerUri(folder.uri),
|
||||
);
|
||||
}
|
||||
|
||||
public verifyZippedSources(): string | undefined {
|
||||
const sourceArchive = this.sourceArchive;
|
||||
if (sourceArchive === undefined) {
|
||||
|
||||
@@ -56,6 +56,11 @@ export interface DatabaseItem {
|
||||
*/
|
||||
getSourceArchiveExplorerUri(): vscode.Uri;
|
||||
|
||||
/**
|
||||
* Returns true if the database's source archive is in the workspace.
|
||||
*/
|
||||
hasSourceArchiveInExplorer(): boolean;
|
||||
|
||||
/**
|
||||
* Holds if `uri` belongs to this database's source archive.
|
||||
*/
|
||||
|
||||
@@ -71,6 +71,14 @@ function eventFired<T>(
|
||||
});
|
||||
}
|
||||
|
||||
type OpenDatabaseOptions = {
|
||||
isTutorialDatabase?: boolean;
|
||||
/**
|
||||
* Whether to add a workspace folder containing the source archive to the workspace. Default is true.
|
||||
*/
|
||||
addSourceArchiveFolder?: boolean;
|
||||
};
|
||||
|
||||
export class DatabaseManager extends DisposableObject {
|
||||
private readonly _onDidChangeDatabaseItem = this.push(
|
||||
new vscode.EventEmitter<DatabaseChangedEvent>(),
|
||||
@@ -107,7 +115,10 @@ export class DatabaseManager extends DisposableObject {
|
||||
uri: vscode.Uri,
|
||||
makeSelected = true,
|
||||
displayName?: string,
|
||||
isTutorialDatabase?: boolean,
|
||||
{
|
||||
isTutorialDatabase = false,
|
||||
addSourceArchiveFolder = true,
|
||||
}: OpenDatabaseOptions = {},
|
||||
): Promise<DatabaseItem> {
|
||||
const databaseItem = await this.createDatabaseItem(uri, displayName);
|
||||
|
||||
@@ -115,6 +126,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
databaseItem,
|
||||
makeSelected,
|
||||
isTutorialDatabase,
|
||||
addSourceArchiveFolder,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,6 +140,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
databaseItem: DatabaseItemImpl,
|
||||
makeSelected: boolean,
|
||||
isTutorialDatabase?: boolean,
|
||||
addSourceArchiveFolder = true,
|
||||
): Promise<DatabaseItem> {
|
||||
const existingItem = this.findDatabaseItem(databaseItem.databaseUri);
|
||||
if (existingItem !== undefined) {
|
||||
@@ -141,7 +154,9 @@ export class DatabaseManager extends DisposableObject {
|
||||
if (makeSelected) {
|
||||
await this.setCurrentDatabaseItem(databaseItem);
|
||||
}
|
||||
await this.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
if (addSourceArchiveFolder) {
|
||||
await this.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
}
|
||||
|
||||
if (isCodespacesTemplate() && !isTutorialDatabase) {
|
||||
await this.createSkeletonPacks(databaseItem);
|
||||
|
||||
@@ -409,7 +409,7 @@ export class DbPanel extends DisposableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
void window.withProgress(
|
||||
await window.withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
title: "Searching for repositories... This might take a while",
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface QLDebugArgs {
|
||||
extensionPacks?: string[] | string;
|
||||
quickEval?: boolean;
|
||||
noDebug?: boolean;
|
||||
additionalRunQueryArgs?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +121,7 @@ export class QLDebugConfigurationProvider
|
||||
extensionPacks,
|
||||
quickEvalContext,
|
||||
noDebug: qlConfiguration.noDebug ?? false,
|
||||
additionalRunQueryArgs: qlConfiguration.additionalRunQueryArgs ?? {},
|
||||
};
|
||||
|
||||
return resultConfiguration;
|
||||
|
||||
@@ -70,6 +70,8 @@ export interface LaunchConfig {
|
||||
quickEvalContext: QuickEvalContext | undefined;
|
||||
/** Run the query without debugging it. */
|
||||
noDebug: boolean;
|
||||
/** Undocumented: Additional arguments to be passed to the `runQuery` API on the query server. */
|
||||
additionalRunQueryArgs: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface LaunchRequest extends Request, DebugProtocol.LaunchRequest {
|
||||
|
||||
@@ -161,6 +161,7 @@ class RunningQuery extends DisposableObject {
|
||||
true,
|
||||
config.additionalPacks,
|
||||
config.extensionPacks,
|
||||
config.additionalRunQueryArgs,
|
||||
queryStorageDir,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -135,6 +135,7 @@ import { TestManagerBase } from "./query-testing/test-manager-base";
|
||||
import { NewQueryRunner, QueryRunner, QueryServerClient } from "./query-server";
|
||||
import { QueriesModule } from "./queries-panel/queries-module";
|
||||
import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider";
|
||||
import { LanguageContextStore } from "./language-context-store";
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -299,12 +300,12 @@ const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
|
||||
|
||||
const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE;
|
||||
|
||||
// This is the minimum version of vscode that we _want_ to support. We want to update the language server library, but that
|
||||
// requires 1.67 or later. If we change the minimum version in the package.json, then anyone on an older version of vscode
|
||||
// This is the minimum version of vscode that we _want_ to support. We want to update to Node 18, but that
|
||||
// requires 1.82 or later. If we change the minimum version in the package.json, then anyone on an older version of vscode will
|
||||
// silently be unable to upgrade. So, the solution is to first bump the minimum version here and release. Then
|
||||
// bump the version in the package.json and release again. This way, anyone on an older version of vscode will get a warning
|
||||
// before silently being refused to upgrade.
|
||||
const MIN_VERSION = "1.67.0";
|
||||
const MIN_VERSION = "1.82.0";
|
||||
|
||||
/**
|
||||
* Returns the CodeQLExtensionInterface, or an empty object if the interface is not
|
||||
@@ -396,10 +397,7 @@ export async function activate(
|
||||
),
|
||||
);
|
||||
|
||||
const variantAnalysisViewSerializer = new VariantAnalysisViewSerializer(
|
||||
ctx,
|
||||
app,
|
||||
);
|
||||
const variantAnalysisViewSerializer = new VariantAnalysisViewSerializer(app);
|
||||
Window.registerWebviewPanelSerializer(
|
||||
VariantAnalysisView.viewType,
|
||||
variantAnalysisViewSerializer,
|
||||
@@ -777,17 +775,22 @@ async function activateWithInstalledDistribution(
|
||||
void dbm.loadPersistedState();
|
||||
|
||||
ctx.subscriptions.push(dbm);
|
||||
|
||||
void extLogger.log("Initializing language context.");
|
||||
const languageContext = new LanguageContextStore(app);
|
||||
|
||||
void extLogger.log("Initializing database panel.");
|
||||
const databaseUI = new DatabaseUI(
|
||||
app,
|
||||
dbm,
|
||||
languageContext,
|
||||
qs,
|
||||
getContextStoragePath(ctx),
|
||||
ctx.extensionPath,
|
||||
);
|
||||
ctx.subscriptions.push(databaseUI);
|
||||
|
||||
QueriesModule.initialize(app, cliServer);
|
||||
QueriesModule.initialize(app, languageContext, cliServer);
|
||||
|
||||
void extLogger.log("Initializing evaluator log viewer.");
|
||||
const evalLogViewer = new EvalLogViewer();
|
||||
@@ -813,7 +816,7 @@ async function activateWithInstalledDistribution(
|
||||
|
||||
void extLogger.log("Initializing results panel interface.");
|
||||
const localQueryResultsView = new ResultsView(
|
||||
ctx,
|
||||
app,
|
||||
dbm,
|
||||
cliServer,
|
||||
queryServerLogger,
|
||||
@@ -836,7 +839,6 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
|
||||
const variantAnalysisManager = new VariantAnalysisManager(
|
||||
ctx,
|
||||
app,
|
||||
cliServer,
|
||||
variantAnalysisStorageDir,
|
||||
@@ -869,6 +871,7 @@ async function activateWithInstalledDistribution(
|
||||
ctx,
|
||||
queryHistoryConfigurationListener,
|
||||
labelProvider,
|
||||
languageContext,
|
||||
async (
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo,
|
||||
@@ -888,7 +891,7 @@ async function activateWithInstalledDistribution(
|
||||
|
||||
void extLogger.log("Initializing compare view.");
|
||||
const compareView = new CompareView(
|
||||
ctx,
|
||||
app,
|
||||
dbm,
|
||||
cliServer,
|
||||
queryServerLogger,
|
||||
@@ -935,7 +938,6 @@ async function activateWithInstalledDistribution(
|
||||
ctx.subscriptions.push(debuggerUI);
|
||||
|
||||
const modelEditorModule = await ModelEditorModule.initialize(
|
||||
ctx,
|
||||
app,
|
||||
dbm,
|
||||
cliServer,
|
||||
@@ -1162,13 +1164,16 @@ function addUnhandledRejectionListener() {
|
||||
const message = redactableError(
|
||||
asError(error),
|
||||
)`Unhandled error: ${getErrorMessage(error)}`;
|
||||
const stack = getErrorStack(error);
|
||||
const fullMessage = stack
|
||||
? `Unhandled error: ${stack}`
|
||||
: message.fullMessage;
|
||||
|
||||
// Add a catch so that showAndLogExceptionWithTelemetry fails, we avoid
|
||||
// triggering "unhandledRejection" and avoid an infinite loop
|
||||
showAndLogExceptionWithTelemetry(
|
||||
extLogger,
|
||||
telemetryListener,
|
||||
message,
|
||||
).catch((telemetryError: unknown) => {
|
||||
showAndLogExceptionWithTelemetry(extLogger, telemetryListener, message, {
|
||||
fullMessage,
|
||||
}).catch((telemetryError: unknown) => {
|
||||
void extLogger.log(
|
||||
`Failed to send error telemetry: ${getErrorMessage(telemetryError)}`,
|
||||
);
|
||||
|
||||
49
extensions/ql-vscode/src/language-context-store.ts
Normal file
49
extensions/ql-vscode/src/language-context-store.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { App } from "./common/app";
|
||||
import { DisposableObject } from "./common/disposable-object";
|
||||
import { AppEvent, AppEventEmitter } from "./common/events";
|
||||
import { QueryLanguage } from "./common/query-language";
|
||||
|
||||
type LanguageFilter = QueryLanguage | "All";
|
||||
|
||||
export class LanguageContextStore extends DisposableObject {
|
||||
public readonly onLanguageContextChanged: AppEvent<void>;
|
||||
private readonly onLanguageContextChangedEmitter: AppEventEmitter<void>;
|
||||
|
||||
private languageFilter: LanguageFilter;
|
||||
|
||||
constructor(private readonly app: App) {
|
||||
super();
|
||||
// State initialization
|
||||
this.languageFilter = "All";
|
||||
|
||||
// Set up event emitters
|
||||
this.onLanguageContextChangedEmitter = this.push(
|
||||
app.createEventEmitter<void>(),
|
||||
);
|
||||
this.onLanguageContextChanged = this.onLanguageContextChangedEmitter.event;
|
||||
}
|
||||
|
||||
public async clearLanguageContext() {
|
||||
this.languageFilter = "All";
|
||||
this.onLanguageContextChangedEmitter.fire();
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLDatabases.languageFilter",
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
public async setLanguageContext(language: QueryLanguage) {
|
||||
this.languageFilter = language;
|
||||
this.onLanguageContextChangedEmitter.fire();
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLDatabases.languageFilter",
|
||||
language,
|
||||
);
|
||||
}
|
||||
|
||||
public shouldInclude(language: QueryLanguage | undefined): boolean {
|
||||
return this.languageFilter === "All" || this.languageFilter === language;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export async function runContextualQuery(
|
||||
false,
|
||||
getOnDiskWorkspaceFolders(),
|
||||
undefined,
|
||||
{},
|
||||
queryStorageDir,
|
||||
undefined,
|
||||
templates,
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
|
||||
import { AstBuilder } from "../ast-viewer/ast-builder";
|
||||
import { qlpackOfDatabase } from "../../local-queries";
|
||||
import { MultiCancellationToken } from "../../common/vscode/multi-cancellation-token";
|
||||
|
||||
/**
|
||||
* Runs templated CodeQL queries to find definitions in
|
||||
@@ -43,6 +44,7 @@ import { qlpackOfDatabase } from "../../local-queries";
|
||||
* generalize this to other custom queries, e.g. showing dataflow to
|
||||
* or from a selected identifier.
|
||||
*/
|
||||
|
||||
export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
private cache: CachedOperation<LocationLink[]>;
|
||||
|
||||
@@ -60,11 +62,11 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
async provideDefinition(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
_token: CancellationToken,
|
||||
token: CancellationToken,
|
||||
): Promise<LocationLink[]> {
|
||||
const fileLinks = this.shouldUseCache()
|
||||
? await this.cache.get(document.uri.toString())
|
||||
: await this.getDefinitions(document.uri.toString());
|
||||
? await this.cache.get(document.uri.toString(), token)
|
||||
: await this.getDefinitions(document.uri.toString(), token);
|
||||
|
||||
const locLinks: LocationLink[] = [];
|
||||
for (const link of fileLinks) {
|
||||
@@ -79,9 +81,13 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
return !(isCanary() && NO_CACHE_CONTEXTUAL_QUERIES.getValue<boolean>());
|
||||
}
|
||||
|
||||
private async getDefinitions(uriString: string): Promise<LocationLink[]> {
|
||||
private async getDefinitions(
|
||||
uriString: string,
|
||||
token: CancellationToken,
|
||||
): Promise<LocationLink[]> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
async (progress, tokenInner) => {
|
||||
const multiToken = new MultiCancellationToken(token, tokenInner);
|
||||
return getLocationsForUriString(
|
||||
this.cli,
|
||||
this.qs,
|
||||
@@ -90,7 +96,7 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
KeyType.DefinitionQuery,
|
||||
this.queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
multiToken,
|
||||
(src, _dest) => src === uriString,
|
||||
);
|
||||
},
|
||||
@@ -126,11 +132,11 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
_context: ReferenceContext,
|
||||
_token: CancellationToken,
|
||||
token: CancellationToken,
|
||||
): Promise<Location[]> {
|
||||
const fileLinks = this.shouldUseCache()
|
||||
? await this.cache.get(document.uri.toString())
|
||||
: await this.getReferences(document.uri.toString());
|
||||
? await this.cache.get(document.uri.toString(), token)
|
||||
: await this.getReferences(document.uri.toString(), token);
|
||||
|
||||
const locLinks: Location[] = [];
|
||||
for (const link of fileLinks) {
|
||||
@@ -148,9 +154,14 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
return !(isCanary() && NO_CACHE_CONTEXTUAL_QUERIES.getValue<boolean>());
|
||||
}
|
||||
|
||||
private async getReferences(uriString: string): Promise<FullLocationLink[]> {
|
||||
private async getReferences(
|
||||
uriString: string,
|
||||
token: CancellationToken,
|
||||
): Promise<FullLocationLink[]> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
async (progress, tokenInner) => {
|
||||
const multiToken = new MultiCancellationToken(token, tokenInner);
|
||||
|
||||
return getLocationsForUriString(
|
||||
this.cli,
|
||||
this.qs,
|
||||
@@ -159,7 +170,7 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
KeyType.DefinitionQuery,
|
||||
this.queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
multiToken,
|
||||
(src, _dest) => src === uriString,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -49,6 +49,7 @@ import { LocalQueryRun } from "./local-query-run";
|
||||
import { createMultiSelectionCommand } from "../common/vscode/selection-commands";
|
||||
import { findLanguage } from "../codeql-cli/query-language";
|
||||
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
|
||||
import { tryGetQueryLanguage } from "../common/query-language";
|
||||
|
||||
interface DatabaseQuickPickItem extends QuickPickItem {
|
||||
databaseItem: DatabaseItem;
|
||||
@@ -364,6 +365,7 @@ export class LocalQueries extends DisposableObject {
|
||||
const initialInfo = await createInitialQueryInfo(selectedQuery, {
|
||||
databaseUri: dbItem.databaseUri.toString(),
|
||||
name: dbItem.name,
|
||||
language: tryGetQueryLanguage(dbItem.language),
|
||||
});
|
||||
|
||||
// When cancellation is requested from the query history view, we just stop the debug session.
|
||||
@@ -454,6 +456,7 @@ export class LocalQueries extends DisposableObject {
|
||||
true,
|
||||
additionalPacks,
|
||||
extensionPacks,
|
||||
{},
|
||||
this.queryStorageDir,
|
||||
undefined,
|
||||
templates,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { redactableError } from "../common/errors";
|
||||
import { showAndLogExceptionWithTelemetry } from "../common/logging";
|
||||
import { extLogger } from "../common/logging/vscode";
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { SuiteInstruction } from "../packaging/suite-instruction";
|
||||
|
||||
export async function qlpackOfDatabase(
|
||||
cli: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||
@@ -38,24 +39,26 @@ export interface QueryConstraints {
|
||||
* @param cli The CLI instance to use.
|
||||
* @param qlpacks The list of packs to search.
|
||||
* @param constraints Constraints on the queries to search for.
|
||||
* @param additionalPacks Additional pack paths to search.
|
||||
* @returns The found queries from the first pack in which any matching queries were found.
|
||||
*/
|
||||
async function resolveQueriesFromPacks(
|
||||
export async function resolveQueriesFromPacks(
|
||||
cli: CodeQLCliServer,
|
||||
qlpacks: string[],
|
||||
constraints: QueryConstraints,
|
||||
additionalPacks: string[] = [],
|
||||
): Promise<string[]> {
|
||||
const suiteFile = (
|
||||
await file({
|
||||
postfix: ".qls",
|
||||
})
|
||||
).path;
|
||||
const suiteYaml = [];
|
||||
const suiteYaml: SuiteInstruction[] = [];
|
||||
for (const qlpack of qlpacks) {
|
||||
suiteYaml.push({
|
||||
from: qlpack,
|
||||
queries: ".",
|
||||
include: constraints,
|
||||
include: constraints as Record<string, string[]>,
|
||||
});
|
||||
}
|
||||
await writeFile(
|
||||
@@ -66,10 +69,10 @@ async function resolveQueriesFromPacks(
|
||||
"utf8",
|
||||
);
|
||||
|
||||
return await cli.resolveQueriesInSuite(
|
||||
suiteFile,
|
||||
getOnDiskWorkspaceFolders(),
|
||||
);
|
||||
return await cli.resolveQueriesInSuite(suiteFile, [
|
||||
...getOnDiskWorkspaceFolders(),
|
||||
...additionalPacks,
|
||||
]);
|
||||
}
|
||||
|
||||
export async function resolveQueriesByLanguagePack(
|
||||
@@ -96,6 +99,7 @@ export async function resolveQueriesByLanguagePack(
|
||||
* @param packsToSearch The list of packs to search.
|
||||
* @param name The name of the query to use in error messages.
|
||||
* @param constraints Constraints on the queries to search for.
|
||||
* @param additionalPacks Additional pack paths to search.
|
||||
* @returns The found queries from the first pack in which any matching queries were found.
|
||||
*/
|
||||
export async function resolveQueries(
|
||||
@@ -103,11 +107,13 @@ export async function resolveQueries(
|
||||
packsToSearch: string[],
|
||||
name: string,
|
||||
constraints: QueryConstraints,
|
||||
additionalPacks: string[] = [],
|
||||
): Promise<string[]> {
|
||||
const queries = await resolveQueriesFromPacks(
|
||||
cli,
|
||||
packsToSearch,
|
||||
constraints,
|
||||
additionalPacks,
|
||||
);
|
||||
if (queries.length > 0) {
|
||||
return queries;
|
||||
|
||||
@@ -74,6 +74,8 @@ import { HistoryItemLabelProvider } from "../query-history/history-item-label-pr
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { redactableError } from "../common/errors";
|
||||
import { ResultsViewCommands } from "../common/commands";
|
||||
import { App } from "../common/app";
|
||||
import { Disposable } from "../common/disposable-object";
|
||||
|
||||
/**
|
||||
* results-view.ts
|
||||
@@ -156,6 +158,12 @@ function numInterpretedPages(
|
||||
return Math.ceil(n / pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* The results view is used for displaying the results of a local query. It is a singleton; only 1 results view exists
|
||||
* in the extension. It is created when the extension is activated and disposed of when the extension is deactivated.
|
||||
* There can be multiple panels linked to this view over the lifetime of the extension, but there is only ever 1 panel
|
||||
* active at a time.
|
||||
*/
|
||||
export class ResultsView extends AbstractWebview<
|
||||
IntoResultsViewMsg,
|
||||
FromResultsViewMsg
|
||||
@@ -167,22 +175,27 @@ export class ResultsView extends AbstractWebview<
|
||||
"codeql-query-results",
|
||||
);
|
||||
|
||||
// Event listeners that should be disposed of when the view is disposed.
|
||||
private disposableEventListeners: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
public ctx: vscode.ExtensionContext,
|
||||
app: App,
|
||||
private databaseManager: DatabaseManager,
|
||||
public cliServer: CodeQLCliServer,
|
||||
public logger: Logger,
|
||||
private labelProvider: HistoryItemLabelProvider,
|
||||
) {
|
||||
super(ctx);
|
||||
this.push(this._diagnosticCollection);
|
||||
this.push(
|
||||
super(app);
|
||||
|
||||
// We can't use this.push for these two event listeners because they need to be disposed of when the view is
|
||||
// disposed, not when the panel is disposed. The results view is a singleton, so we shouldn't be calling this.push.
|
||||
this.disposableEventListeners.push(
|
||||
vscode.window.onDidChangeTextEditorSelection(
|
||||
this.handleSelectionChange.bind(this),
|
||||
),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.disposableEventListeners.push(
|
||||
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
|
||||
if (kind === DatabaseEventKind.Remove) {
|
||||
this._diagnosticCollection.clear();
|
||||
@@ -980,4 +993,12 @@ export class ResultsView extends AbstractWebview<
|
||||
editor.setDecorations(shownLocationLineDecoration, []);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
|
||||
this._diagnosticCollection.dispose();
|
||||
this.disposableEventListeners.forEach((d) => d.dispose());
|
||||
this.disposableEventListeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export async function runQuery({
|
||||
false,
|
||||
additionalPacks,
|
||||
extensionPacks,
|
||||
{},
|
||||
queryStorageDir,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { writeFile, promises } from "fs-extra";
|
||||
import { createReadStream, writeFile } from "fs-extra";
|
||||
import { LINE_ENDINGS, splitStreamAtSeparators } from "../common/split-stream";
|
||||
|
||||
/**
|
||||
* Location information for a single pipeline invocation in the RA.
|
||||
@@ -64,59 +65,64 @@ export async function generateSummarySymbolsFile(
|
||||
async function generateSummarySymbols(
|
||||
summaryPath: string,
|
||||
): Promise<SummarySymbols> {
|
||||
const summary = await promises.readFile(summaryPath, {
|
||||
const stream = createReadStream(summaryPath, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
const symbols: SummarySymbols = {
|
||||
predicates: {},
|
||||
};
|
||||
try {
|
||||
const lines = splitStreamAtSeparators(stream, LINE_ENDINGS);
|
||||
|
||||
const lines = summary.split(/\r?\n/);
|
||||
let lineNumber = 0;
|
||||
while (lineNumber < lines.length) {
|
||||
const startLineNumber = lineNumber;
|
||||
lineNumber++;
|
||||
const startLine = lines[startLineNumber];
|
||||
const nonRecursiveMatch = startLine.match(NON_RECURSIVE_TUPLE_COUNT_REGEXP);
|
||||
let predicateName: string | undefined = undefined;
|
||||
const symbols: SummarySymbols = {
|
||||
predicates: {},
|
||||
};
|
||||
|
||||
let lineNumber = 0;
|
||||
let raStartLine = 0;
|
||||
let iteration = 0;
|
||||
if (nonRecursiveMatch) {
|
||||
predicateName = nonRecursiveMatch.groups!.predicateName;
|
||||
} else {
|
||||
const recursiveMatch = startLine.match(RECURSIVE_TUPLE_COUNT_REGEXP);
|
||||
if (recursiveMatch?.groups) {
|
||||
predicateName = recursiveMatch.groups.predicateName;
|
||||
iteration = parseInt(recursiveMatch.groups.iteration);
|
||||
}
|
||||
}
|
||||
|
||||
if (predicateName !== undefined) {
|
||||
const raStartLine = lineNumber;
|
||||
let raEndLine: number | undefined = undefined;
|
||||
while (lineNumber < lines.length && raEndLine === undefined) {
|
||||
const raLine = lines[lineNumber];
|
||||
const returnMatch = raLine.match(RETURN_REGEXP);
|
||||
let predicateName: string | undefined = undefined;
|
||||
let startLine = 0;
|
||||
for await (const line of lines) {
|
||||
if (predicateName === undefined) {
|
||||
// Looking for the start of the predicate.
|
||||
const nonRecursiveMatch = line.match(NON_RECURSIVE_TUPLE_COUNT_REGEXP);
|
||||
if (nonRecursiveMatch) {
|
||||
iteration = 0;
|
||||
predicateName = nonRecursiveMatch.groups!.predicateName;
|
||||
} else {
|
||||
const recursiveMatch = line.match(RECURSIVE_TUPLE_COUNT_REGEXP);
|
||||
if (recursiveMatch?.groups) {
|
||||
predicateName = recursiveMatch.groups.predicateName;
|
||||
iteration = parseInt(recursiveMatch.groups.iteration);
|
||||
}
|
||||
}
|
||||
if (predicateName !== undefined) {
|
||||
startLine = lineNumber;
|
||||
raStartLine = lineNumber + 1;
|
||||
}
|
||||
} else {
|
||||
const returnMatch = line.match(RETURN_REGEXP);
|
||||
if (returnMatch) {
|
||||
raEndLine = lineNumber;
|
||||
}
|
||||
lineNumber++;
|
||||
}
|
||||
if (raEndLine !== undefined) {
|
||||
let symbol = symbols.predicates[predicateName];
|
||||
if (symbol === undefined) {
|
||||
symbol = {
|
||||
iterations: {},
|
||||
let symbol = symbols.predicates[predicateName];
|
||||
if (symbol === undefined) {
|
||||
symbol = {
|
||||
iterations: {},
|
||||
};
|
||||
symbols.predicates[predicateName] = symbol;
|
||||
}
|
||||
symbol.iterations[iteration] = {
|
||||
startLine,
|
||||
raStartLine,
|
||||
raEndLine: lineNumber,
|
||||
};
|
||||
symbols.predicates[predicateName] = symbol;
|
||||
}
|
||||
symbol.iterations[iteration] = {
|
||||
startLine: lineNumber,
|
||||
raStartLine,
|
||||
raEndLine,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return symbols;
|
||||
predicateName = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
lineNumber++;
|
||||
}
|
||||
|
||||
return symbols;
|
||||
} finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Mode } from "./shared/mode";
|
||||
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
import { interpretResultsSarif } from "../query-results";
|
||||
import { join } from "path";
|
||||
import { assertNever } from "../common/helpers-pure";
|
||||
import { dir } from "tmp-promise";
|
||||
import { writeFile, outputFile } from "fs-extra";
|
||||
import { dump as dumpYaml } from "js-yaml";
|
||||
@@ -16,17 +15,7 @@ import { runQuery } from "../local-queries/run-query";
|
||||
import { QueryMetadata } from "../common/interface-types";
|
||||
import { CancellationTokenSource } from "vscode";
|
||||
import { resolveQueries } from "../local-queries";
|
||||
|
||||
function modeTag(mode: Mode): string {
|
||||
switch (mode) {
|
||||
case Mode.Application:
|
||||
return "application-mode";
|
||||
case Mode.Framework:
|
||||
return "framework-mode";
|
||||
default:
|
||||
assertNever(mode);
|
||||
}
|
||||
}
|
||||
import { modeTag } from "./mode-tag";
|
||||
|
||||
type AutoModelQueriesOptions = {
|
||||
mode: Mode;
|
||||
@@ -63,10 +52,8 @@ export async function runAutoModelQueries({
|
||||
);
|
||||
|
||||
// Generate a pack containing the candidate filters
|
||||
const filterPackDir = await generateCandidateFilterPack(
|
||||
databaseItem.language,
|
||||
candidateMethods,
|
||||
);
|
||||
const { packDir: filterPackDir, cleanup: cleanupFilterPack } =
|
||||
await generateCandidateFilterPack(databaseItem.language, candidateMethods);
|
||||
|
||||
const additionalPacks = [...getOnDiskWorkspaceFolders(), filterPackDir];
|
||||
const extensionPacks = Object.keys(
|
||||
@@ -85,6 +72,8 @@ export async function runAutoModelQueries({
|
||||
token: cancellationTokenSource.token,
|
||||
});
|
||||
|
||||
await cleanupFilterPack();
|
||||
|
||||
if (!completedQuery) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -155,6 +144,11 @@ async function resolveAutomodelQuery(
|
||||
return queries[0];
|
||||
}
|
||||
|
||||
type CandidateFilterPackResult = {
|
||||
packDir: string;
|
||||
cleanup: () => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* generateCandidateFilterPack will create a temporary extension pack.
|
||||
* This pack will contain a filter that will restrict the automodel queries
|
||||
@@ -167,9 +161,9 @@ async function resolveAutomodelQuery(
|
||||
export async function generateCandidateFilterPack(
|
||||
language: string,
|
||||
candidateMethods: MethodSignature[],
|
||||
): Promise<string> {
|
||||
): Promise<CandidateFilterPackResult> {
|
||||
// Pack resides in a temporary directory, to not pollute the workspace.
|
||||
const packDir = (await dir({ unsafeCleanup: true })).path;
|
||||
const { path: packDir, cleanup } = await dir({ unsafeCleanup: true });
|
||||
|
||||
const syntheticConfigPack = {
|
||||
name: "codeql/automodel-filter",
|
||||
@@ -208,7 +202,10 @@ export async function generateCandidateFilterPack(
|
||||
const filterFile = join(packDir, "filter.yml");
|
||||
await writeFile(filterFile, dumpYaml(filter), "utf8");
|
||||
|
||||
return packDir;
|
||||
return {
|
||||
packDir,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
async function interpretAutomodelResults(
|
||||
|
||||
@@ -14,13 +14,13 @@ import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
|
||||
* the order in the UI.
|
||||
* @param mode Whether it is application or framework mode.
|
||||
* @param methods all methods.
|
||||
* @param modeledMethods the currently modeled methods.
|
||||
* @param modeledMethodsBySignature the currently modeled methods.
|
||||
* @returns list of modeled methods that are candidates for modeling.
|
||||
*/
|
||||
export function getCandidates(
|
||||
mode: Mode,
|
||||
methods: Method[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
modeledMethodsBySignature: Record<string, ModeledMethod[]>,
|
||||
): MethodSignature[] {
|
||||
// Sort the same way as the UI so we send the first ones listed in the UI first
|
||||
const grouped = groupMethods(methods, mode);
|
||||
@@ -32,12 +32,11 @@ export function getCandidates(
|
||||
const candidates: MethodSignature[] = [];
|
||||
|
||||
for (const method of sortedMethods) {
|
||||
const modeledMethod: ModeledMethod = modeledMethods[method.signature] ?? {
|
||||
type: "none",
|
||||
};
|
||||
const modeledMethods: ModeledMethod[] =
|
||||
modeledMethodsBySignature[method.signature] ?? [];
|
||||
|
||||
// Anything that is modeled is not a candidate
|
||||
if (modeledMethod.type !== "none") {
|
||||
if (modeledMethods.some((m) => m.type !== "none")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export class AutoModeler {
|
||||
inProgressMethods: string[],
|
||||
) => Promise<void>,
|
||||
private readonly addModeledMethods: (
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
) => Promise<void>,
|
||||
) {
|
||||
this.jobs = new Map<string, CancellationTokenSource>();
|
||||
@@ -59,7 +59,7 @@ export class AutoModeler {
|
||||
public async startModeling(
|
||||
packageName: string,
|
||||
methods: Method[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
mode: Mode,
|
||||
): Promise<void> {
|
||||
if (this.jobs.has(packageName)) {
|
||||
@@ -106,7 +106,7 @@ export class AutoModeler {
|
||||
private async modelPackage(
|
||||
packageName: string,
|
||||
methods: Method[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
mode: Mode,
|
||||
cancellationTokenSource: CancellationTokenSource,
|
||||
): Promise<void> {
|
||||
@@ -203,18 +203,20 @@ export class AutoModeler {
|
||||
// to discussion.
|
||||
for (const candidate of candidateMethods) {
|
||||
if (!(candidate.signature in loadedMethods)) {
|
||||
loadedMethods[candidate.signature] = {
|
||||
type: "neutral",
|
||||
kind: "sink",
|
||||
input: "",
|
||||
output: "",
|
||||
provenance: "ai-generated",
|
||||
signature: candidate.signature,
|
||||
packageName: candidate.packageName,
|
||||
typeName: candidate.typeName,
|
||||
methodName: candidate.methodName,
|
||||
methodParameters: candidate.methodParameters,
|
||||
};
|
||||
loadedMethods[candidate.signature] = [
|
||||
{
|
||||
type: "neutral",
|
||||
kind: "sink",
|
||||
input: "",
|
||||
output: "",
|
||||
provenance: "ai-generated",
|
||||
signature: candidate.signature,
|
||||
packageName: candidate.packageName,
|
||||
typeName: candidate.typeName,
|
||||
methodName: candidate.methodName,
|
||||
methodParameters: candidate.methodParameters,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,43 +2,65 @@ import { DecodedBqrsChunk } from "../common/bqrs-cli-types";
|
||||
import { Call, CallClassification, Method } from "./method";
|
||||
import { ModeledMethodType } from "./modeled-method";
|
||||
import { parseLibraryFilename } from "./library";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { ApplicationModeTuple, FrameworkModeTuple } from "./queries/query";
|
||||
|
||||
export function decodeBqrsToExternalApiUsages(
|
||||
export function decodeBqrsToMethods(
|
||||
chunk: DecodedBqrsChunk,
|
||||
mode: Mode,
|
||||
): Method[] {
|
||||
const methodsByApiName = new Map<string, Method>();
|
||||
|
||||
chunk?.tuples.forEach((tuple) => {
|
||||
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;
|
||||
let usage: Call;
|
||||
let packageName: string;
|
||||
let typeName: string;
|
||||
let methodName: string;
|
||||
let methodParameters: string;
|
||||
let supported: boolean;
|
||||
let library: string;
|
||||
let libraryVersion: string | undefined;
|
||||
let type: ModeledMethodType;
|
||||
let classification: CallClassification;
|
||||
|
||||
const [packageWithType, methodDeclaration] = signature.split("#");
|
||||
if (mode === Mode.Application) {
|
||||
[
|
||||
usage,
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters,
|
||||
supported,
|
||||
library,
|
||||
libraryVersion,
|
||||
type,
|
||||
classification,
|
||||
] = tuple as ApplicationModeTuple;
|
||||
} else {
|
||||
[
|
||||
usage,
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters,
|
||||
supported,
|
||||
library,
|
||||
type,
|
||||
] = tuple as FrameworkModeTuple;
|
||||
|
||||
const packageName = packageWithType.substring(
|
||||
0,
|
||||
packageWithType.lastIndexOf("."),
|
||||
);
|
||||
const typeName = packageWithType.substring(
|
||||
packageWithType.lastIndexOf(".") + 1,
|
||||
);
|
||||
classification = CallClassification.Unknown;
|
||||
}
|
||||
|
||||
const methodName = methodDeclaration.substring(
|
||||
0,
|
||||
methodDeclaration.indexOf("("),
|
||||
);
|
||||
const methodParameters = methodDeclaration.substring(
|
||||
methodDeclaration.indexOf("("),
|
||||
);
|
||||
const signature = `${packageName}.${typeName}#${methodName}${methodParameters}`;
|
||||
|
||||
// For Java, we'll always get back a .jar file, and the library version may be bad because not all library authors
|
||||
// properly specify the version. Therefore, we'll always try to parse the name and version from the library filename
|
||||
// for Java.
|
||||
if (library.endsWith(".jar") || libraryVersion === "") {
|
||||
if (
|
||||
library.endsWith(".jar") ||
|
||||
libraryVersion === "" ||
|
||||
libraryVersion === undefined
|
||||
) {
|
||||
const { name, version } = parseLibraryFilename(library);
|
||||
library = name;
|
||||
if (version) {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["addsTo", "data"],
|
||||
"properties": {
|
||||
"addsTo": {
|
||||
"type": "object",
|
||||
"required": ["pack", "extensible"],
|
||||
"properties": {
|
||||
"pack": {
|
||||
"type": "string"
|
||||
},
|
||||
"extensible": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$ref": "#/definitions/ExtensionPackMetadata",
|
||||
"definitions": {
|
||||
"ExtensionPackMetadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extensionTargets": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"dataExtensions": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"dbscheme": {
|
||||
"type": "string"
|
||||
},
|
||||
"library": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"defaultSuite": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SuiteInstruction"
|
||||
}
|
||||
},
|
||||
"defaultSuiteFile": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["dataExtensions", "extensionTargets", "name", "version"]
|
||||
},
|
||||
"SuiteInstruction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"qlpack": {
|
||||
"type": "string"
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"queries": {
|
||||
"type": "string"
|
||||
},
|
||||
"include": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exclude": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "A single entry in a .qls file."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { QlPackFile } from "../packaging/qlpack-file";
|
||||
|
||||
export type ExtensionPackMetadata = QlPackFile & {
|
||||
// Make both extensionTargets and dataExtensions required
|
||||
extensionTargets: Record<string, string>;
|
||||
dataExtensions: string[] | string;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { join } from "path";
|
||||
import { outputFile, pathExists, readFile } from "fs-extra";
|
||||
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
||||
import { Uri } from "vscode";
|
||||
import Ajv from "ajv";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
import { ProgressCallback } from "../common/vscode/progress";
|
||||
@@ -10,7 +11,7 @@ import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql";
|
||||
import { getErrorMessage } from "../common/helpers-pure";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
|
||||
import { getExtensionsDirectory } from "../config";
|
||||
import { ModelConfig } from "../config";
|
||||
import {
|
||||
autoNameExtensionPack,
|
||||
ExtensionPackName,
|
||||
@@ -18,9 +19,16 @@ import {
|
||||
} from "./extension-pack-name";
|
||||
import { autoPickExtensionsDirectory } from "./extensions-workspace-folder";
|
||||
|
||||
import { ExtensionPackMetadata } from "./extension-pack-metadata";
|
||||
import * as extensionPackMetadataSchemaJson from "./extension-pack-metadata.schema.json";
|
||||
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
const extensionPackValidate = ajv.compile(extensionPackMetadataSchemaJson);
|
||||
|
||||
export async function pickExtensionPack(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
modelConfig: ModelConfig,
|
||||
logger: NotificationLogger,
|
||||
progress: ProgressCallback,
|
||||
maxStep: number,
|
||||
@@ -49,7 +57,9 @@ export async function pickExtensionPack(
|
||||
});
|
||||
|
||||
// Get the `codeQL.model.extensionsDirectory` setting for the language
|
||||
const userExtensionsDirectory = getExtensionsDirectory(databaseItem.language);
|
||||
const userExtensionsDirectory = modelConfig.getExtensionsDirectory(
|
||||
databaseItem.language,
|
||||
);
|
||||
|
||||
// If the setting is not set, automatically pick a suitable directory
|
||||
const extensionsDirectory = userExtensionsDirectory
|
||||
@@ -170,6 +180,22 @@ async function writeExtensionPack(
|
||||
return extensionPack;
|
||||
}
|
||||
|
||||
function validateExtensionPack(
|
||||
extensionPack: unknown,
|
||||
): extensionPack is ExtensionPackMetadata {
|
||||
extensionPackValidate(extensionPack);
|
||||
|
||||
if (extensionPackValidate.errors) {
|
||||
throw new Error(
|
||||
`Invalid extension pack YAML: ${extensionPackValidate.errors
|
||||
.map((error) => `${error.instancePath} ${error.message}`)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function readExtensionPack(
|
||||
path: string,
|
||||
language: string,
|
||||
@@ -188,6 +214,10 @@ async function readExtensionPack(
|
||||
throw new Error(`Could not parse ${qlpackPath}`);
|
||||
}
|
||||
|
||||
if (!validateExtensionPack(qlpack)) {
|
||||
throw new Error(`Could not validate ${qlpackPath}`);
|
||||
}
|
||||
|
||||
const dataExtensionValue = qlpack.dataExtensions;
|
||||
if (
|
||||
!(
|
||||
|
||||
@@ -15,7 +15,11 @@ import { QueryLanguage } from "../common/query-language";
|
||||
import { fetchExternalApiQueries } from "./queries";
|
||||
import { Method } from "./method";
|
||||
import { runQuery } from "../local-queries/run-query";
|
||||
import { decodeBqrsToExternalApiUsages } from "./bqrs";
|
||||
import { decodeBqrsToMethods } from "./bqrs";
|
||||
import {
|
||||
resolveEndpointsQuery,
|
||||
syntheticQueryPackName,
|
||||
} from "./model-editor-queries";
|
||||
|
||||
type RunQueryOptions = {
|
||||
cliServer: CodeQLCliServer;
|
||||
@@ -88,7 +92,28 @@ export async function runExternalApiQueries(
|
||||
await cliServer.resolveQlpacks(additionalPacks, true),
|
||||
);
|
||||
|
||||
const queryPath = join(queryDir, queryNameFromMode(mode));
|
||||
progress({
|
||||
message: "Resolving query",
|
||||
step: 2,
|
||||
maxStep: externalApiQueriesProgressMaxStep,
|
||||
});
|
||||
|
||||
// Resolve the queries from either codeql/java-queries or from the temporary queryDir
|
||||
const queryPath = await resolveEndpointsQuery(
|
||||
cliServer,
|
||||
databaseItem.language,
|
||||
mode,
|
||||
[syntheticQueryPackName],
|
||||
[queryDir],
|
||||
);
|
||||
if (!queryPath) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
extLogger,
|
||||
telemetryListener,
|
||||
redactableError`The ${mode} model editor query could not be found. Try re-opening the model editor. If that doesn't work, try upgrading the CodeQL libraries.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the actual query
|
||||
const completedQuery = await runQuery({
|
||||
@@ -132,7 +157,7 @@ export async function runExternalApiQueries(
|
||||
maxStep: externalApiQueriesProgressMaxStep,
|
||||
});
|
||||
|
||||
return decodeBqrsToExternalApiUsages(bqrsChunk);
|
||||
return decodeBqrsToMethods(bqrsChunk, mode);
|
||||
}
|
||||
|
||||
type GetResultsOptions = {
|
||||
@@ -160,7 +185,5 @@ export async function readQueryResults({
|
||||
}
|
||||
|
||||
function queryNameFromMode(mode: Mode): string {
|
||||
return `FetchExternalApis${
|
||||
mode.charAt(0).toUpperCase() + mode.slice(1)
|
||||
}Mode.ql`;
|
||||
return `${mode.charAt(0).toUpperCase() + mode.slice(1)}ModeEndpoints.ql`;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
import { ExtensionContext, window } from "vscode";
|
||||
import { window } from "vscode";
|
||||
import { App } from "../../common/app";
|
||||
import { DisposableObject } from "../../common/disposable-object";
|
||||
import { MethodModelingViewProvider } from "./method-modeling-view-provider";
|
||||
import { Method } from "../method";
|
||||
import { ModelingStore } from "../modeling-store";
|
||||
import { ModelEditorViewTracker } from "../model-editor-view-tracker";
|
||||
import { ModelConfigListener } from "../../config";
|
||||
import { DatabaseItem } from "../../databases/local-databases";
|
||||
|
||||
export class MethodModelingPanel extends DisposableObject {
|
||||
private readonly provider: MethodModelingViewProvider;
|
||||
|
||||
constructor(context: ExtensionContext) {
|
||||
constructor(
|
||||
app: App,
|
||||
modelingStore: ModelingStore,
|
||||
editorViewTracker: ModelEditorViewTracker,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.provider = new MethodModelingViewProvider(context);
|
||||
// This is here instead of in MethodModelingViewProvider because we need to
|
||||
// dispose this when the extension gets disposed, not when the webview gets
|
||||
// disposed.
|
||||
const modelConfig = this.push(new ModelConfigListener());
|
||||
|
||||
this.provider = new MethodModelingViewProvider(
|
||||
app,
|
||||
modelingStore,
|
||||
editorViewTracker,
|
||||
modelConfig,
|
||||
);
|
||||
this.push(
|
||||
window.registerWebviewViewProvider(
|
||||
MethodModelingViewProvider.viewType,
|
||||
@@ -18,7 +37,10 @@ export class MethodModelingPanel extends DisposableObject {
|
||||
);
|
||||
}
|
||||
|
||||
public async setMethod(method: Method): Promise<void> {
|
||||
await this.provider.setMethod(method);
|
||||
public async setMethod(
|
||||
databaseItem: DatabaseItem,
|
||||
method: Method,
|
||||
): Promise<void> {
|
||||
await this.provider.setMethod(databaseItem, method);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,103 @@
|
||||
import * as vscode from "vscode";
|
||||
import { WebviewViewProvider } from "vscode";
|
||||
import { getHtmlForWebview } from "../../common/vscode/webview-html";
|
||||
import { FromMethodModelingMessage } from "../../common/interface-types";
|
||||
import {
|
||||
FromMethodModelingMessage,
|
||||
ToMethodModelingMessage,
|
||||
} from "../../common/interface-types";
|
||||
import { telemetryListener } from "../../common/vscode/telemetry";
|
||||
import { showAndLogExceptionWithTelemetry } from "../../common/logging/notifications";
|
||||
import { extLogger } from "../../common/logging/vscode/loggers";
|
||||
import { App } from "../../common/app";
|
||||
import { redactableError } from "../../common/errors";
|
||||
import { Method } from "../method";
|
||||
import { ModelingStore } from "../modeling-store";
|
||||
import { AbstractWebviewViewProvider } from "../../common/vscode/abstract-webview-view-provider";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { ModelEditorViewTracker } from "../model-editor-view-tracker";
|
||||
import { ModelConfigListener } from "../../config";
|
||||
import { DatabaseItem } from "../../databases/local-databases";
|
||||
import { convertFromLegacyModeledMethod } from "../shared/modeled-methods-legacy";
|
||||
|
||||
export class MethodModelingViewProvider implements WebviewViewProvider {
|
||||
export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
ToMethodModelingMessage,
|
||||
FromMethodModelingMessage
|
||||
> {
|
||||
public static readonly viewType = "codeQLMethodModeling";
|
||||
|
||||
private webviewView: vscode.WebviewView | undefined = undefined;
|
||||
private method: Method | undefined = undefined;
|
||||
private databaseItem: DatabaseItem | undefined = undefined;
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {}
|
||||
|
||||
/**
|
||||
* This is called when a view first becomes visible. This may happen when the view is
|
||||
* first loaded or when the user hides and then shows a view again.
|
||||
*/
|
||||
public resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
_context: vscode.WebviewViewResolveContext,
|
||||
_token: vscode.CancellationToken,
|
||||
constructor(
|
||||
app: App,
|
||||
private readonly modelingStore: ModelingStore,
|
||||
private readonly editorViewTracker: ModelEditorViewTracker,
|
||||
private readonly modelConfig: ModelConfigListener,
|
||||
) {
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [this.context.extensionUri],
|
||||
};
|
||||
|
||||
const html = getHtmlForWebview(
|
||||
this.context,
|
||||
webviewView.webview,
|
||||
"method-modeling",
|
||||
{
|
||||
allowInlineStyles: true,
|
||||
allowWasmEval: false,
|
||||
},
|
||||
);
|
||||
|
||||
webviewView.webview.html = html;
|
||||
|
||||
webviewView.webview.onDidReceiveMessage(async (msg) => this.onMessage(msg));
|
||||
|
||||
this.webviewView = webviewView;
|
||||
super(app, "method-modeling");
|
||||
}
|
||||
|
||||
public async setMethod(method: Method): Promise<void> {
|
||||
if (this.webviewView) {
|
||||
await this.webviewView.webview.postMessage({
|
||||
protected override async onWebViewLoaded(): Promise<void> {
|
||||
await Promise.all([this.setViewState(), this.setInitialState()]);
|
||||
this.registerToModelingStoreEvents();
|
||||
this.registerToModelConfigEvents();
|
||||
}
|
||||
|
||||
private async setViewState(): Promise<void> {
|
||||
await this.postMessage({
|
||||
t: "setMethodModelingPanelViewState",
|
||||
viewState: {
|
||||
showMultipleModels: this.modelConfig.showMultipleModels,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async setMethod(
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
method: Method | undefined,
|
||||
): Promise<void> {
|
||||
this.method = method;
|
||||
this.databaseItem = databaseItem;
|
||||
|
||||
if (this.isShowingView) {
|
||||
await this.postMessage({
|
||||
t: "setMethod",
|
||||
method,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async onMessage(msg: FromMethodModelingMessage): Promise<void> {
|
||||
private async setInitialState(): Promise<void> {
|
||||
if (this.modelingStore.hasStateForActiveDb()) {
|
||||
const selectedMethod = this.modelingStore.getSelectedMethodDetails();
|
||||
if (selectedMethod) {
|
||||
this.databaseItem = selectedMethod.databaseItem;
|
||||
this.method = selectedMethod.method;
|
||||
|
||||
await this.postMessage({
|
||||
t: "setSelectedMethod",
|
||||
method: selectedMethod.method,
|
||||
modeledMethods: selectedMethod.modeledMethods,
|
||||
isModified: selectedMethod.isModified,
|
||||
});
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: "setInModelingMode",
|
||||
inModelingMode: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override async onMessage(
|
||||
msg: FromMethodModelingMessage,
|
||||
): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case "telemetry": {
|
||||
case "viewLoaded":
|
||||
await this.onWebViewLoaded();
|
||||
break;
|
||||
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
}
|
||||
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
extLogger,
|
||||
@@ -70,6 +107,123 @@ export class MethodModelingViewProvider implements WebviewViewProvider {
|
||||
)`Unhandled error in method modeling view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
|
||||
case "setModeledMethod": {
|
||||
if (!this.databaseItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modelingStore.updateModeledMethods(
|
||||
this.databaseItem,
|
||||
msg.method.signature,
|
||||
convertFromLegacyModeledMethod(msg.method),
|
||||
);
|
||||
this.modelingStore.addModifiedMethod(
|
||||
this.databaseItem,
|
||||
msg.method.signature,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "revealInModelEditor":
|
||||
await this.revealInModelEditor(msg.method);
|
||||
|
||||
break;
|
||||
|
||||
case "startModeling":
|
||||
await this.app.commands.execute(
|
||||
"codeQL.openModelEditorFromModelingPanel",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private async revealInModelEditor(method: Method): Promise<void> {
|
||||
if (!this.databaseItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.editorViewTracker.getView(
|
||||
this.databaseItem.databaseUri.toString(),
|
||||
);
|
||||
await view?.revealMethod(method);
|
||||
}
|
||||
|
||||
private registerToModelingStoreEvents(): void {
|
||||
this.push(
|
||||
this.modelingStore.onModeledMethodsChanged(async (e) => {
|
||||
if (this.webviewView && e.isActiveDb && this.method) {
|
||||
const modeledMethods = e.modeledMethods[this.method.signature];
|
||||
if (modeledMethods) {
|
||||
await this.postMessage({
|
||||
t: "setMultipleModeledMethods",
|
||||
methodSignature: this.method.signature,
|
||||
modeledMethods,
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onModifiedMethodsChanged(async (e) => {
|
||||
if (this.webviewView && e.isActiveDb && this.method) {
|
||||
const isModified = e.modifiedMethods.has(this.method.signature);
|
||||
await this.postMessage({
|
||||
t: "setMethodModified",
|
||||
isModified,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onSelectedMethodChanged(async (e) => {
|
||||
if (this.webviewView) {
|
||||
this.method = e.method;
|
||||
this.databaseItem = e.databaseItem;
|
||||
|
||||
await this.postMessage({
|
||||
t: "setSelectedMethod",
|
||||
method: e.method,
|
||||
modeledMethods: e.modeledMethods,
|
||||
isModified: e.isModified,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onDbOpened(async () => {
|
||||
await this.postMessage({
|
||||
t: "setInModelingMode",
|
||||
inModelingMode: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onDbClosed(async (dbUri) => {
|
||||
if (!this.modelingStore.anyDbsBeingModeled()) {
|
||||
await this.postMessage({
|
||||
t: "setInModelingMode",
|
||||
inModelingMode: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (dbUri === this.databaseItem?.databaseUri.toString()) {
|
||||
await this.setMethod(undefined, undefined);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private registerToModelConfigEvents(): void {
|
||||
this.push(
|
||||
this.modelConfig.onDidChangeConfiguration(() => {
|
||||
void this.setViewState();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
|
||||
import { ModeledMethodType } from "./modeled-method";
|
||||
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
|
||||
|
||||
export type Call = {
|
||||
label: string;
|
||||
@@ -30,6 +30,11 @@ export interface MethodSignature {
|
||||
* e.g. `org.sql2o.Connection#createQuery(String)`
|
||||
*/
|
||||
signature: string;
|
||||
/**
|
||||
* The package name in Java, or the namespace in C#, e.g. `org.sql2o` or `System.Net.Http.Headers`.
|
||||
*
|
||||
* If the class is not in a package, the value should be an empty string.
|
||||
*/
|
||||
packageName: string;
|
||||
typeName: string;
|
||||
methodName: string;
|
||||
@@ -52,3 +57,23 @@ export interface Method extends MethodSignature {
|
||||
supportedType: ModeledMethodType;
|
||||
usages: Usage[];
|
||||
}
|
||||
|
||||
export function getArgumentsList(methodParameters: string): string[] {
|
||||
if (methodParameters === "()") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return methodParameters.substring(1, methodParameters.length - 1).split(",");
|
||||
}
|
||||
|
||||
export function canMethodBeModeled(
|
||||
method: Method,
|
||||
modeledMethods: ModeledMethod[],
|
||||
methodIsUnsaved: boolean,
|
||||
): boolean {
|
||||
return (
|
||||
!method.supported ||
|
||||
modeledMethods.some((modeledMethod) => modeledMethod.type !== "none") ||
|
||||
methodIsUnsaved
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,16 +13,27 @@ import { Method, Usage } from "../method";
|
||||
import { DatabaseItem } from "../../databases/local-databases";
|
||||
import { relative } from "path";
|
||||
import { CodeQLCliServer } from "../../codeql-cli/cli";
|
||||
import { INITIAL_HIDE_MODELED_APIS_VALUE } from "../shared/hide-modeled-apis";
|
||||
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "../shared/hide-modeled-methods";
|
||||
import { getModelingStatus } from "../shared/modeling-status";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { ModeledMethod } from "../modeled-method";
|
||||
import { groupMethods, sortGroupNames, sortMethods } from "../shared/sorting";
|
||||
import { INITIAL_MODE, Mode } from "../shared/mode";
|
||||
|
||||
export class MethodsUsageDataProvider
|
||||
extends DisposableObject
|
||||
implements TreeDataProvider<MethodsUsageTreeViewItem>
|
||||
{
|
||||
private methods: Method[] = [];
|
||||
// sortedMethods is a separate field so we can check if the methods have changed
|
||||
// by reference, which is faster than checking if the methods have changed by value.
|
||||
private sortedMethods: Method[] = [];
|
||||
private databaseItem: DatabaseItem | undefined = undefined;
|
||||
private sourceLocationPrefix: string | undefined = undefined;
|
||||
private hideModeledApis: boolean = INITIAL_HIDE_MODELED_APIS_VALUE;
|
||||
private hideModeledMethods: boolean = INITIAL_HIDE_MODELED_METHODS_VALUE;
|
||||
private mode: Mode = INITIAL_MODE;
|
||||
private modeledMethods: Record<string, ModeledMethod[]> = {};
|
||||
private modifiedMethodSignatures: Set<string> = new Set();
|
||||
|
||||
private readonly onDidChangeTreeDataEmitter = this.push(
|
||||
new EventEmitter<void>(),
|
||||
@@ -46,18 +57,28 @@ export class MethodsUsageDataProvider
|
||||
public async setState(
|
||||
methods: Method[],
|
||||
databaseItem: DatabaseItem,
|
||||
hideModeledApis: boolean,
|
||||
hideModeledMethods: boolean,
|
||||
mode: Mode,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
modifiedMethodSignatures: Set<string>,
|
||||
): Promise<void> {
|
||||
if (
|
||||
this.methods !== methods ||
|
||||
this.databaseItem !== databaseItem ||
|
||||
this.hideModeledApis !== hideModeledApis
|
||||
this.hideModeledMethods !== hideModeledMethods ||
|
||||
this.mode !== mode ||
|
||||
this.modeledMethods !== modeledMethods ||
|
||||
this.modifiedMethodSignatures !== modifiedMethodSignatures
|
||||
) {
|
||||
this.methods = methods;
|
||||
this.sortedMethods = sortMethodsInGroups(methods, mode);
|
||||
this.databaseItem = databaseItem;
|
||||
this.sourceLocationPrefix =
|
||||
await this.databaseItem.getSourceLocationPrefix(this.cliServer);
|
||||
this.hideModeledApis = hideModeledApis;
|
||||
this.hideModeledMethods = hideModeledMethods;
|
||||
this.mode = mode;
|
||||
this.modeledMethods = modeledMethods;
|
||||
this.modifiedMethodSignatures = modifiedMethodSignatures;
|
||||
|
||||
this.onDidChangeTreeDataEmitter.fire();
|
||||
}
|
||||
@@ -68,10 +89,13 @@ export class MethodsUsageDataProvider
|
||||
return {
|
||||
label: `${item.packageName}.${item.typeName}.${item.methodName}${item.methodParameters}`,
|
||||
collapsibleState: TreeItemCollapsibleState.Collapsed,
|
||||
iconPath: new ThemeIcon("symbol-method"),
|
||||
iconPath: this.getModelingStatusIcon(item),
|
||||
};
|
||||
} else {
|
||||
const method = this.getParent(item);
|
||||
if (!method || !isExternalApiUsage(method)) {
|
||||
throw new Error("Parent not found for tree item");
|
||||
}
|
||||
return {
|
||||
label: item.label,
|
||||
description: `${this.relativePathWithinDatabase(item.url.uri)} [${
|
||||
@@ -80,14 +104,33 @@ export class MethodsUsageDataProvider
|
||||
collapsibleState: TreeItemCollapsibleState.None,
|
||||
command: {
|
||||
title: "Show usage",
|
||||
command: "codeQLModelEditor.jumpToUsageLocation",
|
||||
arguments: [method, item, this.databaseItem],
|
||||
command: "codeQLModelEditor.jumpToMethod",
|
||||
arguments: [method.signature, this.databaseItem],
|
||||
},
|
||||
iconPath: new ThemeIcon("error", new ThemeColor("errorForeground")),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getModelingStatusIcon(method: Method): ThemeIcon {
|
||||
const modeledMethods = this.modeledMethods[method.signature] ?? [];
|
||||
const modifiedMethod = this.modifiedMethodSignatures.has(method.signature);
|
||||
|
||||
const status = getModelingStatus(modeledMethods, modifiedMethod);
|
||||
switch (status) {
|
||||
case "unmodeled":
|
||||
return new ThemeIcon("error", new ThemeColor("errorForeground"));
|
||||
case "unsaved":
|
||||
return new ThemeIcon("pass", new ThemeColor("testing.iconPassed"));
|
||||
case "saved":
|
||||
return new ThemeIcon(
|
||||
"pass-filled",
|
||||
new ThemeColor("testing.iconPassed"),
|
||||
);
|
||||
default:
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
|
||||
private relativePathWithinDatabase(uri: string): string {
|
||||
const parsedUri = Uri.parse(uri);
|
||||
if (this.sourceLocationPrefix) {
|
||||
@@ -99,10 +142,10 @@ export class MethodsUsageDataProvider
|
||||
|
||||
getChildren(item?: MethodsUsageTreeViewItem): MethodsUsageTreeViewItem[] {
|
||||
if (item === undefined) {
|
||||
if (this.hideModeledApis) {
|
||||
return this.methods.filter((api) => !api.supported);
|
||||
if (this.hideModeledMethods) {
|
||||
return this.sortedMethods.filter((api) => !api.supported);
|
||||
} else {
|
||||
return this.methods;
|
||||
return this.sortedMethods;
|
||||
}
|
||||
} else if (isExternalApiUsage(item)) {
|
||||
return item.usages;
|
||||
@@ -150,3 +193,15 @@ function usagesAreEqual(u1: Usage, u2: Usage): boolean {
|
||||
u1.url.endColumn === u2.url.endColumn
|
||||
);
|
||||
}
|
||||
|
||||
function sortMethodsInGroups(methods: Method[], mode: Mode): Method[] {
|
||||
const grouped = groupMethods(methods, mode);
|
||||
|
||||
const sortedGroupNames = sortGroupNames(grouped);
|
||||
|
||||
return sortedGroupNames.flatMap((groupName) => {
|
||||
const group = grouped[groupName];
|
||||
|
||||
return sortMethods(group);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,12 +7,18 @@ import {
|
||||
import { Method, Usage } from "../method";
|
||||
import { DatabaseItem } from "../../databases/local-databases";
|
||||
import { CodeQLCliServer } from "../../codeql-cli/cli";
|
||||
import { ModelingStore } from "../modeling-store";
|
||||
import { ModeledMethod } from "../modeled-method";
|
||||
import { Mode } from "../shared/mode";
|
||||
|
||||
export class MethodsUsagePanel extends DisposableObject {
|
||||
private readonly dataProvider: MethodsUsageDataProvider;
|
||||
private readonly treeView: TreeView<MethodsUsageTreeViewItem>;
|
||||
|
||||
public constructor(cliServer: CodeQLCliServer) {
|
||||
public constructor(
|
||||
private readonly modelingStore: ModelingStore,
|
||||
cliServer: CodeQLCliServer,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.dataProvider = new MethodsUsageDataProvider(cliServer);
|
||||
@@ -21,15 +27,27 @@ export class MethodsUsagePanel extends DisposableObject {
|
||||
treeDataProvider: this.dataProvider,
|
||||
});
|
||||
this.push(this.treeView);
|
||||
|
||||
this.registerToModelingStoreEvents();
|
||||
}
|
||||
|
||||
public async setState(
|
||||
methods: Method[],
|
||||
databaseItem: DatabaseItem,
|
||||
hideModeledApis: boolean,
|
||||
hideModeledMethods: boolean,
|
||||
mode: Mode,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
modifiedMethodSignatures: Set<string>,
|
||||
): Promise<void> {
|
||||
await this.dataProvider.setState(methods, databaseItem, hideModeledApis);
|
||||
const numOfApis = hideModeledApis
|
||||
await this.dataProvider.setState(
|
||||
methods,
|
||||
databaseItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
const numOfApis = hideModeledMethods
|
||||
? methods.filter((api) => !api.supported).length
|
||||
: methods.length;
|
||||
this.treeView.badge = {
|
||||
@@ -44,4 +62,58 @@ export class MethodsUsagePanel extends DisposableObject {
|
||||
await this.treeView.reveal(canonicalUsage);
|
||||
}
|
||||
}
|
||||
|
||||
private registerToModelingStoreEvents(): void {
|
||||
this.push(
|
||||
this.modelingStore.onActiveDbChanged(async () => {
|
||||
await this.handleStateChangeEvent();
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onMethodsChanged(async (event) => {
|
||||
if (event.isActiveDb) {
|
||||
await this.handleStateChangeEvent();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onHideModeledMethodsChanged(async (event) => {
|
||||
if (event.isActiveDb) {
|
||||
await this.handleStateChangeEvent();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onModeChanged(async (event) => {
|
||||
if (event.isActiveDb) {
|
||||
await this.handleStateChangeEvent();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onModifiedMethodsChanged(async (event) => {
|
||||
if (event.isActiveDb) {
|
||||
await this.handleStateChangeEvent();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async handleStateChangeEvent(): Promise<void> {
|
||||
const activeState = this.modelingStore.getStateForActiveDb();
|
||||
if (activeState !== undefined) {
|
||||
await this.setState(
|
||||
activeState.methods,
|
||||
activeState.databaseItem,
|
||||
activeState.hideModeledMethods,
|
||||
activeState.mode,
|
||||
activeState.modeledMethods,
|
||||
activeState.modifiedMethodSignatures,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
extensions/ql-vscode/src/model-editor/mode-tag.ts
Normal file
13
extensions/ql-vscode/src/model-editor/mode-tag.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Mode } from "./shared/mode";
|
||||
import { assertNever } from "../common/helpers-pure";
|
||||
|
||||
export function modeTag(mode: Mode): string {
|
||||
switch (mode) {
|
||||
case Mode.Application:
|
||||
return "application-mode";
|
||||
case Mode.Framework:
|
||||
return "framework-mode";
|
||||
default:
|
||||
assertNever(mode);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ExtensionContext } from "vscode";
|
||||
import { ModelEditorView } from "./model-editor-view";
|
||||
import { ModelEditorCommands } from "../common/commands";
|
||||
import { CliVersionConstraint, CodeQLCliServer } from "../codeql-cli/cli";
|
||||
@@ -15,23 +14,24 @@ import { dir } from "tmp-promise";
|
||||
import { isQueryLanguage } from "../common/query-language";
|
||||
import { DisposableObject } from "../common/disposable-object";
|
||||
import { MethodsUsagePanel } from "./methods-usage/methods-usage-panel";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { showResolvableLocation } from "../databases/local-databases/locations";
|
||||
import { Method, Usage } from "./method";
|
||||
import { setUpPack } from "./model-editor-queries";
|
||||
import { MethodModelingPanel } from "./method-modeling/method-modeling-panel";
|
||||
import { ModelingStore } from "./modeling-store";
|
||||
import { showResolvableLocation } from "../databases/local-databases/locations";
|
||||
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
|
||||
import { ModelConfigListener } from "../config";
|
||||
|
||||
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
|
||||
|
||||
export class ModelEditorModule extends DisposableObject {
|
||||
private readonly queryStorageDir: string;
|
||||
private readonly modelingStore: ModelingStore;
|
||||
private readonly editorViewTracker: ModelEditorViewTracker<ModelEditorView>;
|
||||
private readonly methodsUsagePanel: MethodsUsagePanel;
|
||||
private readonly methodModelingPanel: MethodModelingPanel;
|
||||
|
||||
private mostRecentlyActiveView: ModelEditorView | undefined = undefined;
|
||||
|
||||
private constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly app: App,
|
||||
private readonly databaseManager: DatabaseManager,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
@@ -40,26 +40,19 @@ export class ModelEditorModule extends DisposableObject {
|
||||
) {
|
||||
super();
|
||||
this.queryStorageDir = join(baseQueryStorageDir, "model-editor-results");
|
||||
this.methodsUsagePanel = this.push(new MethodsUsagePanel(cliServer));
|
||||
this.methodModelingPanel = this.push(new MethodModelingPanel(ctx));
|
||||
}
|
||||
this.modelingStore = new ModelingStore(app);
|
||||
this.editorViewTracker = new ModelEditorViewTracker();
|
||||
this.methodsUsagePanel = this.push(
|
||||
new MethodsUsagePanel(this.modelingStore, cliServer),
|
||||
);
|
||||
this.methodModelingPanel = this.push(
|
||||
new MethodModelingPanel(app, this.modelingStore, this.editorViewTracker),
|
||||
);
|
||||
|
||||
private handleViewBecameActive(view: ModelEditorView): void {
|
||||
this.mostRecentlyActiveView = view;
|
||||
}
|
||||
|
||||
private handleViewWasDisposed(view: ModelEditorView): void {
|
||||
if (this.mostRecentlyActiveView === view) {
|
||||
this.mostRecentlyActiveView = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private isMostRecentlyActiveView(view: ModelEditorView): boolean {
|
||||
return this.mostRecentlyActiveView === view;
|
||||
this.registerToModelingStoreEvents();
|
||||
}
|
||||
|
||||
public static async initialize(
|
||||
ctx: ExtensionContext,
|
||||
app: App,
|
||||
databaseManager: DatabaseManager,
|
||||
cliServer: CodeQLCliServer,
|
||||
@@ -67,7 +60,6 @@ export class ModelEditorModule extends DisposableObject {
|
||||
queryStorageDir: string,
|
||||
): Promise<ModelEditorModule> {
|
||||
const modelEditorModule = new ModelEditorModule(
|
||||
ctx,
|
||||
app,
|
||||
databaseManager,
|
||||
cliServer,
|
||||
@@ -81,106 +73,14 @@ export class ModelEditorModule extends DisposableObject {
|
||||
|
||||
public getCommands(): ModelEditorCommands {
|
||||
return {
|
||||
"codeQL.openModelEditor": async () => {
|
||||
const db = this.databaseManager.currentDatabaseItem;
|
||||
if (!db) {
|
||||
void showAndLogErrorMessage(this.app.logger, "No database selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const language = db.language;
|
||||
if (
|
||||
!SUPPORTED_LANGUAGES.includes(language) ||
|
||||
!isQueryLanguage(language)
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`The CodeQL Model Editor is not supported for ${language} databases.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return withProgress(
|
||||
async (progress) => {
|
||||
const maxStep = 4;
|
||||
|
||||
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(await this.cliServer.cliConstraints.supportsResolveExtensions())
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
db,
|
||||
this.app.logger,
|
||||
progress,
|
||||
maxStep,
|
||||
);
|
||||
if (!modelFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
progress({
|
||||
message: "Installing dependencies...",
|
||||
step: 3,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
// Create new temporary directory for query files and pack dependencies
|
||||
const queryDir = (await dir({ unsafeCleanup: true })).path;
|
||||
const success = await setUpPack(this.cliServer, queryDir, language);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
progress({
|
||||
message: "Opening editor...",
|
||||
step: 4,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
const view = new ModelEditorView(
|
||||
this.ctx,
|
||||
this.app,
|
||||
this.databaseManager,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
this.queryStorageDir,
|
||||
queryDir,
|
||||
db,
|
||||
modelFile,
|
||||
Mode.Application,
|
||||
this.methodsUsagePanel.setState.bind(this.methodsUsagePanel),
|
||||
this.showMethod.bind(this),
|
||||
this.handleViewBecameActive.bind(this),
|
||||
this.handleViewWasDisposed.bind(this),
|
||||
this.isMostRecentlyActiveView.bind(this),
|
||||
);
|
||||
await view.openView();
|
||||
},
|
||||
{
|
||||
title: "Opening CodeQL Model Editor",
|
||||
},
|
||||
);
|
||||
},
|
||||
"codeQLModelEditor.jumpToUsageLocation": async (
|
||||
usage: Usage,
|
||||
"codeQL.openModelEditor": this.openModelEditor.bind(this),
|
||||
"codeQL.openModelEditorFromModelingPanel":
|
||||
this.openModelEditor.bind(this),
|
||||
"codeQLModelEditor.jumpToMethod": async (
|
||||
methodSignature: string,
|
||||
databaseItem: DatabaseItem,
|
||||
) => {
|
||||
await showResolvableLocation(usage.url, databaseItem, this.app.logger);
|
||||
this.modelingStore.setSelectedMethod(databaseItem, methodSignature);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -189,8 +89,161 @@ export class ModelEditorModule extends DisposableObject {
|
||||
await ensureDir(this.queryStorageDir);
|
||||
}
|
||||
|
||||
private async showMethod(method: Method, usage: Usage): Promise<void> {
|
||||
private registerToModelingStoreEvents(): void {
|
||||
this.push(
|
||||
this.modelingStore.onSelectedMethodChanged(async (event) => {
|
||||
await this.showMethod(event.databaseItem, event.method, event.usage);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async showMethod(
|
||||
databaseItem: DatabaseItem,
|
||||
method: Method,
|
||||
usage: Usage,
|
||||
): Promise<void> {
|
||||
await this.methodsUsagePanel.revealItem(usage);
|
||||
await this.methodModelingPanel.setMethod(method);
|
||||
await this.methodModelingPanel.setMethod(databaseItem, method);
|
||||
await showResolvableLocation(usage.url, databaseItem, this.app.logger);
|
||||
}
|
||||
|
||||
private async openModelEditor(): Promise<void> {
|
||||
{
|
||||
const db = this.databaseManager.currentDatabaseItem;
|
||||
if (!db) {
|
||||
void showAndLogErrorMessage(this.app.logger, "No database selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const language = db.language;
|
||||
if (
|
||||
!SUPPORTED_LANGUAGES.includes(language) ||
|
||||
!isQueryLanguage(language)
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`The CodeQL Model Editor is not supported for ${language} databases.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingView = this.editorViewTracker.getView(
|
||||
db.databaseUri.toString(),
|
||||
);
|
||||
if (existingView) {
|
||||
await existingView.focusView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return withProgress(
|
||||
async (progress) => {
|
||||
const maxStep = 4;
|
||||
|
||||
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(await this.cliServer.cliConstraints.supportsResolveExtensions())
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const modelConfig = this.push(new ModelConfigListener());
|
||||
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
db,
|
||||
modelConfig,
|
||||
this.app.logger,
|
||||
progress,
|
||||
maxStep,
|
||||
);
|
||||
if (!modelFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
progress({
|
||||
message: "Installing dependencies...",
|
||||
step: 3,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
// Create new temporary directory for query files and pack dependencies
|
||||
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const success = await setUpPack(
|
||||
this.cliServer,
|
||||
queryDir,
|
||||
language,
|
||||
modelConfig,
|
||||
);
|
||||
if (!success) {
|
||||
await cleanupQueryDir();
|
||||
return;
|
||||
}
|
||||
|
||||
progress({
|
||||
message: "Opening editor...",
|
||||
step: 4,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
// Check again just before opening the editor to ensure no model editor has been opened between
|
||||
// our first check and now.
|
||||
const existingView = this.editorViewTracker.getView(
|
||||
db.databaseUri.toString(),
|
||||
);
|
||||
if (existingView) {
|
||||
await existingView.focusView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const view = new ModelEditorView(
|
||||
this.app,
|
||||
this.modelingStore,
|
||||
this.editorViewTracker,
|
||||
modelConfig,
|
||||
this.databaseManager,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
this.queryStorageDir,
|
||||
queryDir,
|
||||
db,
|
||||
modelFile,
|
||||
);
|
||||
|
||||
this.modelingStore.onDbClosed(async (dbUri) => {
|
||||
if (dbUri === db.databaseUri.toString()) {
|
||||
await cleanupQueryDir();
|
||||
}
|
||||
});
|
||||
|
||||
this.push(view);
|
||||
this.push({
|
||||
dispose(): void {
|
||||
void cleanupQueryDir();
|
||||
},
|
||||
});
|
||||
|
||||
await view.openView();
|
||||
},
|
||||
{
|
||||
title: "Opening CodeQL Model Editor",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,43 +4,137 @@ import { writeFile } from "fs-extra";
|
||||
import { dump } from "js-yaml";
|
||||
import { prepareExternalApiQuery } from "./external-api-usage-queries";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import { ModelConfig } from "../config";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { resolveQueriesFromPacks } from "../local-queries";
|
||||
import { modeTag } from "./mode-tag";
|
||||
|
||||
export const syntheticQueryPackName = "codeql/external-api-usage";
|
||||
|
||||
/**
|
||||
* setUpPack sets up a directory to use for the data extension editor queries.
|
||||
* setUpPack sets up a directory to use for the data extension editor queries if required.
|
||||
*
|
||||
* There are two cases (example language is Java):
|
||||
* - In case the queries are present in the codeql/java-queries, we don't need to write our own queries
|
||||
* to disk. We still need to create a synthetic query pack so we can pass the queryDir to the query
|
||||
* resolver without caring about whether the queries are present in the pack or not.
|
||||
* - In case the queries are not present in the codeql/java-queries, we need to write our own queries
|
||||
* to disk. We will create a synthetic query pack and install its dependencies so it is fully independent
|
||||
* and we can simply pass it through when resolving the queries.
|
||||
*
|
||||
* These steps together ensure that later steps of the process don't need to keep track of whether the queries
|
||||
* are present in codeql/java-queries or in our own query pack. They just need to resolve the query.
|
||||
*
|
||||
* @param cliServer The CodeQL CLI server to use.
|
||||
* @param queryDir The directory to set up.
|
||||
* @param language The language to use for the queries.
|
||||
* @param modelConfig The model config to use.
|
||||
* @returns true if the setup was successful, false otherwise.
|
||||
*/
|
||||
export async function setUpPack(
|
||||
cliServer: CodeQLCliServer,
|
||||
queryDir: string,
|
||||
language: QueryLanguage,
|
||||
modelConfig: ModelConfig,
|
||||
): Promise<boolean> {
|
||||
// Create the external API query
|
||||
const externalApiQuerySuccess = await prepareExternalApiQuery(
|
||||
queryDir,
|
||||
// Download the required query packs
|
||||
await cliServer.packDownload([`codeql/${language}-queries`]);
|
||||
|
||||
// We'll only check if the application mode query exists in the pack and assume that if it does,
|
||||
// the framework mode query will also exist.
|
||||
const applicationModeQuery = await resolveEndpointsQuery(
|
||||
cliServer,
|
||||
language,
|
||||
Mode.Application,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
if (!externalApiQuerySuccess) {
|
||||
return false;
|
||||
|
||||
if (applicationModeQuery) {
|
||||
// Set up a synthetic pack so CodeQL doesn't crash later when we try
|
||||
// to resolve a query within this directory
|
||||
const syntheticQueryPack = {
|
||||
name: syntheticQueryPackName,
|
||||
version: "0.0.0",
|
||||
dependencies: {},
|
||||
};
|
||||
|
||||
const qlpackFile = join(queryDir, "codeql-pack.yml");
|
||||
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
|
||||
} else {
|
||||
// If we can't resolve the query, we need to write them to desk ourselves.
|
||||
const externalApiQuerySuccess = await prepareExternalApiQuery(
|
||||
queryDir,
|
||||
language,
|
||||
);
|
||||
if (!externalApiQuerySuccess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up a synthetic pack so that the query can be resolved later.
|
||||
const syntheticQueryPack = {
|
||||
name: syntheticQueryPackName,
|
||||
version: "0.0.0",
|
||||
dependencies: {
|
||||
[`codeql/${language}-all`]: "*",
|
||||
},
|
||||
};
|
||||
|
||||
const qlpackFile = join(queryDir, "codeql-pack.yml");
|
||||
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
|
||||
await cliServer.packInstall(queryDir);
|
||||
}
|
||||
|
||||
// Set up a synthetic pack so that the query can be resolved later.
|
||||
const syntheticQueryPack = {
|
||||
name: "codeql/external-api-usage",
|
||||
version: "0.0.0",
|
||||
dependencies: {
|
||||
[`codeql/${language}-all`]: "*",
|
||||
},
|
||||
};
|
||||
|
||||
const qlpackFile = join(queryDir, "codeql-pack.yml");
|
||||
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
|
||||
await cliServer.packInstall(queryDir);
|
||||
|
||||
// Install the other needed query packs
|
||||
await cliServer.packDownload([`codeql/${language}-queries`]);
|
||||
await cliServer.packDownload([`codeql/${language}-automodel-queries`]);
|
||||
// Download any other required packs
|
||||
if (language === "java" && modelConfig.llmGeneration) {
|
||||
await cliServer.packDownload([`codeql/${language}-automodel-queries`]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the query path to the model editor endpoints query. All queries are tagged like this:
|
||||
* modeleditor endpoints <mode>
|
||||
* Example: modeleditor endpoints framework-mode
|
||||
*
|
||||
* @param cliServer The CodeQL CLI server to use.
|
||||
* @param language The language of the query pack to use.
|
||||
* @param mode The mode to resolve the query for.
|
||||
* @param additionalPackNames Additional pack names to search.
|
||||
* @param additionalPackPaths Additional pack paths to search.
|
||||
*/
|
||||
export async function resolveEndpointsQuery(
|
||||
cliServer: CodeQLCliServer,
|
||||
language: string,
|
||||
mode: Mode,
|
||||
additionalPackNames: string[] = [],
|
||||
additionalPackPaths: string[] = [],
|
||||
): Promise<string | undefined> {
|
||||
const packsToSearch = [`codeql/${language}-queries`, ...additionalPackNames];
|
||||
|
||||
// First, resolve the query that we want to run.
|
||||
// All queries are tagged like this:
|
||||
// internal extract automodel <mode> <queryTag>
|
||||
// Example: internal extract automodel framework-mode candidates
|
||||
const queries = await resolveQueriesFromPacks(
|
||||
cliServer,
|
||||
packsToSearch,
|
||||
{
|
||||
kind: "table",
|
||||
"tags contain all": ["modeleditor", "endpoints", modeTag(mode)],
|
||||
},
|
||||
additionalPackPaths,
|
||||
);
|
||||
if (queries.length > 1) {
|
||||
throw new Error(
|
||||
`Found multiple endpoints queries for ${mode}. Can't continue`,
|
||||
);
|
||||
}
|
||||
|
||||
if (queries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return queries[0];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Method } from "./method";
|
||||
|
||||
interface ModelEditorViewInterface {
|
||||
databaseUri: string;
|
||||
|
||||
revealMethod(method: Method): Promise<void>;
|
||||
}
|
||||
|
||||
export class ModelEditorViewTracker<
|
||||
T extends ModelEditorViewInterface = ModelEditorViewInterface,
|
||||
> {
|
||||
private readonly views = new Map<string, T>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
public registerView(view: T): void {
|
||||
const databaseUri = view.databaseUri;
|
||||
|
||||
if (this.views.has(databaseUri)) {
|
||||
throw new Error(`View for database ${databaseUri} already registered`);
|
||||
}
|
||||
|
||||
this.views.set(databaseUri, view);
|
||||
}
|
||||
|
||||
public unregisterView(view: T): void {
|
||||
this.views.delete(view.databaseUri);
|
||||
}
|
||||
|
||||
public getView(databaseUri: string): T | undefined {
|
||||
return this.views.get(databaseUri);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
CancellationTokenSource,
|
||||
ExtensionContext,
|
||||
Tab,
|
||||
TabInputWebview,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
window,
|
||||
@@ -16,8 +17,8 @@ import {
|
||||
import { ProgressCallback, withProgress } from "../common/vscode/progress";
|
||||
import { QueryRunner } from "../query-server";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
} from "../common/logging";
|
||||
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
@@ -25,24 +26,24 @@ import { asError, assertNever, getErrorMessage } from "../common/helpers-pure";
|
||||
import { runFlowModelQueries } from "./flow-model-queries";
|
||||
import { promptImportGithubDatabase } from "../databases/database-fetcher";
|
||||
import { App } from "../common/app";
|
||||
import { showResolvableLocation } from "../databases/local-databases/locations";
|
||||
import { redactableError } from "../common/errors";
|
||||
import {
|
||||
externalApiQueriesProgressMaxStep,
|
||||
runExternalApiQueries,
|
||||
} from "./external-api-usage-queries";
|
||||
import { Method, Usage } from "./method";
|
||||
import { Method } from "./method";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { showFlowGeneration, showLlmGeneration } from "../config";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { ModelConfigListener } from "../config";
|
||||
import { INITIAL_MODE, Mode } from "./shared/mode";
|
||||
import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
|
||||
import { join } from "path";
|
||||
import { pickExtensionPack } from "./extension-pack-picker";
|
||||
import { getLanguageDisplayName } from "../common/query-language";
|
||||
import { AutoModeler } from "./auto-modeler";
|
||||
import { INITIAL_HIDE_MODELED_APIS_VALUE } from "./shared/hide-modeled-apis";
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { ModelingStore } from "./modeling-store";
|
||||
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
|
||||
import { convertFromLegacyModeledMethod } from "./shared/modeled-methods-legacy";
|
||||
|
||||
export class ModelEditorView extends AbstractWebview<
|
||||
ToModelEditorMessage,
|
||||
@@ -50,12 +51,11 @@ export class ModelEditorView extends AbstractWebview<
|
||||
> {
|
||||
private readonly autoModeler: AutoModeler;
|
||||
|
||||
private methods: Method[];
|
||||
private hideModeledApis: boolean;
|
||||
|
||||
public constructor(
|
||||
ctx: ExtensionContext,
|
||||
private readonly app: App,
|
||||
protected readonly app: App,
|
||||
private readonly modelingStore: ModelingStore,
|
||||
private readonly viewTracker: ModelEditorViewTracker<ModelEditorView>,
|
||||
private readonly modelConfig: ModelConfigListener,
|
||||
private readonly databaseManager: DatabaseManager,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly queryRunner: QueryRunner,
|
||||
@@ -63,23 +63,15 @@ export class ModelEditorView extends AbstractWebview<
|
||||
private readonly queryDir: string,
|
||||
private readonly databaseItem: DatabaseItem,
|
||||
private readonly extensionPack: ExtensionPack,
|
||||
private mode: Mode,
|
||||
private readonly updateMethodsUsagePanelState: (
|
||||
methods: Method[],
|
||||
databaseItem: DatabaseItem,
|
||||
hideModeledApis: boolean,
|
||||
) => Promise<void>,
|
||||
private readonly showMethod: (
|
||||
method: Method,
|
||||
usage: Usage,
|
||||
) => Promise<void>,
|
||||
private readonly handleViewBecameActive: (view: ModelEditorView) => void,
|
||||
private readonly handleViewWasDisposed: (view: ModelEditorView) => void,
|
||||
private readonly isMostRecentlyActiveView: (
|
||||
view: ModelEditorView,
|
||||
) => boolean,
|
||||
initialMode: Mode = INITIAL_MODE,
|
||||
) {
|
||||
super(ctx);
|
||||
super(app);
|
||||
|
||||
this.modelingStore.initializeStateForDb(databaseItem, initialMode);
|
||||
this.registerToModelingStoreEvents();
|
||||
this.registerToModelConfigEvents();
|
||||
|
||||
this.viewTracker.registerView(this);
|
||||
|
||||
this.autoModeler = new AutoModeler(
|
||||
app,
|
||||
@@ -95,11 +87,9 @@ export class ModelEditorView extends AbstractWebview<
|
||||
});
|
||||
},
|
||||
async (modeledMethods) => {
|
||||
await this.postMessage({ t: "addModeledMethods", modeledMethods });
|
||||
this.addModeledMethods(modeledMethods);
|
||||
},
|
||||
);
|
||||
this.methods = [];
|
||||
this.hideModeledApis = INITIAL_HIDE_MODELED_APIS_VALUE;
|
||||
}
|
||||
|
||||
public async openView() {
|
||||
@@ -108,17 +98,12 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
panel.onDidChangeViewState(async () => {
|
||||
if (panel.active) {
|
||||
this.handleViewBecameActive(this);
|
||||
await this.updateMethodsUsagePanelState(
|
||||
this.methods,
|
||||
this.databaseItem,
|
||||
this.hideModeledApis,
|
||||
);
|
||||
this.modelingStore.setActiveDb(this.databaseItem);
|
||||
}
|
||||
});
|
||||
|
||||
panel.onDidDispose(() => {
|
||||
this.handleViewWasDisposed(this);
|
||||
this.modelingStore.removeDb(this.databaseItem);
|
||||
// onDidDispose is called after the tab has been closed,
|
||||
// so we want to check if there are any others still open.
|
||||
void this.app.commands.execute(
|
||||
@@ -139,15 +124,20 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
private isAModelEditorOpen(): boolean {
|
||||
return window.tabGroups.all.some((tabGroup) =>
|
||||
tabGroup.tabs.some((tab) => {
|
||||
const viewType: string | undefined = (tab.input as any)?.viewType;
|
||||
// The viewType has a prefix, such as "mainThreadWebview-", but if the
|
||||
// suffix matches that should be enough to identify the view.
|
||||
return viewType && viewType.endsWith("model-editor");
|
||||
}),
|
||||
tabGroup.tabs.some((tab) => this.isTabModelEditorView(tab)),
|
||||
);
|
||||
}
|
||||
|
||||
private isTabModelEditorView(tab: Tab): boolean {
|
||||
if (!(tab.input instanceof TabInputWebview)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The viewType has a prefix, such as "mainThreadWebview-", but if the
|
||||
// suffix matches that should be enough to identify the view.
|
||||
return tab.input.viewType.endsWith("model-editor");
|
||||
}
|
||||
|
||||
protected async getPanelConfig(): Promise<WebviewPanelConfig> {
|
||||
return {
|
||||
viewId: "model-editor",
|
||||
@@ -158,18 +148,20 @@ export class ModelEditorView extends AbstractWebview<
|
||||
preserveFocus: true,
|
||||
view: "model-editor",
|
||||
iconPath: {
|
||||
dark: Uri.file(
|
||||
join(this.ctx.extensionPath, "media/dark/symbol-misc.svg"),
|
||||
dark: Uri.joinPath(
|
||||
Uri.file(this.app.extensionPath),
|
||||
"media/dark/symbol-misc.svg",
|
||||
),
|
||||
light: Uri.file(
|
||||
join(this.ctx.extensionPath, "media/light/symbol-misc.svg"),
|
||||
light: Uri.joinPath(
|
||||
Uri.file(this.app.extensionPath),
|
||||
"media/light/symbol-misc.svg",
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected onPanelDispose(): void {
|
||||
// Nothing to do here
|
||||
this.viewTracker.unregisterView(this);
|
||||
}
|
||||
|
||||
protected async onMessage(msg: FromModelEditorMessage): Promise<void> {
|
||||
@@ -193,7 +185,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
break;
|
||||
case "refreshMethods":
|
||||
await withProgress((progress) => this.loadExternalApiUsages(progress), {
|
||||
await withProgress((progress) => this.loadMethods(progress), {
|
||||
cancellable: false,
|
||||
});
|
||||
|
||||
@@ -202,47 +194,67 @@ export class ModelEditorView extends AbstractWebview<
|
||||
);
|
||||
|
||||
break;
|
||||
case "jumpToUsage":
|
||||
await this.handleJumpToUsage(msg.method, msg.usage);
|
||||
void telemetryListener?.sendUIInteraction("model-editor-jump-to-usage");
|
||||
case "jumpToMethod":
|
||||
await this.handleJumpToMethod(msg.methodSignature);
|
||||
void telemetryListener?.sendUIInteraction(
|
||||
"model-editor-jump-to-method",
|
||||
);
|
||||
|
||||
break;
|
||||
case "saveModeledMethods":
|
||||
await withProgress(
|
||||
async (progress) => {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 500 + externalApiQueriesProgressMaxStep,
|
||||
message: "Writing model files",
|
||||
});
|
||||
await saveModeledMethods(
|
||||
this.extensionPack,
|
||||
this.databaseItem.language,
|
||||
msg.methods,
|
||||
msg.modeledMethods,
|
||||
this.mode,
|
||||
this.cliServer,
|
||||
this.app.logger,
|
||||
);
|
||||
{
|
||||
const methods = this.modelingStore.getMethods(
|
||||
this.databaseItem,
|
||||
msg.methodSignatures,
|
||||
);
|
||||
const modeledMethods = this.modelingStore.getModeledMethods(
|
||||
this.databaseItem,
|
||||
msg.methodSignatures,
|
||||
);
|
||||
const mode = this.modelingStore.getMode(this.databaseItem);
|
||||
|
||||
await Promise.all([
|
||||
this.setViewState(),
|
||||
this.loadExternalApiUsages((update) =>
|
||||
progress({
|
||||
...update,
|
||||
step: update.step + 500,
|
||||
maxStep: 500 + externalApiQueriesProgressMaxStep,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
},
|
||||
{
|
||||
cancellable: false,
|
||||
},
|
||||
);
|
||||
void telemetryListener?.sendUIInteraction(
|
||||
"model-editor-save-modeled-methods",
|
||||
);
|
||||
await withProgress(
|
||||
async (progress) => {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 500 + externalApiQueriesProgressMaxStep,
|
||||
message: "Writing model files",
|
||||
});
|
||||
await saveModeledMethods(
|
||||
this.extensionPack,
|
||||
this.databaseItem.language,
|
||||
methods,
|
||||
modeledMethods,
|
||||
mode,
|
||||
this.cliServer,
|
||||
this.app.logger,
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
this.setViewState(),
|
||||
this.loadMethods((update) =>
|
||||
progress({
|
||||
...update,
|
||||
step: update.step + 500,
|
||||
maxStep: 500 + externalApiQueriesProgressMaxStep,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
},
|
||||
{
|
||||
cancellable: false,
|
||||
},
|
||||
);
|
||||
|
||||
this.modelingStore.removeModifiedMethods(
|
||||
this.databaseItem,
|
||||
Object.keys(modeledMethods),
|
||||
);
|
||||
|
||||
void telemetryListener?.sendUIInteraction(
|
||||
"model-editor-save-modeled-methods",
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case "generateMethod":
|
||||
@@ -255,8 +267,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
case "generateMethodsFromLlm":
|
||||
await this.generateModeledMethodsFromLlm(
|
||||
msg.packageName,
|
||||
msg.methods,
|
||||
msg.modeledMethods,
|
||||
msg.methodSignatures,
|
||||
);
|
||||
void telemetryListener?.sendUIInteraction(
|
||||
"model-editor-generate-methods-from-llm",
|
||||
@@ -275,30 +286,47 @@ export class ModelEditorView extends AbstractWebview<
|
||||
);
|
||||
break;
|
||||
case "switchMode":
|
||||
this.mode = msg.mode;
|
||||
this.methods = [];
|
||||
this.modelingStore.setMode(this.databaseItem, msg.mode);
|
||||
this.modelingStore.setMethods(this.databaseItem, []);
|
||||
await Promise.all([
|
||||
this.postMessage({
|
||||
t: "setMethods",
|
||||
methods: this.methods,
|
||||
methods: [],
|
||||
}),
|
||||
this.setViewState(),
|
||||
withProgress((progress) => this.loadExternalApiUsages(progress), {
|
||||
withProgress((progress) => this.loadMethods(progress), {
|
||||
cancellable: false,
|
||||
}),
|
||||
]);
|
||||
void telemetryListener?.sendUIInteraction("model-editor-switch-modes");
|
||||
|
||||
break;
|
||||
case "hideModeledApis":
|
||||
this.hideModeledApis = msg.hideModeledApis;
|
||||
await this.updateMethodsUsagePanelState(
|
||||
this.methods,
|
||||
case "hideModeledMethods":
|
||||
this.modelingStore.setHideModeledMethods(
|
||||
this.databaseItem,
|
||||
this.hideModeledApis,
|
||||
msg.hideModeledMethods,
|
||||
);
|
||||
void telemetryListener?.sendUIInteraction(
|
||||
"model-editor-hide-modeled-apis",
|
||||
"model-editor-hide-modeled-methods",
|
||||
);
|
||||
break;
|
||||
case "setModeledMethod": {
|
||||
this.setModeledMethods(
|
||||
msg.method.signature,
|
||||
convertFromLegacyModeledMethod(msg.method),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
this.app.logger,
|
||||
telemetryListener,
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in model editor view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -311,31 +339,52 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
await Promise.all([
|
||||
this.setViewState(),
|
||||
withProgress((progress) => this.loadExternalApiUsages(progress), {
|
||||
withProgress((progress) => this.loadMethods(progress), {
|
||||
cancellable: false,
|
||||
}),
|
||||
this.loadExistingModeledMethods(),
|
||||
]);
|
||||
}
|
||||
|
||||
public get databaseUri(): string {
|
||||
return this.databaseItem.databaseUri.toString();
|
||||
}
|
||||
|
||||
public async focusView(): Promise<void> {
|
||||
this.panel?.reveal();
|
||||
}
|
||||
|
||||
public async revealMethod(method: Method): Promise<void> {
|
||||
this.panel?.reveal();
|
||||
|
||||
await this.postMessage({
|
||||
t: "revealMethod",
|
||||
methodSignature: method.signature,
|
||||
});
|
||||
}
|
||||
|
||||
private async setViewState(): Promise<void> {
|
||||
const showLlmButton =
|
||||
this.databaseItem.language === "java" && showLlmGeneration();
|
||||
this.databaseItem.language === "java" && this.modelConfig.llmGeneration;
|
||||
|
||||
const sourceArchiveAvailable =
|
||||
this.databaseItem.hasSourceArchiveInExplorer();
|
||||
|
||||
await this.postMessage({
|
||||
t: "setModelEditorViewState",
|
||||
viewState: {
|
||||
extensionPack: this.extensionPack,
|
||||
showFlowGeneration: showFlowGeneration(),
|
||||
showFlowGeneration: this.modelConfig.flowGeneration,
|
||||
showLlmButton,
|
||||
mode: this.mode,
|
||||
showMultipleModels: this.modelConfig.showMultipleModels,
|
||||
mode: this.modelingStore.getMode(this.databaseItem),
|
||||
sourceArchiveAvailable,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleJumpToUsage(method: Method, usage: Usage) {
|
||||
await this.showMethod(method, usage);
|
||||
await showResolvableLocation(usage.url, this.databaseItem, this.app.logger);
|
||||
protected async handleJumpToMethod(methodSignature: string) {
|
||||
this.modelingStore.setSelectedMethod(this.databaseItem, methodSignature);
|
||||
}
|
||||
|
||||
protected async loadExistingModeledMethods(): Promise<void> {
|
||||
@@ -345,10 +394,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
this.cliServer,
|
||||
this.app.logger,
|
||||
);
|
||||
await this.postMessage({
|
||||
t: "loadModeledMethods",
|
||||
modeledMethods,
|
||||
});
|
||||
this.modelingStore.setModeledMethods(this.databaseItem, modeledMethods);
|
||||
} catch (e: unknown) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
@@ -357,12 +403,12 @@ export class ModelEditorView extends AbstractWebview<
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadExternalApiUsages(
|
||||
progress: ProgressCallback,
|
||||
): Promise<void> {
|
||||
protected async loadMethods(progress: ProgressCallback): Promise<void> {
|
||||
const mode = this.modelingStore.getMode(this.databaseItem);
|
||||
|
||||
try {
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
const queryResult = await runExternalApiQueries(this.mode, {
|
||||
const queryResult = await runExternalApiQueries(mode, {
|
||||
cliServer: this.cliServer,
|
||||
queryRunner: this.queryRunner,
|
||||
databaseItem: this.databaseItem,
|
||||
@@ -378,19 +424,8 @@ export class ModelEditorView extends AbstractWebview<
|
||||
if (!queryResult) {
|
||||
return;
|
||||
}
|
||||
this.methods = queryResult;
|
||||
|
||||
await this.postMessage({
|
||||
t: "setMethods",
|
||||
methods: this.methods,
|
||||
});
|
||||
if (this.isMostRecentlyActiveView(this)) {
|
||||
await this.updateMethodsUsagePanelState(
|
||||
this.methods,
|
||||
this.databaseItem,
|
||||
this.hideModeledApis,
|
||||
);
|
||||
}
|
||||
this.modelingStore.setMethods(this.databaseItem, queryResult);
|
||||
} catch (err) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
this.app.logger,
|
||||
@@ -407,11 +442,13 @@ export class ModelEditorView extends AbstractWebview<
|
||||
async (progress) => {
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
|
||||
const mode = this.modelingStore.getMode(this.databaseItem);
|
||||
|
||||
let addedDatabase: DatabaseItem | undefined;
|
||||
|
||||
// In application mode, we need the database of a specific library to generate
|
||||
// the modeled methods. In framework mode, we'll use the current database.
|
||||
if (this.mode === Mode.Application) {
|
||||
if (mode === Mode.Application) {
|
||||
addedDatabase = await this.promptChooseNewOrExistingDatabase(
|
||||
progress,
|
||||
);
|
||||
@@ -433,16 +470,19 @@ export class ModelEditorView extends AbstractWebview<
|
||||
queryStorageDir: this.queryStorageDir,
|
||||
databaseItem: addedDatabase ?? this.databaseItem,
|
||||
onResults: async (modeledMethods) => {
|
||||
const modeledMethodsByName: Record<string, ModeledMethod> = {};
|
||||
const modeledMethodsByName: Record<string, ModeledMethod[]> = {};
|
||||
|
||||
for (const modeledMethod of modeledMethods) {
|
||||
modeledMethodsByName[modeledMethod.signature] = modeledMethod;
|
||||
if (!(modeledMethod.signature in modeledMethodsByName)) {
|
||||
modeledMethodsByName[modeledMethod.signature] = [];
|
||||
}
|
||||
|
||||
modeledMethodsByName[modeledMethod.signature].push(
|
||||
modeledMethod,
|
||||
);
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: "addModeledMethods",
|
||||
modeledMethods: modeledMethodsByName,
|
||||
});
|
||||
this.addModeledMethods(modeledMethodsByName);
|
||||
},
|
||||
progress,
|
||||
token: tokenSource.token,
|
||||
@@ -463,14 +503,22 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
private async generateModeledMethodsFromLlm(
|
||||
packageName: string,
|
||||
methods: Method[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
methodSignatures: string[],
|
||||
): Promise<void> {
|
||||
const methods = this.modelingStore.getMethods(
|
||||
this.databaseItem,
|
||||
methodSignatures,
|
||||
);
|
||||
const modeledMethods = this.modelingStore.getModeledMethods(
|
||||
this.databaseItem,
|
||||
methodSignatures,
|
||||
);
|
||||
const mode = this.modelingStore.getMode(this.databaseItem);
|
||||
await this.autoModeler.startModeling(
|
||||
packageName,
|
||||
methods,
|
||||
modeledMethods,
|
||||
this.mode,
|
||||
mode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -483,9 +531,19 @@ export class ModelEditorView extends AbstractWebview<
|
||||
return;
|
||||
}
|
||||
|
||||
let existingView = this.viewTracker.getView(
|
||||
addedDatabase.databaseUri.toString(),
|
||||
);
|
||||
if (existingView) {
|
||||
await existingView.focusView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
addedDatabase,
|
||||
this.modelConfig,
|
||||
this.app.logger,
|
||||
progress,
|
||||
3,
|
||||
@@ -494,9 +552,22 @@ export class ModelEditorView extends AbstractWebview<
|
||||
return;
|
||||
}
|
||||
|
||||
// Check again just before opening the editor to ensure no model editor has been opened between
|
||||
// our first check and now.
|
||||
existingView = this.viewTracker.getView(
|
||||
addedDatabase.databaseUri.toString(),
|
||||
);
|
||||
if (existingView) {
|
||||
await existingView.focusView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const view = new ModelEditorView(
|
||||
this.ctx,
|
||||
this.app,
|
||||
this.modelingStore,
|
||||
this.viewTracker,
|
||||
this.modelConfig,
|
||||
this.databaseManager,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
@@ -505,11 +576,6 @@ export class ModelEditorView extends AbstractWebview<
|
||||
addedDatabase,
|
||||
modelFile,
|
||||
Mode.Framework,
|
||||
this.updateMethodsUsagePanelState,
|
||||
this.showMethod,
|
||||
this.handleViewBecameActive,
|
||||
this.handleViewWasDisposed,
|
||||
this.isMostRecentlyActiveView,
|
||||
);
|
||||
await view.openView();
|
||||
});
|
||||
@@ -578,6 +644,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
this.cliServer,
|
||||
this.databaseItem.language,
|
||||
makeSelected,
|
||||
false,
|
||||
);
|
||||
if (!addedDatabase) {
|
||||
void this.app.logger.log("No database chosen");
|
||||
@@ -586,4 +653,65 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
return addedDatabase;
|
||||
}
|
||||
|
||||
private registerToModelingStoreEvents() {
|
||||
this.push(
|
||||
this.modelingStore.onMethodsChanged(async (event) => {
|
||||
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
|
||||
await this.postMessage({
|
||||
t: "setMethods",
|
||||
methods: event.methods,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onModeledMethodsChanged(async (event) => {
|
||||
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
|
||||
await this.postMessage({
|
||||
t: "setModeledMethods",
|
||||
methods: event.modeledMethods,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onModifiedMethodsChanged(async (event) => {
|
||||
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
|
||||
await this.postMessage({
|
||||
t: "setModifiedMethods",
|
||||
methodSignatures: [...event.modifiedMethods],
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private registerToModelConfigEvents() {
|
||||
this.push(
|
||||
this.modelConfig.onDidChangeConfiguration(() => {
|
||||
void this.setViewState();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private addModeledMethods(modeledMethods: Record<string, ModeledMethod[]>) {
|
||||
this.modelingStore.addModeledMethods(this.databaseItem, modeledMethods);
|
||||
|
||||
this.modelingStore.addModifiedMethods(
|
||||
this.databaseItem,
|
||||
new Set(Object.keys(modeledMethods)),
|
||||
);
|
||||
}
|
||||
|
||||
private setModeledMethods(signature: string, methods: ModeledMethod[]) {
|
||||
this.modelingStore.updateModeledMethods(
|
||||
this.databaseItem,
|
||||
signature,
|
||||
methods,
|
||||
);
|
||||
this.modelingStore.addModifiedMethod(this.databaseItem, signature);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$ref": "#/definitions/ModelExtensionFile",
|
||||
"definitions": {
|
||||
"ModelExtensionFile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addsTo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pack": {
|
||||
"type": "string"
|
||||
},
|
||||
"extensible": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["pack", "extensible"]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/DataTuple"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["addsTo", "data"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["extensions"]
|
||||
},
|
||||
"DataTuple": {
|
||||
"type": ["boolean", "number", "string"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
type ExtensibleReference = {
|
||||
pack: string;
|
||||
extensible: string;
|
||||
};
|
||||
|
||||
export type DataTuple = boolean | number | string;
|
||||
|
||||
type DataRow = DataTuple[];
|
||||
|
||||
type ModelExtension = {
|
||||
addsTo: ExtensibleReference;
|
||||
data: DataRow[];
|
||||
};
|
||||
|
||||
export type ModelExtensionFile = {
|
||||
extensions: ModelExtension[];
|
||||
};
|
||||
@@ -15,7 +15,7 @@ export async function saveModeledMethods(
|
||||
extensionPack: ExtensionPack,
|
||||
language: string,
|
||||
methods: Method[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
mode: Mode,
|
||||
cliServer: CodeQLCliServer,
|
||||
logger: NotificationLogger,
|
||||
@@ -45,12 +45,12 @@ async function loadModeledMethodFiles(
|
||||
extensionPack: ExtensionPack,
|
||||
cliServer: CodeQLCliServer,
|
||||
logger: NotificationLogger,
|
||||
): Promise<Record<string, Record<string, ModeledMethod>>> {
|
||||
): Promise<Record<string, Record<string, ModeledMethod[]>>> {
|
||||
const modelFiles = await listModelFiles(extensionPack.path, cliServer);
|
||||
|
||||
const modeledMethodsByFile: Record<
|
||||
string,
|
||||
Record<string, ModeledMethod>
|
||||
Record<string, ModeledMethod[]>
|
||||
> = {};
|
||||
|
||||
for (const modelFile of modelFiles) {
|
||||
@@ -78,8 +78,8 @@ export async function loadModeledMethods(
|
||||
extensionPack: ExtensionPack,
|
||||
cliServer: CodeQLCliServer,
|
||||
logger: NotificationLogger,
|
||||
): Promise<Record<string, ModeledMethod>> {
|
||||
const existingModeledMethods: Record<string, ModeledMethod> = {};
|
||||
): Promise<Record<string, ModeledMethod[]>> {
|
||||
const existingModeledMethods: Record<string, ModeledMethod[]> = {};
|
||||
|
||||
const modeledMethodsByFile = await loadModeledMethodFiles(
|
||||
extensionPack,
|
||||
@@ -88,7 +88,11 @@ export async function loadModeledMethods(
|
||||
);
|
||||
for (const modeledMethods of Object.values(modeledMethodsByFile)) {
|
||||
for (const [key, value] of Object.entries(modeledMethods)) {
|
||||
existingModeledMethods[key] = value;
|
||||
if (!(key in existingModeledMethods)) {
|
||||
existingModeledMethods[key] = [];
|
||||
}
|
||||
|
||||
existingModeledMethods[key].push(...value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface ModeledMethod extends MethodSignature {
|
||||
type: ModeledMethodType;
|
||||
input: string;
|
||||
output: string;
|
||||
kind: string;
|
||||
kind: ModeledMethodKind;
|
||||
provenance: Provenance;
|
||||
}
|
||||
|
||||
export type ModeledMethodKind = string;
|
||||
|
||||
433
extensions/ql-vscode/src/model-editor/modeling-store.ts
Normal file
433
extensions/ql-vscode/src/model-editor/modeling-store.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import { App } from "../common/app";
|
||||
import { DisposableObject } from "../common/disposable-object";
|
||||
import { AppEvent, AppEventEmitter } from "../common/events";
|
||||
import { DatabaseItem } from "../databases/local-databases";
|
||||
import { Method, Usage } from "./method";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "./shared/hide-modeled-methods";
|
||||
import { INITIAL_MODE, Mode } from "./shared/mode";
|
||||
|
||||
interface DbModelingState {
|
||||
databaseItem: DatabaseItem;
|
||||
methods: Method[];
|
||||
hideModeledMethods: boolean;
|
||||
mode: Mode;
|
||||
modeledMethods: Record<string, ModeledMethod[]>;
|
||||
modifiedMethodSignatures: Set<string>;
|
||||
selectedMethod: Method | undefined;
|
||||
selectedUsage: Usage | undefined;
|
||||
}
|
||||
|
||||
interface MethodsChangedEvent {
|
||||
methods: Method[];
|
||||
dbUri: string;
|
||||
isActiveDb: boolean;
|
||||
}
|
||||
|
||||
interface HideModeledMethodsChangedEvent {
|
||||
hideModeledMethods: boolean;
|
||||
isActiveDb: boolean;
|
||||
}
|
||||
|
||||
interface ModeChangedEvent {
|
||||
mode: Mode;
|
||||
isActiveDb: boolean;
|
||||
}
|
||||
|
||||
interface ModeledMethodsChangedEvent {
|
||||
modeledMethods: Record<string, ModeledMethod[]>;
|
||||
dbUri: string;
|
||||
isActiveDb: boolean;
|
||||
}
|
||||
|
||||
interface ModifiedMethodsChangedEvent {
|
||||
modifiedMethods: Set<string>;
|
||||
dbUri: string;
|
||||
isActiveDb: boolean;
|
||||
}
|
||||
|
||||
interface SelectedMethodChangedEvent {
|
||||
databaseItem: DatabaseItem;
|
||||
method: Method;
|
||||
usage: Usage;
|
||||
modeledMethods: ModeledMethod[];
|
||||
isModified: boolean;
|
||||
}
|
||||
|
||||
export class ModelingStore extends DisposableObject {
|
||||
public readonly onActiveDbChanged: AppEvent<void>;
|
||||
public readonly onDbOpened: AppEvent<string>;
|
||||
public readonly onDbClosed: AppEvent<string>;
|
||||
public readonly onMethodsChanged: AppEvent<MethodsChangedEvent>;
|
||||
public readonly onHideModeledMethodsChanged: AppEvent<HideModeledMethodsChangedEvent>;
|
||||
public readonly onModeChanged: AppEvent<ModeChangedEvent>;
|
||||
public readonly onModeledMethodsChanged: AppEvent<ModeledMethodsChangedEvent>;
|
||||
public readonly onModifiedMethodsChanged: AppEvent<ModifiedMethodsChangedEvent>;
|
||||
public readonly onSelectedMethodChanged: AppEvent<SelectedMethodChangedEvent>;
|
||||
|
||||
private readonly state: Map<string, DbModelingState>;
|
||||
private activeDb: string | undefined;
|
||||
|
||||
private readonly onActiveDbChangedEventEmitter: AppEventEmitter<void>;
|
||||
private readonly onDbOpenedEventEmitter: AppEventEmitter<string>;
|
||||
private readonly onDbClosedEventEmitter: AppEventEmitter<string>;
|
||||
private readonly onMethodsChangedEventEmitter: AppEventEmitter<MethodsChangedEvent>;
|
||||
private readonly onHideModeledMethodsChangedEventEmitter: AppEventEmitter<HideModeledMethodsChangedEvent>;
|
||||
private readonly onModeChangedEventEmitter: AppEventEmitter<ModeChangedEvent>;
|
||||
private readonly onModeledMethodsChangedEventEmitter: AppEventEmitter<ModeledMethodsChangedEvent>;
|
||||
private readonly onModifiedMethodsChangedEventEmitter: AppEventEmitter<ModifiedMethodsChangedEvent>;
|
||||
private readonly onSelectedMethodChangedEventEmitter: AppEventEmitter<SelectedMethodChangedEvent>;
|
||||
|
||||
constructor(app: App) {
|
||||
super();
|
||||
|
||||
// State initialization
|
||||
this.state = new Map<string, DbModelingState>();
|
||||
|
||||
// Event initialization
|
||||
this.onActiveDbChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<void>(),
|
||||
);
|
||||
this.onActiveDbChanged = this.onActiveDbChangedEventEmitter.event;
|
||||
|
||||
this.onDbOpenedEventEmitter = this.push(app.createEventEmitter<string>());
|
||||
this.onDbOpened = this.onDbOpenedEventEmitter.event;
|
||||
|
||||
this.onDbClosedEventEmitter = this.push(app.createEventEmitter<string>());
|
||||
this.onDbClosed = this.onDbClosedEventEmitter.event;
|
||||
|
||||
this.onMethodsChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<MethodsChangedEvent>(),
|
||||
);
|
||||
this.onMethodsChanged = this.onMethodsChangedEventEmitter.event;
|
||||
|
||||
this.onHideModeledMethodsChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<HideModeledMethodsChangedEvent>(),
|
||||
);
|
||||
this.onHideModeledMethodsChanged =
|
||||
this.onHideModeledMethodsChangedEventEmitter.event;
|
||||
|
||||
this.onModeChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<ModeChangedEvent>(),
|
||||
);
|
||||
this.onModeChanged = this.onModeChangedEventEmitter.event;
|
||||
|
||||
this.onModeledMethodsChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<ModeledMethodsChangedEvent>(),
|
||||
);
|
||||
this.onModeledMethodsChanged =
|
||||
this.onModeledMethodsChangedEventEmitter.event;
|
||||
|
||||
this.onModifiedMethodsChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<ModifiedMethodsChangedEvent>(),
|
||||
);
|
||||
this.onModifiedMethodsChanged =
|
||||
this.onModifiedMethodsChangedEventEmitter.event;
|
||||
|
||||
this.onSelectedMethodChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<SelectedMethodChangedEvent>(),
|
||||
);
|
||||
this.onSelectedMethodChanged =
|
||||
this.onSelectedMethodChangedEventEmitter.event;
|
||||
}
|
||||
|
||||
public initializeStateForDb(
|
||||
databaseItem: DatabaseItem,
|
||||
mode: Mode = INITIAL_MODE,
|
||||
) {
|
||||
const dbUri = databaseItem.databaseUri.toString();
|
||||
this.state.set(dbUri, {
|
||||
databaseItem,
|
||||
methods: [],
|
||||
hideModeledMethods: INITIAL_HIDE_MODELED_METHODS_VALUE,
|
||||
mode,
|
||||
modeledMethods: {},
|
||||
modifiedMethodSignatures: new Set(),
|
||||
selectedMethod: undefined,
|
||||
selectedUsage: undefined,
|
||||
});
|
||||
|
||||
this.onDbOpenedEventEmitter.fire(dbUri);
|
||||
}
|
||||
|
||||
public setActiveDb(databaseItem: DatabaseItem) {
|
||||
this.activeDb = databaseItem.databaseUri.toString();
|
||||
this.onActiveDbChangedEventEmitter.fire();
|
||||
}
|
||||
|
||||
public removeDb(databaseItem: DatabaseItem) {
|
||||
const dbUri = databaseItem.databaseUri.toString();
|
||||
|
||||
if (!this.state.has(dbUri)) {
|
||||
throw Error("Cannot remove a database that has not been initialized");
|
||||
}
|
||||
|
||||
if (this.activeDb === dbUri) {
|
||||
this.activeDb = undefined;
|
||||
this.onActiveDbChangedEventEmitter.fire();
|
||||
}
|
||||
|
||||
this.state.delete(dbUri);
|
||||
this.onDbClosedEventEmitter.fire(dbUri);
|
||||
}
|
||||
|
||||
public getStateForActiveDb(): DbModelingState | undefined {
|
||||
if (!this.activeDb) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.state.get(this.activeDb);
|
||||
}
|
||||
|
||||
public hasStateForActiveDb(): boolean {
|
||||
return !!this.getStateForActiveDb();
|
||||
}
|
||||
|
||||
public anyDbsBeingModeled(): boolean {
|
||||
return this.state.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the methods for the given database item and method signatures.
|
||||
* If the `methodSignatures` argument is not provided or is undefined, returns all methods.
|
||||
*/
|
||||
public getMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methodSignatures?: string[],
|
||||
): Method[] {
|
||||
const methods = this.getState(dbItem).methods;
|
||||
if (!methodSignatures) {
|
||||
return methods;
|
||||
}
|
||||
return methods.filter((method) =>
|
||||
methodSignatures.includes(method.signature),
|
||||
);
|
||||
}
|
||||
|
||||
public setMethods(dbItem: DatabaseItem, methods: Method[]) {
|
||||
const dbState = this.getState(dbItem);
|
||||
const dbUri = dbItem.databaseUri.toString();
|
||||
|
||||
dbState.methods = [...methods];
|
||||
|
||||
this.onMethodsChangedEventEmitter.fire({
|
||||
methods,
|
||||
dbUri,
|
||||
isActiveDb: dbUri === this.activeDb,
|
||||
});
|
||||
}
|
||||
|
||||
public setHideModeledMethods(
|
||||
dbItem: DatabaseItem,
|
||||
hideModeledMethods: boolean,
|
||||
) {
|
||||
const dbState = this.getState(dbItem);
|
||||
const dbUri = dbItem.databaseUri.toString();
|
||||
|
||||
dbState.hideModeledMethods = hideModeledMethods;
|
||||
|
||||
this.onHideModeledMethodsChangedEventEmitter.fire({
|
||||
hideModeledMethods,
|
||||
isActiveDb: dbUri === this.activeDb,
|
||||
});
|
||||
}
|
||||
|
||||
public setMode(dbItem: DatabaseItem, mode: Mode) {
|
||||
const dbState = this.getState(dbItem);
|
||||
const dbUri = dbItem.databaseUri.toString();
|
||||
|
||||
dbState.mode = mode;
|
||||
|
||||
this.onModeChangedEventEmitter.fire({
|
||||
mode,
|
||||
isActiveDb: dbUri === this.activeDb,
|
||||
});
|
||||
}
|
||||
|
||||
public getMode(dbItem: DatabaseItem) {
|
||||
return this.getState(dbItem).mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modeled methods for the given database item and method signatures.
|
||||
* If the `methodSignatures` argument is not provided or is undefined, returns all modeled methods.
|
||||
*/
|
||||
public getModeledMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methodSignatures?: string[],
|
||||
): Record<string, ModeledMethod[]> {
|
||||
const modeledMethods = this.getState(dbItem).modeledMethods;
|
||||
if (!methodSignatures) {
|
||||
return modeledMethods;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(modeledMethods).filter(([key]) =>
|
||||
methodSignatures.includes(key),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public addModeledMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methods: Record<string, ModeledMethod[]>,
|
||||
) {
|
||||
this.changeModeledMethods(dbItem, (state) => {
|
||||
const newModeledMethods = {
|
||||
...methods,
|
||||
// Keep all methods that are already modeled in some form in the state
|
||||
...Object.fromEntries(
|
||||
Object.entries(state.modeledMethods).filter(([_, value]) =>
|
||||
value.some((m) => m.type !== "none"),
|
||||
),
|
||||
),
|
||||
};
|
||||
state.modeledMethods = newModeledMethods;
|
||||
});
|
||||
}
|
||||
|
||||
public setModeledMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methods: Record<string, ModeledMethod[]>,
|
||||
) {
|
||||
this.changeModeledMethods(dbItem, (state) => {
|
||||
state.modeledMethods = { ...methods };
|
||||
});
|
||||
}
|
||||
|
||||
public updateModeledMethods(
|
||||
dbItem: DatabaseItem,
|
||||
signature: string,
|
||||
modeledMethods: ModeledMethod[],
|
||||
) {
|
||||
this.changeModeledMethods(dbItem, (state) => {
|
||||
const newModeledMethods = { ...state.modeledMethods };
|
||||
newModeledMethods[signature] = modeledMethods;
|
||||
state.modeledMethods = newModeledMethods;
|
||||
});
|
||||
}
|
||||
|
||||
public setModifiedMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methodSignatures: Set<string>,
|
||||
) {
|
||||
this.changeModifiedMethods(dbItem, (state) => {
|
||||
state.modifiedMethodSignatures = new Set(methodSignatures);
|
||||
});
|
||||
}
|
||||
|
||||
public addModifiedMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methodSignatures: Iterable<string>,
|
||||
) {
|
||||
this.changeModifiedMethods(dbItem, (state) => {
|
||||
const newModifiedMethods = new Set([
|
||||
...state.modifiedMethodSignatures,
|
||||
...methodSignatures,
|
||||
]);
|
||||
state.modifiedMethodSignatures = newModifiedMethods;
|
||||
});
|
||||
}
|
||||
|
||||
public addModifiedMethod(dbItem: DatabaseItem, methodSignature: string) {
|
||||
this.addModifiedMethods(dbItem, [methodSignature]);
|
||||
}
|
||||
|
||||
public removeModifiedMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methodSignatures: string[],
|
||||
) {
|
||||
this.changeModifiedMethods(dbItem, (state) => {
|
||||
const newModifiedMethods = Array.from(
|
||||
state.modifiedMethodSignatures,
|
||||
).filter((s) => !methodSignatures.includes(s));
|
||||
|
||||
state.modifiedMethodSignatures = new Set(newModifiedMethods);
|
||||
});
|
||||
}
|
||||
|
||||
public setSelectedMethod(dbItem: DatabaseItem, methodSignature: string) {
|
||||
const dbState = this.getState(dbItem);
|
||||
|
||||
const method = dbState.methods.find((m) => m.signature === methodSignature);
|
||||
if (method === undefined) {
|
||||
throw new Error(
|
||||
`No method with signature "${methodSignature}" found in modeling store`,
|
||||
);
|
||||
}
|
||||
|
||||
const usage = method.usages[0];
|
||||
|
||||
dbState.selectedMethod = method;
|
||||
dbState.selectedUsage = usage;
|
||||
|
||||
this.onSelectedMethodChangedEventEmitter.fire({
|
||||
databaseItem: dbItem,
|
||||
method,
|
||||
usage,
|
||||
modeledMethods: dbState.modeledMethods[method.signature] ?? [],
|
||||
isModified: dbState.modifiedMethodSignatures.has(method.signature),
|
||||
});
|
||||
}
|
||||
|
||||
public getSelectedMethodDetails() {
|
||||
const dbState = this.getStateForActiveDb();
|
||||
if (!dbState) {
|
||||
throw new Error("No active state found in modeling store");
|
||||
}
|
||||
|
||||
const selectedMethod = dbState.selectedMethod;
|
||||
if (!selectedMethod) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
databaseItem: dbState.databaseItem,
|
||||
method: selectedMethod,
|
||||
usage: dbState.selectedUsage,
|
||||
modeledMethods: dbState.modeledMethods[selectedMethod.signature] ?? [],
|
||||
isModified: dbState.modifiedMethodSignatures.has(
|
||||
selectedMethod.signature,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private getState(databaseItem: DatabaseItem): DbModelingState {
|
||||
if (!this.state.has(databaseItem.databaseUri.toString())) {
|
||||
throw Error(
|
||||
"Cannot get state for a database that has not been initialized",
|
||||
);
|
||||
}
|
||||
|
||||
return this.state.get(databaseItem.databaseUri.toString())!;
|
||||
}
|
||||
|
||||
private changeModifiedMethods(
|
||||
dbItem: DatabaseItem,
|
||||
updateState: (state: DbModelingState) => void,
|
||||
) {
|
||||
const state = this.getState(dbItem);
|
||||
|
||||
updateState(state);
|
||||
|
||||
this.onModifiedMethodsChangedEventEmitter.fire({
|
||||
modifiedMethods: state.modifiedMethodSignatures,
|
||||
dbUri: dbItem.databaseUri.toString(),
|
||||
isActiveDb: dbItem.databaseUri.toString() === this.activeDb,
|
||||
});
|
||||
}
|
||||
|
||||
private changeModeledMethods(
|
||||
dbItem: DatabaseItem,
|
||||
updateState: (state: DbModelingState) => void,
|
||||
) {
|
||||
const state = this.getState(dbItem);
|
||||
|
||||
updateState(state);
|
||||
|
||||
this.onModeledMethodsChangedEventEmitter.fire({
|
||||
modeledMethods: state.modeledMethods,
|
||||
dbUri: dbItem.databaseUri.toString(),
|
||||
isActiveDb: dbItem.databaseUri.toString() === this.activeDb,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { ModeledMethod, ModeledMethodType, Provenance } from "./modeled-method";
|
||||
import { DataTuple } from "./model-extension-file";
|
||||
|
||||
export type ExtensiblePredicateDefinition = {
|
||||
extensiblePredicate: string;
|
||||
generateMethodDefinition: (method: ModeledMethod) => Tuple[];
|
||||
readModeledMethod: (row: Tuple[]) => ModeledMethod;
|
||||
generateMethodDefinition: (method: ModeledMethod) => DataTuple[];
|
||||
readModeledMethod: (row: DataTuple[]) => ModeledMethod;
|
||||
|
||||
supportedKinds?: string[];
|
||||
};
|
||||
|
||||
type Tuple = boolean | number | string;
|
||||
|
||||
function readRowToMethod(row: Tuple[]): string {
|
||||
function readRowToMethod(row: DataTuple[]): string {
|
||||
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,130 +2,151 @@ import { Query } from "./query";
|
||||
|
||||
export const fetchExternalApisQuery: Query = {
|
||||
applicationModeQuery: `/**
|
||||
* @name Usage of APIs coming from external libraries
|
||||
* @description A list of 3rd party APIs used in the codebase.
|
||||
* @tags telemetry
|
||||
* @kind problem
|
||||
* @id cs/telemetry/fetch-external-apis
|
||||
* @name Fetch endpoints for use in the model editor (application mode)
|
||||
* @description A list of 3rd party endpoints (methods and attributes) used in the codebase. Excludes test and generated code.
|
||||
* @kind table
|
||||
* @id csharp/utils/modeleditor/application-mode-endpoints
|
||||
* @tags modeleditor endpoints application-mode
|
||||
*/
|
||||
|
||||
private import csharp
|
||||
private import AutomodelVsCode
|
||||
import csharp
|
||||
import ApplicationModeEndpointsQuery
|
||||
import ModelEditor
|
||||
|
||||
class ExternalApi extends CallableMethod {
|
||||
ExternalApi() {
|
||||
this.isUnboundDeclaration() and
|
||||
this.fromLibrary() and
|
||||
this.(Modifiable).isEffectivelyPublic()
|
||||
}
|
||||
}
|
||||
private Call aUsage(ExternalEndpoint api) { result.getTarget().getUnboundDeclaration() = api }
|
||||
|
||||
private Call aUsage(ExternalApi api) { result.getTarget().getUnboundDeclaration() = api }
|
||||
|
||||
from
|
||||
ExternalApi api, string apiName, boolean supported, Call usage, string type, string classification
|
||||
from ExternalEndpoint endpoint, boolean supported, Call usage, string type, string classification
|
||||
where
|
||||
apiName = api.getApiName() and
|
||||
supported = isSupported(api) and
|
||||
usage = aUsage(api) and
|
||||
type = supportedType(api) and
|
||||
supported = isSupported(endpoint) and
|
||||
usage = aUsage(endpoint) and
|
||||
type = supportedType(endpoint) and
|
||||
classification = methodClassification(usage)
|
||||
select usage, apiName, supported.toString(), "supported", api.dllName(), api.dllVersion(), type,
|
||||
"type", classification, "classification"
|
||||
select usage, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
|
||||
endpoint.getParameterTypes(), supported, endpoint.dllName(), endpoint.dllVersion(), type,
|
||||
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
|
||||
* @name Fetch endpoints for use in the model editor (framework mode)
|
||||
* @description A list of endpoints accessible (methods and attributes) for consumers of the library. Excludes test and generated code.
|
||||
* @kind table
|
||||
* @id csharp/utils/modeleditor/framework-mode-endpoints
|
||||
* @tags modeleditor endpoints framework-mode
|
||||
*/
|
||||
|
||||
private import csharp
|
||||
private import dotnet
|
||||
private import semmle.code.csharp.frameworks.Test
|
||||
private import AutomodelVsCode
|
||||
import csharp
|
||||
import FrameworkModeEndpointsQuery
|
||||
import ModelEditor
|
||||
|
||||
class PublicMethod extends CallableMethod {
|
||||
PublicMethod() { this.fromSource() and not this.getFile() instanceof TestFile }
|
||||
}
|
||||
|
||||
from PublicMethod publicMethod, string apiName, boolean supported, string type
|
||||
from PublicEndpointFromSource endpoint, 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"
|
||||
supported = isSupported(endpoint) and
|
||||
type = supportedType(endpoint)
|
||||
select endpoint, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
|
||||
endpoint.getParameterTypes(), supported, endpoint.getFile().getBaseName(), type
|
||||
`,
|
||||
dependencies: {
|
||||
"AutomodelVsCode.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
|
||||
|
||||
private import csharp
|
||||
private import dotnet
|
||||
private import semmle.code.csharp.dispatch.Dispatch
|
||||
private import semmle.code.csharp.dataflow.ExternalFlow
|
||||
private import semmle.code.csharp.dataflow.FlowSummary
|
||||
private import semmle.code.csharp.dataflow.internal.DataFlowImplCommon as DataFlowImplCommon
|
||||
private import semmle.code.csharp.dataflow.internal.DataFlowPrivate
|
||||
"ApplicationModeEndpointsQuery.qll": `private import csharp
|
||||
private import semmle.code.csharp.dataflow.ExternalFlow as ExternalFlow
|
||||
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.DataFlowPrivate
|
||||
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]
|
||||
private predicate isTestNamespace(Namespace ns) {
|
||||
ns.getFullName()
|
||||
.matches([
|
||||
"NUnit.Framework%", "Xunit%", "Microsoft.VisualStudio.TestTools.UnitTesting%", "Moq%"
|
||||
])
|
||||
}
|
||||
private import ModelEditor
|
||||
|
||||
/**
|
||||
* A test library.
|
||||
* A class of effectively public callables in library code.
|
||||
*/
|
||||
class TestLibrary extends RefType {
|
||||
TestLibrary() { isTestNamespace(this.getNamespace()) }
|
||||
class ExternalEndpoint extends Endpoint {
|
||||
ExternalEndpoint() { this.fromLibrary() }
|
||||
|
||||
/** Gets a node that is an input to a call to this API. */
|
||||
private ArgumentNode getAnInput() {
|
||||
result
|
||||
.getCall()
|
||||
.(DataFlowDispatch::NonDelegateDataFlowCall)
|
||||
.getATarget(_)
|
||||
.getUnboundDeclaration() = this
|
||||
}
|
||||
|
||||
/** Gets a node that is an output from a call to this API. */
|
||||
private DataFlow::Node getAnOutput() {
|
||||
exists(Call c, DataFlowDispatch::NonDelegateDataFlowCall dc |
|
||||
dc.getDispatchCall().getCall() = c and
|
||||
c.getTarget().getUnboundDeclaration() = this
|
||||
|
|
||||
result = DataFlowDispatch::getAnOutNode(dc, _)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate hasSummary() {
|
||||
Endpoint.super.hasSummary()
|
||||
or
|
||||
defaultAdditionalTaintStep(this.getAnInput(), _)
|
||||
}
|
||||
|
||||
override predicate isSource() {
|
||||
this.getAnOutput() instanceof RemoteFlowSource or ExternalFlow::sourceNode(this.getAnOutput(), _)
|
||||
}
|
||||
|
||||
override predicate isSink() { ExternalFlow::sinkNode(this.getAnInput(), _) }
|
||||
}
|
||||
`,
|
||||
"FrameworkModeEndpointsQuery.qll": `private import csharp
|
||||
private import semmle.code.csharp.frameworks.Test
|
||||
private import ModelEditor
|
||||
|
||||
/**
|
||||
* A class of effectively public callables from source code.
|
||||
*/
|
||||
class PublicEndpointFromSource extends Endpoint {
|
||||
PublicEndpointFromSource() { this.fromSource() and not this.getFile() instanceof TestFile }
|
||||
|
||||
override predicate isSource() { this instanceof SourceCallable }
|
||||
|
||||
override predicate isSink() { this instanceof SinkCallable }
|
||||
}`,
|
||||
"ModelEditor.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
|
||||
|
||||
private import csharp
|
||||
private import semmle.code.csharp.dataflow.FlowSummary
|
||||
private import semmle.code.csharp.dataflow.internal.DataFlowPrivate
|
||||
private import semmle.code.csharp.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
|
||||
private import semmle.code.csharp.frameworks.Test
|
||||
|
||||
/** Holds if the given callable is not worth supporting. */
|
||||
private predicate isUninteresting(DotNet::Declaration c) {
|
||||
private predicate isUninteresting(Callable c) {
|
||||
c.getDeclaringType() instanceof TestLibrary or
|
||||
c.(Constructor).isParameterless() or
|
||||
c.getDeclaringType() instanceof AnonymousClass
|
||||
}
|
||||
|
||||
/**
|
||||
* An callable method from either the C# Standard Library, a 3rd party library, or from the source.
|
||||
* A callable method or accessor from either the C# Standard Library, a 3rd party library, or from the source.
|
||||
*/
|
||||
class CallableMethod extends DotNet::Declaration {
|
||||
CallableMethod() {
|
||||
this.(Modifiable).isEffectivelyPublic() and
|
||||
not isUninteresting(this)
|
||||
class Endpoint extends Callable {
|
||||
Endpoint() {
|
||||
[this.(Modifiable), this.(Accessor).getDeclaration()].isEffectivelyPublic() and
|
||||
not isUninteresting(this) and
|
||||
this.isUnboundDeclaration()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Gets the namespace of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getNamespace() { this.getDeclaringType().hasQualifiedName(result, _) }
|
||||
|
||||
/**
|
||||
* Gets the namespace and signature of this API.
|
||||
* Gets the unbound type name of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getApiName() { result = this.getNamespace() + "." + this.getSignature() }
|
||||
string getTypeName() { result = nestedName(this.getDeclaringType().getUnboundDeclaration()) }
|
||||
|
||||
/**
|
||||
* Gets the parameter types of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getParameterTypes() { result = "(" + parameterQualifiedTypeNamesToString(this) + ")" }
|
||||
|
||||
private string getDllName() { result = this.getLocation().(Assembly).getName() }
|
||||
|
||||
@@ -143,44 +164,17 @@ class CallableMethod extends DotNet::Declaration {
|
||||
not exists(this.getDllVersion()) and result = ""
|
||||
}
|
||||
|
||||
/** Gets a node that is an input to a call to this API. */
|
||||
private ArgumentNode getAnInput() {
|
||||
result
|
||||
.getCall()
|
||||
.(DataFlowDispatch::NonDelegateDataFlowCall)
|
||||
.getATarget(_)
|
||||
.getUnboundDeclaration() = this
|
||||
}
|
||||
|
||||
/** Gets a node that is an output from a call to this API. */
|
||||
private DataFlow::Node getAnOutput() {
|
||||
exists(
|
||||
Call c, DataFlowDispatch::NonDelegateDataFlowCall dc, DataFlowImplCommon::ReturnKindExt ret
|
||||
|
|
||||
dc.getDispatchCall().getCall() = c and
|
||||
c.getTarget().getUnboundDeclaration() = this
|
||||
|
|
||||
result = ret.getAnOutNode(dc)
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if this API has a supported summary. */
|
||||
pragma[nomagic]
|
||||
predicate hasSummary() {
|
||||
this instanceof SummarizedCallable
|
||||
or
|
||||
defaultAdditionalTaintStep(this.getAnInput(), _)
|
||||
}
|
||||
predicate hasSummary() { this instanceof SummarizedCallable }
|
||||
|
||||
/** Holds if this API is a known source. */
|
||||
pragma[nomagic]
|
||||
predicate isSource() {
|
||||
this.getAnOutput() instanceof RemoteFlowSource or sourceNode(this.getAnOutput(), _)
|
||||
}
|
||||
abstract predicate isSource();
|
||||
|
||||
/** Holds if this API is a known sink. */
|
||||
pragma[nomagic]
|
||||
predicate isSink() { sinkNode(this.getAnInput(), _) }
|
||||
abstract predicate isSink();
|
||||
|
||||
/** Holds if this API is a known neutral. */
|
||||
pragma[nomagic]
|
||||
@@ -195,23 +189,20 @@ class CallableMethod extends DotNet::Declaration {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSupported(CallableMethod callableMethod) {
|
||||
callableMethod.isSupported() and result = true
|
||||
or
|
||||
not callableMethod.isSupported() and
|
||||
result = false
|
||||
boolean isSupported(Endpoint endpoint) {
|
||||
if endpoint.isSupported() then result = true else result = false
|
||||
}
|
||||
|
||||
string supportedType(CallableMethod method) {
|
||||
method.isSink() and result = "sink"
|
||||
string supportedType(Endpoint endpoint) {
|
||||
endpoint.isSink() and result = "sink"
|
||||
or
|
||||
method.isSource() and result = "source"
|
||||
endpoint.isSource() and result = "source"
|
||||
or
|
||||
method.hasSummary() and result = "summary"
|
||||
endpoint.hasSummary() and result = "summary"
|
||||
or
|
||||
method.isNeutral() and result = "neutral"
|
||||
endpoint.isNeutral() and result = "neutral"
|
||||
or
|
||||
not method.isSupported() and result = ""
|
||||
not endpoint.isSupported() and result = ""
|
||||
}
|
||||
|
||||
string methodClassification(Call method) {
|
||||
@@ -222,18 +213,51 @@ string methodClassification(Call method) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the nested name of the declaration.
|
||||
* Gets the nested name of the type \`t\`.
|
||||
*
|
||||
* If the declaration is not a nested type, the result is the same as \`getName()\`.
|
||||
* If the type 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.
|
||||
*/
|
||||
private string nestedName(Declaration declaration) {
|
||||
not exists(declaration.getDeclaringType().getUnboundDeclaration()) and
|
||||
result = declaration.getName()
|
||||
private string nestedName(Type t) {
|
||||
not exists(t.getDeclaringType().getUnboundDeclaration()) and
|
||||
result = t.getName()
|
||||
or
|
||||
nestedName(declaration.getDeclaringType().getUnboundDeclaration()) + "+" + declaration.getName() =
|
||||
result
|
||||
nestedName(t.getDeclaringType().getUnboundDeclaration()) + "+" + t.getName() = result
|
||||
}
|
||||
|
||||
// Temporary copy of csharp/ql/src/Telemetry/TestLibrary.qll
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate isTestNamespace(Namespace ns) {
|
||||
ns.getFullName()
|
||||
.matches([
|
||||
"NUnit.Framework%", "Xunit%", "Microsoft.VisualStudio.TestTools.UnitTesting%", "Moq%"
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* A test library.
|
||||
*/
|
||||
class TestLibrary extends RefType {
|
||||
TestLibrary() { isTestNamespace(this.getNamespace()) }
|
||||
}
|
||||
|
||||
// Temporary copy of csharp/ql/lib/semmle/code/csharp/dataflow/ExternalFlow.qll
|
||||
private import semmle.code.csharp.dataflow.internal.FlowSummaryImplSpecific
|
||||
|
||||
/**
|
||||
* A callable where there exists a MaD sink model that applies to it.
|
||||
*/
|
||||
class SinkCallable extends Callable {
|
||||
SinkCallable() { sinkElement(this, _, _, _) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A callable where there exists a MaD source model that applies to it.
|
||||
*/
|
||||
class SourceCallable extends Callable {
|
||||
SourceCallable() { sourceElement(this, _, _, _) }
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -2,66 +2,113 @@ import { Query } from "./query";
|
||||
|
||||
export const fetchExternalApisQuery: Query = {
|
||||
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
|
||||
* @kind problem
|
||||
* @id java/telemetry/fetch-external-apis
|
||||
* @name Fetch endpoints for use in the model editor (application mode)
|
||||
* @description A list of 3rd party endpoints (methods) used in the codebase. Excludes test and generated code.
|
||||
* @kind table
|
||||
* @id java/utils/modeleditor/application-mode-endpoints
|
||||
* @tags modeleditor endpoints application-mode
|
||||
*/
|
||||
|
||||
import java
|
||||
import AutomodelVsCode
|
||||
|
||||
class ExternalApi extends CallableMethod {
|
||||
ExternalApi() { not this.fromSource() }
|
||||
}
|
||||
|
||||
private Call aUsage(ExternalApi api) { result.getCallee().getSourceDeclaration() = api }
|
||||
|
||||
from
|
||||
ExternalApi externalApi, string apiName, boolean supported, Call usage, string type,
|
||||
string classification
|
||||
where
|
||||
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: {
|
||||
"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
|
||||
private import ApplicationModeEndpointsQuery
|
||||
private import ModelEditor
|
||||
|
||||
private Call aUsage(ExternalEndpoint endpoint) {
|
||||
result.getCallee().getSourceDeclaration() = endpoint
|
||||
}
|
||||
|
||||
from ExternalEndpoint endpoint, boolean supported, Call usage, string type, string classification
|
||||
where
|
||||
supported = isSupported(endpoint) and
|
||||
usage = aUsage(endpoint) and
|
||||
type = supportedType(endpoint) and
|
||||
classification = usageClassification(usage)
|
||||
select usage, endpoint.getPackageName(), endpoint.getTypeName(), endpoint.getName(),
|
||||
endpoint.getParameterTypes(), supported, endpoint.jarContainer(), endpoint.jarVersion(), type,
|
||||
classification
|
||||
`,
|
||||
frameworkModeQuery: `/**
|
||||
* @name Fetch endpoints for use in the model editor (framework mode)
|
||||
* @description A list of endpoints accessible (methods) for consumers of the library. Excludes test and generated code.
|
||||
* @kind table
|
||||
* @id java/utils/modeleditor/framework-mode-endpoints
|
||||
* @tags modeleditor endpoints framework-mode
|
||||
*/
|
||||
|
||||
private import java
|
||||
private import FrameworkModeEndpointsQuery
|
||||
private import ModelEditor
|
||||
|
||||
from PublicEndpointFromSource endpoint, boolean supported, string type
|
||||
where
|
||||
supported = isSupported(endpoint) and
|
||||
type = supportedType(endpoint)
|
||||
select endpoint, endpoint.getPackageName(), endpoint.getTypeName(), endpoint.getName(),
|
||||
endpoint.getParameterTypes(), supported,
|
||||
endpoint.getCompilationUnit().getParentContainer().getBaseName(), type
|
||||
`,
|
||||
dependencies: {
|
||||
"ApplicationModeEndpointsQuery.qll": `private import java
|
||||
private import semmle.code.java.dataflow.ExternalFlow
|
||||
private import semmle.code.java.dataflow.FlowSources
|
||||
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 ModelEditor
|
||||
|
||||
/**
|
||||
* A class of effectively public callables in library code.
|
||||
*/
|
||||
class ExternalEndpoint extends Endpoint {
|
||||
ExternalEndpoint() { not this.fromSource() }
|
||||
|
||||
/** Gets a node that is an input to a call to this API. */
|
||||
private DataFlow::Node getAnInput() {
|
||||
exists(Call call | call.getCallee().getSourceDeclaration() = this |
|
||||
result.asExpr().(Argument).getCall() = call or
|
||||
result.(ArgumentNode).getCall().asCall() = call
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets a node that is an output from a call to this API. */
|
||||
private DataFlow::Node getAnOutput() {
|
||||
exists(Call call | call.getCallee().getSourceDeclaration() = this |
|
||||
result.asExpr() = call or
|
||||
result.(DataFlow::PostUpdateNode).getPreUpdateNode().(ArgumentNode).getCall().asCall() = call
|
||||
)
|
||||
}
|
||||
|
||||
override predicate hasSummary() {
|
||||
Endpoint.super.hasSummary()
|
||||
or
|
||||
TaintTracking::localAdditionalTaintStep(this.getAnInput(), _)
|
||||
}
|
||||
|
||||
override predicate isSource() {
|
||||
this.getAnOutput() instanceof RemoteFlowSource or sourceNode(this.getAnOutput(), _)
|
||||
}
|
||||
|
||||
override predicate isSink() { sinkNode(this.getAnInput(), _) }
|
||||
}
|
||||
`,
|
||||
"FrameworkModeEndpointsQuery.qll": `private import java
|
||||
private import semmle.code.java.dataflow.internal.DataFlowPrivate
|
||||
private import semmle.code.java.dataflow.internal.FlowSummaryImplSpecific
|
||||
private import semmle.code.java.dataflow.internal.ModelExclusions
|
||||
private import ModelEditor
|
||||
|
||||
/**
|
||||
* A class of effectively public callables from source code.
|
||||
*/
|
||||
class PublicEndpointFromSource extends Endpoint, ModelApi {
|
||||
override predicate isSource() { sourceElement(this, _, _, _) }
|
||||
|
||||
override predicate isSink() { sinkElement(this, _, _, _) }
|
||||
}
|
||||
`,
|
||||
"ModelEditor.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
|
||||
|
||||
private import java
|
||||
private import semmle.code.java.dataflow.ExternalFlow
|
||||
private import semmle.code.java.dataflow.FlowSummary
|
||||
private import semmle.code.java.dataflow.TaintTracking
|
||||
private import semmle.code.java.dataflow.internal.ModelExclusions
|
||||
|
||||
@@ -75,17 +122,23 @@ private predicate isUninteresting(Callable c) {
|
||||
/**
|
||||
* A callable method from either the Standard Library, a 3rd party library or from the source.
|
||||
*/
|
||||
class CallableMethod extends Callable {
|
||||
CallableMethod() { not isUninteresting(this) }
|
||||
class Endpoint extends Callable {
|
||||
Endpoint() { not isUninteresting(this) }
|
||||
|
||||
/**
|
||||
* Gets information about the external API in the form expected by the MaD modeling framework.
|
||||
* Gets the package name of this endpoint.
|
||||
*/
|
||||
string getApiName() {
|
||||
result =
|
||||
this.getDeclaringType().getPackage() + "." + this.getDeclaringType().nestedName() + "#" +
|
||||
this.getName() + paramsString(this)
|
||||
}
|
||||
string getPackageName() { result = this.getDeclaringType().getPackage().getName() }
|
||||
|
||||
/**
|
||||
* Gets the type name of this endpoint.
|
||||
*/
|
||||
string getTypeName() { result = this.getDeclaringType().nestedName() }
|
||||
|
||||
/**
|
||||
* Gets the parameter types of this endpoint.
|
||||
*/
|
||||
string getParameterTypes() { result = paramsString(this) }
|
||||
|
||||
private string getJarName() {
|
||||
result = this.getCompilationUnit().getParentContainer*().(JarFile).getBaseName()
|
||||
@@ -113,43 +166,23 @@ class CallableMethod extends Callable {
|
||||
not exists(this.getJarVersion()) and result = ""
|
||||
}
|
||||
|
||||
/** Gets a node that is an input to a call to this API. */
|
||||
private DataFlow::Node getAnInput() {
|
||||
exists(Call call | call.getCallee().getSourceDeclaration() = this |
|
||||
result.asExpr().(Argument).getCall() = call or
|
||||
result.(ArgumentNode).getCall().asCall() = call
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets a node that is an output from a call to this API. */
|
||||
private DataFlow::Node getAnOutput() {
|
||||
exists(Call call | call.getCallee().getSourceDeclaration() = this |
|
||||
result.asExpr() = call or
|
||||
result.(DataFlow::PostUpdateNode).getPreUpdateNode().(ArgumentNode).getCall().asCall() = call
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if this API has a supported summary. */
|
||||
pragma[nomagic]
|
||||
predicate hasSummary() {
|
||||
this = any(SummarizedCallable sc).asCallable() or
|
||||
TaintTracking::localAdditionalTaintStep(this.getAnInput(), _)
|
||||
}
|
||||
predicate hasSummary() { this = any(SummarizedCallable sc).asCallable() }
|
||||
|
||||
/** Holds if this API is a known source. */
|
||||
pragma[nomagic]
|
||||
predicate isSource() {
|
||||
this.getAnOutput() instanceof RemoteFlowSource or sourceNode(this.getAnOutput(), _)
|
||||
}
|
||||
abstract predicate isSource();
|
||||
|
||||
/** Holds if this API is a known sink. */
|
||||
pragma[nomagic]
|
||||
predicate isSink() { sinkNode(this.getAnInput(), _) }
|
||||
abstract predicate isSink();
|
||||
|
||||
/** Holds if this API is a known neutral. */
|
||||
pragma[nomagic]
|
||||
predicate isNeutral() {
|
||||
exists(string namespace, string type, string name, string signature, string kind, string provenance |
|
||||
neutralModel(namespace, type, name, signature, kind, provenance) and
|
||||
exists(string namespace, string type, string name, string signature |
|
||||
neutralModel(namespace, type, name, signature, _, _) and
|
||||
this = interpretElement(namespace, type, false, name, signature, "")
|
||||
)
|
||||
}
|
||||
@@ -163,108 +196,38 @@ class CallableMethod extends Callable {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSupported(CallableMethod method) {
|
||||
method.isSupported() and result = true
|
||||
boolean isSupported(Endpoint endpoint) {
|
||||
endpoint.isSupported() and result = true
|
||||
or
|
||||
not method.isSupported() and result = false
|
||||
not endpoint.isSupported() and result = false
|
||||
}
|
||||
|
||||
string supportedType(CallableMethod method) {
|
||||
method.isSink() and result = "sink"
|
||||
string supportedType(Endpoint endpoint) {
|
||||
endpoint.isSink() and result = "sink"
|
||||
or
|
||||
method.isSource() and result = "source"
|
||||
endpoint.isSource() and result = "source"
|
||||
or
|
||||
method.hasSummary() and result = "summary"
|
||||
endpoint.hasSummary() and result = "summary"
|
||||
or
|
||||
method.isNeutral() and result = "neutral"
|
||||
endpoint.isNeutral() and result = "neutral"
|
||||
or
|
||||
not method.isSupported() and result = ""
|
||||
not endpoint.isSupported() and result = ""
|
||||
}
|
||||
|
||||
string methodClassification(Call method) {
|
||||
isInTestFile(method.getLocation().getFile()) and result = "test"
|
||||
string usageClassification(Call usage) {
|
||||
isInTestFile(usage.getLocation().getFile()) and result = "test"
|
||||
or
|
||||
method.getFile() instanceof GeneratedFile and result = "generated"
|
||||
usage.getFile() instanceof GeneratedFile and result = "generated"
|
||||
or
|
||||
not isInTestFile(method.getLocation().getFile()) and
|
||||
not method.getFile() instanceof GeneratedFile and
|
||||
not isInTestFile(usage.getLocation().getFile()) and
|
||||
not usage.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%"
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* A test library.
|
||||
*/
|
||||
class TestLibrary extends RefType {
|
||||
TestLibrary() { isTestPackage(this.getPackage()) }
|
||||
}
|
||||
|
||||
/** Holds if the given file is a test file. */
|
||||
private predicate isInTestFile(File file) {
|
||||
// Temporarily copied from java/ql/lib/semmle/code/java/dataflow/internal/ModelExclusions.qll
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that represents all callables for which we might be
|
||||
* interested in having a MaD model.
|
||||
*/
|
||||
class ModelApi extends SrcCallable {
|
||||
ModelApi() {
|
||||
this.fromSource() and
|
||||
this.isEffectivelyPublic() and
|
||||
not isUninterestingForModels(this)
|
||||
}
|
||||
not file.getAbsolutePath().matches(["%/ql/test/%", "%/ql/automodel/test/%"]) // allows our test cases to work
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { Call, CallClassification } from "../method";
|
||||
import { ModeledMethodType } from "../modeled-method";
|
||||
|
||||
export type 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 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.
|
||||
* - packageName: the package name of the external API. This is a string.
|
||||
* - typeName: the type name of the external API. This is a string.
|
||||
* - methodName: the method name of the external API. This is a string.
|
||||
* - methodParameters: the parameters of the external API. This is a string.
|
||||
* - supported: whether the external API is modeled. This is a boolean.
|
||||
* - 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.
|
||||
*/
|
||||
applicationModeQuery: string;
|
||||
/**
|
||||
@@ -21,18 +24,40 @@ export type 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.
|
||||
* - packageName: the package name of the method. This is a string.
|
||||
* - typeName: the type name of the method. This is a string.
|
||||
* - methodName: the method name of the method. This is a string.
|
||||
* - methodParameters: the parameters of the method. 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.
|
||||
* - libraryName: the name of the file or library that contains the method. This is a string and usually the basename of a file.
|
||||
* - 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;
|
||||
};
|
||||
};
|
||||
|
||||
export type ApplicationModeTuple = [
|
||||
Call,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
boolean,
|
||||
string,
|
||||
string,
|
||||
ModeledMethodType,
|
||||
CallClassification,
|
||||
];
|
||||
|
||||
export type FrameworkModeTuple = [
|
||||
Call,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
boolean,
|
||||
string,
|
||||
ModeledMethodType,
|
||||
];
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const INITIAL_HIDE_MODELED_APIS_VALUE = true;
|
||||
@@ -0,0 +1 @@
|
||||
export const INITIAL_HIDE_MODELED_METHODS_VALUE = true;
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* A class that keeps track of which methods are in progress for each package.
|
||||
*
|
||||
* This class is immutable and therefore is safe to be used in a react useState hook.
|
||||
* This class is immutable and therefore is safe to be used in a React useState hook.
|
||||
*/
|
||||
export class InProgressMethods {
|
||||
// A map of in-progress method signatures for each package.
|
||||
|
||||
@@ -2,3 +2,5 @@ export enum Mode {
|
||||
Application = "application",
|
||||
Framework = "framework",
|
||||
}
|
||||
|
||||
export const INITIAL_MODE = Mode.Application;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ModeledMethod } from "../modeled-method";
|
||||
|
||||
/**
|
||||
* Converts a single ModeledMethod to a ModeledMethod[] for legacy usage. This function should always be used instead
|
||||
* of the trivial conversion to track usages of this conversion.
|
||||
*
|
||||
* This method should only be called inside a `onMessage` function (or its equivalent). If it's used anywhere else,
|
||||
* consider whether the boundary is correct: the boundary should as close as possible to the webview -> extension host
|
||||
* boundary.
|
||||
*
|
||||
* @param modeledMethod The single ModeledMethod
|
||||
*/
|
||||
export function convertFromLegacyModeledMethod(
|
||||
modeledMethod: ModeledMethod | undefined,
|
||||
): ModeledMethod[] {
|
||||
return modeledMethod ? [modeledMethod] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ModeledMethod[] to a single ModeledMethod for legacy usage. This function should always be used instead
|
||||
* of the trivial conversion to track usages of this conversion.
|
||||
*
|
||||
* This method should only be called inside a `postMessage` call. If it's used anywhere else, consider whether the
|
||||
* boundary is correct: the boundary should as close as possible to the extension host -> webview boundary.
|
||||
*
|
||||
* @param modeledMethods The ModeledMethod[]
|
||||
*/
|
||||
export function convertToLegacyModeledMethod(
|
||||
modeledMethods: ModeledMethod[],
|
||||
): ModeledMethod | undefined {
|
||||
return modeledMethods[0];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ModeledMethod } from "../modeled-method";
|
||||
|
||||
export type ModelingStatus = "unmodeled" | "unsaved" | "saved";
|
||||
|
||||
export function getModelingStatus(
|
||||
modeledMethods: ModeledMethod[],
|
||||
methodIsUnsaved: boolean,
|
||||
): ModelingStatus {
|
||||
if (modeledMethods.length > 0) {
|
||||
if (methodIsUnsaved) {
|
||||
return "unsaved";
|
||||
} else if (modeledMethods.some((m) => m.type !== "none")) {
|
||||
return "saved";
|
||||
}
|
||||
}
|
||||
return "unmodeled";
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user