Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a81ad05ed | ||
|
|
76e983d19c | ||
|
|
a3015c0fa3 | ||
|
|
88d0bda049 | ||
|
|
d2ec54e89e | ||
|
|
4559c5a38d | ||
|
|
16bd106abc | ||
|
|
e5dcec8d8e | ||
|
|
ad3565d3ad | ||
|
|
5fe12ecd74 | ||
|
|
318214642f | ||
|
|
227fe3ee6b | ||
|
|
978a82dd1a | ||
|
|
04f72a7da9 | ||
|
|
a0954a1dc0 | ||
|
|
cc1bf74370 | ||
|
|
2f7908773a | ||
|
|
0efd02979e | ||
|
|
bd9776c4b7 | ||
|
|
35e9da83ec | ||
|
|
4f5ca0bca9 | ||
|
|
43f314b2b5 | ||
|
|
4bdf579ce2 | ||
|
|
aba3039eef | ||
|
|
bbff791c65 | ||
|
|
1ed50b3081 | ||
|
|
67336a24e7 | ||
|
|
48174c327d | ||
|
|
43f2539b42 | ||
|
|
462a7a722a | ||
|
|
4101bb252e | ||
|
|
4ff4e4827e | ||
|
|
8daa92ad49 | ||
|
|
371e83bff9 | ||
|
|
6fa0227a1e | ||
|
|
c38e4ce265 | ||
|
|
de06ed148d | ||
|
|
21bcd62ba8 | ||
|
|
76c034f79a | ||
|
|
d8d394ce40 | ||
|
|
213f4ce92f | ||
|
|
2d1726763f | ||
|
|
abfd9b3cbd | ||
|
|
6114f6a7fd | ||
|
|
61e674e9f6 | ||
|
|
006cc8c52a | ||
|
|
ffe7fdcb46 | ||
|
|
49cceffe1b | ||
|
|
011782395a | ||
|
|
558009543f | ||
|
|
aaef5bde2c | ||
|
|
f52f595d56 | ||
|
|
50196d8430 | ||
|
|
2ecfbfbb42 | ||
|
|
9508dffe6d | ||
|
|
b4a72bbcab | ||
|
|
4ceaaf92cc | ||
|
|
ef28c9531b | ||
|
|
c86c602e39 | ||
|
|
3bee2905e5 | ||
|
|
9ac8a15cd5 | ||
|
|
81b8104064 | ||
|
|
65f58b1f98 | ||
|
|
7e872aa6d6 | ||
|
|
0383a91a68 | ||
|
|
bb6ebe5750 | ||
|
|
71aa3d145f | ||
|
|
2f1f80029b | ||
|
|
ad18cfa284 | ||
|
|
92ed1c6ac9 | ||
|
|
e71e04a8f1 | ||
|
|
ef127c279c | ||
|
|
4afac5fa4d | ||
|
|
29ae97aa82 | ||
|
|
9319d7e8ef | ||
|
|
689db3713b | ||
|
|
0b9fcb884b | ||
|
|
23e29a1fdc | ||
|
|
90d636a026 | ||
|
|
3e3e12afb9 | ||
|
|
421f5d23ec | ||
|
|
0fa91f32cb | ||
|
|
3d21b203be | ||
|
|
3972b8f4c1 | ||
|
|
2d1707db00 | ||
|
|
72aa4f0561 | ||
|
|
fd57cc95e9 | ||
|
|
04c392be7e | ||
|
|
38da598214 | ||
|
|
3f2c9b647c | ||
|
|
7d5b4369c1 | ||
|
|
aade33fa88 | ||
|
|
2a8a90bdfc | ||
|
|
f36048cc95 | ||
|
|
517feeca21 | ||
|
|
9436a49118 | ||
|
|
0e02cb08fd | ||
|
|
26244efc50 | ||
|
|
6339eeffe5 | ||
|
|
8cc2f598eb | ||
|
|
46a1dd57f4 | ||
|
|
9d99fc521e | ||
|
|
bcf79354ee | ||
|
|
27a8636bac | ||
|
|
92a99938c9 | ||
|
|
ed61eb0a95 | ||
|
|
50d495b522 | ||
|
|
526d5c2c44 | ||
|
|
1720f9201e | ||
|
|
e62de1ca22 | ||
|
|
d052ddb742 | ||
|
|
af53a02ea5 | ||
|
|
8e2d18da8c | ||
|
|
2c5004387d | ||
|
|
3fc3b259ba | ||
|
|
cd95f68692 | ||
|
|
59c3b1ba2f | ||
|
|
fa85865fe5 | ||
|
|
5575d4142c | ||
|
|
ae6263a07f | ||
|
|
9af75634fa | ||
|
|
04b8681272 | ||
|
|
d5549f2894 | ||
|
|
b510b85ca0 | ||
|
|
5ad754a3a2 | ||
|
|
4f04f9db6e | ||
|
|
025a1a1383 | ||
|
|
f28c1f91d9 | ||
|
|
c609377a9c | ||
|
|
2579d12f24 | ||
|
|
c18f7953e7 | ||
|
|
3a292b02b6 | ||
|
|
7baf2d0a2a | ||
|
|
328289eb1c | ||
|
|
95d93eeb61 | ||
|
|
b54cc27cab | ||
|
|
c9ca1ee7b3 | ||
|
|
649d6d94a3 | ||
|
|
bf68d21830 | ||
|
|
64b33b76cb | ||
|
|
c189df3fd6 | ||
|
|
277869ebca | ||
|
|
303513a566 | ||
|
|
8712106b3d | ||
|
|
cdb9506583 | ||
|
|
94a311a550 | ||
|
|
791e7e9c4d | ||
|
|
6cfa7e2cd3 | ||
|
|
7196c26181 | ||
|
|
735f177283 | ||
|
|
f857e5ec6c | ||
|
|
a5e02950c2 | ||
|
|
4a928f1298 | ||
|
|
f59012862e | ||
|
|
5f5418a297 | ||
|
|
548a216b56 | ||
|
|
c943c89fc6 | ||
|
|
06de6077ba | ||
|
|
cef1fcc95d | ||
|
|
1ed8b225db | ||
|
|
f0354c87f4 | ||
|
|
5e06a615cd | ||
|
|
e11aa7af18 | ||
|
|
f4ddc17851 | ||
|
|
ebce2826cb | ||
|
|
4c411acef4 | ||
|
|
ddc941f464 | ||
|
|
c5ff2c6f76 | ||
|
|
85ac16bb22 | ||
|
|
e7ee4a33c7 | ||
|
|
ac0da04542 | ||
|
|
3337117970 | ||
|
|
9b61ff5714 | ||
|
|
d25db48452 | ||
|
|
251f354076 | ||
|
|
9c6ae226fb | ||
|
|
a502ee85d1 | ||
|
|
eec72e0cbd | ||
|
|
7a1acce133 | ||
|
|
84b4bfe663 | ||
|
|
16df990183 | ||
|
|
969dd26041 | ||
|
|
9df1f91318 | ||
|
|
48ddc66d47 | ||
|
|
85e3869607 | ||
|
|
5bb2a763e3 | ||
|
|
2110709d72 | ||
|
|
493033edc0 | ||
|
|
bf8e77b9b9 | ||
|
|
c7e5581027 | ||
|
|
c78802a1ed | ||
|
|
39f9c082b9 | ||
|
|
ca1ef5192d | ||
|
|
1d6fef9169 | ||
|
|
81f80ddbe5 | ||
|
|
b53657344c | ||
|
|
95e818898e | ||
|
|
a7e014a87e | ||
|
|
cca65e5a48 | ||
|
|
a75249f3e4 | ||
|
|
053a4b0392 | ||
|
|
d1362bf44f | ||
|
|
580832ea7b | ||
|
|
ddca0bb851 | ||
|
|
d9a04ea895 | ||
|
|
48ccb27e49 | ||
|
|
a2b5ad07ff | ||
|
|
cc9cbf7f06 | ||
|
|
ad5c43c9ba | ||
|
|
9c27d01d47 | ||
|
|
64ac33e3bb | ||
|
|
329fb87e12 | ||
|
|
bd5da2b0f0 | ||
|
|
55c21888af | ||
|
|
d49e6e19a6 | ||
|
|
edb1af09c4 | ||
|
|
ab3822d1cc | ||
|
|
69120e0799 | ||
|
|
7785dfead2 | ||
|
|
29c29f9e3a | ||
|
|
b7dafc31bb | ||
|
|
2f5a306c2d | ||
|
|
0ef6b45b19 | ||
|
|
d9f33d34e3 | ||
|
|
5758e03a17 | ||
|
|
5d9f80cce8 | ||
|
|
867ee530b1 | ||
|
|
27e6a55756 | ||
|
|
b237bafa2f | ||
|
|
d0bde800f7 | ||
|
|
da0090aa99 | ||
|
|
66c9879ce3 | ||
|
|
9c2585116a | ||
|
|
e46c0e25e8 | ||
|
|
658b0ce243 | ||
|
|
c084e31416 | ||
|
|
9046844f0c | ||
|
|
5a9b49b9bb | ||
|
|
0672133bca | ||
|
|
c0de99bc42 | ||
|
|
6dbb1a27b9 | ||
|
|
dc1bace4c6 | ||
|
|
afe3c56ca8 | ||
|
|
a6f42e3eb3 | ||
|
|
9c2bd2a57b | ||
|
|
f42f474113 | ||
|
|
17c31e1539 | ||
|
|
b0fb4d6bc9 | ||
|
|
f8690bcebc | ||
|
|
b0410ec5de | ||
|
|
19e0058e61 |
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "extensions/ql-vscode"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "thursday" # Thursday is arbitrary
|
||||
labels:
|
||||
- "Update dependencies"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: ".github"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "thursday" # Thursday is arbitrary
|
||||
labels:
|
||||
- "Update dependencies"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
|
||||
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@@ -26,6 +26,7 @@ jobs:
|
||||
with:
|
||||
languages: javascript
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
tools: latest
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@main
|
||||
|
||||
17
.github/workflows/dependency-review.yml
vendored
Normal file
17
.github/workflows/dependency-review.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: 'Dependency Review'
|
||||
on:
|
||||
- pull_request
|
||||
- workflow_dispatch
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: dsp-testing/dependency-review-action@main
|
||||
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.14.0'
|
||||
node-version: '16.13.0'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: extensions/ql-vscode
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.14.0'
|
||||
node-version: '16.13.0'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: extensions/ql-vscode
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.3', 'v2.7.6', 'nightly']
|
||||
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.3', 'v2.7.6', 'v2.8.4', 'nightly']
|
||||
env:
|
||||
CLI_VERSION: ${{ matrix.version }}
|
||||
NIGHTLY_URL: ${{ needs.find-nightly.outputs.url }}
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.14.0'
|
||||
node-version: '16.13.0'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: extensions/ql-vscode
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '10.18.1'
|
||||
node-version: '16.13.0'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -147,11 +147,13 @@ jobs:
|
||||
If this was an authentication problem, please make sure the \
|
||||
auth token hasn't expired."
|
||||
|
||||
# TODO This job is currently broken and is blocked on https://github.com/github/vscode-codeql/issues/1085
|
||||
open-vsx-publish:
|
||||
name: Publish to Open VSX Registry
|
||||
needs: build
|
||||
environment: publish-open-vsx
|
||||
runs-on: ubuntu-latest
|
||||
if: 1 == 0
|
||||
env:
|
||||
OPEN_VSX_TOKEN: ${{ secrets.OPEN_VSX_TOKEN }}
|
||||
steps:
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
**/* @github/codeql-vscode-reviewers
|
||||
/extensions/ql-vscode/src/remote-queries/ @github/code-scanning-secexp-reviewers
|
||||
|
||||
@@ -124,6 +124,7 @@ From inside of VSCode, open the `launch.json` file and in the _Launch Integratio
|
||||
1. Download the VSIX from the draft GitHub release at the top of [the releases page](https://github.com/github/vscode-codeql/releases) that is created when the release build finishes.
|
||||
1. Unzip the `.vsix` and inspect its `package.json` to make sure the version is what you expect,
|
||||
or look at the source if there's any doubt the right code is being shipped.
|
||||
1. Install the `.vsix` file into your vscode IDE and ensure the extension can load properly. Run a single command (like run query, or add database).
|
||||
1. Go to the actions tab of the vscode-codeql repository and select the [Release workflow](https://github.com/github/vscode-codeql/actions?query=workflow%3ARelease).
|
||||
- If there is an authentication failure when publishing, be sure to check that the authentication keys haven't expired. See below.
|
||||
1. Approve the deployments of the correct Release workflow. This will automatically publish to Open VSX and VS Code Marketplace.
|
||||
@@ -143,12 +144,7 @@ To regenerate the Open VSX token:
|
||||
1. Go to the [Access Tokens](https://open-vsx.org/user-settings/tokens) page and generate a new token.
|
||||
1. Update the secret in the `publish-open-vsx` environment in the project settings.
|
||||
|
||||
To regenerate the VSCode Marketplace token:
|
||||
|
||||
1. Follow the instructions on [getting a PAT for Azure DevOps](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token).
|
||||
1. Update the secret in the `publish-vscode-marketplace` environment in the project settings.
|
||||
|
||||
Not that Azure DevOps PATs expire yearly and must be regenerated.
|
||||
To regenerate the VSCode Marketplace token, please see our internal documentation. Note that Azure DevOps PATs expire every 90 days and must be regenerated.
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
## 1.6.3 - 4 April 2022
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 1.6.2 - 4 April 2022
|
||||
|
||||
- Fix a bug where the AST viewer was not synchronizing its selected node when the editor selection changes. [#1230](https://github.com/github/vscode-codeql/pull/1230)
|
||||
- Avoid synchronizing the `codeQL.cli.executablePath` setting. [#1252](https://github.com/github/vscode-codeql/pull/1252)
|
||||
- Open the directory in the finder/explorer (instead of just highlighting it) when running the "Open query directory" command from the query history view. [#1235](https://github.com/github/vscode-codeql/pull/1235)
|
||||
- Ensure query label in the query history view changes are persisted across restarts. [#1235](https://github.com/github/vscode-codeql/pull/1235)
|
||||
- Prints end-of-query evaluator log summaries to the Query Server Console. [#1264](https://github.com/github/vscode-codeql/pull/1264)
|
||||
|
||||
## 1.6.1 - 17 March 2022
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 1.6.0 - 7 March 2022
|
||||
|
||||
- Fix a bug where database upgrades could not be resolved if some of the target pack's dependencies are outside of the workspace. [#1138](https://github.com/github/vscode-codeql/pull/1138)
|
||||
- Open the query server logs for query errors (instead of the extension log). This will make it easier to track down query errors. [#1158](https://github.com/github/vscode-codeql/pull/1158)
|
||||
- Fix a bug where queries took a long time to run if there are no folders in the workspace. [#1157](https://github.com/github/vscode-codeql/pull/1157)
|
||||
- [BREAKING CHANGE] The `codeQL.runningQueries.customLogDirectory` setting is deprecated and no longer has any function. Instead, all query log files will be stored in the query history directory, next to the query results. [#1178](https://github.com/github/vscode-codeql/pull/1178)
|
||||
- Add a _Open query directory_ command for query items. This command opens the directory containing all artifacts for a query. [#1179](https://github.com/github/vscode-codeql/pull/1179)
|
||||
- Add options to display evaluator logs for a given query run. Some information that was previously found in the query server output may now be found here. [#1186](https://github.com/github/vscode-codeql/pull/1186)
|
||||
|
||||
## 1.5.11 - 10 February 2022
|
||||
|
||||
- Fix a bug where invoking _View AST_ from the file explorer would not view the selected file. Instead it would view the active editor. Also, prevent the _View AST_ from appearing if the current selection includes a directory or multiple files. [#1113](https://github.com/github/vscode-codeql/pull/1113)
|
||||
- Add query history items as soon as a query is run, including new icons for each history item. [#1094](https://github.com/github/vscode-codeql/pull/1094)
|
||||
- Save query history items across restarts. Items will be saved for 30 days and can be overwritten by setting the `codeQL.queryHistory.ttl` configuration setting. [#1130](https://github.com/github/vscode-codeql/pull/1130)
|
||||
- Allow in-progress query items to be cancelled from the query history view. [#1105](https://github.com/github/vscode-codeql/pull/1105)
|
||||
|
||||
## 1.5.10 - 25 January 2022
|
||||
|
||||
- Fix a bug where the results view moved column even when it was already visible. [#1070](https://github.com/github/vscode-codeql/pull/1070)
|
||||
@@ -24,7 +58,7 @@
|
||||
- Fix a bug with importing large databases. Databases over 4GB can now be imported directly from LGTM or from a zip file. This functionality is only available when using CodeQL CLI version 2.6.0 or later. [#971](https://github.com/github/vscode-codeql/pull/971)
|
||||
- Replace certain control codes (`U+0000` - `U+001F`) with their corresponding control labels (`U+2400` - `U+241F`) in the results view. [#963](https://github.com/github/vscode-codeql/pull/963)
|
||||
- Allow case-insensitive project slugs for GitHub repositories when adding a CodeQL database from LGTM. [#978](https://github.com/github/vscode-codeql/pull/961)
|
||||
- Add a _CodeQL: Preview Query Help_ command to generate Markdown previews of `.qhelp` query help files. This command should only be run in trusted workspaces. See https://codeql.github.com/docs/codeql-cli/testing-query-help-files for more information about query help. [#988](https://github.com/github/vscode-codeql/pull/988)
|
||||
- Add a _CodeQL: Preview Query Help_ command to generate Markdown previews of `.qhelp` query help files. This command should only be run in trusted workspaces. See [the CodeQL CLI docs](https://codeql.github.com/docs/codeql-cli/testing-query-help-files) for more information about query help. [#988](https://github.com/github/vscode-codeql/pull/988)
|
||||
- Make "Open Referenced File" command accessible from the active editor menu. [#989](https://github.com/github/vscode-codeql/pull/989)
|
||||
- Fix a bug where result set names in the result set drop-down were disappearing when viewing a sorted table. [#1007](https://github.com/github/vscode-codeql/pull/1007)
|
||||
- Allow query result locations with 0 as the end column value. These are treated as the first column in the line. [#1002](https://github.com/github/vscode-codeql/pull/1002)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as gulp from 'gulp';
|
||||
import * as replace from 'gulp-replace';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const replace = require('gulp-replace');
|
||||
|
||||
/** Inject the application insights key into the telemetry file */
|
||||
export function injectAppInsightsKey() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as gulp from 'gulp';
|
||||
import { compileTypeScript, watchTypeScript, copyViewCss, cleanOutput } from './typescript';
|
||||
import { compileTypeScript, watchTypeScript, copyViewCss, cleanOutput, watchCss } from './typescript';
|
||||
import { compileTextMateGrammar } from './textmate';
|
||||
import { copyTestData } from './tests';
|
||||
import { compileView } from './webpack';
|
||||
import { compileView, watchView } from './webpack';
|
||||
import { packageExtension } from './package';
|
||||
import { injectAppInsightsKey } from './appInsights';
|
||||
|
||||
@@ -14,5 +14,15 @@ export const buildWithoutPackage =
|
||||
)
|
||||
);
|
||||
|
||||
export { cleanOutput, compileTextMateGrammar, watchTypeScript, compileTypeScript, copyTestData, injectAppInsightsKey };
|
||||
export {
|
||||
cleanOutput,
|
||||
compileTextMateGrammar,
|
||||
watchTypeScript,
|
||||
watchView,
|
||||
compileTypeScript,
|
||||
copyTestData,
|
||||
injectAppInsightsKey,
|
||||
compileView,
|
||||
watchCss
|
||||
};
|
||||
export default gulp.series(buildWithoutPackage, injectAppInsightsKey, packageExtension);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"strict": true,
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"lib": ["es6"],
|
||||
"lib": ["ES2021"],
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"rootDir": ".",
|
||||
@@ -16,7 +16,8 @@
|
||||
"noImplicitReturns": true,
|
||||
"experimentalDecorators": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
"noUnusedParameters": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ export function watchTypeScript() {
|
||||
gulp.watch('src/**/*.ts', compileTypeScript);
|
||||
}
|
||||
|
||||
export function watchCss() {
|
||||
gulp.watch('src/**/*.css', copyViewCss);
|
||||
}
|
||||
|
||||
/** Copy CSS files for the results view into the output directory. */
|
||||
export function copyViewCss() {
|
||||
return gulp.src('src/**/view/*.css')
|
||||
|
||||
@@ -2,7 +2,23 @@ import * as webpack from 'webpack';
|
||||
import { config } from './webpack.config';
|
||||
|
||||
export function compileView(cb: (err?: Error) => void) {
|
||||
webpack(config).run((error, stats) => {
|
||||
doWebpack(config, true, cb);
|
||||
}
|
||||
|
||||
export function watchView(cb: (err?: Error) => void) {
|
||||
const watchConfig = {
|
||||
...config,
|
||||
watch: true,
|
||||
watchOptions: {
|
||||
aggregateTimeout: 200,
|
||||
poll: 1000,
|
||||
}
|
||||
};
|
||||
doWebpack(watchConfig, false, cb);
|
||||
}
|
||||
|
||||
function doWebpack(internalConfig: webpack.Configuration, failOnError: boolean, cb: (err?: Error) => void) {
|
||||
const resultCb = (error: Error | undefined, stats?: webpack.Stats) => {
|
||||
if (error) {
|
||||
cb(error);
|
||||
}
|
||||
@@ -20,11 +36,16 @@ export function compileView(cb: (err?: Error) => void) {
|
||||
errors: true
|
||||
}));
|
||||
if (stats.hasErrors()) {
|
||||
cb(new Error('Compilation errors detected.'));
|
||||
return;
|
||||
if (failOnError) {
|
||||
cb(new Error('Compilation errors detected.'));
|
||||
return;
|
||||
} else {
|
||||
console.error('Compilation errors detected.');
|
||||
}
|
||||
}
|
||||
cb();
|
||||
}
|
||||
};
|
||||
|
||||
cb();
|
||||
});
|
||||
webpack(internalConfig, resultCb);
|
||||
}
|
||||
|
||||
4
extensions/ql-vscode/media/dark/github.svg
Normal file
4
extensions/ql-vscode/media/dark/github.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- From https://github.com/microsoft/vscode-icons -->
|
||||
<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.97553 0C3.57186 0 0 3.57186 0 7.97553C0 11.4985 2.29969 14.4832 5.43119 15.5596C5.82263 15.6086 5.96942 15.3639 5.96942 15.1682C5.96942 14.9725 5.96942 14.4832 5.96942 13.7982C3.76758 14.2875 3.27829 12.7217 3.27829 12.7217C2.93578 11.792 2.39755 11.5474 2.39755 11.5474C1.66361 11.0581 2.44648 11.0581 2.44648 11.0581C3.22936 11.107 3.66972 11.8899 3.66972 11.8899C4.40367 13.1131 5.52905 12.7706 5.96942 12.5749C6.01835 12.0367 6.263 11.6942 6.45872 11.4985C4.69725 11.3028 2.83792 10.6177 2.83792 7.53517C2.83792 6.65443 3.1315 5.96942 3.66972 5.38226C3.62079 5.23547 3.32722 4.40367 3.76758 3.32722C3.76758 3.32722 4.4526 3.1315 5.96942 4.15902C6.6055 3.9633 7.29052 3.91437 7.97553 3.91437C8.66055 3.91437 9.34557 4.01223 9.98165 4.15902C11.4985 3.1315 12.1835 3.32722 12.1835 3.32722C12.6239 4.40367 12.3303 5.23547 12.2813 5.43119C12.7706 5.96942 13.1131 6.70336 13.1131 7.5841C13.1131 10.6667 11.2538 11.3028 9.49235 11.4985C9.78593 11.7431 10.0306 12.2324 10.0306 12.9664C10.0306 14.0428 10.0306 14.8746 10.0306 15.1682C10.0306 15.3639 10.1774 15.6086 10.5688 15.5596C13.7492 14.4832 16 11.4985 16 7.97553C15.9511 3.57186 12.3792 0 7.97553 0Z" fill="#C5C5C5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
7
extensions/ql-vscode/media/drive.svg
Normal file
7
extensions/ql-vscode/media/drive.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<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="M15.5 12.1952C15.5 12.9126 14.9137 13.4996 14.1957 13.4996H1.80435C1.08696 13.4996 0.5 12.9126 0.5 12.1952L0.5 9.80435C0.5 9.08696 1.08696 8.5 1.80435 8.5H14.1956C14.9137 8.5 15.5 9.08696 15.5 9.80435L15.5 12.1952Z" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.45654 11.5H13.5435" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 9.5C13.224 9.5 13 9.725 13 10C13 10.275 13.224 10.5 13.5 10.5C13.776 10.5 14 10.275 14 10C14 9.725 13.776 9.5 13.5 9.5" fill="#959DA5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 9.5C11.224 9.5 11 9.725 11 10C11 10.275 11.224 10.5 11.5 10.5C11.776 10.5 12 10.275 12 10C12 9.725 11.776 9.5 11.5 9.5" fill="#959DA5"/>
|
||||
<path d="M15.5 9.81464L13.8728 2.76261C13.6922 2.06804 12.9572 1.5 12.2391 1.5H3.76087C3.04348 1.5 2.30848 2.06804 2.12783 2.76261L0.5 9.8" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
16
extensions/ql-vscode/media/globe.svg
Normal file
16
extensions/ql-vscode/media/globe.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="7.5" cy="7.5" r="7" stroke="#959DA5"/>
|
||||
<mask id="mask0_394_2982" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="15" height="15">
|
||||
<circle cx="7.5" cy="7.5" r="7.5" fill="#C4C4C4"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_394_2982)">
|
||||
<path d="M14.5 7.5C14.5 9.42971 13.6822 11.1907 12.5493 12.4721C11.4035 13.7683 10.0054 14.5 8.90625 14.5C7.84644 14.5 6.81131 13.8113 6.01569 12.5383C5.22447 11.2724 4.71875 9.49235 4.71875 7.5C4.71875 5.50765 5.22447 3.72765 6.01569 2.4617C6.81131 1.1887 7.84644 0.5 8.90625 0.5C10.0054 0.5 11.4035 1.23172 12.5493 2.52786C13.6822 3.80934 14.5 5.57029 14.5 7.5Z" stroke="#959DA5"/>
|
||||
</g>
|
||||
<mask id="mask1_394_2982" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="1" y="0" width="16" height="15">
|
||||
<circle cx="9.375" cy="7.5" r="7.5" fill="#C4C4C4"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_394_2982)">
|
||||
<path d="M10.2812 7.5C10.2812 9.49235 9.77553 11.2724 8.98431 12.5383C8.18869 13.8113 7.15356 14.5 6.09375 14.5C4.99456 14.5 3.5965 13.7683 2.45067 12.4721C1.31781 11.1907 0.5 9.42971 0.5 7.5C0.5 5.57029 1.31781 3.80934 2.45067 2.52786C3.5965 1.23172 4.99456 0.5 6.09375 0.5C7.15356 0.5 8.18869 1.1887 8.98431 2.4617C9.77553 3.72765 10.2812 5.50765 10.2812 7.5Z" stroke="#959DA5"/>
|
||||
</g>
|
||||
<line y1="7.5" x2="15" y2="7.5" stroke="#959DA5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
11
extensions/ql-vscode/media/light/github.svg
Normal file
11
extensions/ql-vscode/media/light/github.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<!-- From https://github.com/microsoft/vscode-icons -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97578 0C3.57211 0 0.000244141 3.57186 0.000244141 7.97553C0.000244141 11.4985 2.29994 14.4832 5.43144 15.5596C5.82287 15.6086 5.96966 15.3639 5.96966 15.1682C5.96966 14.9725 5.96966 14.4832 5.96966 13.7982C3.76783 14.2875 3.27853 12.7217 3.27853 12.7217C2.93602 11.792 2.3978 11.5474 2.3978 11.5474C1.66385 11.0581 2.44673 11.0581 2.44673 11.0581C3.2296 11.107 3.66997 11.8899 3.66997 11.8899C4.40391 13.1131 5.5293 12.7706 5.96966 12.5749C6.01859 12.0367 6.26324 11.6942 6.45896 11.4985C4.69749 11.3028 2.83816 10.6177 2.83816 7.53517C2.83816 6.65443 3.13174 5.96942 3.66997 5.38226C3.62104 5.23547 3.32746 4.40367 3.76783 3.32722C3.76783 3.32722 4.45284 3.1315 5.96966 4.15902C6.60575 3.9633 7.29076 3.91437 7.97578 3.91437C8.66079 3.91437 9.34581 4.01223 9.98189 4.15902C11.4987 3.1315 12.1837 3.32722 12.1837 3.32722C12.6241 4.40367 12.3305 5.23547 12.2816 5.43119C12.7709 5.96942 13.1134 6.70336 13.1134 7.5841C13.1134 10.6667 11.2541 11.3028 9.4926 11.4985C9.78618 11.7431 10.0308 12.2324 10.0308 12.9664C10.0308 14.0428 10.0308 14.8746 10.0308 15.1682C10.0308 15.3639 10.1776 15.6086 10.5691 15.5596C13.7495 14.4832 16.0002 11.4985 16.0002 7.97553C15.9513 3.57186 12.3794 0 7.97578 0Z" fill="#424242"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.000244141)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4986
extensions/ql-vscode/package-lock.json
generated
4986
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.5.10",
|
||||
"version": "1.6.3",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -13,7 +13,9 @@
|
||||
"url": "https://github.com/github/vscode-codeql"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.57.0"
|
||||
"vscode": "^1.59.0",
|
||||
"node": ">=14.17.1",
|
||||
"npm": ">=7.20.6"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
@@ -43,14 +45,17 @@
|
||||
"onCommand:codeQLDatabases.chooseDatabaseFolder",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseArchive",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseInternet",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseGithub",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
|
||||
"onCommand:codeQL.setCurrentDatabase",
|
||||
"onCommand:codeQL.viewAst",
|
||||
"onCommand:codeQL.viewCfg",
|
||||
"onCommand:codeQL.openReferencedFile",
|
||||
"onCommand:codeQL.previewQueryHelp",
|
||||
"onCommand:codeQL.chooseDatabaseFolder",
|
||||
"onCommand:codeQL.chooseDatabaseArchive",
|
||||
"onCommand:codeQL.chooseDatabaseInternet",
|
||||
"onCommand:codeQL.chooseDatabaseGithub",
|
||||
"onCommand:codeQL.chooseDatabaseLgtm",
|
||||
"onCommand:codeQLDatabases.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.setCurrentDatabase",
|
||||
@@ -131,7 +136,7 @@
|
||||
"title": "CodeQL",
|
||||
"properties": {
|
||||
"codeQL.cli.executablePath": {
|
||||
"scope": "window",
|
||||
"scope": "machine-overridable",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. If empty, the extension will look for a CodeQL executable on your shell PATH, or if CodeQL is not on your PATH, download and manage its own CodeQL executable."
|
||||
@@ -205,7 +210,8 @@
|
||||
null
|
||||
],
|
||||
"default": null,
|
||||
"description": "Path to a directory where the CodeQL extension should store query server logs. If empty, the extension stores logs in a temporary workspace folder and deletes the contents after each run."
|
||||
"description": "Path to a directory where the CodeQL extension should store query server logs. If empty, the extension stores logs in a temporary workspace folder and deletes the contents after each run.",
|
||||
"markdownDeprecationMessage": "This property is deprecated and no longer has any effect. All query logs are stored in the query history folder next to the query results."
|
||||
},
|
||||
"codeQL.runningQueries.quickEvalCodelens": {
|
||||
"type": "boolean",
|
||||
@@ -222,6 +228,12 @@
|
||||
"default": "%q on %d - %s, %r result count [%t]",
|
||||
"markdownDescription": "Default string for how to label query history items.\n* %t is the time of the query\n* %q is the human-readable query name\n* %f is the query file name\n* %d is the database name\n* %r is the number of results\n* %s is a status string"
|
||||
},
|
||||
"codeQL.queryHistory.ttl": {
|
||||
"type": "number",
|
||||
"default": 30,
|
||||
"description": "Number of days to retain queries in the query history before being automatically deleted.",
|
||||
"scope": "machine"
|
||||
},
|
||||
"codeQL.runningTests.additionalTestArguments": {
|
||||
"scope": "window",
|
||||
"type": "array",
|
||||
@@ -248,7 +260,7 @@
|
||||
"scope": "application",
|
||||
"description": "Specifies whether or not to write telemetry events to the extension log."
|
||||
},
|
||||
"codeQL.remoteQueries.repositoryLists": {
|
||||
"codeQL.variantAnalysis.repositoryLists": {
|
||||
"type": [
|
||||
"object",
|
||||
null
|
||||
@@ -262,14 +274,14 @@
|
||||
}
|
||||
},
|
||||
"default": null,
|
||||
"markdownDescription": "[For internal use only] Lists of GitHub repositories that you want to query remotely. This should be a JSON object where each key is a user-specified name for this repository list, and the value is an array of GitHub repositories (of the form `<owner>/<repo>`)."
|
||||
"markdownDescription": "[For internal use only] Lists of GitHub repositories that you want to run variant analysis against. This should be a JSON object where each key is a user-specified name for this repository list, and the value is an array of GitHub repositories (of the form `<owner>/<repo>`)."
|
||||
},
|
||||
"codeQL.remoteQueries.controllerRepo": {
|
||||
"codeQL.variantAnalysis.controllerRepo": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"pattern": "^$|^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+/[a-zA-Z0-9-_]+$",
|
||||
"patternErrorMessage": "Please enter a valid GitHub repository",
|
||||
"markdownDescription": "[For internal use only] The name of the GitHub repository where you can view the progress and results of the \"Run Remote query\" command. The repository should be of the form `<owner>/<repo>`)."
|
||||
"markdownDescription": "[For internal use only] The name of the GitHub repository where you can view the progress and results of the \"Run Variant Analysis\" command. The repository should be of the form `<owner>/<repo>`)."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -287,12 +299,8 @@
|
||||
"title": "CodeQL: Run Query on Multiple Databases"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runRemoteQuery",
|
||||
"title": "CodeQL: Run Remote Query"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.showFakeRemoteQueryResults",
|
||||
"title": "CodeQL: [Internal] Show fake remote query results"
|
||||
"command": "codeQL.runVariantAnalysis",
|
||||
"title": "CodeQL: Run Variant Analysis"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
@@ -350,6 +358,14 @@
|
||||
"dark": "media/dark/cloud-download.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseGithub",
|
||||
"title": "Download Database from GitHub",
|
||||
"icon": {
|
||||
"light": "media/light/github.svg",
|
||||
"dark": "media/dark/github.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseLgtm",
|
||||
"title": "Download from LGTM",
|
||||
@@ -366,6 +382,10 @@
|
||||
"command": "codeQL.viewAst",
|
||||
"title": "CodeQL: View AST"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"title": "CodeQL: View CFG"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.upgradeCurrentDatabase",
|
||||
"title": "CodeQL: Upgrade Current Database"
|
||||
@@ -418,6 +438,10 @@
|
||||
"command": "codeQL.chooseDatabaseInternet",
|
||||
"title": "CodeQL: Download Database"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseGithub",
|
||||
"title": "CodeQL: Download Database from GitHub"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseLgtm",
|
||||
"title": "CodeQL: Download Database from LGTM"
|
||||
@@ -494,6 +518,22 @@
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"title": "Show Query Log"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQueryDirectory",
|
||||
"title": "Open query directory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showEvalLog",
|
||||
"title": "Show Evaluator Log (Raw)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showEvalLogSummary",
|
||||
"title": "Show Evaluator Log (Summary)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.cancel",
|
||||
"title": "Cancel"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"title": "Show Query Text"
|
||||
@@ -522,6 +562,10 @@
|
||||
"command": "codeQLQueryHistory.compareWith",
|
||||
"title": "Compare Results"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openOnGithub",
|
||||
"title": "Open Variant Analysis on GitHub"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryResults.nextPathStep",
|
||||
"title": "CodeQL: Show Next Step on Path"
|
||||
@@ -582,6 +626,11 @@
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseGithub",
|
||||
"when": "config.codeQL.canary && view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseLgtm",
|
||||
"when": "view == codeQLDatabases",
|
||||
@@ -662,7 +711,7 @@
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
@@ -672,12 +721,27 @@
|
||||
{
|
||||
"command": "codeQLQueryHistory.compareWith",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQueryDirectory",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && !hasRemoteServer"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showEvalLog",
|
||||
"group": "9_qlCommands",
|
||||
"when": "codeql.supportsEvalLog && (viewItem == rawResultsItem || viewItem == interpretedResultsItem || viewItem == cancelledResultsItem)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showEvalLogSummary",
|
||||
"group": "9_qlCommands",
|
||||
"when": "codeql.supportsEvalLog && (viewItem == rawResultsItem || viewItem == interpretedResultsItem || viewItem == cancelledResultsItem)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
@@ -687,37 +751,42 @@
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewCsvResults",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && viewItem != interpretedResultsItem"
|
||||
"when": "viewItem == rawResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewCsvAlerts",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
|
||||
"when": "viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewSarifAlerts",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
|
||||
"when": "viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewDil",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.previewQueryHelp",
|
||||
"command": "codeQLQueryHistory.cancel",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && resourceScheme == .qhelp && isWorkspaceTrusted"
|
||||
"when": "viewItem == inProgressResultsItem || viewItem == inProgressRemoteResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openOnGithub",
|
||||
"group": "9_qlCommands",
|
||||
"when": "viewItem == remoteResultsItem || viewItem == inProgressRemoteResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLTests.showOutputDifferences",
|
||||
"group": "qltest@1",
|
||||
"when": "view == test-explorer && viewItem == testWithSource"
|
||||
"when": "viewItem == testWithSource"
|
||||
},
|
||||
{
|
||||
"command": "codeQLTests.acceptOutput",
|
||||
"group": "qltest@2",
|
||||
"when": "view == test-explorer && viewItem == testWithSource"
|
||||
"when": "viewItem == testWithSource"
|
||||
}
|
||||
],
|
||||
"explorer/context": [
|
||||
@@ -729,7 +798,12 @@
|
||||
{
|
||||
"command": "codeQL.viewAst",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
"when": "resourceScheme == codeql-zip-archive && !explorerResourceIsFolder && !listMultiSelection"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
@@ -761,13 +835,9 @@
|
||||
"when": "resourceLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runRemoteQuery",
|
||||
"command": "codeQL.runVariantAnalysis",
|
||||
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.showFakeRemoteQueryResults",
|
||||
"when": "config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
"when": "false"
|
||||
@@ -792,6 +862,14 @@
|
||||
"command": "codeQL.viewAst",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseGithub",
|
||||
"when": "config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.setCurrentDatabase",
|
||||
"when": "false"
|
||||
@@ -836,6 +914,10 @@
|
||||
"command": "codeQLDatabases.chooseDatabaseInternet",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseGithub",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseLgtm",
|
||||
"when": "false"
|
||||
@@ -860,6 +942,26 @@
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showEvalLog",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showEvalLogSummary",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQueryDirectory",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.cancel",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openOnGithub",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"when": "false"
|
||||
@@ -927,13 +1029,17 @@
|
||||
"when": "editorLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runRemoteQuery",
|
||||
"command": "codeQL.runVariantAnalysis",
|
||||
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewAst",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEval",
|
||||
"when": "editorLangId == ql"
|
||||
@@ -992,6 +1098,8 @@
|
||||
"build": "gulp",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:extension": "tsc --watch",
|
||||
"watch:webpack": "gulp watchView",
|
||||
"watch:css": "gulp watchCss",
|
||||
"test": "mocha --exit -r ts-node/register test/pure-tests/**/*.ts",
|
||||
"preintegration": "rm -rf ./out/vscode-tests && gulp",
|
||||
"integration": "node ./out/vscode-tests/run-integration-tests.js no-workspace,minimal-workspace",
|
||||
@@ -1003,20 +1111,27 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^18.5.6",
|
||||
"@primer/octicons-react": "^16.3.0",
|
||||
"@primer/react": "^35.0.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"classnames": "~2.2.6",
|
||||
"fs-extra": "^9.0.1",
|
||||
"d3": "^6.3.1",
|
||||
"d3-graphviz": "^2.6.1",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob-promise": "^3.4.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"minimist": "~1.2.5",
|
||||
"minimist": "~1.2.6",
|
||||
"nanoid": "^3.2.0",
|
||||
"node-fetch": "~2.6.7",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"semver": "~7.3.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"stream": "^0.0.2",
|
||||
"stream-chain": "~2.2.4",
|
||||
"stream-json": "~1.7.3",
|
||||
"styled-components": "^5.3.3",
|
||||
"tmp": "^0.1.0",
|
||||
"tmp-promise": "~3.0.2",
|
||||
"tree-kill": "~1.2.2",
|
||||
@@ -1033,21 +1148,24 @@
|
||||
"@types/chai-as-promised": "~7.1.2",
|
||||
"@types/child-process-promise": "^2.2.1",
|
||||
"@types/classnames": "~2.2.9",
|
||||
"@types/d3": "^6.2.0",
|
||||
"@types/d3-graphviz": "^2.6.6",
|
||||
"@types/del": "^4.0.0",
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/google-protobuf": "^3.2.7",
|
||||
"@types/gulp": "^4.0.9",
|
||||
"@types/gulp-replace": "0.0.31",
|
||||
"@types/gulp-replace": "^1.1.0",
|
||||
"@types/gulp-sourcemaps": "0.0.32",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/jszip": "~3.1.6",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^12.14.1",
|
||||
"@types/nanoid": "^3.0.0",
|
||||
"@types/node": "^16.11.25",
|
||||
"@types/node-fetch": "~2.5.2",
|
||||
"@types/proxyquire": "~1.3.28",
|
||||
"@types/react": "^16.8.17",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/sarif": "~2.1.2",
|
||||
"@types/semver": "~7.2.0",
|
||||
"@types/sinon": "~7.5.2",
|
||||
@@ -1057,8 +1175,8 @@
|
||||
"@types/through2": "^2.0.36",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"@types/unzipper": "~0.10.1",
|
||||
"@types/vscode": "^1.57.0",
|
||||
"@types/webpack": "^4.32.1",
|
||||
"@types/vscode": "^1.59.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/xml2js": "~0.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
@@ -1072,8 +1190,8 @@
|
||||
"eslint-plugin-react": "~7.19.0",
|
||||
"glob": "^7.1.4",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-replace": "^1.0.0",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"husky": "~4.2.5",
|
||||
"jsonc-parser": "^2.3.0",
|
||||
@@ -1083,16 +1201,16 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "~2.0.5",
|
||||
"proxyquire": "~2.1.3",
|
||||
"sinon": "~9.0.0",
|
||||
"sinon": "~13.0.1",
|
||||
"sinon-chai": "~3.5.0",
|
||||
"style-loader": "~0.23.1",
|
||||
"through2": "^3.0.1",
|
||||
"through2": "^4.0.2",
|
||||
"ts-loader": "^8.1.0",
|
||||
"ts-node": "^8.3.0",
|
||||
"ts-node": "^10.7.0",
|
||||
"ts-protoc-gen": "^0.9.0",
|
||||
"typescript": "^4.3.2",
|
||||
"typescript": "^4.5.5",
|
||||
"typescript-formatter": "^7.2.2",
|
||||
"vsce": "^1.65.0",
|
||||
"vsce": "^2.7.0",
|
||||
"vscode-test": "^1.4.0",
|
||||
"webpack": "^5.28.0",
|
||||
"webpack-cli": "^4.6.0"
|
||||
@@ -1100,7 +1218,7 @@
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run format-staged",
|
||||
"pre-push": "npm run lint"
|
||||
"pre-push": "npm run lint && scripts/forbid-mocha-only"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
6
extensions/ql-vscode/scripts/forbid-mocha-only
Executable file
6
extensions/ql-vscode/scripts/forbid-mocha-only
Executable file
@@ -0,0 +1,6 @@
|
||||
if grep -rq --include '*.test.ts' 'it.only\|describe.only' './test' './src'; then
|
||||
echo 'There is a .only() in the tests. Please remove it.'
|
||||
exit 1;
|
||||
else
|
||||
exit 0;
|
||||
fi
|
||||
15
extensions/ql-vscode/src/additional-typings.d.ts
vendored
Normal file
15
extensions/ql-vscode/src/additional-typings.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* The d3 library is designed to work in both the browser and
|
||||
* node. Consequently their typings files refer to both node
|
||||
* types like `Buffer` (which don't exist in the browser), and browser
|
||||
* types like `Blob` (which don't exist in node). Instead of sticking
|
||||
* all of `dom` in `compilerOptions.lib`, it suffices just to put in a
|
||||
* stub definition of the affected types so that compilation
|
||||
* succeeds.
|
||||
*/
|
||||
|
||||
declare type RequestInit = Record<string, unknown>;
|
||||
declare type ElementTagNameMap = any;
|
||||
declare type NodeListOf<T> = Record<string, T>;
|
||||
declare type Node = Record<string, unknown>;
|
||||
declare type XMLDocument = Record<string, unknown>;
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
TextEditorSelectionChangeEvent,
|
||||
TextEditorSelectionChangeKind,
|
||||
Location,
|
||||
Range
|
||||
Range,
|
||||
Uri
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -104,7 +105,7 @@ class AstViewerDataProvider extends DisposableObject implements TreeDataProvider
|
||||
export class AstViewer extends DisposableObject {
|
||||
private treeView: TreeView<AstItem>;
|
||||
private treeDataProvider: AstViewerDataProvider;
|
||||
private currentFile: string | undefined;
|
||||
private currentFileUri: Uri | undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -125,12 +126,12 @@ export class AstViewer extends DisposableObject {
|
||||
this.push(window.onDidChangeTextEditorSelection(this.updateTreeSelection, this));
|
||||
}
|
||||
|
||||
updateRoots(roots: AstItem[], db: DatabaseItem, fileName: string) {
|
||||
updateRoots(roots: AstItem[], db: DatabaseItem, fileUri: Uri) {
|
||||
this.treeDataProvider.roots = roots;
|
||||
this.treeDataProvider.db = db;
|
||||
this.treeDataProvider.refresh();
|
||||
this.treeView.message = `AST for ${path.basename(fileName)}`;
|
||||
this.currentFile = fileName;
|
||||
this.treeView.message = `AST for ${path.basename(fileUri.fsPath)}`;
|
||||
this.currentFileUri = fileUri;
|
||||
// Handle error on reveal. This could happen if
|
||||
// the tree view is disposed during the reveal.
|
||||
this.treeView.reveal(roots[0], { focus: false })?.then(
|
||||
@@ -174,7 +175,7 @@ export class AstViewer extends DisposableObject {
|
||||
|
||||
if (
|
||||
this.treeView.visible &&
|
||||
e.textEditor.document.uri.fsPath === this.currentFile &&
|
||||
e.textEditor.document.uri.fsPath === this.currentFileUri?.fsPath &&
|
||||
e.selections.length === 1
|
||||
) {
|
||||
const selection = e.selections[0];
|
||||
@@ -199,6 +200,6 @@ export class AstViewer extends DisposableObject {
|
||||
this.treeDataProvider.db = undefined;
|
||||
this.treeDataProvider.refresh();
|
||||
this.treeView.message = undefined;
|
||||
this.currentFile = undefined;
|
||||
this.currentFileUri = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
11
extensions/ql-vscode/src/blob.d.ts
vendored
11
extensions/ql-vscode/src/blob.d.ts
vendored
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* The npm library jszip is designed to work in both the browser and
|
||||
* node. Consequently its typings @types/jszip refers to both node
|
||||
* types like `Buffer` (which don't exist in the browser), and browser
|
||||
* types like `Blob` (which don't exist in node). Instead of sticking
|
||||
* all of `dom` in `compilerOptions.lib`, it suffices just to put in a
|
||||
* stub definition of the type `Blob` here so that compilation
|
||||
* succeeds.
|
||||
*/
|
||||
|
||||
declare type Blob = string;
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as semver from 'semver';
|
||||
import { runCodeQlCliCommand } from './cli';
|
||||
import { Logger } from './logging';
|
||||
import { getErrorMessage } from './pure/helpers-pure';
|
||||
|
||||
/**
|
||||
* Get the version of a CodeQL CLI.
|
||||
@@ -18,7 +19,7 @@ export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): P
|
||||
} catch (e) {
|
||||
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted.
|
||||
// Either way, we can't determine compatibility.
|
||||
void logger.log(`Failed to run 'codeql version'. Reason: ${e.message}`);
|
||||
void logger.log(`Failed to run 'codeql version'. Reason: ${getErrorMessage(e)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as cpp from 'child-process-promise';
|
||||
import * as child_process from 'child_process';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as sarif from 'sarif';
|
||||
import { SemVer } from 'semver';
|
||||
@@ -7,17 +8,17 @@ import { Readable } from 'stream';
|
||||
import { StringDecoder } from 'string_decoder';
|
||||
import * as tk from 'tree-kill';
|
||||
import { promisify } from 'util';
|
||||
import { CancellationToken, Disposable, Uri } from 'vscode';
|
||||
import { CancellationToken, commands, Disposable, Uri } from 'vscode';
|
||||
|
||||
import { BQRSInfo, DecodedBqrsChunk } from './pure/bqrs-cli-types';
|
||||
import { CliConfig } from './config';
|
||||
import { DistributionProvider, FindDistributionResultKind } from './distribution';
|
||||
import { assertNever } from './pure/helpers-pure';
|
||||
import { assertNever, getErrorMessage, getErrorStack } from './pure/helpers-pure';
|
||||
import { QueryMetadata, SortDirection } from './pure/interface-types';
|
||||
import { Logger, ProgressReporter } from './logging';
|
||||
import { CompilationMessage } from './pure/messages';
|
||||
import { sarifParser } from './sarif-parser';
|
||||
import { dbSchemeToLanguage } from './helpers';
|
||||
import { dbSchemeToLanguage, walkDirectory } from './helpers';
|
||||
|
||||
/**
|
||||
* The version of the SARIF format that we are using.
|
||||
@@ -345,7 +346,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
stderrBuffers.length == 0
|
||||
? new Error(`${description} failed: ${err}`)
|
||||
: new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString('utf8')}`);
|
||||
newError.stack += (err.stack || '');
|
||||
newError.stack += getErrorStack(err);
|
||||
throw newError;
|
||||
} finally {
|
||||
void this.logger.log(Buffer.concat(stderrBuffers).toString('utf8'));
|
||||
@@ -403,7 +404,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
try {
|
||||
if (cancellationToken !== undefined) {
|
||||
cancellationRegistration = cancellationToken.onCancellationRequested(_e => {
|
||||
tk(child.pid);
|
||||
tk(child.pid || 0);
|
||||
});
|
||||
}
|
||||
if (logger !== undefined) {
|
||||
@@ -447,7 +448,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
try {
|
||||
yield JSON.parse(event) as EventType;
|
||||
} catch (err) {
|
||||
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
|
||||
throw new Error(`Parsing output of ${description} failed: ${(err as any).stderr || getErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,7 +503,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
try {
|
||||
return JSON.parse(result) as OutputType;
|
||||
} catch (err) {
|
||||
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
|
||||
throw new Error(`Parsing output of ${description} failed: ${(err as any).stderr || getErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,8 +515,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
async resolveLibraryPath(workspaces: string[], queryPath: string): Promise<QuerySetup> {
|
||||
const subcommandArgs = [
|
||||
'--query', queryPath,
|
||||
'--additional-packs',
|
||||
workspaces.join(path.delimiter)
|
||||
...this.getAdditionalPacksArg(workspaces)
|
||||
];
|
||||
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, 'Resolving library paths');
|
||||
}
|
||||
@@ -528,8 +528,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
const subcommandArgs = [
|
||||
'--format', 'bylanguage',
|
||||
queryUri.fsPath,
|
||||
'--additional-packs',
|
||||
workspaces.join(path.delimiter)
|
||||
...this.getAdditionalPacksArg(workspaces)
|
||||
];
|
||||
return JSON.parse(await this.runCodeQlCliCommand(['resolve', 'queries'], subcommandArgs, 'Resolving query by language'));
|
||||
}
|
||||
@@ -562,6 +561,17 @@ export class CodeQLCliServer implements Disposable {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues an internal clear-cache command to the cli server. This
|
||||
* command is used to clear the qlpack cache of the server.
|
||||
*
|
||||
* This cache is generally cleared every 1s. This method is used
|
||||
* to force an early clearing of the cache.
|
||||
*/
|
||||
public async clearCache(): Promise<void> {
|
||||
await this.runCodeQlCliCommand(['clear-cache'], [], 'Clearing qlpack cache');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs QL tests.
|
||||
* @param testPaths Full paths of the tests to run.
|
||||
@@ -573,7 +583,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
): AsyncGenerator<TestCompleted, void, unknown> {
|
||||
|
||||
const subcommandArgs = this.cliConfig.additionalTestArguments.concat([
|
||||
'--additional-packs', workspaces.join(path.delimiter),
|
||||
...this.getAdditionalPacksArg(workspaces),
|
||||
'--threads',
|
||||
this.cliConfig.numberTestThreads.toString(),
|
||||
...testPaths
|
||||
@@ -595,8 +605,12 @@ export class CodeQLCliServer implements Disposable {
|
||||
|
||||
/** Resolves the ML models that should be available when evaluating a query. */
|
||||
async resolveMlModels(additionalPacks: string[]): Promise<MlModelsInfo> {
|
||||
return await this.runJsonCodeQlCliCommand<MlModelsInfo>(['resolve', 'ml-models'], ['--additional-packs',
|
||||
additionalPacks.join(path.delimiter)], 'Resolving ML models', false);
|
||||
return await this.runJsonCodeQlCliCommand<MlModelsInfo>(
|
||||
['resolve', 'ml-models'],
|
||||
this.getAdditionalPacksArg(additionalPacks),
|
||||
'Resolving ML models',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -651,6 +665,26 @@ export class CodeQLCliServer implements Disposable {
|
||||
return await this.runCodeQlCliCommand(['generate', 'query-help'], subcommandArgs, `Generating qhelp in markdown format at ${outputDirectory}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a summary of an evaluation log.
|
||||
* @param endSummaryPath The path to write only the end of query part of the human-readable summary to.
|
||||
* @param inputPath The path of an evaluation event log.
|
||||
* @param outputPath The path to write a human-readable summary of it to.
|
||||
*/
|
||||
async generateLogSummary(
|
||||
inputPath: string,
|
||||
outputPath: string,
|
||||
endSummaryPath: string,
|
||||
): Promise<string> {
|
||||
const subcommandArgs = [
|
||||
'--format=text',
|
||||
`--end-summary=${endSummaryPath}`,
|
||||
inputPath,
|
||||
outputPath
|
||||
];
|
||||
return await this.runCodeQlCliCommand(['generate', 'log-summary'], subcommandArgs, 'Generating log summary');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the results from a bqrs.
|
||||
* @param bqrsPath The path to the bqrs.
|
||||
@@ -674,20 +708,13 @@ export class CodeQLCliServer implements Disposable {
|
||||
return await this.runJsonCodeQlCliCommand<DecodedBqrsChunk>(['bqrs', 'decode'], subcommandArgs, 'Reading bqrs data');
|
||||
}
|
||||
|
||||
async runInterpretCommand(format: string, metadata: QueryMetadata, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo) {
|
||||
async runInterpretCommand(format: string, additonalArgs: string[], metadata: QueryMetadata, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo) {
|
||||
const args = [
|
||||
'--output', interpretedResultsPath,
|
||||
'--format', format,
|
||||
// Forward all of the query metadata.
|
||||
...Object.entries(metadata).map(([key, value]) => `-t=${key}=${value}`)
|
||||
];
|
||||
if (format == SARIF_FORMAT) {
|
||||
// TODO: This flag means that we don't group interpreted results
|
||||
// by primary location. We may want to revisit whether we call
|
||||
// interpretation with and without this flag, or do some
|
||||
// grouping client-side.
|
||||
args.push('--no-group-results');
|
||||
}
|
||||
].concat(additonalArgs);
|
||||
if (sourceInfo !== undefined) {
|
||||
args.push(
|
||||
'--source-archive', sourceInfo.sourceArchive,
|
||||
@@ -709,13 +736,47 @@ export class CodeQLCliServer implements Disposable {
|
||||
await this.runCodeQlCliCommand(['bqrs', 'interpret'], args, 'Interpreting query results');
|
||||
}
|
||||
|
||||
async interpretBqrs(metadata: QueryMetadata, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
|
||||
await this.runInterpretCommand(SARIF_FORMAT, metadata, resultsPath, interpretedResultsPath, sourceInfo);
|
||||
async interpretBqrsSarif(metadata: QueryMetadata, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
|
||||
const additionalArgs = [
|
||||
// TODO: This flag means that we don't group interpreted results
|
||||
// by primary location. We may want to revisit whether we call
|
||||
// interpretation with and without this flag, or do some
|
||||
// grouping client-side.
|
||||
'--no-group-results'
|
||||
];
|
||||
|
||||
await this.runInterpretCommand(SARIF_FORMAT, additionalArgs, metadata, resultsPath, interpretedResultsPath, sourceInfo);
|
||||
return await sarifParser(interpretedResultsPath);
|
||||
}
|
||||
|
||||
// Warning: this function is untenable for large dot files,
|
||||
async readDotFiles(dir: string): Promise<string[]> {
|
||||
const dotFiles: Promise<string>[] = [];
|
||||
for await (const file of walkDirectory(dir)) {
|
||||
if (file.endsWith('.dot')) {
|
||||
dotFiles.push(fs.readFile(file, 'utf8'));
|
||||
}
|
||||
}
|
||||
return Promise.all(dotFiles);
|
||||
}
|
||||
|
||||
async interpretBqrsGraph(metadata: QueryMetadata, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<string[]> {
|
||||
const additionalArgs = sourceInfo
|
||||
? ['--dot-location-url-format', 'file://' + sourceInfo.sourceLocationPrefix + '{path}:{start:line}:{start:column}:{end:line}:{end:column}']
|
||||
: [];
|
||||
|
||||
await this.runInterpretCommand('dot', additionalArgs, metadata, resultsPath, interpretedResultsPath, sourceInfo);
|
||||
|
||||
try {
|
||||
const dot = await this.readDotFiles(interpretedResultsPath);
|
||||
return dot;
|
||||
} catch (err) {
|
||||
throw new Error(`Reading output of interpretation failed: ${getErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async generateResultsCsv(metadata: QueryMetadata, resultsPath: string, csvPath: string, sourceInfo?: SourceInfo): Promise<void> {
|
||||
await this.runInterpretCommand(CSV_FORMAT, metadata, resultsPath, csvPath, sourceInfo);
|
||||
await this.runInterpretCommand(CSV_FORMAT, [], metadata, resultsPath, csvPath, sourceInfo);
|
||||
}
|
||||
|
||||
async sortBqrs(resultsPath: string, sortedResultsPath: string, resultSet: string, sortKeys: number[], sortDirections: SortDirection[]): Promise<void> {
|
||||
@@ -761,7 +822,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
* @returns A list of database upgrade script directories
|
||||
*/
|
||||
async resolveUpgrades(dbScheme: string, searchPath: string[], allowDowngradesIfPossible: boolean, targetDbScheme?: string): Promise<UpgradesInfo> {
|
||||
const args = ['--additional-packs', searchPath.join(path.delimiter), '--dbscheme', dbScheme];
|
||||
const args = [...this.getAdditionalPacksArg(searchPath), '--dbscheme', dbScheme];
|
||||
if (targetDbScheme) {
|
||||
args.push('--target-dbscheme', targetDbScheme);
|
||||
if (allowDowngradesIfPossible && await this.cliConstraints.supportsDowngrades()) {
|
||||
@@ -783,7 +844,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
* @returns A dictionary mapping qlpack name to the directory it comes from
|
||||
*/
|
||||
resolveQlpacks(additionalPacks: string[], searchPath?: string[]): Promise<QlpacksInfo> {
|
||||
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
|
||||
const args = this.getAdditionalPacksArg(additionalPacks);
|
||||
if (searchPath?.length) {
|
||||
args.push('--search-path', path.join(...searchPath));
|
||||
}
|
||||
@@ -829,7 +890,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
* @returns A list of query files found.
|
||||
*/
|
||||
async resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
|
||||
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
|
||||
const args = this.getAdditionalPacksArg(additionalPacks);
|
||||
if (searchPath !== undefined) {
|
||||
args.push('--search-path', path.join(...searchPath));
|
||||
}
|
||||
@@ -862,8 +923,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
'-o',
|
||||
outputPath,
|
||||
dir,
|
||||
'--additional-packs',
|
||||
workspaceFolders.join(path.delimiter)
|
||||
...this.getAdditionalPacksArg(workspaceFolders)
|
||||
];
|
||||
if (!precompile && await this.cliConstraints.supportsNoPrecompile()) {
|
||||
args.push('--no-precompile');
|
||||
@@ -900,6 +960,10 @@ export class CodeQLCliServer implements Disposable {
|
||||
public async getVersion() {
|
||||
if (!this._version) {
|
||||
this._version = await this.refreshVersion();
|
||||
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
|
||||
await commands.executeCommand(
|
||||
'setContext', 'codeql.supportsEvalLog', await this.cliConstraints.supportsPerQueryEvalLog()
|
||||
);
|
||||
}
|
||||
return this._version;
|
||||
}
|
||||
@@ -918,6 +982,12 @@ export class CodeQLCliServer implements Disposable {
|
||||
throw new Error('No distribution found');
|
||||
}
|
||||
}
|
||||
|
||||
private getAdditionalPacksArg(paths: string[]): string[] {
|
||||
return paths.length
|
||||
? ['--additional-packs', paths.join(path.delimiter)]
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1004,7 +1074,7 @@ export async function runCodeQlCliCommand(
|
||||
void logger.log('CLI command succeeded.');
|
||||
return result.stdout;
|
||||
} catch (err) {
|
||||
throw new Error(`${description} failed: ${err.stderr || err}`);
|
||||
throw new Error(`${description} failed: ${(err as any).stderr || getErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,8 +1130,8 @@ class SplitBuffer {
|
||||
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.substr(0, this.searchIndex);
|
||||
this.buffer = this.buffer.substr(this.searchIndex + separator.length);
|
||||
const line = this.buffer.slice(0, this.searchIndex);
|
||||
this.buffer = this.buffer.slice(this.searchIndex + separator.length);
|
||||
this.searchIndex = 0;
|
||||
return line;
|
||||
}
|
||||
@@ -1204,6 +1274,23 @@ export class CliVersionConstraint {
|
||||
*/
|
||||
public static CLI_VERSION_WITH_PACKAGING = new SemVer('2.6.0');
|
||||
|
||||
/**
|
||||
* CLI version where the `--evaluator-log` and related options to the query server were introduced,
|
||||
* on a per-query server basis.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_STRUCTURED_EVAL_LOG = new SemVer('2.8.2');
|
||||
|
||||
/**
|
||||
* CLI version that supports rotating structured logs to produce one per query.
|
||||
*
|
||||
* Note that 2.8.4 supports generating the evaluation logs and summaries,
|
||||
* but 2.9.0 includes a new option to produce the end-of-query summary logs to
|
||||
* the query server console. For simplicity we gate all features behind 2.9.0,
|
||||
* but if a user is tied to the 2.8 release, we can enable evaluator logs
|
||||
* and summaries for them.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_PER_QUERY_EVAL_LOG = new SemVer('2.9.0');
|
||||
|
||||
constructor(private readonly cli: CodeQLCliServer) {
|
||||
/**/
|
||||
}
|
||||
@@ -1259,4 +1346,12 @@ export class CliVersionConstraint {
|
||||
async supportsPackaging() {
|
||||
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_PACKAGING);
|
||||
}
|
||||
|
||||
async supportsStructuredEvalLog() {
|
||||
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_STRUCTURED_EVAL_LOG);
|
||||
}
|
||||
|
||||
async supportsPerQueryEvalLog() {
|
||||
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'vscode';
|
||||
import { showAndLogErrorMessage, showAndLogWarningMessage } from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { getErrorMessage, getErrorStack } from './pure/helpers-pure';
|
||||
import { telemetryListener } from './telemetry';
|
||||
|
||||
export class UserCancellationException extends Error {
|
||||
@@ -121,8 +122,9 @@ export function commandRunner(
|
||||
try {
|
||||
return await task(...args);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
const errorMessage = `${e.message || e} (${commandId})`;
|
||||
const errorMessage = `${getErrorMessage(e) || e} (${commandId})`;
|
||||
error = e instanceof Error ? e : new Error(errorMessage);
|
||||
const errorStack = getErrorStack(e);
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
if (e.silent) {
|
||||
@@ -132,8 +134,8 @@ export function commandRunner(
|
||||
}
|
||||
} else {
|
||||
// Include the full stack in the error log only.
|
||||
const fullMessage = e.stack
|
||||
? `${errorMessage}\n${e.stack}`
|
||||
const fullMessage = errorStack
|
||||
? `${errorMessage}\n${errorStack}`
|
||||
: errorMessage;
|
||||
void showAndLogErrorMessage(errorMessage, {
|
||||
fullMessage
|
||||
@@ -160,7 +162,8 @@ export function commandRunner(
|
||||
export function commandRunnerWithProgress<R>(
|
||||
commandId: string,
|
||||
task: ProgressTask<R>,
|
||||
progressOptions: Partial<ProgressOptions>
|
||||
progressOptions: Partial<ProgressOptions>,
|
||||
outputLogger = logger
|
||||
): Disposable {
|
||||
return commands.registerCommand(commandId, async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
@@ -172,21 +175,23 @@ export function commandRunnerWithProgress<R>(
|
||||
try {
|
||||
return await withProgress(progressOptionsWithDefaults, task, ...args);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
const errorMessage = `${e.message || e} (${commandId})`;
|
||||
const errorMessage = `${getErrorMessage(e) || e} (${commandId})`;
|
||||
error = e instanceof Error ? e : new Error(errorMessage);
|
||||
const errorStack = getErrorStack(e);
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
if (e.silent) {
|
||||
void logger.log(errorMessage);
|
||||
void outputLogger.log(errorMessage);
|
||||
} else {
|
||||
void showAndLogWarningMessage(errorMessage);
|
||||
void showAndLogWarningMessage(errorMessage, { outputLogger });
|
||||
}
|
||||
} else {
|
||||
// Include the full stack in the error log only.
|
||||
const fullMessage = e.stack
|
||||
? `${errorMessage}\n${e.stack}`
|
||||
const fullMessage = errorStack
|
||||
? `${errorMessage}\n${errorStack}`
|
||||
: errorMessage;
|
||||
void showAndLogErrorMessage(errorMessage, {
|
||||
outputLogger,
|
||||
fullMessage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { tmpDir } from '../run-queries';
|
||||
import { CompletedQuery } from '../query-results';
|
||||
import { tmpDir } from '../helpers';
|
||||
import {
|
||||
FromCompareViewMessage,
|
||||
ToCompareViewMessage,
|
||||
@@ -21,10 +20,12 @@ import { DatabaseManager } from '../databases';
|
||||
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
|
||||
import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli-types';
|
||||
import resultsDiff from './resultsDiff';
|
||||
import { CompletedLocalQueryInfo } from '../query-results';
|
||||
import { getErrorMessage } from '../pure/helpers-pure';
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedQuery;
|
||||
to: CompletedQuery;
|
||||
from: CompletedLocalQueryInfo;
|
||||
to: CompletedLocalQueryInfo;
|
||||
}
|
||||
|
||||
export class CompareInterfaceManager extends DisposableObject {
|
||||
@@ -39,15 +40,15 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
private cliServer: CodeQLCliServer,
|
||||
private logger: Logger,
|
||||
private showQueryResultsCallback: (
|
||||
item: CompletedQuery
|
||||
item: CompletedLocalQueryInfo
|
||||
) => Promise<void>
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async showResults(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery,
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo,
|
||||
selectedResultSetName?: string
|
||||
) {
|
||||
this.comparePair = { from, to };
|
||||
@@ -70,7 +71,7 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
try {
|
||||
rows = this.compareResults(fromResultSet, toResultSet);
|
||||
} catch (e) {
|
||||
message = e.message;
|
||||
message = getErrorMessage(e);
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
@@ -80,18 +81,14 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
// since we split the description into several rows
|
||||
// only run interpolation if the label is user-defined
|
||||
// otherwise we will wind up with duplicated rows
|
||||
name: from.options.label
|
||||
? from.interpolate(from.getLabel())
|
||||
: from.queryName,
|
||||
status: from.statusString,
|
||||
time: from.time,
|
||||
name: from.getShortLabel(),
|
||||
status: from.completedQuery.statusString,
|
||||
time: from.startTime,
|
||||
},
|
||||
toQuery: {
|
||||
name: to.options.label
|
||||
? to.interpolate(to.getLabel())
|
||||
: to.queryName,
|
||||
status: to.statusString,
|
||||
time: to.time,
|
||||
name: to.getShortLabel(),
|
||||
status: to.completedQuery.statusString,
|
||||
time: to.startTime,
|
||||
},
|
||||
},
|
||||
columns: fromResultSet.schema.columns,
|
||||
@@ -99,7 +96,7 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
currentResultSetName: currentResultSetName,
|
||||
rows,
|
||||
message,
|
||||
datebaseUri: to.database.databaseUri,
|
||||
databaseUri: to.initialInfo.databaseInfo.databaseUri,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -121,14 +118,14 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
],
|
||||
}
|
||||
));
|
||||
this.panel.onDidDispose(
|
||||
this.push(this.panel.onDidDispose(
|
||||
() => {
|
||||
this.panel = undefined;
|
||||
this.comparePair = undefined;
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
));
|
||||
|
||||
const scriptPathOnDisk = Uri.file(
|
||||
ctx.asAbsolutePath('out/compareView.js')
|
||||
@@ -141,13 +138,14 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
[stylesheetPathOnDisk]
|
||||
[stylesheetPathOnDisk],
|
||||
false
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
this.push(panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
);
|
||||
));
|
||||
}
|
||||
return this.panel;
|
||||
}
|
||||
@@ -191,15 +189,15 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
}
|
||||
|
||||
private async findCommonResultSetNames(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery,
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo,
|
||||
selectedResultSetName: string | undefined
|
||||
): Promise<[string[], string, RawResultSet, RawResultSet]> {
|
||||
const fromSchemas = await this.cliServer.bqrsInfo(
|
||||
from.query.resultsPaths.resultsPath
|
||||
from.completedQuery.query.resultsPaths.resultsPath
|
||||
);
|
||||
const toSchemas = await this.cliServer.bqrsInfo(
|
||||
to.query.resultsPaths.resultsPath
|
||||
to.completedQuery.query.resultsPaths.resultsPath
|
||||
);
|
||||
const fromSchemaNames = fromSchemas['result-sets'].map(
|
||||
(schema) => schema.name
|
||||
@@ -215,12 +213,12 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
const fromResultSet = await this.getResultSet(
|
||||
fromSchemas,
|
||||
currentResultSetName,
|
||||
from.query.resultsPaths.resultsPath
|
||||
from.completedQuery.query.resultsPaths.resultsPath
|
||||
);
|
||||
const toResultSet = await this.getResultSet(
|
||||
toSchemas,
|
||||
currentResultSetName,
|
||||
to.query.resultsPaths.resultsPath
|
||||
to.completedQuery.query.resultsPaths.resultsPath
|
||||
);
|
||||
return [
|
||||
commonResultSetNames,
|
||||
|
||||
@@ -17,7 +17,7 @@ const emptyComparison: SetComparisonsMessage = {
|
||||
columns: [],
|
||||
commonResultSetNames: [],
|
||||
currentResultSetName: '',
|
||||
datebaseUri: '',
|
||||
databaseUri: '',
|
||||
message: 'Empty comparison'
|
||||
};
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function CompareTable(props: Props) {
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(rows.from, comparison.datebaseUri)}
|
||||
{createRows(rows.from, comparison.databaseUri)}
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
@@ -86,7 +86,7 @@ export default function CompareTable(props: Props) {
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(rows.to, comparison.datebaseUri)}
|
||||
{createRows(rows.to, comparison.databaseUri)}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es6",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["ES2021", "dom"],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"rootDir": "..",
|
||||
@@ -17,7 +14,5 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DisposableObject } from './pure/disposable-object';
|
||||
import { workspace, Event, EventEmitter, ConfigurationChangeEvent, ConfigurationTarget } from 'vscode';
|
||||
import { DistributionManager } from './distribution';
|
||||
import { logger } from './logging';
|
||||
import { ONE_DAY_IN_MS } from './pure/helpers-pure';
|
||||
|
||||
/** Helper class to look up a labelled (and possibly nested) setting. */
|
||||
export class Setting {
|
||||
@@ -54,8 +55,11 @@ const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
|
||||
export const CUSTOM_CODEQL_PATH_SETTING = new Setting('executablePath', DISTRIBUTION_SETTING);
|
||||
const INCLUDE_PRERELEASE_SETTING = new Setting('includePrerelease', DISTRIBUTION_SETTING);
|
||||
const PERSONAL_ACCESS_TOKEN_SETTING = new Setting('personalAccessToken', DISTRIBUTION_SETTING);
|
||||
|
||||
// Query History configuration
|
||||
const QUERY_HISTORY_SETTING = new Setting('queryHistory', ROOT_SETTING);
|
||||
const QUERY_HISTORY_FORMAT_SETTING = new Setting('format', QUERY_HISTORY_SETTING);
|
||||
const QUERY_HISTORY_TTL = new Setting('format', QUERY_HISTORY_SETTING);
|
||||
|
||||
/** When these settings change, the distribution should be updated. */
|
||||
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING];
|
||||
@@ -71,7 +75,6 @@ export interface DistributionConfig {
|
||||
}
|
||||
|
||||
// Query server configuration
|
||||
|
||||
const RUNNING_QUERIES_SETTING = new Setting('runningQueries', ROOT_SETTING);
|
||||
const NUMBER_OF_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_QUERIES_SETTING);
|
||||
const SAVE_CACHE_SETTING = new Setting('saveCache', RUNNING_QUERIES_SETTING);
|
||||
@@ -91,7 +94,10 @@ export const PAGE_SIZE = new Setting('pageSize', RESULTS_DISPLAY_SETTING);
|
||||
const CUSTOM_LOG_DIRECTORY_SETTING = new Setting('customLogDirectory', RUNNING_QUERIES_SETTING);
|
||||
|
||||
/** When these settings change, the running query server should be restarted. */
|
||||
const QUERY_SERVER_RESTARTING_SETTINGS = [NUMBER_OF_THREADS_SETTING, SAVE_CACHE_SETTING, CACHE_SIZE_SETTING, MEMORY_SETTING, DEBUG_SETTING, CUSTOM_LOG_DIRECTORY_SETTING];
|
||||
const QUERY_SERVER_RESTARTING_SETTINGS = [
|
||||
NUMBER_OF_THREADS_SETTING, SAVE_CACHE_SETTING, CACHE_SIZE_SETTING, MEMORY_SETTING,
|
||||
DEBUG_SETTING, CUSTOM_LOG_DIRECTORY_SETTING,
|
||||
];
|
||||
|
||||
export interface QueryServerConfig {
|
||||
codeQlPath: string;
|
||||
@@ -106,10 +112,11 @@ export interface QueryServerConfig {
|
||||
}
|
||||
|
||||
/** When these settings change, the query history should be refreshed. */
|
||||
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING];
|
||||
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING, QUERY_HISTORY_TTL];
|
||||
|
||||
export interface QueryHistoryConfig {
|
||||
format: string;
|
||||
ttlInMillis: number;
|
||||
onDidChangeConfiguration: Event<void>;
|
||||
}
|
||||
|
||||
@@ -251,6 +258,13 @@ export class QueryHistoryConfigListener extends ConfigListener implements QueryH
|
||||
public get format(): string {
|
||||
return QUERY_HISTORY_FORMAT_SETTING.getValue<string>();
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration value is in days, but return the value in milliseconds to make it easier to use.
|
||||
*/
|
||||
public get ttlInMillis(): number {
|
||||
return (QUERY_HISTORY_TTL.getValue<number>() || 30) * ONE_DAY_IN_MS;
|
||||
}
|
||||
}
|
||||
|
||||
export class CliConfigListener extends ConfigListener implements CliConfig {
|
||||
@@ -309,10 +323,10 @@ export function isCanary() {
|
||||
export const NO_CACHE_AST_VIEWER = new Setting('disableCache', AST_VIEWER_SETTING);
|
||||
|
||||
// Settings for remote queries
|
||||
const REMOTE_QUERIES_SETTING = new Setting('remoteQueries', ROOT_SETTING);
|
||||
const REMOTE_QUERIES_SETTING = new Setting('variantAnalysis', ROOT_SETTING);
|
||||
|
||||
/**
|
||||
* Lists of GitHub repositories that you want to query remotely via the "Run Remote query" command.
|
||||
* Lists of GitHub repositories that you want to query remotely via the "Run Variant Analysis" command.
|
||||
* Note: This command is only available for internal users.
|
||||
*
|
||||
* This setting should be a JSON object where each key is a user-specified name (string),
|
||||
@@ -329,7 +343,7 @@ export async function setRemoteRepositoryLists(lists: Record<string, string[]> |
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the "controller" repository that you want to use with the "Run Remote query" command.
|
||||
* The name of the "controller" repository that you want to use with the "Run Variant Analysis" command.
|
||||
* Note: This command is only available for internal users.
|
||||
*
|
||||
* This setting should be a GitHub repository of the form `<owner>/<repo>`.
|
||||
@@ -345,13 +359,12 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to insecurely load ML models from CodeQL packs.
|
||||
*
|
||||
* This setting is for internal users only.
|
||||
* The branch of "github/codeql-variant-analysis-action" to use with the "Run Variant Analysis" command.
|
||||
* Default value is "main".
|
||||
* Note: This command is only available for internal users.
|
||||
*/
|
||||
const SHOULD_INSECURELY_LOAD_MODELS_FROM_PACKS =
|
||||
new Setting('shouldInsecurelyLoadModelsFromPacks', RUNNING_QUERIES_SETTING);
|
||||
const ACTION_BRANCH = new Setting('actionBranch', REMOTE_QUERIES_SETTING);
|
||||
|
||||
export function shouldInsecurelyLoadMlModelsFromPacks(): boolean {
|
||||
return SHOULD_INSECURELY_LOAD_MODELS_FROM_PACKS.getValue<boolean>();
|
||||
export function getActionBranch(): string {
|
||||
return ACTION_BRANCH.getValue<string>() || 'main';
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DecodedBqrsChunk, BqrsId, EntityValue } from '../pure/bqrs-cli-types';
|
||||
import { DatabaseItem } from '../databases';
|
||||
import { ChildAstItem, AstItem } from '../astViewer';
|
||||
import fileRangeFromURI from './fileRangeFromURI';
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
/**
|
||||
* A class that wraps a tree of QL results from a query that
|
||||
@@ -17,7 +18,7 @@ export default class AstBuilder {
|
||||
queryResults: QueryWithResults,
|
||||
private cli: CodeQLCliServer,
|
||||
public db: DatabaseItem,
|
||||
public fileName: string
|
||||
public fileName: Uri
|
||||
) {
|
||||
this.bqrsPath = queryResults.query.resultsPaths.resultsPath;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export enum KeyType {
|
||||
DefinitionQuery = 'DefinitionQuery',
|
||||
ReferenceQuery = 'ReferenceQuery',
|
||||
PrintAstQuery = 'PrintAstQuery',
|
||||
PrintCfgQuery = 'PrintCfgQuery',
|
||||
}
|
||||
|
||||
export function tagOfKeyType(keyType: KeyType): string {
|
||||
@@ -12,6 +13,8 @@ export function tagOfKeyType(keyType: KeyType): string {
|
||||
return 'ide-contextual-queries/local-references';
|
||||
case KeyType.PrintAstQuery:
|
||||
return 'ide-contextual-queries/print-ast';
|
||||
case KeyType.PrintCfgQuery:
|
||||
return 'ide-contextual-queries/print-cfg';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +26,8 @@ export function nameOfKeyType(keyType: KeyType): string {
|
||||
return 'references';
|
||||
case KeyType.PrintAstQuery:
|
||||
return 'print AST';
|
||||
case KeyType.PrintCfgQuery:
|
||||
return 'print CFG';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +37,7 @@ export function kindOfKeyType(keyType: KeyType): string {
|
||||
case KeyType.ReferenceQuery:
|
||||
return 'definitions';
|
||||
case KeyType.PrintAstQuery:
|
||||
case KeyType.PrintCfgQuery:
|
||||
return 'graph';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { decodeSourceArchiveUri, encodeArchiveBasePath } from '../archive-filesystem-provider';
|
||||
import { ColumnKindCode, EntityValue, getResultSetSchema, ResultSetSchema } from '../pure/bqrs-cli-types';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
@@ -7,16 +5,17 @@ import { DatabaseManager, DatabaseItem } from '../databases';
|
||||
import fileRangeFromURI from './fileRangeFromURI';
|
||||
import * as messages from '../pure/messages';
|
||||
import { QueryServerClient } from '../queryserver-client';
|
||||
import { QueryWithResults, compileAndRunQueryAgainstDatabase } from '../run-queries';
|
||||
import { QueryWithResults, compileAndRunQueryAgainstDatabase, createInitialQueryInfo } from '../run-queries';
|
||||
import { ProgressCallback } from '../commandRunner';
|
||||
import { KeyType } from './keyType';
|
||||
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
|
||||
import { CancellationToken, LocationLink, Uri } from 'vscode';
|
||||
|
||||
export const SELECT_QUERY_NAME = '#select';
|
||||
export const TEMPLATE_NAME = 'selectedSourceFile';
|
||||
|
||||
export interface FullLocationLink extends vscode.LocationLink {
|
||||
originUri: vscode.Uri;
|
||||
export interface FullLocationLink extends LocationLink {
|
||||
originUri: Uri;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,6 +28,7 @@ export interface FullLocationLink extends vscode.LocationLink {
|
||||
* @param dbm The database manager
|
||||
* @param uriString The selected source file and location
|
||||
* @param keyType The contextual query type to run
|
||||
* @param queryStorageDir The directory to store the query results
|
||||
* @param progress A progress callback
|
||||
* @param token A CancellationToken
|
||||
* @param filter A function that will filter extraneous results
|
||||
@@ -39,11 +39,12 @@ export async function getLocationsForUriString(
|
||||
dbm: DatabaseManager,
|
||||
uriString: string,
|
||||
keyType: KeyType,
|
||||
queryStorageDir: string,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
token: CancellationToken,
|
||||
filter: (src: string, dest: string) => boolean
|
||||
): Promise<FullLocationLink[]> {
|
||||
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString, true));
|
||||
const uri = decodeSourceArchiveUri(Uri.parse(uriString, true));
|
||||
const sourceArchiveUri = encodeArchiveBasePath(uri.sourceArchiveZipPath);
|
||||
|
||||
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
|
||||
@@ -56,12 +57,21 @@ export async function getLocationsForUriString(
|
||||
|
||||
const links: FullLocationLink[] = [];
|
||||
for (const query of await resolveQueries(cli, qlpack, keyType)) {
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
Uri.file(query),
|
||||
{
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(),
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const results = await compileAndRunQueryAgainstDatabase(
|
||||
cli,
|
||||
qs,
|
||||
db,
|
||||
false,
|
||||
vscode.Uri.file(query),
|
||||
initialInfo,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
templates
|
||||
|
||||
@@ -18,7 +18,7 @@ import { CachedOperation } from '../helpers';
|
||||
import { ProgressCallback, withProgress } from '../commandRunner';
|
||||
import * as messages from '../pure/messages';
|
||||
import { QueryServerClient } from '../queryserver-client';
|
||||
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from '../run-queries';
|
||||
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo, QueryWithResults } from '../run-queries';
|
||||
import AstBuilder from './astBuilder';
|
||||
import {
|
||||
KeyType,
|
||||
@@ -41,6 +41,7 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
private cli: CodeQLCliServer,
|
||||
private qs: QueryServerClient,
|
||||
private dbm: DatabaseManager,
|
||||
private queryStorageDir: string,
|
||||
) {
|
||||
this.cache = new CachedOperation<LocationLink[]>(this.getDefinitions.bind(this));
|
||||
}
|
||||
@@ -68,6 +69,7 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
this.dbm,
|
||||
uriString,
|
||||
KeyType.DefinitionQuery,
|
||||
this.queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
(src, _dest) => src === uriString
|
||||
@@ -83,6 +85,7 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
private cli: CodeQLCliServer,
|
||||
private qs: QueryServerClient,
|
||||
private dbm: DatabaseManager,
|
||||
private queryStorageDir: string,
|
||||
) {
|
||||
this.cache = new CachedOperation<FullLocationLink[]>(this.getReferences.bind(this));
|
||||
}
|
||||
@@ -115,6 +118,7 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
this.dbm,
|
||||
uriString,
|
||||
KeyType.DefinitionQuery,
|
||||
this.queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
(src, _dest) => src === uriString
|
||||
@@ -123,33 +127,39 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
type QueryWithDb = {
|
||||
query: QueryWithResults,
|
||||
dbUri: Uri
|
||||
};
|
||||
|
||||
export class TemplatePrintAstProvider {
|
||||
private cache: CachedOperation<QueryWithResults>;
|
||||
private cache: CachedOperation<QueryWithDb>;
|
||||
|
||||
constructor(
|
||||
private cli: CodeQLCliServer,
|
||||
private qs: QueryServerClient,
|
||||
private dbm: DatabaseManager,
|
||||
private queryStorageDir: string,
|
||||
) {
|
||||
this.cache = new CachedOperation<QueryWithResults>(this.getAst.bind(this));
|
||||
this.cache = new CachedOperation<QueryWithDb>(this.getAst.bind(this));
|
||||
}
|
||||
|
||||
async provideAst(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
document?: TextDocument
|
||||
fileUri?: Uri
|
||||
): Promise<AstBuilder | undefined> {
|
||||
if (!document) {
|
||||
if (!fileUri) {
|
||||
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
|
||||
}
|
||||
const queryResults = this.shouldCache()
|
||||
? await this.cache.get(document.uri.toString(), progress, token)
|
||||
: await this.getAst(document.uri.toString(), progress, token);
|
||||
const { query, dbUri } = this.shouldCache()
|
||||
? await this.cache.get(fileUri.toString(), progress, token)
|
||||
: await this.getAst(fileUri.toString(), progress, token);
|
||||
|
||||
return new AstBuilder(
|
||||
queryResults, this.cli,
|
||||
this.dbm.findDatabaseItem(Uri.parse(queryResults.database.databaseUri!, true))!,
|
||||
document.fileName
|
||||
query, this.cli,
|
||||
this.dbm.findDatabaseItem(dbUri)!,
|
||||
fileUri,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,7 +171,7 @@ export class TemplatePrintAstProvider {
|
||||
uriString: string,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
): Promise<QueryWithResults> {
|
||||
): Promise<QueryWithDb> {
|
||||
const uri = Uri.parse(uriString, true);
|
||||
if (uri.scheme !== zipArchiveScheme) {
|
||||
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
|
||||
@@ -195,15 +205,86 @@ export class TemplatePrintAstProvider {
|
||||
}
|
||||
};
|
||||
|
||||
return await compileAndRunQueryAgainstDatabase(
|
||||
this.cli,
|
||||
this.qs,
|
||||
db,
|
||||
false,
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
Uri.file(query),
|
||||
progress,
|
||||
token,
|
||||
templates
|
||||
{
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(),
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return {
|
||||
query: await compileAndRunQueryAgainstDatabase(
|
||||
this.cli,
|
||||
this.qs,
|
||||
db,
|
||||
initialInfo,
|
||||
this.queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
templates
|
||||
),
|
||||
dbUri: db.databaseUri
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TemplatePrintCfgProvider {
|
||||
private cache: CachedOperation<[Uri, messages.TemplateDefinitions] | undefined>;
|
||||
|
||||
constructor(
|
||||
private cli: CodeQLCliServer,
|
||||
private dbm: DatabaseManager,
|
||||
) {
|
||||
this.cache = new CachedOperation<[Uri, messages.TemplateDefinitions] | undefined>(this.getCfgUri.bind(this));
|
||||
}
|
||||
|
||||
async provideCfgUri(document?: TextDocument): Promise<[Uri, messages.TemplateDefinitions] | undefined> {
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
return await this.cache.get(document.uri.toString());
|
||||
}
|
||||
|
||||
private async getCfgUri(uriString: string): Promise<[Uri, messages.TemplateDefinitions]> {
|
||||
const uri = Uri.parse(uriString, true);
|
||||
if (uri.scheme !== zipArchiveScheme) {
|
||||
throw new Error('CFG Viewing is only available for databases with zipped source archives.');
|
||||
}
|
||||
|
||||
const zippedArchive = decodeSourceArchiveUri(uri);
|
||||
const sourceArchiveUri = encodeArchiveBasePath(zippedArchive.sourceArchiveZipPath);
|
||||
const db = this.dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
|
||||
|
||||
if (!db) {
|
||||
throw new Error('Can\'t infer database from the provided source.');
|
||||
}
|
||||
|
||||
const qlpack = await qlpackOfDatabase(this.cli, db);
|
||||
if (!qlpack) {
|
||||
throw new Error('Can\'t infer qlpack from database source archive.');
|
||||
}
|
||||
const queries = await resolveQueries(this.cli, qlpack, KeyType.PrintCfgQuery);
|
||||
if (queries.length > 1) {
|
||||
throw new Error(`Found multiple Print CFG queries. Can't continue. Make sure there is exacly one query with the tag ${KeyType.PrintCfgQuery}`);
|
||||
}
|
||||
if (queries.length === 0) {
|
||||
throw new Error(`Did not find any Print CFG queries. Can't continue. Make sure there is exacly one query with the tag ${KeyType.PrintCfgQuery}`);
|
||||
}
|
||||
|
||||
const queryUri = Uri.file(queries[0]);
|
||||
|
||||
const templates: messages.TemplateDefinitions = {
|
||||
[TEMPLATE_NAME]: {
|
||||
values: {
|
||||
tuples: [[{
|
||||
stringValue: zippedArchive.pathWithinSourceArchive
|
||||
}]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [queryUri, templates];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ import {
|
||||
ProgressCallback,
|
||||
} from './commandRunner';
|
||||
import { logger } from './logging';
|
||||
import { tmpDir } from './run-queries';
|
||||
import { tmpDir } from './helpers';
|
||||
import { Credentials } from './authentication';
|
||||
import { REPO_REGEX, getErrorMessage } from './pure/helpers-pure';
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
|
||||
@@ -46,6 +48,7 @@ export async function promptImportInternetDatabase(
|
||||
|
||||
const item = await databaseArchiveFetcher(
|
||||
databaseUrl,
|
||||
{},
|
||||
databaseManager,
|
||||
storagePath,
|
||||
progress,
|
||||
@@ -61,6 +64,79 @@ export async function promptImportInternetDatabase(
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from GitHub.
|
||||
* User enters a GitHub repository and then the user is asked which language
|
||||
* to download (if there is more than one)
|
||||
*
|
||||
* @param databaseManager the DatabaseManager
|
||||
* @param storagePath where to store the unzipped database.
|
||||
*/
|
||||
export async function promptImportGithubDatabase(
|
||||
databaseManager: DatabaseManager,
|
||||
storagePath: string,
|
||||
credentials: Credentials,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
cli?: CodeQLCliServer
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
progress({
|
||||
message: 'Choose repository',
|
||||
step: 1,
|
||||
maxStep: 2
|
||||
});
|
||||
const githubRepo = await window.showInputBox({
|
||||
title: 'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
|
||||
placeHolder: 'https://github.com/<owner>/<repo> or <owner>/<repo>',
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (!githubRepo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!looksLikeGithubRepo(githubRepo)) {
|
||||
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
|
||||
}
|
||||
|
||||
const databaseUrl = await convertGithubNwoToDatabaseUrl(githubRepo, credentials, progress);
|
||||
if (!databaseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const octokit = await credentials.getOctokit();
|
||||
/**
|
||||
* The 'token' property of the token object returned by `octokit.auth()`.
|
||||
* The object is undocumented, but looks something like this:
|
||||
* {
|
||||
* token: 'xxxx',
|
||||
* tokenType: 'oauth',
|
||||
* type: 'token',
|
||||
* }
|
||||
* We only need the actual token string.
|
||||
*/
|
||||
const octokitToken = (await octokit.auth() as { token: string })?.token;
|
||||
if (!octokitToken) {
|
||||
// Just print a generic error message for now. Ideally we could show more debugging info, like the
|
||||
// octokit object, but that would expose a user token.
|
||||
throw new Error('Unable to get GitHub token.');
|
||||
}
|
||||
const item = await databaseArchiveFetcher(
|
||||
databaseUrl,
|
||||
{ 'Accept': 'application/zip', 'Authorization': `Bearer ${octokitToken}` },
|
||||
databaseManager,
|
||||
storagePath,
|
||||
progress,
|
||||
token,
|
||||
cli
|
||||
);
|
||||
if (item) {
|
||||
await commands.executeCommand('codeQLDatabases.focus');
|
||||
void showAndLogInformationMessage('Database downloaded and imported successfully.');
|
||||
return item;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from lgtm.
|
||||
* User enters a project url and then the user is asked which language
|
||||
@@ -90,10 +166,11 @@ export async function promptImportLgtmDatabase(
|
||||
}
|
||||
|
||||
if (looksLikeLgtmUrl(lgtmUrl)) {
|
||||
const databaseUrl = await convertToDatabaseUrl(lgtmUrl, progress);
|
||||
const databaseUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progress);
|
||||
if (databaseUrl) {
|
||||
const item = await databaseArchiveFetcher(
|
||||
databaseUrl,
|
||||
{},
|
||||
databaseManager,
|
||||
storagePath,
|
||||
progress,
|
||||
@@ -140,6 +217,7 @@ export async function importArchiveDatabase(
|
||||
try {
|
||||
const item = await databaseArchiveFetcher(
|
||||
databaseUrl,
|
||||
{},
|
||||
databaseManager,
|
||||
storagePath,
|
||||
progress,
|
||||
@@ -152,7 +230,7 @@ export async function importArchiveDatabase(
|
||||
}
|
||||
return item;
|
||||
} catch (e) {
|
||||
if (e.message.includes('unexpected end of file')) {
|
||||
if (getErrorMessage(e).includes('unexpected end of file')) {
|
||||
throw new Error('Database is corrupt or too large. Try unzipping outside of VS Code and importing the unzipped folder instead.');
|
||||
} else {
|
||||
// delegate
|
||||
@@ -166,6 +244,7 @@ export async function importArchiveDatabase(
|
||||
* or in the local filesystem.
|
||||
*
|
||||
* @param databaseUrl URL from which to grab the database
|
||||
* @param requestHeaders Headers to send with the request
|
||||
* @param databaseManager the DatabaseManager
|
||||
* @param storagePath where to store the unzipped database.
|
||||
* @param progress callback to send progress messages to
|
||||
@@ -173,6 +252,7 @@ export async function importArchiveDatabase(
|
||||
*/
|
||||
async function databaseArchiveFetcher(
|
||||
databaseUrl: string,
|
||||
requestHeaders: { [key: string]: string },
|
||||
databaseManager: DatabaseManager,
|
||||
storagePath: string,
|
||||
progress: ProgressCallback,
|
||||
@@ -193,7 +273,7 @@ async function databaseArchiveFetcher(
|
||||
if (isFile(databaseUrl)) {
|
||||
await readAndUnzip(databaseUrl, unzipPath, cli, progress);
|
||||
} else {
|
||||
await fetchAndUnzip(databaseUrl, unzipPath, cli, progress);
|
||||
await fetchAndUnzip(databaseUrl, requestHeaders, unzipPath, cli, progress);
|
||||
}
|
||||
|
||||
progress({
|
||||
@@ -292,6 +372,7 @@ async function readAndUnzip(
|
||||
|
||||
async function fetchAndUnzip(
|
||||
databaseUrl: string,
|
||||
requestHeaders: { [key: string]: string },
|
||||
unzipPath: string,
|
||||
cli?: CodeQLCliServer,
|
||||
progress?: ProgressCallback
|
||||
@@ -310,7 +391,10 @@ async function fetchAndUnzip(
|
||||
step: 1,
|
||||
});
|
||||
|
||||
const response = await checkForFailingResponse(await fetch(databaseUrl), 'Error downloading database');
|
||||
const response = await checkForFailingResponse(
|
||||
await fetch(databaseUrl, { headers: requestHeaders }),
|
||||
'Error downloading database'
|
||||
);
|
||||
const archiveFileStream = fs.createWriteStream(archivePath);
|
||||
|
||||
const contentLength = response.headers.get('content-length');
|
||||
@@ -381,6 +465,81 @@ export async function findDirWithFile(
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL pattern is https://github.com/{owner}/{name}/{subpages}.
|
||||
*
|
||||
* This function accepts any URL that matches the pattern above. It also accepts just the
|
||||
* name with owner (NWO): `<owner>/<repo>`.
|
||||
*
|
||||
* @param githubRepo The GitHub repository URL or NWO
|
||||
*
|
||||
* @return true if this looks like a valid GitHub repository URL or NWO
|
||||
*/
|
||||
export function looksLikeGithubRepo(
|
||||
githubRepo: string | undefined
|
||||
): githubRepo is string {
|
||||
if (!githubRepo) {
|
||||
return false;
|
||||
}
|
||||
if (REPO_REGEX.test(githubRepo) || convertGitHubUrlToNwo(githubRepo)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GitHub repository URL to the corresponding NWO.
|
||||
* @param githubUrl The GitHub repository URL
|
||||
* @return The corresponding NWO, or undefined if the URL is not valid
|
||||
*/
|
||||
function convertGitHubUrlToNwo(githubUrl: string): string | undefined {
|
||||
try {
|
||||
const uri = Uri.parse(githubUrl, true);
|
||||
if (uri.scheme !== 'https') {
|
||||
return;
|
||||
}
|
||||
if (uri.authority !== 'github.com' && uri.authority !== 'www.github.com') {
|
||||
return;
|
||||
}
|
||||
const paths = uri.path.split('/').filter((segment: string) => segment);
|
||||
const nwo = `${paths[0]}/${paths[1]}`;
|
||||
if (REPO_REGEX.test(nwo)) {
|
||||
return nwo;
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
// Ignore the error here, since we catch failures at a higher level.
|
||||
// In particular: returning undefined leads to an error in 'promptImportGithubDatabase'.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function convertGithubNwoToDatabaseUrl(
|
||||
githubRepo: string,
|
||||
credentials: Credentials,
|
||||
progress: ProgressCallback): Promise<string | undefined> {
|
||||
try {
|
||||
const nwo = convertGitHubUrlToNwo(githubRepo) || githubRepo;
|
||||
const [owner, repo] = nwo.split('/');
|
||||
|
||||
const octokit = await credentials.getOctokit();
|
||||
const response = await octokit.request('GET /repos/:owner/:repo/code-scanning/codeql/databases', { owner, repo });
|
||||
|
||||
const languages = response.data.map((db: any) => db.language);
|
||||
|
||||
const language = await promptForLanguage(languages, progress);
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `https://api.github.com/repos/${owner}/${repo}/code-scanning/codeql/databases/${language}`;
|
||||
|
||||
} catch (e) {
|
||||
void logger.log(`Error: ${getErrorMessage(e)}`);
|
||||
throw new Error(`Unable to get database for '${githubRepo}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL pattern is https://lgtm.com/projects/{provider}/{org}/{name}/{irrelevant-subpages}.
|
||||
* There are several possibilities for the provider: in addition to GitHub.com (g),
|
||||
@@ -416,7 +575,7 @@ export function looksLikeLgtmUrl(lgtmUrl: string | undefined): lgtmUrl is string
|
||||
return false;
|
||||
}
|
||||
|
||||
const paths = uri.path.split('/').filter((segment) => segment);
|
||||
const paths = uri.path.split('/').filter((segment: string) => segment);
|
||||
return paths.length >= 4 && paths[0] === 'projects';
|
||||
} catch (e) {
|
||||
return false;
|
||||
@@ -434,7 +593,7 @@ function convertRawLgtmSlug(maybeSlug: string): string | undefined {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
function extractProjectSlug(lgtmUrl: string): string | undefined {
|
||||
// Only matches the '/g/' provider (github)
|
||||
const re = new RegExp('https://lgtm.com/projects/g/(.*[^/])');
|
||||
@@ -446,7 +605,7 @@ function extractProjectSlug(lgtmUrl: string): string | undefined {
|
||||
}
|
||||
|
||||
// exported for testing
|
||||
export async function convertToDatabaseUrl(
|
||||
export async function convertLgtmUrlToDatabaseUrl(
|
||||
lgtmUrl: string,
|
||||
progress: ProgressCallback) {
|
||||
try {
|
||||
@@ -467,7 +626,9 @@ export async function convertToDatabaseUrl(
|
||||
}
|
||||
}
|
||||
|
||||
const language = await promptForLanguage(projectJson, progress);
|
||||
const languages = projectJson?.languages?.map((lang: { language: string }) => lang.language) || [];
|
||||
|
||||
const language = await promptForLanguage(languages, progress);
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
@@ -479,7 +640,7 @@ export async function convertToDatabaseUrl(
|
||||
language,
|
||||
].join('/')}`;
|
||||
} catch (e) {
|
||||
void logger.log(`Error: ${e.message}`);
|
||||
void logger.log(`Error: ${getErrorMessage(e)}`);
|
||||
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
|
||||
}
|
||||
}
|
||||
@@ -487,7 +648,7 @@ export async function convertToDatabaseUrl(
|
||||
async function downloadLgtmProjectMetadata(lgtmUrl: string): Promise<any> {
|
||||
const uri = Uri.parse(lgtmUrl, true);
|
||||
const paths = ['api', 'v1.0'].concat(
|
||||
uri.path.split('/').filter((segment) => segment)
|
||||
uri.path.split('/').filter((segment: string) => segment)
|
||||
).slice(0, 6);
|
||||
const projectUrl = `https://lgtm.com/${paths.join('/')}`;
|
||||
const projectResponse = await fetch(projectUrl);
|
||||
@@ -495,7 +656,7 @@ async function downloadLgtmProjectMetadata(lgtmUrl: string): Promise<any> {
|
||||
}
|
||||
|
||||
async function promptForLanguage(
|
||||
projectJson: any,
|
||||
languages: string[],
|
||||
progress: ProgressCallback
|
||||
): Promise<string | undefined> {
|
||||
progress({
|
||||
@@ -503,17 +664,19 @@ async function promptForLanguage(
|
||||
step: 2,
|
||||
maxStep: 2
|
||||
});
|
||||
if (!projectJson?.languages?.length) {
|
||||
return;
|
||||
if (!languages.length) {
|
||||
throw new Error('No databases found');
|
||||
}
|
||||
if (projectJson.languages.length === 1) {
|
||||
return projectJson.languages[0].language;
|
||||
if (languages.length === 1) {
|
||||
return languages[0];
|
||||
}
|
||||
|
||||
return await window.showQuickPick(
|
||||
projectJson.languages.map((lang: { language: string }) => lang.language), {
|
||||
placeHolder: 'Select the database language to download:'
|
||||
}
|
||||
languages,
|
||||
{
|
||||
placeHolder: 'Select the database language to download:',
|
||||
ignoreFocusOut: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,11 +33,13 @@ import * as qsClient from './queryserver-client';
|
||||
import { upgradeDatabaseExplicit } from './upgrades';
|
||||
import {
|
||||
importArchiveDatabase,
|
||||
promptImportGithubDatabase,
|
||||
promptImportInternetDatabase,
|
||||
promptImportLgtmDatabase,
|
||||
} from './databaseFetcher';
|
||||
import { CancellationToken } from 'vscode';
|
||||
import { asyncFilter } from './pure/helpers-pure';
|
||||
import { asyncFilter, getErrorMessage } from './pure/helpers-pure';
|
||||
import { Credentials } from './authentication';
|
||||
|
||||
type ThemableIconPath = { light: string; dark: string } | string;
|
||||
|
||||
@@ -219,7 +221,8 @@ export class DatabaseUI extends DisposableObject {
|
||||
private databaseManager: DatabaseManager,
|
||||
private readonly queryServer: qsClient.QueryServerClient | undefined,
|
||||
private readonly storagePath: string,
|
||||
readonly extensionPath: string
|
||||
readonly extensionPath: string,
|
||||
private readonly getCredentials: () => Promise<Credentials>
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -291,6 +294,20 @@ export class DatabaseUI extends DisposableObject {
|
||||
}
|
||||
)
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
'codeQLDatabases.chooseDatabaseGithub',
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
const credentials = await this.getCredentials();
|
||||
await this.handleChooseDatabaseGithub(credentials, progress, token);
|
||||
},
|
||||
{
|
||||
title: 'Adding database from GitHub',
|
||||
})
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
'codeQLDatabases.chooseDatabaseLgtm',
|
||||
@@ -376,7 +393,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
try {
|
||||
return await this.chooseAndSetDatabase(true, progress, token);
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(e.message);
|
||||
void showAndLogErrorMessage(getErrorMessage(e));
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -444,7 +461,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
try {
|
||||
return await this.chooseAndSetDatabase(false, progress, token);
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(e.message);
|
||||
void showAndLogErrorMessage(getErrorMessage(e));
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -462,6 +479,21 @@ export class DatabaseUI extends DisposableObject {
|
||||
);
|
||||
};
|
||||
|
||||
handleChooseDatabaseGithub = async (
|
||||
credentials: Credentials,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
): Promise<DatabaseItem | undefined> => {
|
||||
return await promptImportGithubDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer
|
||||
);
|
||||
};
|
||||
|
||||
handleChooseDatabaseLgtm = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
@@ -590,8 +622,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${e.message
|
||||
}`
|
||||
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${getErrorMessage(e)}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DisposableObject } from './pure/disposable-object';
|
||||
import { Logger, logger } from './logging';
|
||||
import { registerDatabases, Dataset, deregisterDatabases } from './pure/messages';
|
||||
import { QueryServerClient } from './queryserver-client';
|
||||
import { getErrorMessage } from './pure/helpers-pure';
|
||||
|
||||
/**
|
||||
* databases.ts
|
||||
@@ -359,7 +360,7 @@ export class DatabaseItemImpl implements DatabaseItem {
|
||||
}
|
||||
catch (e) {
|
||||
this._contents = undefined;
|
||||
this._error = e;
|
||||
this._error = e instanceof Error ? e : new Error(String(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -726,7 +727,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
}
|
||||
} catch (e) {
|
||||
// database list had an unexpected type - nothing to be done?
|
||||
void showAndLogErrorMessage(`Database list loading failed: ${e.message}`);
|
||||
void showAndLogErrorMessage(`Database list loading failed: ${getErrorMessage(e)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -841,7 +842,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
void logger.log('Deleting database from filesystem.');
|
||||
fs.remove(item.databaseUri.fsPath).then(
|
||||
() => void logger.log(`Deleted '${item.databaseUri.fsPath}'`),
|
||||
e => void logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${e.message}`));
|
||||
e => void logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${getErrorMessage(e)}`));
|
||||
}
|
||||
|
||||
// note that we use undefined as the item in order to reset the entire tree
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'source-map-support/register';
|
||||
import {
|
||||
CancellationToken,
|
||||
CancellationTokenSource,
|
||||
commands,
|
||||
Disposable,
|
||||
ExtensionContext,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
} from 'vscode';
|
||||
import { LanguageClient } from 'vscode-languageclient';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
|
||||
@@ -40,7 +43,8 @@ import { DatabaseUI } from './databases-ui';
|
||||
import {
|
||||
TemplateQueryDefinitionProvider,
|
||||
TemplateQueryReferenceProvider,
|
||||
TemplatePrintAstProvider
|
||||
TemplatePrintAstProvider,
|
||||
TemplatePrintCfgProvider
|
||||
} from './contextual/templateProvider';
|
||||
import {
|
||||
DEFAULT_DISTRIBUTION_VERSION_RANGE,
|
||||
@@ -52,17 +56,26 @@ import {
|
||||
GithubApiError,
|
||||
GithubRateLimitedError
|
||||
} from './distribution';
|
||||
import * as helpers from './helpers';
|
||||
import { assertNever } from './pure/helpers-pure';
|
||||
import {
|
||||
findLanguage,
|
||||
tmpDirDisposal,
|
||||
showBinaryChoiceDialog,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogWarningMessage,
|
||||
showAndLogInformationMessage,
|
||||
showInformationMessageWithAction,
|
||||
tmpDir
|
||||
} from './helpers';
|
||||
import { asError, assertNever, getErrorMessage } from './pure/helpers-pure';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
import { InterfaceManager } from './interface';
|
||||
import { WebviewReveal } from './interface-utils';
|
||||
import { ideServerLogger, logger, queryServerLogger } from './logging';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
import { CompletedQuery } from './query-results';
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from './query-results';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { displayQuickQuery } from './quick-query';
|
||||
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal } from './run-queries';
|
||||
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo } from './run-queries';
|
||||
import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
import { CompareInterfaceManager } from './compare/compare-interface';
|
||||
@@ -79,12 +92,10 @@ import { CodeQlStatusBarHandler } from './status-bar';
|
||||
|
||||
import { Credentials } from './authentication';
|
||||
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
|
||||
import { RemoteQuery } from './remote-queries/remote-query';
|
||||
import { RemoteQueryResult } from './remote-queries/remote-query-result';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { RemoteQueriesInterfaceManager } from './remote-queries/remote-queries-interface';
|
||||
import { sampleRemoteQuery, sampleRemoteQueryResult } from './remote-queries/sample-data';
|
||||
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
|
||||
import { AnalysesResultsManager } from './remote-queries/analyses-results-manager';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -186,7 +197,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
const shouldUpdateOnNextActivationKey = 'shouldUpdateOnNextActivation';
|
||||
|
||||
registerErrorStubs([checkForUpdatesCommand], command => (async () => {
|
||||
void helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
|
||||
void showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
|
||||
}));
|
||||
|
||||
interface DistributionUpdateConfig {
|
||||
@@ -198,7 +209,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
|
||||
const minSecondsSinceLastUpdateCheck = config.isUserInitiated ? 0 : 86400;
|
||||
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
|
||||
helpers.showAndLogInformationMessage : async (message: string) => void logger.log(message);
|
||||
showAndLogInformationMessage : async (message: string) => void logger.log(message);
|
||||
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
|
||||
|
||||
// We do want to auto update if there is no distribution at all
|
||||
@@ -220,7 +231,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
|
||||
'Do you wish to upgrade?';
|
||||
await ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
|
||||
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, 'Restart and Upgrade')) {
|
||||
if (await showInformationMessageWithAction(updateAvailableMessage, 'Restart and Upgrade')) {
|
||||
await commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
} else {
|
||||
@@ -233,7 +244,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress));
|
||||
|
||||
await ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
|
||||
void helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
|
||||
void showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -260,7 +271,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
|
||||
// or updating the distribution.
|
||||
const alertFunction = (codeQlInstalled && !config.isUserInitiated) ?
|
||||
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
|
||||
showAndLogWarningMessage : showAndLogErrorMessage;
|
||||
const taskDescription = (willUpdateCodeQl ? 'update' :
|
||||
codeQlInstalled ? 'check for updates to' : 'install') + ' CodeQL CLI';
|
||||
|
||||
@@ -295,20 +306,20 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
}
|
||||
})();
|
||||
|
||||
void helpers.showAndLogWarningMessage(
|
||||
void showAndLogWarningMessage(
|
||||
`The current version of the CodeQL CLI (${result.version.raw}) ` +
|
||||
`is incompatible with this extension. ${fixGuidanceMessage}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FindDistributionResultKind.UnknownCompatibilityDistribution:
|
||||
void helpers.showAndLogWarningMessage(
|
||||
void showAndLogWarningMessage(
|
||||
'Compatibility with the configured CodeQL CLI could not be determined. ' +
|
||||
'You may experience problems using the extension.'
|
||||
);
|
||||
break;
|
||||
case FindDistributionResultKind.NoDistribution:
|
||||
void helpers.showAndLogErrorMessage('The CodeQL CLI could not be found.');
|
||||
void showAndLogErrorMessage('The CodeQL CLI could not be found.');
|
||||
break;
|
||||
default:
|
||||
assertNever(result);
|
||||
@@ -335,7 +346,7 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
|
||||
registerErrorStubs([checkForUpdatesCommand], command => async () => {
|
||||
const installActionName = 'Install CodeQL CLI';
|
||||
const chosenAction = await void helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, {
|
||||
const chosenAction = await void showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, {
|
||||
items: [installActionName]
|
||||
});
|
||||
if (chosenAction === installActionName) {
|
||||
@@ -423,7 +434,8 @@ async function activateWithInstalledDistribution(
|
||||
dbm,
|
||||
qs,
|
||||
getContextStoragePath(ctx),
|
||||
ctx.extensionPath
|
||||
ctx.extensionPath,
|
||||
() => Credentials.initialize(ctx),
|
||||
);
|
||||
databaseUI.init();
|
||||
ctx.subscriptions.push(databaseUI);
|
||||
@@ -431,17 +443,28 @@ async function activateWithInstalledDistribution(
|
||||
void logger.log('Initializing query history manager.');
|
||||
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
|
||||
ctx.subscriptions.push(queryHistoryConfigurationListener);
|
||||
const showResults = async (item: CompletedQuery) =>
|
||||
const showResults = async (item: CompletedLocalQueryInfo) =>
|
||||
showResultsForCompletedQuery(item, WebviewReveal.Forced);
|
||||
const queryStorageDir = path.join(ctx.globalStorageUri.fsPath, 'queries');
|
||||
await fs.ensureDir(queryStorageDir);
|
||||
|
||||
void logger.log('Initializing query history.');
|
||||
const qhm = new QueryHistoryManager(
|
||||
qs,
|
||||
ctx.extensionPath,
|
||||
dbm,
|
||||
queryStorageDir,
|
||||
ctx,
|
||||
queryHistoryConfigurationListener,
|
||||
showResults,
|
||||
async (from: CompletedQuery, to: CompletedQuery) =>
|
||||
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
|
||||
showResultsForComparison(from, to),
|
||||
);
|
||||
|
||||
qhm.onWillOpenQueryItem(async item => {
|
||||
if (item.t === 'local' && item.completed) {
|
||||
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.Forced);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.subscriptions.push(qhm);
|
||||
void logger.log('Initializing results panel interface.');
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
|
||||
@@ -461,18 +484,18 @@ async function activateWithInstalledDistribution(
|
||||
archiveFilesystemProvider.activate(ctx);
|
||||
|
||||
async function showResultsForComparison(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo
|
||||
): Promise<void> {
|
||||
try {
|
||||
await cmpm.showResults(from, to);
|
||||
} catch (e) {
|
||||
void helpers.showAndLogErrorMessage(e.message);
|
||||
void showAndLogErrorMessage(getErrorMessage(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function showResultsForCompletedQuery(
|
||||
query: CompletedQuery,
|
||||
query: CompletedLocalQueryInfo,
|
||||
forceReveal: WebviewReveal
|
||||
): Promise<void> {
|
||||
await intm.showResults(query, forceReveal, false);
|
||||
@@ -492,22 +515,43 @@ async function activateWithInstalledDistribution(
|
||||
if (databaseItem === undefined) {
|
||||
throw new Error('Can\'t run query without a selected database');
|
||||
}
|
||||
const info = await compileAndRunQueryAgainstDatabase(
|
||||
cliServer,
|
||||
qs,
|
||||
databaseItem,
|
||||
quickEval,
|
||||
selectedQuery,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
range
|
||||
);
|
||||
const item = qhm.buildCompletedQuery(info);
|
||||
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
|
||||
// Note we must update the query history view after showing results as the
|
||||
// display and sorting might depend on the number of results
|
||||
await qhm.addCompletedQuery(item);
|
||||
const databaseInfo = {
|
||||
name: databaseItem.name,
|
||||
databaseUri: databaseItem.databaseUri.toString(),
|
||||
};
|
||||
|
||||
// handle cancellation from the history view.
|
||||
const source = new CancellationTokenSource();
|
||||
token.onCancellationRequested(() => source.cancel());
|
||||
|
||||
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
|
||||
const item = new LocalQueryInfo(initialInfo, queryHistoryConfigurationListener, source);
|
||||
qhm.addQuery(item);
|
||||
try {
|
||||
const completedQueryInfo = await compileAndRunQueryAgainstDatabase(
|
||||
cliServer,
|
||||
qs,
|
||||
databaseItem,
|
||||
initialInfo,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
source.token,
|
||||
undefined,
|
||||
item,
|
||||
);
|
||||
item.completeThisQuery(completedQueryInfo);
|
||||
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.NotForced);
|
||||
// Note we must update the query history view after showing results as the
|
||||
// display and sorting might depend on the number of results
|
||||
} catch (e) {
|
||||
const err = asError(e);
|
||||
err.message = `Error running query: ${err.message}`;
|
||||
item.failureReason = err.message;
|
||||
throw e;
|
||||
} finally {
|
||||
await qhm.refreshTreeView();
|
||||
source.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,11 +571,11 @@ async function activateWithInstalledDistribution(
|
||||
try {
|
||||
await cliServer.generateQueryHelp(pathToQhelp, absolutePathToMd);
|
||||
await commands.executeCommand('markdown.showPreviewToSide', uri);
|
||||
} catch (err) {
|
||||
const errorMessage = err.message.includes('Generating qhelp in markdown') ? (
|
||||
} catch (e) {
|
||||
const errorMessage = getErrorMessage(e).includes('Generating qhelp in markdown') ? (
|
||||
`Could not generate markdown from ${pathToQhelp}: Bad formatting in .qhelp file.`
|
||||
) : `Could not open a preview of the generated file (${absolutePathToMd}).`;
|
||||
void helpers.showAndLogErrorMessage(errorMessage, { fullMessage: `${errorMessage}\n${err}` });
|
||||
void showAndLogErrorMessage(errorMessage, { fullMessage: `${errorMessage}\n${e}` });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +592,7 @@ async function activateWithInstalledDistribution(
|
||||
const uri = Uri.file(resolved.resolvedPath);
|
||||
await window.showTextDocument(uri, { preview: false });
|
||||
} else {
|
||||
void helpers.showAndLogErrorMessage(
|
||||
void showAndLogErrorMessage(
|
||||
'Jumping from a .qlref file to the .ql file it references is not '
|
||||
+ 'supported with the CLI version you are running.\n'
|
||||
+ `Please upgrade your CLI to version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_QLREF
|
||||
@@ -602,7 +646,10 @@ async function activateWithInstalledDistribution(
|
||||
{
|
||||
title: 'Running query',
|
||||
cancellable: true
|
||||
}
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger
|
||||
)
|
||||
);
|
||||
interface DatabaseQuickPickItem extends QuickPickItem {
|
||||
@@ -618,15 +665,15 @@ async function activateWithInstalledDistribution(
|
||||
) => {
|
||||
let filteredDBs = dbm.databaseItems;
|
||||
if (filteredDBs.length === 0) {
|
||||
void helpers.showAndLogErrorMessage('No databases found. Please add a suitable database to your workspace.');
|
||||
void showAndLogErrorMessage('No databases found. Please add a suitable database to your workspace.');
|
||||
return;
|
||||
}
|
||||
// If possible, only show databases with the right language (otherwise show all databases).
|
||||
const queryLanguage = await helpers.findLanguage(cliServer, uri);
|
||||
const queryLanguage = await findLanguage(cliServer, uri);
|
||||
if (queryLanguage) {
|
||||
filteredDBs = dbm.databaseItems.filter(db => db.language === queryLanguage);
|
||||
if (filteredDBs.length === 0) {
|
||||
void helpers.showAndLogErrorMessage(`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`);
|
||||
void showAndLogErrorMessage(`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -651,19 +698,19 @@ async function activateWithInstalledDistribution(
|
||||
for (const item of quickpick) {
|
||||
try {
|
||||
await compileAndRunQuery(false, uri, progress, token, item.databaseItem);
|
||||
} catch (error) {
|
||||
} catch (e) {
|
||||
skippedDatabases.push(item.label);
|
||||
errors.push(error.message);
|
||||
errors.push(getErrorMessage(e));
|
||||
}
|
||||
}
|
||||
if (skippedDatabases.length > 0) {
|
||||
void logger.log(`Errors:\n${errors.join('\n')}`);
|
||||
void helpers.showAndLogWarningMessage(
|
||||
void showAndLogWarningMessage(
|
||||
`The following databases were skipped:\n${skippedDatabases.join('\n')}.\nFor details about the errors, see the logs.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
void helpers.showAndLogErrorMessage('No databases selected.');
|
||||
void showAndLogErrorMessage('No databases selected.');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -690,7 +737,7 @@ async function activateWithInstalledDistribution(
|
||||
// files may be hidden from the user.
|
||||
if (dirFound) {
|
||||
const fileString = files.map(file => path.basename(file)).join(', ');
|
||||
const res = await helpers.showBinaryChoiceDialog(
|
||||
const res = await showBinaryChoiceDialog(
|
||||
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`
|
||||
);
|
||||
if (!res) {
|
||||
@@ -734,7 +781,11 @@ async function activateWithInstalledDistribution(
|
||||
{
|
||||
title: 'Running queries',
|
||||
cancellable: true
|
||||
})
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
@@ -747,7 +798,10 @@ async function activateWithInstalledDistribution(
|
||||
{
|
||||
title: 'Running query',
|
||||
cancellable: true
|
||||
})
|
||||
},
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger
|
||||
)
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
@@ -762,7 +816,11 @@ async function activateWithInstalledDistribution(
|
||||
{
|
||||
title: 'Running query',
|
||||
cancellable: true
|
||||
})
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger
|
||||
)
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
@@ -773,18 +831,27 @@ async function activateWithInstalledDistribution(
|
||||
displayQuickQuery(ctx, cliServer, databaseUI, progress, token),
|
||||
{
|
||||
title: 'Run Quick Query'
|
||||
}
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger
|
||||
)
|
||||
);
|
||||
|
||||
void logger.log('Initializing remote queries interface.');
|
||||
const rqm = new RemoteQueriesManager(ctx, logger, cliServer);
|
||||
const rqm = new RemoteQueriesManager(ctx, cliServer, qhm, queryStorageDir, logger);
|
||||
ctx.subscriptions.push(rqm);
|
||||
|
||||
// wait until after the remote queries manager is initialized to read the query history
|
||||
// since the rqm is notified of queries being added.
|
||||
await qhm.readQueryHistory();
|
||||
|
||||
|
||||
registerRemoteQueryTextProvider();
|
||||
|
||||
// The "runRemoteQuery" command is internal-only.
|
||||
// The "runVariantAnalysis" command is internal-only.
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress('codeQL.runRemoteQuery', async (
|
||||
commandRunnerWithProgress('codeQL.runVariantAnalysis', async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined
|
||||
@@ -804,23 +871,23 @@ async function activateWithInstalledDistribution(
|
||||
throw new Error('Remote queries require the CodeQL Canary version to run.');
|
||||
}
|
||||
}, {
|
||||
title: 'Run Remote Query',
|
||||
title: 'Run Variant Analysis',
|
||||
cancellable: true
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.monitorRemoteQuery', async (
|
||||
query: RemoteQuery,
|
||||
queryItem: RemoteQueryHistoryItem,
|
||||
token: CancellationToken) => {
|
||||
await rqm.monitorRemoteQuery(query, token);
|
||||
await rqm.monitorRemoteQuery(queryItem, token);
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.showFakeRemoteQueryResults', async () => {
|
||||
const analysisResultsManager = new AnalysesResultsManager(ctx, logger);
|
||||
const rqim = new RemoteQueriesInterfaceManager(ctx, logger, analysisResultsManager);
|
||||
await rqim.showResults(sampleRemoteQuery, sampleRemoteQueryResult);
|
||||
commandRunner('codeQL.autoDownloadRemoteQueryResults', async (
|
||||
queryResult: RemoteQueryResult,
|
||||
token: CancellationToken) => {
|
||||
await rqm.autoDownloadRemoteQueryResults(queryResult, token);
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
@@ -843,7 +910,7 @@ async function activateWithInstalledDistribution(
|
||||
token: CancellationToken
|
||||
) => {
|
||||
await qs.restartQueryServer(progress, token);
|
||||
void helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', {
|
||||
void showAndLogInformationMessage('CodeQL Query Server restarted.', {
|
||||
outputLogger: queryServerLogger,
|
||||
});
|
||||
}, {
|
||||
@@ -869,6 +936,18 @@ async function activateWithInstalledDistribution(
|
||||
title: 'Choose a Database from an Archive'
|
||||
})
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress('codeQL.chooseDatabaseGithub', async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
const credentials = await Credentials.initialize(ctx);
|
||||
await databaseUI.handleChooseDatabaseGithub(credentials, progress, token);
|
||||
},
|
||||
{
|
||||
title: 'Adding database from GitHub',
|
||||
})
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', (
|
||||
progress: ProgressCallback,
|
||||
@@ -899,7 +978,7 @@ async function activateWithInstalledDistribution(
|
||||
commandRunner('codeQL.copyVersion', async () => {
|
||||
const text = `CodeQL extension version: ${extension?.packageJSON.version} \nCodeQL CLI version: ${await getCliVersion()} \nPlatform: ${os.platform()} ${os.arch()}`;
|
||||
await env.clipboard.writeText(text);
|
||||
void helpers.showAndLogInformationMessage(text);
|
||||
void showAndLogInformationMessage(text);
|
||||
}));
|
||||
|
||||
const getCliVersion = async () => {
|
||||
@@ -921,7 +1000,7 @@ async function activateWithInstalledDistribution(
|
||||
const credentials = await Credentials.initialize(ctx);
|
||||
const octokit = await credentials.getOctokit();
|
||||
const userInfo = await octokit.users.getAuthenticated();
|
||||
void helpers.showAndLogInformationMessage(`Authenticated to GitHub as user: ${userInfo.data.login}`);
|
||||
void showAndLogInformationMessage(`Authenticated to GitHub as user: ${userInfo.data.login}`);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -945,37 +1024,46 @@ async function activateWithInstalledDistribution(
|
||||
}
|
||||
));
|
||||
|
||||
commands.registerCommand('codeQL.showLogs', () => {
|
||||
logger.show();
|
||||
});
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.showLogs', async () => {
|
||||
logger.show();
|
||||
})
|
||||
);
|
||||
|
||||
void logger.log('Starting language server.');
|
||||
ctx.subscriptions.push(client.start());
|
||||
|
||||
// Jump-to-definition and find-references
|
||||
void logger.log('Registering jump-to-definition handlers.');
|
||||
|
||||
// Store contextual queries in a temporary folder so that they are removed
|
||||
// when the application closes. There is no need for the user to interact with them.
|
||||
const contextualQueryStorageDir = path.join(tmpDir.name, 'contextual-query-storage');
|
||||
await fs.ensureDir(contextualQueryStorageDir);
|
||||
languages.registerDefinitionProvider(
|
||||
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
|
||||
new TemplateQueryDefinitionProvider(cliServer, qs, dbm)
|
||||
new TemplateQueryDefinitionProvider(cliServer, qs, dbm, contextualQueryStorageDir)
|
||||
);
|
||||
|
||||
languages.registerReferenceProvider(
|
||||
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
|
||||
new TemplateQueryReferenceProvider(cliServer, qs, dbm)
|
||||
new TemplateQueryReferenceProvider(cliServer, qs, dbm, contextualQueryStorageDir)
|
||||
);
|
||||
|
||||
const astViewer = new AstViewer();
|
||||
const templateProvider = new TemplatePrintAstProvider(cliServer, qs, dbm);
|
||||
const printAstTemplateProvider = new TemplatePrintAstProvider(cliServer, qs, dbm, contextualQueryStorageDir);
|
||||
const cfgTemplateProvider = new TemplatePrintCfgProvider(cliServer, dbm);
|
||||
|
||||
ctx.subscriptions.push(astViewer);
|
||||
ctx.subscriptions.push(commandRunnerWithProgress('codeQL.viewAst', async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
token: CancellationToken,
|
||||
selectedFile: Uri
|
||||
) => {
|
||||
const ast = await templateProvider.provideAst(
|
||||
const ast = await printAstTemplateProvider.provideAst(
|
||||
progress,
|
||||
token,
|
||||
window.activeTextEditor?.document,
|
||||
selectedFile ?? window.activeTextEditor?.document.uri,
|
||||
);
|
||||
if (ast) {
|
||||
astViewer.updateRoots(await ast.getRoots(), ast.db, ast.fileName);
|
||||
@@ -985,6 +1073,25 @@ async function activateWithInstalledDistribution(
|
||||
title: 'Calculate AST'
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
'codeQL.viewCfg',
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
const res = await cfgTemplateProvider.provideCfgUri(window.activeTextEditor?.document);
|
||||
if (res) {
|
||||
await compileAndRunQuery(false, res[0], progress, token, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Calculating Control Flow Graph',
|
||||
cancellable: true
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
await commands.executeCommand('codeQLDatabases.removeOrphanedDatabases');
|
||||
|
||||
void logger.log('Successfully finished extension initialization.');
|
||||
@@ -1003,13 +1110,10 @@ async function activateWithInstalledDistribution(
|
||||
}
|
||||
|
||||
function getContextStoragePath(ctx: ExtensionContext) {
|
||||
return ctx.storagePath || ctx.globalStoragePath;
|
||||
return ctx.storageUri?.fsPath || ctx.globalStorageUri.fsPath;
|
||||
}
|
||||
|
||||
async function initializeLogging(ctx: ExtensionContext): Promise<void> {
|
||||
const storagePath = getContextStoragePath(ctx);
|
||||
await logger.setLogStoragePath(storagePath, false);
|
||||
await ideServerLogger.setLogStoragePath(storagePath, false);
|
||||
ctx.subscriptions.push(logger);
|
||||
ctx.subscriptions.push(queryServerLogger);
|
||||
ctx.subscriptions.push(ideServerLogger);
|
||||
@@ -1019,7 +1123,7 @@ const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';
|
||||
|
||||
/**
|
||||
* This text provider lets us open readonly files in the editor.
|
||||
*
|
||||
*
|
||||
* TODO: Consolidate this with the 'codeql' text provider in query-history.ts.
|
||||
*/
|
||||
function registerRemoteQueryTextProvider() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as fs from 'fs-extra';
|
||||
import * as glob from 'glob-promise';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import {
|
||||
ExtensionContext,
|
||||
Uri,
|
||||
@@ -14,6 +15,17 @@ import { UserCancellationException } from './commandRunner';
|
||||
import { logger } from './logging';
|
||||
import { QueryMetadata } from './pure/interface-types';
|
||||
|
||||
// Shared temporary folder for the extension.
|
||||
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
|
||||
export const upgradesTmpDir = path.join(tmpDir.name, 'upgrades');
|
||||
fs.ensureDirSync(upgradesTmpDir);
|
||||
|
||||
export const tmpDirDisposal = {
|
||||
dispose: () => {
|
||||
tmpDir.removeCallback();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show an error message and log it to the console
|
||||
*
|
||||
@@ -533,3 +545,38 @@ export async function tryGetQueryMetadata(cliServer: CodeQLCliServer, queryPath:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file in the query directory that indicates when this query was created.
|
||||
* This is important for keeping track of when queries should be removed.
|
||||
*
|
||||
* @param queryPath The directory that will containt all files relevant to a query result.
|
||||
* It does not need to exist.
|
||||
*/
|
||||
export async function createTimestampFile(storagePath: string) {
|
||||
const timestampPath = path.join(storagePath, 'timestamp');
|
||||
await fs.ensureDir(storagePath);
|
||||
await fs.writeFile(timestampPath, Date.now().toString(), 'utf8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively walk a directory and return the full path to all files found.
|
||||
* Symbolic links are ignored.
|
||||
*
|
||||
* @param dir the directory to walk
|
||||
*
|
||||
* @return An iterator of the full path to all files recursively found in the directory.
|
||||
*/
|
||||
export async function* walkDirectory(dir: string): AsyncIterableIterator<string> {
|
||||
const seenFiles = new Set<string>();
|
||||
for await (const d of await fs.opendir(dir)) {
|
||||
const entry = path.join(dir, d.name);
|
||||
seenFiles.add(entry);
|
||||
if (d.isDirectory()) {
|
||||
yield* walkDirectory(entry);
|
||||
} else if (d.isFile()) {
|
||||
yield entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ export function getHtmlForWebview(
|
||||
webview: Webview,
|
||||
scriptUriOnDisk: Uri,
|
||||
stylesheetUrisOnDisk: Uri[],
|
||||
allowInlineStyles: boolean
|
||||
): string {
|
||||
// Convert the on-disk URIs into webview URIs.
|
||||
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
|
||||
@@ -128,8 +129,13 @@ export function getHtmlForWebview(
|
||||
// Use a nonce in the content security policy to uniquely identify the above resources.
|
||||
const nonce = getNonce();
|
||||
|
||||
const stylesheetsHtmlLines = stylesheetWebviewUris.map(stylesheetWebviewUri =>
|
||||
`<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">`);
|
||||
const stylesheetsHtmlLines = allowInlineStyles
|
||||
? stylesheetWebviewUris.map(uri => createStylesLinkWithoutNonce(uri))
|
||||
: stylesheetWebviewUris.map(uri => createStylesLinkWithNonce(nonce, uri));
|
||||
|
||||
const styleSrc = allowInlineStyles
|
||||
? `${webview.cspSource} vscode-file: 'unsafe-inline'`
|
||||
: `'nonce-${nonce}'`;
|
||||
|
||||
/*
|
||||
* Content security policy:
|
||||
@@ -143,7 +149,7 @@ export function getHtmlForWebview(
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${styleSrc}; connect-src ${webview.cspSource};">
|
||||
${stylesheetsHtmlLines.join(` ${os.EOL}`)}
|
||||
</head>
|
||||
<body>
|
||||
@@ -243,3 +249,11 @@ export async function jumpToLocation(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createStylesLinkWithNonce(nonce: string, uri: Uri): string {
|
||||
return `<link nonce="${nonce}" rel="stylesheet" href="${uri}">`;
|
||||
}
|
||||
|
||||
function createStylesLinkWithoutNonce(uri: Uri): string {
|
||||
return `<link rel="stylesheet" href="${uri}">`;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
import * as cli from './cli';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DatabaseEventKind, DatabaseItem, DatabaseManager } from './databases';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { assertNever } from './pure/helpers-pure';
|
||||
import { showAndLogErrorMessage, tmpDir } from './helpers';
|
||||
import { assertNever, getErrorMessage, getErrorStack } from './pure/helpers-pure';
|
||||
import {
|
||||
FromResultsViewMsg,
|
||||
Interpretation,
|
||||
@@ -27,13 +27,14 @@ import {
|
||||
InterpretedResultsSortState,
|
||||
SortDirection,
|
||||
ALERTS_TABLE_NAME,
|
||||
GRAPH_TABLE_NAME,
|
||||
RawResultsSortState,
|
||||
} from './pure/interface-types';
|
||||
import { Logger } from './logging';
|
||||
import * as messages from './pure/messages';
|
||||
import { commandRunner } from './commandRunner';
|
||||
import { CompletedQuery, interpretResults } from './query-results';
|
||||
import { QueryInfo, tmpDir } from './run-queries';
|
||||
import { CompletedQueryInfo, interpretResultsSarif, interpretGraphResults } from './query-results';
|
||||
import { QueryEvaluationInfo } from './run-queries';
|
||||
import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils';
|
||||
import {
|
||||
WebviewReveal,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-types';
|
||||
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
|
||||
import { PAGE_SIZE } from './config';
|
||||
import { CompletedLocalQueryInfo } from './query-results';
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -87,16 +89,40 @@ function sortInterpretedResults(
|
||||
}
|
||||
}
|
||||
|
||||
function numPagesOfResultSet(resultSet: RawResultSet): number {
|
||||
return Math.ceil(resultSet.schema.rows / PAGE_SIZE.getValue<number>());
|
||||
function interpretedPageSize(interpretation: Interpretation | undefined): number {
|
||||
if (interpretation?.data.t == 'GraphInterpretationData') {
|
||||
// Graph views always have one result per page.
|
||||
return 1;
|
||||
}
|
||||
return PAGE_SIZE.getValue<number>();
|
||||
}
|
||||
|
||||
function numPagesOfResultSet(resultSet: RawResultSet, interpretation?: Interpretation): number {
|
||||
const pageSize = interpretedPageSize(interpretation);
|
||||
|
||||
const n = interpretation?.data.t == 'GraphInterpretationData'
|
||||
? interpretation.data.dot.length
|
||||
: resultSet.schema.rows;
|
||||
|
||||
return Math.ceil(n / pageSize);
|
||||
}
|
||||
|
||||
function numInterpretedPages(interpretation: Interpretation | undefined): number {
|
||||
return Math.ceil((interpretation?.sarif.runs[0].results?.length || 0) / PAGE_SIZE.getValue<number>());
|
||||
if (!interpretation) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const pageSize = interpretedPageSize(interpretation);
|
||||
|
||||
const n = interpretation.data.t == 'GraphInterpretationData'
|
||||
? interpretation.data.dot.length
|
||||
: interpretation.data.runs[0].results?.length || 0;
|
||||
|
||||
return Math.ceil(n / pageSize);
|
||||
}
|
||||
|
||||
export class InterfaceManager extends DisposableObject {
|
||||
private _displayedQuery?: CompletedQuery;
|
||||
private _displayedQuery?: CompletedLocalQueryInfo;
|
||||
private _interpretation?: Interpretation;
|
||||
private _panel: vscode.WebviewPanel | undefined;
|
||||
private _panelLoaded = false;
|
||||
@@ -176,14 +202,15 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
));
|
||||
|
||||
this._panel.onDidDispose(
|
||||
this.push(this._panel.onDidDispose(
|
||||
() => {
|
||||
this._panel = undefined;
|
||||
this._displayedQuery = undefined;
|
||||
this._panelLoaded = false;
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
));
|
||||
const scriptPathOnDisk = vscode.Uri.file(
|
||||
ctx.asAbsolutePath('out/resultsView.js')
|
||||
);
|
||||
@@ -193,13 +220,14 @@ export class InterfaceManager extends DisposableObject {
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
[stylesheetPathOnDisk]
|
||||
[stylesheetPathOnDisk],
|
||||
false
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
this.push(panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
);
|
||||
));
|
||||
}
|
||||
return this._panel;
|
||||
}
|
||||
@@ -238,7 +266,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: 'resultsUpdating' });
|
||||
await this._displayedQuery.updateInterpretedSortState(sortState);
|
||||
await this._displayedQuery.completedQuery.updateInterpretedSortState(sortState);
|
||||
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
|
||||
}
|
||||
|
||||
@@ -254,7 +282,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: 'resultsUpdating' });
|
||||
await this._displayedQuery.updateSortState(
|
||||
await this._displayedQuery.completedQuery.updateSortState(
|
||||
this.cliServer,
|
||||
resultSetName,
|
||||
sortState
|
||||
@@ -303,7 +331,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
await this.changeInterpretedSortState(msg.sortState);
|
||||
break;
|
||||
case 'changePage':
|
||||
if (msg.selectedTable === ALERTS_TABLE_NAME) {
|
||||
if (msg.selectedTable === ALERTS_TABLE_NAME || msg.selectedTable === GRAPH_TABLE_NAME) {
|
||||
await this.showPageOfInterpretedResults(msg.pageNumber);
|
||||
}
|
||||
else {
|
||||
@@ -314,7 +342,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
// sortedResultsInfo doesn't have an entry for the current
|
||||
// result set. Use this to determine whether or not we use
|
||||
// the sorted bqrs file.
|
||||
this._displayedQuery?.sortedResultsInfo.has(msg.selectedTable) || false
|
||||
!!this._displayedQuery?.completedQuery.sortedResultsInfo[msg.selectedTable]
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -325,8 +353,8 @@ export class InterfaceManager extends DisposableObject {
|
||||
assertNever(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(e.message, {
|
||||
fullMessage: e.stack
|
||||
void showAndLogErrorMessage(getErrorMessage(e), {
|
||||
fullMessage: getErrorStack(e)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -347,7 +375,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
/**
|
||||
* Show query results in webview panel.
|
||||
* @param results Evaluation info for the executed query.
|
||||
* @param fullQuery Evaluation info for the executed query.
|
||||
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
|
||||
* @param forceReveal Force the webview panel to be visible and
|
||||
* Appropriate when the user has just performed an explicit
|
||||
@@ -355,27 +383,27 @@ export class InterfaceManager extends DisposableObject {
|
||||
* history entry.
|
||||
*/
|
||||
public async showResults(
|
||||
results: CompletedQuery,
|
||||
fullQuery: CompletedLocalQueryInfo,
|
||||
forceReveal: WebviewReveal,
|
||||
shouldKeepOldResultsWhileRendering = false
|
||||
): Promise<void> {
|
||||
if (results.result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
if (fullQuery.completedQuery.result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._interpretation = undefined;
|
||||
const interpretationPage = await this.interpretResultsInfo(
|
||||
results.query,
|
||||
results.interpretedResultsSortState
|
||||
fullQuery.completedQuery.query,
|
||||
fullQuery.completedQuery.interpretedResultsSortState
|
||||
);
|
||||
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
Object.entries(fullQuery.completedQuery.sortedResultsInfo).forEach(
|
||||
([k, v]) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
|
||||
);
|
||||
|
||||
this._displayedQuery = results;
|
||||
this._displayedQuery = fullQuery;
|
||||
|
||||
const panel = this.getPanel();
|
||||
await this.waitForPanelLoaded();
|
||||
@@ -388,7 +416,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
// more asynchronous message to not so abruptly interrupt
|
||||
// user's workflow by immediately revealing the panel.
|
||||
const showButton = 'View Results';
|
||||
const queryName = results.queryName;
|
||||
const queryName = fullQuery.getShortLabel();
|
||||
const resultPromise = vscode.window.showInformationMessage(
|
||||
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''
|
||||
}.`,
|
||||
@@ -407,7 +435,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
// Note that the resultSetSchemas will return offsets for the default (unsorted) page,
|
||||
// which may not be correct. However, in this case, it doesn't matter since we only
|
||||
// need the first offset, which will be the same no matter which sorting we use.
|
||||
const resultSetSchemas = await this.getResultSetSchemas(results);
|
||||
const resultSetSchemas = await this.getResultSetSchemas(fullQuery.completedQuery);
|
||||
const resultSetNames = resultSetSchemas.map(schema => schema.name);
|
||||
|
||||
const selectedTable = getDefaultResultSetName(resultSetNames);
|
||||
@@ -417,7 +445,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
// Use sorted results path if it exists. This may happen if we are
|
||||
// reloading the results view after it has been sorted in the past.
|
||||
const resultsPath = results.getResultsPath(selectedTable);
|
||||
const resultsPath = fullQuery.completedQuery.getResultsPath(selectedTable);
|
||||
const pageSize = PAGE_SIZE.getValue<number>();
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
resultsPath,
|
||||
@@ -432,11 +460,11 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
);
|
||||
const resultSet = transformBqrsResultSet(schema, chunk);
|
||||
results.setResultCount(interpretationPage?.numTotalResults || resultSet.schema.rows);
|
||||
fullQuery.completedQuery.setResultCount(interpretationPage?.numTotalResults || resultSet.schema.rows);
|
||||
const parsedResultSets: ParsedResultSets = {
|
||||
pageNumber: 0,
|
||||
pageSize,
|
||||
numPages: numPagesOfResultSet(resultSet),
|
||||
numPages: numPagesOfResultSet(resultSet, this._interpretation),
|
||||
numInterpretedPages: numInterpretedPages(this._interpretation),
|
||||
resultSet: { ...resultSet, t: 'RawResultSet' },
|
||||
selectedTable: undefined,
|
||||
@@ -446,17 +474,17 @@ export class InterfaceManager extends DisposableObject {
|
||||
await this.postMessage({
|
||||
t: 'setState',
|
||||
interpretation: interpretationPage,
|
||||
origResultsPaths: results.query.resultsPaths,
|
||||
origResultsPaths: fullQuery.completedQuery.query.resultsPaths,
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
fullQuery.completedQuery.query.resultsPaths.resultsPath
|
||||
),
|
||||
parsedResultSets,
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
database: fullQuery.initialInfo.databaseInfo,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
metadata: results.query.metadata,
|
||||
queryName: results.toString(),
|
||||
queryPath: results.query.program.queryPath
|
||||
metadata: fullQuery.completedQuery.query.metadata,
|
||||
queryName: fullQuery.label,
|
||||
queryPath: fullQuery.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
|
||||
@@ -472,29 +500,29 @@ export class InterfaceManager extends DisposableObject {
|
||||
if (this._interpretation === undefined) {
|
||||
throw new Error('Trying to show interpreted results but interpretation was undefined');
|
||||
}
|
||||
if (this._interpretation.sarif.runs[0].results === undefined) {
|
||||
if (this._interpretation.data.t === 'SarifInterpretationData' && this._interpretation.data.runs[0].results === undefined) {
|
||||
throw new Error('Trying to show interpreted results but results were undefined');
|
||||
}
|
||||
|
||||
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery);
|
||||
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery.completedQuery);
|
||||
const resultSetNames = resultSetSchemas.map(schema => schema.name);
|
||||
|
||||
await this.postMessage({
|
||||
t: 'showInterpretedPage',
|
||||
interpretation: this.getPageOfInterpretedResults(pageNumber),
|
||||
database: this._displayedQuery.database,
|
||||
metadata: this._displayedQuery.query.metadata,
|
||||
database: this._displayedQuery.initialInfo.databaseInfo,
|
||||
metadata: this._displayedQuery.completedQuery.query.metadata,
|
||||
pageNumber,
|
||||
resultSetNames,
|
||||
pageSize: PAGE_SIZE.getValue(),
|
||||
pageSize: interpretedPageSize(this._interpretation),
|
||||
numPages: numInterpretedPages(this._interpretation),
|
||||
queryName: this._displayedQuery.toString(),
|
||||
queryPath: this._displayedQuery.query.program.queryPath
|
||||
queryName: this._displayedQuery.label,
|
||||
queryPath: this._displayedQuery.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
|
||||
private async getResultSetSchemas(results: CompletedQuery, selectedTable = ''): Promise<ResultSetSchema[]> {
|
||||
const resultsPath = results.getResultsPath(selectedTable);
|
||||
private async getResultSetSchemas(completedQuery: CompletedQueryInfo, selectedTable = ''): Promise<ResultSetSchema[]> {
|
||||
const resultsPath = completedQuery.getResultsPath(selectedTable);
|
||||
const schemas = await this.cliServer.bqrsInfo(
|
||||
resultsPath,
|
||||
PAGE_SIZE.getValue()
|
||||
@@ -521,17 +549,17 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
Object.entries(results.completedQuery.sortedResultsInfo).forEach(
|
||||
([k, v]) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
|
||||
);
|
||||
|
||||
const resultSetSchemas = await this.getResultSetSchemas(results, sorted ? selectedTable : '');
|
||||
const resultSetSchemas = await this.getResultSetSchemas(results.completedQuery, sorted ? selectedTable : '');
|
||||
|
||||
// If there is a specific sorted table selected, a different bqrs file is loaded that doesn't have all the result set names.
|
||||
// Make sure that we load all result set names here.
|
||||
// See https://github.com/github/vscode-codeql/issues/1005
|
||||
const allResultSetSchemas = sorted ? await this.getResultSetSchemas(results, '') : resultSetSchemas;
|
||||
const allResultSetSchemas = sorted ? await this.getResultSetSchemas(results.completedQuery, '') : resultSetSchemas;
|
||||
const resultSetNames = allResultSetSchemas.map(schema => schema.name);
|
||||
|
||||
const schema = resultSetSchemas.find(
|
||||
@@ -542,7 +570,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
const pageSize = PAGE_SIZE.getValue<number>();
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
results.getResultsPath(selectedTable, sorted),
|
||||
results.completedQuery.getResultsPath(selectedTable, sorted),
|
||||
schema.name,
|
||||
{
|
||||
offset: schema.pagination?.offsets[pageNumber],
|
||||
@@ -564,17 +592,17 @@ export class InterfaceManager extends DisposableObject {
|
||||
await this.postMessage({
|
||||
t: 'setState',
|
||||
interpretation: this._interpretation,
|
||||
origResultsPaths: results.query.resultsPaths,
|
||||
origResultsPaths: results.completedQuery.query.resultsPaths,
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
results.completedQuery.query.resultsPaths.resultsPath
|
||||
),
|
||||
parsedResultSets,
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
database: results.initialInfo.databaseInfo,
|
||||
shouldKeepOldResultsWhileRendering: false,
|
||||
metadata: results.query.metadata,
|
||||
queryName: results.toString(),
|
||||
queryPath: results.query.program.queryPath
|
||||
metadata: results.completedQuery.query.metadata,
|
||||
queryName: results.label,
|
||||
queryPath: results.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
|
||||
@@ -589,28 +617,45 @@ export class InterfaceManager extends DisposableObject {
|
||||
void this.logger.log('No results path. Cannot display interpreted results.');
|
||||
return undefined;
|
||||
}
|
||||
let data;
|
||||
let numTotalResults;
|
||||
if (metadata?.kind === GRAPH_TABLE_NAME) {
|
||||
data = await interpretGraphResults(
|
||||
this.cliServer,
|
||||
metadata,
|
||||
resultsPaths,
|
||||
sourceInfo
|
||||
);
|
||||
numTotalResults = data.dot.length;
|
||||
} else {
|
||||
const sarif = await interpretResultsSarif(
|
||||
this.cliServer,
|
||||
metadata,
|
||||
resultsPaths,
|
||||
sourceInfo
|
||||
);
|
||||
|
||||
const sarif = await interpretResults(
|
||||
this.cliServer,
|
||||
metadata,
|
||||
resultsPaths,
|
||||
sourceInfo
|
||||
);
|
||||
sarif.runs.forEach(run => {
|
||||
if (run.results) {
|
||||
sortInterpretedResults(run.results, sortState);
|
||||
}
|
||||
});
|
||||
|
||||
sarif.runs.forEach(run => {
|
||||
if (run.results !== undefined) {
|
||||
sortInterpretedResults(run.results, sortState);
|
||||
}
|
||||
});
|
||||
sarif.sortState = sortState;
|
||||
data = sarif;
|
||||
|
||||
const numTotalResults = sarif.runs[0]?.results?.length || 0;
|
||||
numTotalResults = (() => {
|
||||
return sarif.runs?.[0]?.results
|
||||
? sarif.runs[0].results.length
|
||||
: 0;
|
||||
})();
|
||||
}
|
||||
|
||||
const interpretation: Interpretation = {
|
||||
sarif,
|
||||
data,
|
||||
sourceLocationPrefix,
|
||||
numTruncatedResults: 0,
|
||||
numTotalResults,
|
||||
sortState,
|
||||
numTotalResults
|
||||
};
|
||||
this._interpretation = interpretation;
|
||||
return interpretation;
|
||||
@@ -619,7 +664,6 @@ export class InterfaceManager extends DisposableObject {
|
||||
private getPageOfInterpretedResults(
|
||||
pageNumber: number
|
||||
): Interpretation {
|
||||
|
||||
function getPageOfRun(run: Sarif.Run): Sarif.Run {
|
||||
return {
|
||||
...run, results: run.results?.slice(
|
||||
@@ -629,32 +673,44 @@ export class InterfaceManager extends DisposableObject {
|
||||
};
|
||||
}
|
||||
|
||||
if (this._interpretation === undefined) {
|
||||
const interp = this._interpretation;
|
||||
if (interp === undefined) {
|
||||
throw new Error('Tried to get interpreted results before interpretation finished');
|
||||
}
|
||||
if (this._interpretation.sarif.runs.length !== 1) {
|
||||
void this.logger.log(`Warning: SARIF file had ${this._interpretation.sarif.runs.length} runs, expected 1`);
|
||||
|
||||
if (interp.data.t !== 'SarifInterpretationData')
|
||||
return interp;
|
||||
|
||||
if (interp.data.runs.length !== 1) {
|
||||
void this.logger.log(`Warning: SARIF file had ${interp.data.runs.length} runs, expected 1`);
|
||||
}
|
||||
const interp = this._interpretation;
|
||||
|
||||
return {
|
||||
...interp,
|
||||
sarif: { ...interp.sarif, runs: [getPageOfRun(interp.sarif.runs[0])] },
|
||||
data: {
|
||||
...interp.data,
|
||||
runs: [getPageOfRun(interp.data.runs[0])]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async interpretResultsInfo(
|
||||
query: QueryInfo,
|
||||
query: QueryEvaluationInfo,
|
||||
sortState: InterpretedResultsSortState | undefined
|
||||
): Promise<Interpretation | undefined> {
|
||||
if (
|
||||
(await query.canHaveInterpretedResults()) &&
|
||||
query.canHaveInterpretedResults() &&
|
||||
query.quickEvalPosition === undefined // never do results interpretation if quickEval
|
||||
) {
|
||||
try {
|
||||
const sourceLocationPrefix = await query.dbItem.getSourceLocationPrefix(
|
||||
const dbItem = this.databaseManager.findDatabaseItem(Uri.file(query.dbItemPath));
|
||||
if (!dbItem) {
|
||||
throw new Error(`Could not find database item for ${query.dbItemPath}`);
|
||||
}
|
||||
const sourceLocationPrefix = await dbItem.getSourceLocationPrefix(
|
||||
this.cliServer
|
||||
);
|
||||
const sourceArchiveUri = query.dbItem.sourceArchive;
|
||||
const sourceArchiveUri = dbItem.sourceArchive;
|
||||
const sourceInfo =
|
||||
sourceArchiveUri === undefined
|
||||
? undefined
|
||||
@@ -673,7 +729,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
// If interpretation fails, accept the error and continue
|
||||
// trying to render uninterpreted results anyway.
|
||||
void showAndLogErrorMessage(
|
||||
`Showing raw results instead of interpreted ones due to an error. ${e.message}`
|
||||
`Showing raw results instead of interpreted ones due to an error. ${getErrorMessage(e)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -712,9 +768,8 @@ export class InterfaceManager extends DisposableObject {
|
||||
try {
|
||||
await this.showProblemResultsAsDiagnostics(interpretation, database);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : e.toString();
|
||||
void this.logger.log(
|
||||
`Exception while computing problem results as diagnostics: ${msg}`
|
||||
`Exception while computing problem results as diagnostics: ${getErrorMessage(e)}`
|
||||
);
|
||||
this._diagnosticCollection.clear();
|
||||
}
|
||||
@@ -724,9 +779,12 @@ export class InterfaceManager extends DisposableObject {
|
||||
interpretation: Interpretation,
|
||||
databaseItem: DatabaseItem
|
||||
): Promise<void> {
|
||||
const { sarif, sourceLocationPrefix } = interpretation;
|
||||
const { data, sourceLocationPrefix } = interpretation;
|
||||
|
||||
if (!sarif.runs || !sarif.runs[0].results) {
|
||||
if (data.t !== 'SarifInterpretationData')
|
||||
return;
|
||||
|
||||
if (!data.runs || !data.runs[0].results) {
|
||||
void this.logger.log(
|
||||
'Didn\'t find a run in the sarif results. Error processing sarif?'
|
||||
);
|
||||
@@ -735,7 +793,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
const diagnostics: [Uri, ReadonlyArray<Diagnostic>][] = [];
|
||||
|
||||
for (const result of sarif.runs[0].results) {
|
||||
for (const result of data.runs[0].results) {
|
||||
const message = result.message.text;
|
||||
if (message === undefined) {
|
||||
void this.logger.log('Sarif had result without plaintext message');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
|
||||
import { window as Window, OutputChannel, Progress } from 'vscode';
|
||||
import { DisposableObject } from './pure/disposable-object';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
@@ -26,18 +26,6 @@ export interface Logger {
|
||||
* @param location log to remove
|
||||
*/
|
||||
removeAdditionalLogLocation(location: string | undefined): void;
|
||||
|
||||
/**
|
||||
* The base location where all side log files are stored.
|
||||
*/
|
||||
getBaseLocation(): string | undefined;
|
||||
|
||||
/**
|
||||
* Sets the location where logs are stored.
|
||||
* @param storagePath The path where logs are stored.
|
||||
* @param isCustomLogDirectory Whether the logs are stored in a custom, user-specified directory.
|
||||
*/
|
||||
setLogStoragePath(storagePath: string, isCustomLogDirectory: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export type ProgressReporter = Progress<{ message: string }>;
|
||||
@@ -46,27 +34,15 @@ export type ProgressReporter = Progress<{ message: string }>;
|
||||
export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
public readonly outputChannel: OutputChannel;
|
||||
private readonly additionalLocations = new Map<string, AdditionalLogLocation>();
|
||||
private additionalLogLocationPath: string | undefined;
|
||||
isCustomLogDirectory: boolean;
|
||||
|
||||
constructor(private title: string) {
|
||||
constructor(title: string) {
|
||||
super();
|
||||
this.outputChannel = Window.createOutputChannel(title);
|
||||
this.push(this.outputChannel);
|
||||
this.isCustomLogDirectory = false;
|
||||
}
|
||||
|
||||
async setLogStoragePath(storagePath: string, isCustomLogDirectory: boolean): Promise<void> {
|
||||
this.additionalLogLocationPath = path.join(storagePath, this.title);
|
||||
|
||||
this.isCustomLogDirectory = isCustomLogDirectory;
|
||||
|
||||
if (!this.isCustomLogDirectory) {
|
||||
// clear out any old state from previous runs
|
||||
await fs.remove(this.additionalLogLocationPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is asynchronous and will only resolve once the message is written
|
||||
* to the side log (if required). It is not necessary to await the results of this
|
||||
@@ -74,31 +50,41 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
* continuing.
|
||||
*/
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
if (options.trailingNewline === undefined) {
|
||||
options.trailingNewline = true;
|
||||
}
|
||||
|
||||
if (options.trailingNewline) {
|
||||
this.outputChannel.appendLine(message);
|
||||
} else {
|
||||
this.outputChannel.append(message);
|
||||
}
|
||||
|
||||
if (this.additionalLogLocationPath && options.additionalLogLocation) {
|
||||
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
|
||||
let additional = this.additionalLocations.get(logPath);
|
||||
if (!additional) {
|
||||
const msg = `| Log being saved to ${logPath} |`;
|
||||
const separator = new Array(msg.length).fill('-').join('');
|
||||
this.outputChannel.appendLine(separator);
|
||||
this.outputChannel.appendLine(msg);
|
||||
this.outputChannel.appendLine(separator);
|
||||
additional = new AdditionalLogLocation(logPath, !this.isCustomLogDirectory);
|
||||
this.additionalLocations.set(logPath, additional);
|
||||
this.track(additional);
|
||||
try {
|
||||
if (options.trailingNewline === undefined) {
|
||||
options.trailingNewline = true;
|
||||
}
|
||||
if (options.trailingNewline) {
|
||||
this.outputChannel.appendLine(message);
|
||||
} else {
|
||||
this.outputChannel.append(message);
|
||||
}
|
||||
|
||||
await additional.log(message, options);
|
||||
if (options.additionalLogLocation) {
|
||||
if (!path.isAbsolute(options.additionalLogLocation)) {
|
||||
throw new Error(`Additional Log Location must be an absolute path: ${options.additionalLogLocation}`);
|
||||
}
|
||||
const logPath = options.additionalLogLocation;
|
||||
let additional = this.additionalLocations.get(logPath);
|
||||
if (!additional) {
|
||||
const msg = `| Log being saved to ${logPath} |`;
|
||||
const separator = new Array(msg.length).fill('-').join('');
|
||||
this.outputChannel.appendLine(separator);
|
||||
this.outputChannel.appendLine(msg);
|
||||
this.outputChannel.appendLine(separator);
|
||||
additional = new AdditionalLogLocation(logPath);
|
||||
this.additionalLocations.set(logPath, additional);
|
||||
}
|
||||
|
||||
await additional.log(message, options);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === 'Channel has been closed') {
|
||||
// Output channel is closed logging to console instead
|
||||
console.log('Output channel is closed logging to console instead:', message);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,26 +93,15 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
}
|
||||
|
||||
removeAdditionalLogLocation(location: string | undefined): void {
|
||||
if (this.additionalLogLocationPath && location) {
|
||||
const logPath = location.startsWith(this.additionalLogLocationPath)
|
||||
? location
|
||||
: path.join(this.additionalLogLocationPath, location);
|
||||
const additional = this.additionalLocations.get(logPath);
|
||||
if (additional) {
|
||||
this.disposeAndStopTracking(additional);
|
||||
this.additionalLocations.delete(logPath);
|
||||
}
|
||||
if (location) {
|
||||
this.additionalLocations.delete(location);
|
||||
}
|
||||
}
|
||||
|
||||
getBaseLocation() {
|
||||
return this.additionalLogLocationPath;
|
||||
}
|
||||
}
|
||||
|
||||
class AdditionalLogLocation extends Disposable {
|
||||
constructor(private location: string, private shouldDeleteLogs: boolean) {
|
||||
super(() => { /**/ });
|
||||
class AdditionalLogLocation {
|
||||
constructor(private location: string) {
|
||||
/**/
|
||||
}
|
||||
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
@@ -139,12 +114,6 @@ class AdditionalLogLocation extends Disposable {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
if (this.shouldDeleteLogs) {
|
||||
await fs.remove(this.location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The global logger for the extension. */
|
||||
|
||||
@@ -79,11 +79,11 @@ export interface WholeFileLocation {
|
||||
|
||||
export type ResolvableLocationValue = WholeFileLocation | LineColumnLocation;
|
||||
|
||||
export type UrlValue = ResolvableLocationValue | string;
|
||||
export type UrlValue = ResolvableLocationValue | string;
|
||||
|
||||
export type ColumnValue = EntityValue | number | string | boolean;
|
||||
export type CellValue = EntityValue | number | string | boolean;
|
||||
|
||||
export type ResultRow = ColumnValue[];
|
||||
export type ResultRow = CellValue[];
|
||||
|
||||
export interface RawResultSet {
|
||||
readonly schema: ResultSetSchema;
|
||||
@@ -104,6 +104,6 @@ export function transformBqrsResultSet(
|
||||
}
|
||||
|
||||
export interface DecodedBqrsChunk {
|
||||
tuples: ColumnValue[][];
|
||||
tuples: CellValue[][];
|
||||
next?: number;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
LineColumnLocation,
|
||||
WholeFileLocation
|
||||
} from './bqrs-cli-types';
|
||||
import { createRemoteFileRef } from './location-link-utils';
|
||||
|
||||
/**
|
||||
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
|
||||
@@ -93,3 +94,30 @@ export function isWholeFileLoc(loc: UrlValue): loc is WholeFileLocation {
|
||||
export function isStringLoc(loc: UrlValue): loc is string {
|
||||
return typeof loc === 'string';
|
||||
}
|
||||
|
||||
export function tryGetRemoteLocation(
|
||||
loc: UrlValue | undefined,
|
||||
fileLinkPrefix: string
|
||||
): string | undefined {
|
||||
const resolvableLocation = tryGetResolvableLocation(loc);
|
||||
if (!resolvableLocation) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Remote locations have the following format:
|
||||
// file:/home/runner/work/<repo>/<repo/relative/path/to/file
|
||||
// So we need to drop the first 6 parts of the path.
|
||||
|
||||
// TODO: We can make this more robust to other path formats.
|
||||
const locationParts = resolvableLocation.uri.split('/');
|
||||
const trimmedLocation = locationParts.slice(6, locationParts.length).join('/');
|
||||
|
||||
const fileLink = {
|
||||
fileLinkPrefix,
|
||||
filePath: trimmedLocation,
|
||||
};
|
||||
return createRemoteFileRef(
|
||||
fileLink,
|
||||
resolvableLocation.startLine,
|
||||
resolvableLocation.endLine);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/**
|
||||
* helpers-pure.ts
|
||||
* ------------
|
||||
@@ -29,3 +30,27 @@ export const asyncFilter = async function <T>(arr: T[], predicate: (arg0: T) =>
|
||||
const results = await Promise.all(arr.map(predicate));
|
||||
return arr.filter((_, index) => results[index]);
|
||||
};
|
||||
|
||||
export const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
export const ONE_HOUR_IN_MS = 1000 * 60 * 60;
|
||||
export const TWO_HOURS_IN_MS = 1000 * 60 * 60 * 2;
|
||||
export const THREE_HOURS_IN_MS = 1000 * 60 * 60 * 3;
|
||||
|
||||
/**
|
||||
* This regex matches strings of the form `owner/repo` where:
|
||||
* - `owner` is made up of alphanumeric characters or single hyphens, starting and ending in an alphanumeric character
|
||||
* - `repo` is made up of alphanumeric characters, hyphens, or underscores
|
||||
*/
|
||||
export const REPO_REGEX = /^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+\/[a-zA-Z0-9-_]+$/;
|
||||
|
||||
export function getErrorMessage(e: any) {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
export function getErrorStack(e: any) {
|
||||
return e instanceof Error ? e.stack ?? '' : '';
|
||||
}
|
||||
|
||||
export function asError(e: any): Error {
|
||||
return e instanceof Error ? e : new Error(String(e));
|
||||
}
|
||||
|
||||
@@ -10,15 +10,17 @@ import { RawResultSet, ResultRow, ResultSetSchema, Column, ResolvableLocationVal
|
||||
|
||||
export const SELECT_TABLE_NAME = '#select';
|
||||
export const ALERTS_TABLE_NAME = 'alerts';
|
||||
export const GRAPH_TABLE_NAME = 'graph';
|
||||
|
||||
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
|
||||
export type PathTableResultSet = {
|
||||
t: 'SarifResultSet';
|
||||
export type InterpretedResultSet<T> = {
|
||||
t: 'InterpretedResultSet';
|
||||
readonly schema: ResultSetSchema;
|
||||
name: string;
|
||||
} & Interpretation;
|
||||
interpretation: InterpretationT<T>;
|
||||
};
|
||||
|
||||
export type ResultSet = RawTableResultSet | PathTableResultSet;
|
||||
export type ResultSet = RawTableResultSet | InterpretedResultSet<InterpretationData>;
|
||||
|
||||
/**
|
||||
* Only ever show this many rows in a raw result table.
|
||||
@@ -46,18 +48,31 @@ export interface PreviousExecution {
|
||||
durationSeconds: number;
|
||||
}
|
||||
|
||||
export interface Interpretation {
|
||||
sourceLocationPrefix: string;
|
||||
numTruncatedResults: number;
|
||||
numTotalResults: number;
|
||||
export type SarifInterpretationData = {
|
||||
t: 'SarifInterpretationData';
|
||||
/**
|
||||
* sortState being undefined means don't sort, just present results in the order
|
||||
* they appear in the sarif file.
|
||||
*/
|
||||
sortState?: InterpretedResultsSortState;
|
||||
sarif: sarif.Log;
|
||||
} & sarif.Log;
|
||||
|
||||
export type GraphInterpretationData = {
|
||||
t: 'GraphInterpretationData';
|
||||
dot: string[];
|
||||
};
|
||||
|
||||
export type InterpretationData = SarifInterpretationData | GraphInterpretationData;
|
||||
|
||||
export interface InterpretationT<T> {
|
||||
sourceLocationPrefix: string;
|
||||
numTruncatedResults: number;
|
||||
numTotalResults: number;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export type Interpretation = InterpretationT<InterpretationData>;
|
||||
|
||||
export interface ResultsPaths {
|
||||
resultsPath: string;
|
||||
interpretedResultsPath: string;
|
||||
@@ -316,7 +331,7 @@ export interface SetComparisonsMessage {
|
||||
readonly currentResultSetName: string;
|
||||
readonly rows: QueryCompareResult | undefined;
|
||||
readonly message: string | undefined;
|
||||
readonly datebaseUri: string;
|
||||
readonly databaseUri: string;
|
||||
}
|
||||
|
||||
export enum DiffKind {
|
||||
@@ -357,8 +372,9 @@ export function getDefaultResultSetName(
|
||||
// Choose first available result set from the array
|
||||
return [
|
||||
ALERTS_TABLE_NAME,
|
||||
GRAPH_TABLE_NAME,
|
||||
SELECT_TABLE_NAME,
|
||||
resultSetNames[0],
|
||||
resultSetNames[0]
|
||||
].filter((resultSetName) => resultSetNames.includes(resultSetName))[0];
|
||||
}
|
||||
|
||||
@@ -412,3 +428,4 @@ export interface RemoteQueryDownloadAllAnalysesResultsMessage {
|
||||
t: 'remoteQueryDownloadAllAnalysesResults';
|
||||
analysisSummaries: AnalysisSummary[];
|
||||
}
|
||||
|
||||
|
||||
15
extensions/ql-vscode/src/pure/location-link-utils.ts
Normal file
15
extensions/ql-vscode/src/pure/location-link-utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { FileLink } from '../remote-queries/shared/analysis-result';
|
||||
|
||||
export function createRemoteFileRef(
|
||||
fileLink: FileLink,
|
||||
startLine?: number,
|
||||
endLine?: number
|
||||
): string {
|
||||
if (startLine && endLine) {
|
||||
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}#L${startLine}-L${endLine}`;
|
||||
} else if (startLine) {
|
||||
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}#L${startLine}`;
|
||||
} else {
|
||||
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}`;
|
||||
}
|
||||
}
|
||||
@@ -646,6 +646,35 @@ export interface ClearCacheParams {
|
||||
*/
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters to start a new structured log
|
||||
*/
|
||||
export interface StartLogParams {
|
||||
/**
|
||||
* The dataset for which we want to start a new structured log
|
||||
*/
|
||||
db: Dataset;
|
||||
/**
|
||||
* The path where we want to place the new structured log
|
||||
*/
|
||||
logPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters to terminate a structured log
|
||||
*/
|
||||
export interface EndLogParams {
|
||||
/**
|
||||
* The dataset for which we want to terminated the log
|
||||
*/
|
||||
db: Dataset;
|
||||
/**
|
||||
* The path of the log to terminate, will be a no-op if we aren't logging here
|
||||
*/
|
||||
logPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for trimming the cache of a dataset
|
||||
*/
|
||||
@@ -682,6 +711,26 @@ export interface ClearCacheResult {
|
||||
deletionMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of starting a new structured log.
|
||||
*/
|
||||
export interface StartLogResult {
|
||||
/**
|
||||
* A user friendly message saying what happened.
|
||||
*/
|
||||
outcomeMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of terminating a structured log.
|
||||
*/
|
||||
export interface EndLogResult {
|
||||
/**
|
||||
* A user friendly message saying what happened.
|
||||
*/
|
||||
outcomeMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for running a set of queries
|
||||
*/
|
||||
@@ -1018,6 +1067,16 @@ export const compileUpgrade = new rpc.RequestType<WithProgressId<CompileUpgradeP
|
||||
*/
|
||||
export const compileUpgradeSequence = new rpc.RequestType<WithProgressId<CompileUpgradeSequenceParams>, CompileUpgradeSequenceResult, void, void>('compilation/compileUpgradeSequence');
|
||||
|
||||
/**
|
||||
* Start a new structured log in the evaluator, terminating the previous one if it exists
|
||||
*/
|
||||
export const startLog = new rpc.RequestType<WithProgressId<StartLogParams>, StartLogResult, void, void>('evaluation/startLog');
|
||||
|
||||
/**
|
||||
* Terminate a structured log in the evaluator. Is a no-op if we aren't logging to the given location
|
||||
*/
|
||||
export const endLog = new rpc.RequestType<WithProgressId<EndLogParams>, EndLogResult, void, void>('evaluation/endLog');
|
||||
|
||||
/**
|
||||
* Clear the cache of a dataset
|
||||
*/
|
||||
|
||||
@@ -127,35 +127,49 @@ export function parseSarifLocation(
|
||||
userVisibleFile
|
||||
} as ParsedSarifLocation;
|
||||
} else {
|
||||
const region = physicalLocation.region;
|
||||
// We assume that the SARIF we're given always has startLine
|
||||
// This is not mandated by the SARIF spec, but should be true of
|
||||
// SARIF output by our own tools.
|
||||
const startLine = region.startLine!;
|
||||
|
||||
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
|
||||
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
|
||||
const endLine = region.endLine === undefined ? startLine : region.endLine;
|
||||
const startColumn = region.startColumn === undefined ? 1 : region.startColumn;
|
||||
|
||||
// We also assume that our tools will always supply `endColumn` field, which is
|
||||
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
|
||||
// length we don't know at this point in the code.
|
||||
//
|
||||
// It is off by one with respect to the way vscode counts columns in selections.
|
||||
const endColumn = region.endColumn! - 1;
|
||||
const region = parseSarifRegion(physicalLocation.region);
|
||||
|
||||
return {
|
||||
uri: effectiveLocation,
|
||||
userVisibleFile,
|
||||
startLine,
|
||||
startColumn,
|
||||
endLine,
|
||||
endColumn,
|
||||
...region
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSarifRegion(
|
||||
region: Sarif.Region
|
||||
): {
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
startColumn: number,
|
||||
endColumn: number
|
||||
} {
|
||||
// The SARIF we're given should have a startLine, but we
|
||||
// fall back to 1, just in case something has gone wrong.
|
||||
const startLine = region.startLine ?? 1;
|
||||
|
||||
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
|
||||
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
|
||||
const endLine = region.endLine === undefined ? startLine : region.endLine;
|
||||
const startColumn = region.startColumn === undefined ? 1 : region.startColumn;
|
||||
|
||||
// Our tools should always supply `endColumn` field, which is fortunate, since
|
||||
// the SARIF spec says that it defaults to the end of the line, whose
|
||||
// length we don't know at this point in the code. We fall back to 1,
|
||||
// just in case something has gone wrong.
|
||||
//
|
||||
// It is off by one with respect to the way vscode counts columns in selections.
|
||||
const endColumn = (region.endColumn ?? 1) - 1;
|
||||
|
||||
return {
|
||||
startLine,
|
||||
startColumn,
|
||||
endLine,
|
||||
endColumn
|
||||
};
|
||||
}
|
||||
|
||||
export function isNoLocation(loc: ParsedSarifLocation): loc is NoLocation {
|
||||
return 'hint' in loc;
|
||||
}
|
||||
|
||||
135
extensions/ql-vscode/src/query-history-scrubber.ts
Normal file
135
extensions/ql-vscode/src/query-history-scrubber.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { Disposable, ExtensionContext } from 'vscode';
|
||||
import { logger } from './logging';
|
||||
|
||||
const LAST_SCRUB_TIME_KEY = 'lastScrubTime';
|
||||
|
||||
type Counter = {
|
||||
increment: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers an interval timer that will periodically check for queries old enought
|
||||
* to be deleted.
|
||||
*
|
||||
* Note that this scrubber will clean all queries from all workspaces. It should not
|
||||
* run too often and it should only run from one workspace at a time.
|
||||
*
|
||||
* Generally, `wakeInterval` should be significantly shorter than `throttleTime`.
|
||||
*
|
||||
* @param wakeInterval How often to check to see if the job should run.
|
||||
* @param throttleTime How often to actually run the job.
|
||||
* @param maxQueryTime The maximum age of a query before is ready for deletion.
|
||||
* @param queryDirectory The directory containing all queries.
|
||||
* @param ctx The extension context.
|
||||
*/
|
||||
export function registerQueryHistoryScubber(
|
||||
wakeInterval: number,
|
||||
throttleTime: number,
|
||||
maxQueryTime: number,
|
||||
queryDirectory: string,
|
||||
ctx: ExtensionContext,
|
||||
|
||||
// optional counter to keep track of how many times the scrubber has run
|
||||
counter?: Counter
|
||||
): Disposable {
|
||||
const deregister = setInterval(scrubQueries, wakeInterval, throttleTime, maxQueryTime, queryDirectory, ctx, counter);
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
clearInterval(deregister);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function scrubQueries(
|
||||
throttleTime: number,
|
||||
maxQueryTime: number,
|
||||
queryDirectory: string,
|
||||
ctx: ExtensionContext,
|
||||
counter?: Counter
|
||||
) {
|
||||
const lastScrubTime = ctx.globalState.get<number>(LAST_SCRUB_TIME_KEY);
|
||||
const now = Date.now();
|
||||
|
||||
// If we have never scrubbed before, or if the last scrub was more than `throttleTime` ago,
|
||||
// then scrub again.
|
||||
if (lastScrubTime === undefined || now - lastScrubTime >= throttleTime) {
|
||||
await ctx.globalState.update(LAST_SCRUB_TIME_KEY, now);
|
||||
|
||||
let scrubCount = 0; // total number of directories deleted
|
||||
try {
|
||||
counter?.increment();
|
||||
void logger.log('Scrubbing query directory. Removing old queries.');
|
||||
if (!(await fs.pathExists(queryDirectory))) {
|
||||
void logger.log(`Cannot scrub. Query directory does not exist: ${queryDirectory}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseNames = await fs.readdir(queryDirectory);
|
||||
const errors: string[] = [];
|
||||
for (const baseName of baseNames) {
|
||||
const dir = path.join(queryDirectory, baseName);
|
||||
const scrubResult = await scrubDirectory(dir, now, maxQueryTime);
|
||||
if (scrubResult.errorMsg) {
|
||||
errors.push(scrubResult.errorMsg);
|
||||
}
|
||||
if (scrubResult.deleted) {
|
||||
scrubCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(os.EOL + errors.join(os.EOL));
|
||||
}
|
||||
} catch (e) {
|
||||
void logger.log(`Error while scrubbing queries: ${e}`);
|
||||
} finally {
|
||||
void logger.log(`Scrubbed ${scrubCount} old queries.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function scrubDirectory(dir: string, now: number, maxQueryTime: number): Promise<{
|
||||
errorMsg?: string,
|
||||
deleted: boolean
|
||||
}> {
|
||||
const timestampFile = path.join(dir, 'timestamp');
|
||||
try {
|
||||
let deleted = true;
|
||||
if (!(await fs.stat(dir)).isDirectory()) {
|
||||
void logger.log(` ${dir} is not a directory. Deleting.`);
|
||||
await fs.remove(dir);
|
||||
} else if (!(await fs.pathExists(timestampFile))) {
|
||||
void logger.log(` ${dir} has no timestamp file. Deleting.`);
|
||||
await fs.remove(dir);
|
||||
} else if (!(await fs.stat(timestampFile)).isFile()) {
|
||||
void logger.log(` ${timestampFile} is not a file. Deleting.`);
|
||||
await fs.remove(dir);
|
||||
} else {
|
||||
const timestampText = await fs.readFile(timestampFile, 'utf8');
|
||||
const timestamp = parseInt(timestampText, 10);
|
||||
|
||||
if (Number.isNaN(timestamp)) {
|
||||
void logger.log(` ${dir} has invalid timestamp '${timestampText}'. Deleting.`);
|
||||
await fs.remove(dir);
|
||||
} else if (now - timestamp > maxQueryTime) {
|
||||
void logger.log(` ${dir} is older than ${maxQueryTime / 1000} seconds. Deleting.`);
|
||||
await fs.remove(dir);
|
||||
} else {
|
||||
void logger.log(` ${dir} is not older than ${maxQueryTime / 1000} seconds. Keeping.`);
|
||||
deleted = false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
deleted
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
errorMsg: ` Could not delete '${dir}': ${err}`,
|
||||
deleted: false
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,63 @@
|
||||
import { env } from 'vscode';
|
||||
import { CancellationTokenSource, env } from 'vscode';
|
||||
|
||||
import { QueryWithResults, tmpDir, QueryInfo } from './run-queries';
|
||||
import { QueryWithResults, QueryEvaluationInfo } from './run-queries';
|
||||
import * as messages from './pure/messages';
|
||||
import * as cli from './cli';
|
||||
import * as sarif from 'sarif';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState, ResultsPaths } from './pure/interface-types';
|
||||
import {
|
||||
RawResultsSortState,
|
||||
SortedResultSetInfo,
|
||||
QueryMetadata,
|
||||
InterpretedResultsSortState,
|
||||
ResultsPaths,
|
||||
SarifInterpretationData,
|
||||
GraphInterpretationData
|
||||
} from './pure/interface-types';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { QueryHistoryItemOptions } from './query-history';
|
||||
import { DatabaseInfo } from './pure/interface-types';
|
||||
import { QueryStatus } from './query-status';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
|
||||
export class CompletedQuery implements QueryWithResults {
|
||||
readonly date: Date;
|
||||
readonly time: string;
|
||||
readonly query: QueryInfo;
|
||||
/**
|
||||
* query-results.ts
|
||||
* ----------------
|
||||
*
|
||||
* A collection of classes and functions that collectively
|
||||
* manage query results.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A description of the information about a query
|
||||
* that is available before results are populated.
|
||||
*/
|
||||
export interface InitialQueryInfo {
|
||||
userSpecifiedLabel?: string; // if missing, use a default label
|
||||
readonly queryText: string; // text of the selected file, or the selected text when doing quick eval
|
||||
readonly isQuickQuery: boolean;
|
||||
readonly isQuickEval: boolean;
|
||||
readonly quickEvalPosition?: messages.Position;
|
||||
readonly queryPath: string;
|
||||
readonly databaseInfo: DatabaseInfo
|
||||
readonly start: Date;
|
||||
readonly id: string; // unique id for this query.
|
||||
}
|
||||
|
||||
export class CompletedQueryInfo implements QueryWithResults {
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly result: messages.EvaluationResult;
|
||||
readonly database: DatabaseInfo;
|
||||
readonly logFileLocation?: string;
|
||||
options: QueryHistoryItemOptions;
|
||||
resultCount: number;
|
||||
|
||||
/**
|
||||
* This dispose method is called when the query is removed from the history view.
|
||||
*/
|
||||
dispose: () => void;
|
||||
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
*/
|
||||
sortedResultsInfo: Map<string, SortedResultSetInfo>;
|
||||
sortedResultsInfo: Record<string, SortedResultSetInfo>;
|
||||
|
||||
/**
|
||||
* How we're currently sorting alerts. This is not mere interface
|
||||
@@ -35,20 +68,23 @@ export class CompletedQuery implements QueryWithResults {
|
||||
*/
|
||||
interpretedResultsSortState: InterpretedResultsSortState | undefined;
|
||||
|
||||
/**
|
||||
* Note that in the {@link FullQueryInfo.slurp} method, we create a CompletedQueryInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(
|
||||
evaluation: QueryWithResults,
|
||||
public config: QueryHistoryConfig,
|
||||
) {
|
||||
this.query = evaluation.query;
|
||||
this.result = evaluation.result;
|
||||
this.database = evaluation.database;
|
||||
this.logFileLocation = evaluation.logFileLocation;
|
||||
this.options = evaluation.options;
|
||||
|
||||
// Use the dispose method from the evaluation.
|
||||
// The dispose will clean up any additional log locations that this
|
||||
// query may have created.
|
||||
this.dispose = evaluation.dispose;
|
||||
|
||||
this.date = new Date();
|
||||
this.time = this.date.toLocaleString(env.language);
|
||||
this.sortedResultsInfo = new Map();
|
||||
this.sortedResultsInfo = {};
|
||||
this.resultCount = 0;
|
||||
}
|
||||
|
||||
@@ -56,26 +92,16 @@ export class CompletedQuery implements QueryWithResults {
|
||||
this.resultCount = value;
|
||||
}
|
||||
|
||||
get databaseName(): string {
|
||||
return this.database.name;
|
||||
}
|
||||
get queryName(): string {
|
||||
return getQueryName(this.query);
|
||||
}
|
||||
get queryFileName(): string {
|
||||
return getQueryFileName(this.query);
|
||||
}
|
||||
|
||||
get statusString(): string {
|
||||
switch (this.result.resultType) {
|
||||
case messages.QueryResultType.CANCELLATION:
|
||||
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
|
||||
return `cancelled after ${Math.round(this.result.evaluationTime / 1000)} seconds`;
|
||||
case messages.QueryResultType.OOM:
|
||||
return 'out of memory';
|
||||
case messages.QueryResultType.SUCCESS:
|
||||
return `finished in ${this.result.evaluationTime / 1000} seconds`;
|
||||
return `finished in ${Math.round(this.result.evaluationTime / 1000)} seconds`;
|
||||
case messages.QueryResultType.TIMEOUT:
|
||||
return `timed out after ${this.result.evaluationTime / 1000} seconds`;
|
||||
return `timed out after ${Math.round(this.result.evaluationTime / 1000)} seconds`;
|
||||
case messages.QueryResultType.OTHER_ERROR:
|
||||
default:
|
||||
return this.result.message ? `failed: ${this.result.message}` : 'failed';
|
||||
@@ -86,52 +112,26 @@ export class CompletedQuery implements QueryWithResults {
|
||||
if (!useSorted) {
|
||||
return this.query.resultsPaths.resultsPath;
|
||||
}
|
||||
return this.sortedResultsInfo.get(selectedTable)?.resultsPath
|
||||
return this.sortedResultsInfo[selectedTable]?.resultsPath
|
||||
|| this.query.resultsPaths.resultsPath;
|
||||
}
|
||||
|
||||
interpolate(template: string): string {
|
||||
const { databaseName, queryName, time, resultCount, statusString, queryFileName } = this;
|
||||
const replacements: { [k: string]: string } = {
|
||||
t: time,
|
||||
q: queryName,
|
||||
d: databaseName,
|
||||
r: resultCount.toString(),
|
||||
s: statusString,
|
||||
f: queryFileName,
|
||||
'%': '%',
|
||||
};
|
||||
return template.replace(/%(.)/g, (match, key) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
});
|
||||
}
|
||||
|
||||
getLabel(): string {
|
||||
return this.options?.label
|
||||
|| this.config.format;
|
||||
}
|
||||
|
||||
get didRunSuccessfully(): boolean {
|
||||
return this.result.resultType === messages.QueryResultType.SUCCESS;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.interpolate(this.getLabel());
|
||||
}
|
||||
|
||||
async updateSortState(
|
||||
server: cli.CodeQLCliServer,
|
||||
resultSetName: string,
|
||||
sortState?: RawResultsSortState
|
||||
): Promise<void> {
|
||||
if (sortState === undefined) {
|
||||
this.sortedResultsInfo.delete(resultSetName);
|
||||
delete this.sortedResultsInfo[resultSetName];
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedResultSetInfo: SortedResultSetInfo = {
|
||||
resultsPath: path.join(tmpDir.name, `sortedResults${this.query.queryID}-${resultSetName}.bqrs`),
|
||||
resultsPath: this.query.getSortedResultSetPath(resultSetName),
|
||||
sortState
|
||||
};
|
||||
|
||||
@@ -142,7 +142,7 @@ export class CompletedQuery implements QueryWithResults {
|
||||
[sortState.columnIndex],
|
||||
[sortState.sortDirection]
|
||||
);
|
||||
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
|
||||
this.sortedResultsInfo[resultSetName] = sortedResultSetInfo;
|
||||
}
|
||||
|
||||
async updateInterpretedSortState(sortState?: InterpretedResultsSortState): Promise<void> {
|
||||
@@ -152,49 +152,39 @@ export class CompletedQuery implements QueryWithResults {
|
||||
|
||||
|
||||
/**
|
||||
* Gets a human-readable name for an evaluated query.
|
||||
* Uses metadata if it exists, and defaults to the query file name.
|
||||
* Call cli command to interpret SARIF results.
|
||||
*/
|
||||
export function getQueryName(query: QueryInfo) {
|
||||
if (query.quickEvalPosition !== undefined) {
|
||||
return 'Quick evaluation of ' + getQueryFileName(query);
|
||||
} else if (query.metadata?.name) {
|
||||
return query.metadata.name;
|
||||
} else {
|
||||
return getQueryFileName(query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file name for an evaluated query.
|
||||
* Defaults to the query file name and may contain position information for quick eval queries.
|
||||
*/
|
||||
export function getQueryFileName(query: QueryInfo) {
|
||||
// Queries run through quick evaluation are not usually the entire query file.
|
||||
// Label them differently and include the line numbers.
|
||||
if (query.quickEvalPosition !== undefined) {
|
||||
const { line, endLine, fileName } = query.quickEvalPosition;
|
||||
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
|
||||
return `${path.basename(fileName)}:${lineInfo}`;
|
||||
}
|
||||
return path.basename(query.program.queryPath);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call cli command to interpret results.
|
||||
*/
|
||||
export async function interpretResults(
|
||||
server: cli.CodeQLCliServer,
|
||||
export async function interpretResultsSarif(
|
||||
cli: cli.CodeQLCliServer,
|
||||
metadata: QueryMetadata | undefined,
|
||||
resultsPaths: ResultsPaths,
|
||||
sourceInfo?: cli.SourceInfo
|
||||
): Promise<sarif.Log> {
|
||||
): Promise<SarifInterpretationData> {
|
||||
const { resultsPath, interpretedResultsPath } = resultsPaths;
|
||||
if (await fs.pathExists(interpretedResultsPath)) {
|
||||
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));
|
||||
return { ...JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8')), t: 'SarifInterpretationData' };
|
||||
}
|
||||
return await server.interpretBqrs(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
|
||||
const res = await cli.interpretBqrsSarif(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
|
||||
return { ...res, t: 'SarifInterpretationData' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Call cli command to interpret graph results.
|
||||
*/
|
||||
export async function interpretGraphResults(
|
||||
cli: cli.CodeQLCliServer,
|
||||
metadata: QueryMetadata | undefined,
|
||||
resultsPaths: ResultsPaths,
|
||||
sourceInfo?: cli.SourceInfo
|
||||
): Promise<GraphInterpretationData> {
|
||||
const { resultsPath, interpretedResultsPath } = resultsPaths;
|
||||
if (await fs.pathExists(interpretedResultsPath)) {
|
||||
const dot = await cli.readDotFiles(interpretedResultsPath);
|
||||
return { dot, t: 'GraphInterpretationData' };
|
||||
}
|
||||
|
||||
const dot = await cli.interpretBqrsGraph(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
|
||||
return { dot, t: 'GraphInterpretationData' };
|
||||
}
|
||||
|
||||
export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) {
|
||||
@@ -211,3 +201,162 @@ export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) {
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in Interface and Compare-Interface for queries that we know have been complated.
|
||||
*/
|
||||
export type CompletedLocalQueryInfo = LocalQueryInfo & {
|
||||
completedQuery: CompletedQueryInfo
|
||||
};
|
||||
|
||||
export type QueryHistoryInfo = LocalQueryInfo | RemoteQueryHistoryItem;
|
||||
|
||||
export class LocalQueryInfo {
|
||||
readonly t = 'local';
|
||||
|
||||
public failureReason: string | undefined;
|
||||
public completedQuery: CompletedQueryInfo | undefined;
|
||||
public evalLogLocation: string | undefined;
|
||||
public evalLogSummaryLocation: string | undefined;
|
||||
private config: QueryHistoryConfig | undefined;
|
||||
|
||||
/**
|
||||
* Note that in the {@link slurpQueryHistory} method, we create a FullQueryInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(
|
||||
public readonly initialInfo: InitialQueryInfo,
|
||||
config: QueryHistoryConfig,
|
||||
private cancellationSource?: CancellationTokenSource // used to cancel in progress queries
|
||||
) {
|
||||
this.setConfig(config);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.cancellationSource?.cancel();
|
||||
// query is no longer in progress, can delete the cancellation token source
|
||||
this.cancellationSource?.dispose();
|
||||
delete this.cancellationSource;
|
||||
}
|
||||
|
||||
get startTime() {
|
||||
return this.initialInfo.start.toLocaleString(env.language);
|
||||
}
|
||||
|
||||
interpolate(template: string): string {
|
||||
const { resultCount = 0, statusString = 'in progress' } = this.completedQuery || {};
|
||||
const replacements: { [k: string]: string } = {
|
||||
t: this.startTime,
|
||||
q: this.getQueryName(),
|
||||
d: this.initialInfo.databaseInfo.name,
|
||||
r: resultCount.toString(),
|
||||
s: statusString,
|
||||
f: this.getQueryFileName(),
|
||||
'%': '%',
|
||||
};
|
||||
return template.replace(/%(.)/g, (match, key) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a label for this query that includes interpolated values.
|
||||
*/
|
||||
get label(): string {
|
||||
return this.interpolate(
|
||||
this.initialInfo.userSpecifiedLabel ?? this.config?.format ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoids getting the default label for the query.
|
||||
* If there is a custom label for this query, interpolate and use that.
|
||||
* Otherwise, use the name of the query.
|
||||
*
|
||||
* @returns the name of the query, unless there is a custom label for this query.
|
||||
*/
|
||||
getShortLabel(): string {
|
||||
return this.initialInfo.userSpecifiedLabel
|
||||
? this.interpolate(this.initialInfo.userSpecifiedLabel)
|
||||
: this.getQueryName();
|
||||
}
|
||||
|
||||
/**
|
||||
* The query's file name, unless it is a quick eval.
|
||||
* Queries run through quick evaluation are not usually the entire query file.
|
||||
* Label them differently and include the line numbers.
|
||||
*/
|
||||
getQueryFileName() {
|
||||
if (this.initialInfo.quickEvalPosition) {
|
||||
const { line, endLine, fileName } = this.initialInfo.quickEvalPosition;
|
||||
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
|
||||
return `${path.basename(fileName)}:${lineInfo}`;
|
||||
}
|
||||
return path.basename(this.initialInfo.queryPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Three cases:
|
||||
*
|
||||
* - If this is a completed query, use the query name from the query metadata.
|
||||
* - If this is a quick eval, return the query name with a prefix
|
||||
* - Otherwise, return the query file name.
|
||||
*/
|
||||
getQueryName() {
|
||||
if (this.initialInfo.quickEvalPosition) {
|
||||
return 'Quick evaluation of ' + this.getQueryFileName();
|
||||
} else if (this.completedQuery?.query.metadata?.name) {
|
||||
return this.completedQuery?.query.metadata?.name;
|
||||
} else {
|
||||
return this.getQueryFileName();
|
||||
}
|
||||
}
|
||||
|
||||
get completed(): boolean {
|
||||
return !!this.completedQuery;
|
||||
}
|
||||
|
||||
completeThisQuery(info: QueryWithResults) {
|
||||
this.completedQuery = new CompletedQueryInfo(info);
|
||||
|
||||
// dispose of the cancellation token source and also ensure the source is not serialized as JSON
|
||||
this.cancellationSource?.dispose();
|
||||
delete this.cancellationSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a failure reason, then this query has failed.
|
||||
* If there is no completed query, then this query is still running.
|
||||
* If there is a completed query, then check if didRunSuccessfully.
|
||||
* If true, then this query has completed successfully, otherwise it has failed.
|
||||
*/
|
||||
get status(): QueryStatus {
|
||||
if (this.failureReason) {
|
||||
return QueryStatus.Failed;
|
||||
} else if (!this.completedQuery) {
|
||||
return QueryStatus.InProgress;
|
||||
} else if (this.completedQuery.didRunSuccessfully) {
|
||||
return QueryStatus.Completed;
|
||||
} else {
|
||||
return QueryStatus.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `config` property must not be serialized since it contains a listerner
|
||||
* for global configuration changes. Instead, It should be set when the query
|
||||
* is deserialized.
|
||||
*
|
||||
* @param config the global query history config object
|
||||
*/
|
||||
setConfig(config: QueryHistoryConfig) {
|
||||
// avoid serializing config property
|
||||
Object.defineProperty(this, 'config', {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
value: config
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
99
extensions/ql-vscode/src/query-serialization.ts
Normal file
99
extensions/ql-vscode/src/query-serialization.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { asyncFilter, getErrorMessage, getErrorStack } from './pure/helpers-pure';
|
||||
import { CompletedQueryInfo, LocalQueryInfo, QueryHistoryInfo } from './query-results';
|
||||
import { QueryEvaluationInfo } from './run-queries';
|
||||
|
||||
export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConfig): Promise<QueryHistoryInfo[]> {
|
||||
try {
|
||||
if (!(await fs.pathExists(fsPath))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await fs.readFile(fsPath, 'utf8');
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.version !== 1) {
|
||||
void showAndLogErrorMessage(`Unsupported query history format: v${obj.version}. `);
|
||||
return [];
|
||||
}
|
||||
|
||||
const queries = obj.queries;
|
||||
const parsedQueries = queries.map((q: QueryHistoryInfo) => {
|
||||
|
||||
// Need to explicitly set prototype since reading in from JSON will not
|
||||
// do this automatically. Note that we can't call the constructor here since
|
||||
// the constructor invokes extra logic that we don't want to do.
|
||||
if (q.t === 'local') {
|
||||
Object.setPrototypeOf(q, LocalQueryInfo.prototype);
|
||||
|
||||
// The config object is a global, se we need to set it explicitly
|
||||
// and ensure it is not serialized to JSON.
|
||||
q.setConfig(config);
|
||||
|
||||
// Date instances are serialized as strings. Need to
|
||||
// convert them back to Date instances.
|
||||
(q.initialInfo as any).start = new Date(q.initialInfo.start);
|
||||
if (q.completedQuery) {
|
||||
// Again, need to explicitly set prototypes.
|
||||
Object.setPrototypeOf(q.completedQuery, CompletedQueryInfo.prototype);
|
||||
Object.setPrototypeOf(q.completedQuery.query, QueryEvaluationInfo.prototype);
|
||||
// slurped queries do not need to be disposed
|
||||
q.completedQuery.dispose = () => { /**/ };
|
||||
}
|
||||
} else if (q.t === 'remote') {
|
||||
// noop
|
||||
}
|
||||
return q;
|
||||
});
|
||||
|
||||
// filter out queries that have been deleted on disk
|
||||
// most likely another workspace has deleted them because the
|
||||
// queries aged out.
|
||||
return asyncFilter(parsedQueries, async (q) => {
|
||||
if (q.t === 'remote') {
|
||||
// the slurper doesn't know where the remote queries are stored
|
||||
// so we need to assume here that they exist. Later, we check to
|
||||
// see if they exist on disk.
|
||||
return true;
|
||||
}
|
||||
const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath;
|
||||
return !!resultsPath && await fs.pathExists(resultsPath);
|
||||
});
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage('Error loading query history.', {
|
||||
fullMessage: ['Error loading query history.', getErrorStack(e)].join('\n'),
|
||||
});
|
||||
// since the query history is invalid, it should be deleted so this error does not happen on next startup.
|
||||
await fs.remove(fsPath);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the query history to disk. It is not necessary that the parent directory
|
||||
* exists, but if it does, it must be writable. An existing file will be overwritten.
|
||||
*
|
||||
* Any errors will be rethrown.
|
||||
*
|
||||
* @param queries the list of queries to save.
|
||||
* @param fsPath the path to save the queries to.
|
||||
*/
|
||||
export async function splatQueryHistory(queries: QueryHistoryInfo[], fsPath: string): Promise<void> {
|
||||
try {
|
||||
if (!(await fs.pathExists(fsPath))) {
|
||||
await fs.mkdir(path.dirname(fsPath), { recursive: true });
|
||||
}
|
||||
// remove incomplete local queries since they cannot be recreated on restart
|
||||
const filteredQueries = queries.filter(q => q.t === 'local' ? q.completedQuery !== undefined : true);
|
||||
const data = JSON.stringify({
|
||||
version: 1,
|
||||
queries: filteredQueries
|
||||
}, null, 2);
|
||||
await fs.writeFile(fsPath, data);
|
||||
} catch (e) {
|
||||
throw new Error(`Error saving query history to ${fsPath}: ${getErrorMessage(e)}`);
|
||||
}
|
||||
}
|
||||
5
extensions/ql-vscode/src/query-status.ts
Normal file
5
extensions/ql-vscode/src/query-status.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum QueryStatus {
|
||||
InProgress = 'InProgress',
|
||||
Completed = 'Completed',
|
||||
Failed = 'Failed',
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as cp from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { DisposableObject } from './pure/disposable-object';
|
||||
import { Disposable, CancellationToken, commands } from 'vscode';
|
||||
import { createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
|
||||
@@ -9,8 +11,6 @@ import { Logger, ProgressReporter } from './logging';
|
||||
import { completeQuery, EvaluationResult, progress, ProgressMessage, WithProgressId } from './pure/messages';
|
||||
import * as messages from './pure/messages';
|
||||
import { ProgressCallback, ProgressTask } from './commandRunner';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as helpers from './helpers';
|
||||
|
||||
type ServerOpts = {
|
||||
logger: Logger;
|
||||
@@ -68,7 +68,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
this.queryServerStartListeners.push(e);
|
||||
}
|
||||
|
||||
public activeQueryName: string | undefined;
|
||||
public activeQueryLogFile: string | undefined;
|
||||
|
||||
constructor(
|
||||
readonly config: QueryServerConfig,
|
||||
@@ -89,26 +89,6 @@ export class QueryServerClient extends DisposableObject {
|
||||
this.evaluationResultCallbacks = {};
|
||||
}
|
||||
|
||||
async initLogger() {
|
||||
let storagePath = this.opts.contextStoragePath;
|
||||
let isCustomLogDirectory = false;
|
||||
if (this.config.customLogDirectory) {
|
||||
try {
|
||||
if (!(await fs.pathExists(this.config.customLogDirectory))) {
|
||||
await fs.mkdir(this.config.customLogDirectory);
|
||||
}
|
||||
void this.logger.log(`Saving query server logs to user-specified directory: ${this.config.customLogDirectory}.`);
|
||||
storagePath = this.config.customLogDirectory;
|
||||
isCustomLogDirectory = true;
|
||||
} catch (e) {
|
||||
void helpers.showAndLogErrorMessage(`${this.config.customLogDirectory} is not a valid directory. Logs will be stored in a temporary workspace directory instead.`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.logger.setLogStoragePath(storagePath, isCustomLogDirectory);
|
||||
|
||||
}
|
||||
|
||||
get logger(): Logger {
|
||||
return this.opts.logger;
|
||||
}
|
||||
@@ -150,7 +130,6 @@ export class QueryServerClient extends DisposableObject {
|
||||
|
||||
/** Starts a new query server process, sending progress messages to the given reporter. */
|
||||
private async startQueryServerImpl(progressReporter: ProgressReporter): Promise<void> {
|
||||
await this.initLogger();
|
||||
const ramArgs = await this.cliServer.resolveRam(this.config.queryMemoryMb, progressReporter);
|
||||
const args = ['--threads', this.config.numThreads.toString()].concat(ramArgs);
|
||||
|
||||
@@ -167,16 +146,29 @@ export class QueryServerClient extends DisposableObject {
|
||||
args.push('--require-db-registration');
|
||||
}
|
||||
|
||||
if (await this.cliServer.cliConstraints.supportsOldEvalStats()) {
|
||||
if (await this.cliServer.cliConstraints.supportsOldEvalStats() && !(await this.cliServer.cliConstraints.supportsPerQueryEvalLog())) {
|
||||
args.push('--old-eval-stats');
|
||||
}
|
||||
|
||||
if (await this.cliServer.cliConstraints.supportsStructuredEvalLog()) {
|
||||
const structuredLogFile = `${this.opts.contextStoragePath}/structured-evaluator-log.json`;
|
||||
await fs.ensureFile(structuredLogFile);
|
||||
|
||||
args.push('--evaluator-log');
|
||||
args.push(structuredLogFile);
|
||||
|
||||
// We hard-code the verbosity level to 5 and minify to false.
|
||||
// This will be the behavior of the per-query structured logging in the CLI after 2.8.3.
|
||||
args.push('--evaluator-log-level');
|
||||
args.push('5');
|
||||
}
|
||||
|
||||
if (this.config.debug) {
|
||||
args.push('--debug', '--tuple-counting');
|
||||
}
|
||||
|
||||
if (cli.shouldDebugQueryServer()) {
|
||||
args.push('-J=-agentlib:jdwp=transport=dt_socket,address=localhost:9010,server=y,suspend=n,quiet=y');
|
||||
args.push('-J=-agentlib:jdwp=transport=dt_socket,address=localhost:9010,server=n,suspend=y,quiet=y');
|
||||
}
|
||||
|
||||
const child = cli.spawnServer(
|
||||
@@ -187,7 +179,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
this.logger,
|
||||
data => this.logger.log(data.toString(), {
|
||||
trailingNewline: false,
|
||||
additionalLogLocation: this.activeQueryName
|
||||
additionalLogLocation: this.activeQueryLogFile
|
||||
}),
|
||||
undefined, // no listener for stdout
|
||||
progressReporter
|
||||
@@ -198,10 +190,6 @@ export class QueryServerClient extends DisposableObject {
|
||||
if (!(res.runId in this.evaluationResultCallbacks)) {
|
||||
void this.logger.log(`No callback associated with run id ${res.runId}, continuing without executing any callback`);
|
||||
} else {
|
||||
const baseLocation = this.logger.getBaseLocation();
|
||||
if (baseLocation && this.activeQueryName) {
|
||||
res.logFileLocation = path.join(baseLocation, this.activeQueryName);
|
||||
}
|
||||
this.evaluationResultCallbacks[res.runId](res);
|
||||
}
|
||||
return {};
|
||||
@@ -234,7 +222,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
}
|
||||
|
||||
get serverProcessPid(): number {
|
||||
return this.serverProcess!.child.pid;
|
||||
return this.serverProcess!.child.pid || 0;
|
||||
}
|
||||
|
||||
async sendRequest<P, R, E, RO>(type: RequestType<WithProgressId<P>, R, E, RO>, parameter: P, token?: CancellationToken, progress?: (res: ProgressMessage) => void): Promise<R> {
|
||||
@@ -262,8 +250,23 @@ export class QueryServerClient extends DisposableObject {
|
||||
*/
|
||||
private updateActiveQuery(method: string, parameter: any): void {
|
||||
if (method === messages.compileQuery.method) {
|
||||
const queryPath = parameter?.queryToCheck?.queryPath || 'unknown';
|
||||
this.activeQueryName = `query-${path.basename(queryPath)}-${this.nextProgress}.log`;
|
||||
this.activeQueryLogFile = findQueryLogFile(path.dirname(parameter.resultPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findQueryLogFile(resultPath: string): string {
|
||||
return path.join(resultPath, 'query.log');
|
||||
}
|
||||
|
||||
export function findQueryEvalLogFile(resultPath: string): string {
|
||||
return path.join(resultPath, 'evaluator-log.jsonl');
|
||||
}
|
||||
|
||||
export function findQueryEvalLogSummaryFile(resultPath: string): string {
|
||||
return path.join(resultPath, 'evaluator-log.summary');
|
||||
}
|
||||
|
||||
export function findQueryEvalLogEndSummaryFile(resultPath: string): string {
|
||||
return path.join(resultPath, 'evaluator-log-end.summary');
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ProgressCallback,
|
||||
UserCancellationException
|
||||
} from './commandRunner';
|
||||
import { getErrorMessage } from './pure/helpers-pure';
|
||||
|
||||
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
|
||||
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
|
||||
@@ -132,7 +133,7 @@ export async function displayQuickQuery(
|
||||
await Window.showTextDocument(await workspace.openTextDocument(qlFile));
|
||||
} catch (e) {
|
||||
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
|
||||
throw new UserCancellationException(e.message);
|
||||
throw new UserCancellationException(getErrorMessage(e));
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
import { ExtensionContext } from 'vscode';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CancellationToken, ExtensionContext } from 'vscode';
|
||||
|
||||
import { Credentials } from '../authentication';
|
||||
import { Logger } from '../logging';
|
||||
import { downloadArtifactFromLink } from './gh-actions-api-client';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { AnalysisSummary } from './shared/remote-query-result';
|
||||
import * as sarif from 'sarif';
|
||||
import { AnalysisResults, QueryResult } from './shared/analysis-result';
|
||||
import { AnalysisResults, AnalysisAlert, AnalysisRawResults } from './shared/analysis-result';
|
||||
import { UserCancellationException } from '../commandRunner';
|
||||
import { sarifParser } from '../sarif-parser';
|
||||
import { extractAnalysisAlerts } from './sarif-processing';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import { extractRawResults } from './bqrs-processing';
|
||||
import { asyncFilter, getErrorMessage } from '../pure/helpers-pure';
|
||||
import { createDownloadPath } from './download-link';
|
||||
|
||||
export class AnalysesResultsManager {
|
||||
// Store for the results of various analyses for a single remote query.
|
||||
private readonly analysesResults: AnalysisResults[];
|
||||
// Store for the results of various analyses for each remote query.
|
||||
// The key is the queryId and is also the name of the directory where results are stored.
|
||||
private readonly analysesResults: Map<string, AnalysisResults[]>;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
readonly storagePath: string,
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
this.analysesResults = [];
|
||||
this.analysesResults = new Map();
|
||||
}
|
||||
|
||||
public async downloadAnalysisResults(
|
||||
analysisSummary: AnalysisSummary,
|
||||
publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
|
||||
): Promise<void> {
|
||||
if (this.analysesResults.some(x => x.nwo === analysisSummary.nwo)) {
|
||||
// We already have the results for this analysis, don't download again.
|
||||
if (this.isAnalysisInMemory(analysisSummary)) {
|
||||
// We already have the results for this analysis in memory, don't download again.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,64 +43,160 @@ export class AnalysesResultsManager {
|
||||
|
||||
void this.logger.log(`Downloading and processing results for ${analysisSummary.nwo}`);
|
||||
|
||||
await this.downloadSingleAnalysisResults(analysisSummary, credentials);
|
||||
await this.downloadSingleAnalysisResults(analysisSummary, credentials, publishResults);
|
||||
}
|
||||
|
||||
public async downloadAllResults(
|
||||
analysisSummaries: AnalysisSummary[],
|
||||
/**
|
||||
* Loads the array analysis results. For each analysis results, if it is not downloaded yet,
|
||||
* it will be downloaded. If it is already downloaded, it will be loaded into memory.
|
||||
* If it is already in memory, this will be a no-op.
|
||||
*
|
||||
* @param allAnalysesToLoad List of analyses to ensure are downloaded and in memory
|
||||
* @param token Optional cancellation token
|
||||
* @param publishResults Optional function to publish the results after loading
|
||||
*/
|
||||
public async loadAnalysesResults(
|
||||
allAnalysesToLoad: AnalysisSummary[],
|
||||
token?: CancellationToken,
|
||||
publishResults: (analysesResults: AnalysisResults[]) => Promise<void> = () => Promise.resolve()
|
||||
): Promise<void> {
|
||||
// Filter out analyses that we have already in memory.
|
||||
const analysesToDownload = allAnalysesToLoad.filter(x => !this.isAnalysisInMemory(x));
|
||||
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
|
||||
void this.logger.log('Downloading and processing all results');
|
||||
void this.logger.log('Downloading and processing analyses results');
|
||||
|
||||
for (const analysis of analysisSummaries) {
|
||||
await this.downloadSingleAnalysisResults(analysis, credentials);
|
||||
const batchSize = 3;
|
||||
const numOfBatches = Math.ceil(analysesToDownload.length / batchSize);
|
||||
const allFailures = [];
|
||||
|
||||
for (let i = 0; i < analysesToDownload.length; i += batchSize) {
|
||||
if (token?.isCancellationRequested) {
|
||||
throw new UserCancellationException('Downloading of analyses results has been cancelled', true);
|
||||
}
|
||||
|
||||
const batch = analysesToDownload.slice(i, i + batchSize);
|
||||
const batchTasks = batch.map(analysis => this.downloadSingleAnalysisResults(analysis, credentials, publishResults));
|
||||
|
||||
const nwos = batch.map(a => a.nwo).join(', ');
|
||||
void this.logger.log(`Downloading batch ${Math.floor(i / batchSize) + 1} of ${numOfBatches} (${nwos})`);
|
||||
|
||||
const taskResults = await Promise.allSettled(batchTasks);
|
||||
const failedTasks = taskResults.filter(x => x.status === 'rejected') as Array<PromiseRejectedResult>;
|
||||
if (failedTasks.length > 0) {
|
||||
const failures = failedTasks.map(t => t.reason.message);
|
||||
failures.forEach(f => void this.logger.log(f));
|
||||
allFailures.push(...failures);
|
||||
}
|
||||
}
|
||||
|
||||
if (allFailures.length > 0) {
|
||||
throw Error(allFailures.join(os.EOL));
|
||||
}
|
||||
}
|
||||
|
||||
public getAnalysesResults(): AnalysisResults[] {
|
||||
return [...this.analysesResults];
|
||||
public getAnalysesResults(queryId: string): AnalysisResults[] {
|
||||
return [...this.internalGetAnalysesResults(queryId)];
|
||||
}
|
||||
|
||||
private internalGetAnalysesResults(queryId: string): AnalysisResults[] {
|
||||
return this.analysesResults.get(queryId) || [];
|
||||
}
|
||||
|
||||
public removeAnalysesResults(queryId: string) {
|
||||
this.analysesResults.delete(queryId);
|
||||
}
|
||||
|
||||
private async downloadSingleAnalysisResults(
|
||||
analysis: AnalysisSummary,
|
||||
credentials: Credentials
|
||||
credentials: Credentials,
|
||||
publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
|
||||
): Promise<void> {
|
||||
const artifactPath = await downloadArtifactFromLink(credentials, analysis.downloadLink);
|
||||
const analysisResults: AnalysisResults = {
|
||||
nwo: analysis.nwo,
|
||||
status: 'InProgress',
|
||||
interpretedResults: []
|
||||
};
|
||||
const queryId = analysis.downloadLink.queryId;
|
||||
const resultsForQuery = this.internalGetAnalysesResults(queryId);
|
||||
resultsForQuery.push(analysisResults);
|
||||
this.analysesResults.set(queryId, resultsForQuery);
|
||||
void publishResults([...resultsForQuery]);
|
||||
const pos = resultsForQuery.length - 1;
|
||||
|
||||
let analysisResults: AnalysisResults;
|
||||
|
||||
if (path.extname(artifactPath) === '.sarif') {
|
||||
const queryResults = await this.readResults(artifactPath);
|
||||
analysisResults = { nwo: analysis.nwo, results: queryResults };
|
||||
} else {
|
||||
void this.logger.log('Cannot download results. Only alert and path queries are fully supported.');
|
||||
analysisResults = { nwo: analysis.nwo, results: [] };
|
||||
let artifactPath;
|
||||
try {
|
||||
artifactPath = await downloadArtifactFromLink(credentials, this.storagePath, analysis.downloadLink);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Could not download the analysis results for ${analysis.nwo}: ${getErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
this.analysesResults.push(analysisResults);
|
||||
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(analysis.nwo, analysis.databaseSha);
|
||||
|
||||
let newAnaysisResults: AnalysisResults;
|
||||
const fileExtension = path.extname(artifactPath);
|
||||
if (fileExtension === '.sarif') {
|
||||
const queryResults = await this.readSarifResults(artifactPath, fileLinkPrefix);
|
||||
newAnaysisResults = {
|
||||
...analysisResults,
|
||||
interpretedResults: queryResults,
|
||||
status: 'Completed'
|
||||
};
|
||||
} else if (fileExtension === '.bqrs') {
|
||||
const queryResults = await this.readBqrsResults(artifactPath, fileLinkPrefix);
|
||||
newAnaysisResults = {
|
||||
...analysisResults,
|
||||
rawResults: queryResults,
|
||||
status: 'Completed'
|
||||
};
|
||||
} else {
|
||||
void this.logger.log(`Cannot download results. File type '${fileExtension}' not supported.`);
|
||||
newAnaysisResults = {
|
||||
...analysisResults,
|
||||
status: 'Failed'
|
||||
};
|
||||
}
|
||||
resultsForQuery[pos] = newAnaysisResults;
|
||||
void publishResults([...resultsForQuery]);
|
||||
}
|
||||
|
||||
private async readResults(filePath: string): Promise<QueryResult[]> {
|
||||
const queryResults: QueryResult[] = [];
|
||||
|
||||
const sarifContents = await fs.readFile(filePath, 'utf8');
|
||||
const sarifLog = JSON.parse(sarifContents) as sarif.Log;
|
||||
public async loadDownloadedAnalyses(
|
||||
allAnalysesToCheck: AnalysisSummary[]
|
||||
) {
|
||||
|
||||
// Read the sarif file and extract information that we want to display
|
||||
// in the UI. For now we're only getting the message texts but we'll gradually
|
||||
// extract more information based on the UX we want to build.
|
||||
// Find all analyses that are already downloaded.
|
||||
const allDownloadedAnalyses = await asyncFilter(allAnalysesToCheck, x => this.isAnalysisDownloaded(x));
|
||||
// Now, ensure that all of these analyses are in memory. Some may already be in memory. These are ignored.
|
||||
await this.loadAnalysesResults(allDownloadedAnalyses);
|
||||
}
|
||||
|
||||
sarifLog.runs?.forEach(run => {
|
||||
run?.results?.forEach(result => {
|
||||
if (result?.message?.text) {
|
||||
queryResults.push({
|
||||
message: result.message.text
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
private async isAnalysisDownloaded(analysis: AnalysisSummary): Promise<boolean> {
|
||||
return await fs.pathExists(createDownloadPath(this.storagePath, analysis.downloadLink));
|
||||
}
|
||||
|
||||
return queryResults;
|
||||
private async readBqrsResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisRawResults> {
|
||||
return await extractRawResults(this.cliServer, this.logger, filePath, fileLinkPrefix);
|
||||
}
|
||||
|
||||
private async readSarifResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisAlert[]> {
|
||||
const sarifLog = await sarifParser(filePath);
|
||||
|
||||
const processedSarif = extractAnalysisAlerts(sarifLog, fileLinkPrefix);
|
||||
if (processedSarif.errors.length) {
|
||||
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
|
||||
}
|
||||
|
||||
return processedSarif.alerts;
|
||||
}
|
||||
|
||||
private isAnalysisInMemory(analysis: AnalysisSummary): boolean {
|
||||
return this.internalGetAnalysesResults(analysis.downloadLink.queryId).some(x => x.nwo === analysis.nwo);
|
||||
}
|
||||
|
||||
private createGitHubDotcomFileLinkPrefix(nwo: string, sha: string): string {
|
||||
return `https://github.com/${nwo}/blob/${sha}`;
|
||||
}
|
||||
}
|
||||
|
||||
35
extensions/ql-vscode/src/remote-queries/bqrs-processing.ts
Normal file
35
extensions/ql-vscode/src/remote-queries/bqrs-processing.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import { Logger } from '../logging';
|
||||
import { transformBqrsResultSet } from '../pure/bqrs-cli-types';
|
||||
import { AnalysisRawResults } from './shared/analysis-result';
|
||||
import { MAX_RAW_RESULTS } from './shared/result-limits';
|
||||
|
||||
export async function extractRawResults(
|
||||
cliServer: CodeQLCliServer,
|
||||
logger: Logger,
|
||||
filePath: string,
|
||||
fileLinkPrefix: string,
|
||||
): Promise<AnalysisRawResults> {
|
||||
const bqrsInfo = await cliServer.bqrsInfo(filePath);
|
||||
const resultSets = bqrsInfo['result-sets'];
|
||||
|
||||
if (resultSets.length < 1) {
|
||||
throw new Error('No result sets found in results file.');
|
||||
}
|
||||
if (resultSets.length > 1) {
|
||||
void logger.log('Multiple result sets found in results file. Only the first one will be used.');
|
||||
}
|
||||
|
||||
const schema = resultSets[0];
|
||||
|
||||
const chunk = await cliServer.bqrsDecode(
|
||||
filePath,
|
||||
schema.name,
|
||||
{ pageSize: MAX_RAW_RESULTS });
|
||||
|
||||
const resultSet = transformBqrsResultSet(schema, chunk);
|
||||
|
||||
const capped = !!chunk.next;
|
||||
|
||||
return { schema, resultSet, fileLinkPrefix, capped };
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Represents a link to an artifact to be downloaded.
|
||||
* Represents a link to an artifact to be downloaded.
|
||||
*/
|
||||
export interface DownloadLink {
|
||||
/**
|
||||
* A unique id of the artifact being downloaded.
|
||||
* A unique id of the artifact being downloaded.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The URL path to use against the GitHub API to download the
|
||||
* linked artifact.
|
||||
* linked artifact.
|
||||
*/
|
||||
urlPath: string;
|
||||
|
||||
@@ -17,4 +19,22 @@ export interface DownloadLink {
|
||||
* An optional path to follow inside the downloaded archive containing the artifact.
|
||||
*/
|
||||
innerFilePath?: string;
|
||||
|
||||
/**
|
||||
* A unique id of the remote query run. This is used to determine where to store artifacts and data from the run.
|
||||
*/
|
||||
queryId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a downloadLink to the path where the artifact should be stored.
|
||||
*
|
||||
* @param storagePath The base directory to store artifacts in.
|
||||
* @param downloadLink The DownloadLink
|
||||
* @param extension An optional file extension to append to the artifact (no `.`).
|
||||
*
|
||||
* @returns A full path to the download location of the artifact
|
||||
*/
|
||||
export function createDownloadPath(storagePath: string, downloadLink: DownloadLink, extension = '') {
|
||||
return path.join(storagePath, downloadLink.queryId, downloadLink.id + (extension ? `.${extension}` : ''));
|
||||
}
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import * as unzipper from 'unzipper';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { showAndLogWarningMessage } from '../helpers';
|
||||
import { showAndLogWarningMessage, tmpDir } from '../helpers';
|
||||
import { Credentials } from '../authentication';
|
||||
import { logger } from '../logging';
|
||||
import { tmpDir } from '../run-queries';
|
||||
import { RemoteQueryWorkflowResult } from './remote-query-workflow-result';
|
||||
import { DownloadLink } from './download-link';
|
||||
import { DownloadLink, createDownloadPath } from './download-link';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { RemoteQueryResultIndex, RemoteQueryResultIndexItem } from './remote-query-result-index';
|
||||
import { RemoteQueryFailureIndexItem, RemoteQueryResultIndex, RemoteQuerySuccessIndexItem } from './remote-query-result-index';
|
||||
|
||||
interface ApiResultIndexItem {
|
||||
interface ApiSuccessIndexItem {
|
||||
nwo: string;
|
||||
id: string;
|
||||
sha?: string;
|
||||
results_count: number;
|
||||
bqrs_file_size: number;
|
||||
sarif_file_size?: number;
|
||||
}
|
||||
|
||||
interface ApiFailureIndexItem {
|
||||
nwo: string;
|
||||
id: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface ApiResultIndex {
|
||||
successes: ApiSuccessIndexItem[];
|
||||
failures: ApiFailureIndexItem[];
|
||||
}
|
||||
|
||||
export async function getRemoteQueryIndex(
|
||||
credentials: Credentials,
|
||||
remoteQuery: RemoteQuery
|
||||
@@ -32,49 +43,59 @@ export async function getRemoteQueryIndex(
|
||||
|
||||
const artifactList = await listWorkflowRunArtifacts(credentials, owner, repoName, workflowRunId);
|
||||
const resultIndexArtifactId = getArtifactIDfromName('result-index', workflowUri, artifactList);
|
||||
const resultIndexItems = await getResultIndexItems(credentials, owner, repoName, resultIndexArtifactId);
|
||||
const resultIndex = await getResultIndex(credentials, owner, repoName, resultIndexArtifactId);
|
||||
|
||||
const allResultsArtifactId = getArtifactIDfromName('all-results', workflowUri, artifactList);
|
||||
|
||||
const items = resultIndexItems.map(item => {
|
||||
const successes = resultIndex?.successes.map(item => {
|
||||
const artifactId = getArtifactIDfromName(item.id, workflowUri, artifactList);
|
||||
|
||||
return {
|
||||
id: item.id.toString(),
|
||||
artifactId: artifactId,
|
||||
nwo: item.nwo,
|
||||
sha: item.sha,
|
||||
resultCount: item.results_count,
|
||||
bqrsFileSize: item.bqrs_file_size,
|
||||
sarifFileSize: item.sarif_file_size,
|
||||
} as RemoteQueryResultIndexItem;
|
||||
sarifFileSize: item.sarif_file_size
|
||||
} as RemoteQuerySuccessIndexItem;
|
||||
});
|
||||
|
||||
const failures = resultIndex?.failures.map(item => {
|
||||
return {
|
||||
id: item.id.toString(),
|
||||
nwo: item.nwo,
|
||||
error: item.error
|
||||
} as RemoteQueryFailureIndexItem;
|
||||
});
|
||||
|
||||
return {
|
||||
allResultsArtifactId,
|
||||
artifactsUrlPath,
|
||||
items,
|
||||
successes: successes || [],
|
||||
failures: failures || []
|
||||
};
|
||||
}
|
||||
|
||||
export async function downloadArtifactFromLink(
|
||||
credentials: Credentials,
|
||||
storagePath: string,
|
||||
downloadLink: DownloadLink
|
||||
): Promise<string> {
|
||||
|
||||
const octokit = await credentials.getOctokit();
|
||||
|
||||
// Download the zipped artifact.
|
||||
const response = await octokit.request(`GET ${downloadLink.urlPath}/zip`, {});
|
||||
const extractedPath = createDownloadPath(storagePath, downloadLink);
|
||||
|
||||
const zipFilePath = path.join(tmpDir.name, `${downloadLink.id}.zip`);
|
||||
await saveFile(`${zipFilePath}`, response.data as ArrayBuffer);
|
||||
// first check if we already have the artifact
|
||||
if (!(await fs.pathExists(extractedPath))) {
|
||||
// Download the zipped artifact.
|
||||
const response = await octokit.request(`GET ${downloadLink.urlPath}/zip`, {});
|
||||
|
||||
// Extract the zipped artifact.
|
||||
const extractedPath = path.join(tmpDir.name, downloadLink.id);
|
||||
await unzipFile(zipFilePath, extractedPath);
|
||||
const zipFilePath = createDownloadPath(storagePath, downloadLink, 'zip');
|
||||
await saveFile(`${zipFilePath}`, response.data as ArrayBuffer);
|
||||
|
||||
return downloadLink.innerFilePath
|
||||
? path.join(extractedPath, downloadLink.innerFilePath)
|
||||
: extractedPath;
|
||||
// Extract the zipped artifact.
|
||||
await unzipFile(zipFilePath, extractedPath);
|
||||
}
|
||||
return path.join(extractedPath, downloadLink.innerFilePath || '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,17 +106,17 @@ export async function downloadArtifactFromLink(
|
||||
* @param workflowRunId The ID of the workflow run to get the result index for.
|
||||
* @returns An object containing the result index.
|
||||
*/
|
||||
async function getResultIndexItems(
|
||||
async function getResultIndex(
|
||||
credentials: Credentials,
|
||||
owner: string,
|
||||
repo: string,
|
||||
artifactId: number
|
||||
): Promise<ApiResultIndexItem[]> {
|
||||
): Promise<ApiResultIndex | undefined> {
|
||||
const artifactPath = await downloadArtifact(credentials, owner, repo, artifactId);
|
||||
const indexFilePath = path.join(artifactPath, 'index.json');
|
||||
if (!(await fs.pathExists(indexFilePath))) {
|
||||
void showAndLogWarningMessage('Could not find an `index.json` file in the result artifact.');
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
const resultIndex = await fs.readFile(path.join(artifactPath, 'index.json'), 'utf8');
|
||||
|
||||
@@ -109,8 +130,8 @@ async function getResultIndexItems(
|
||||
/**
|
||||
* Gets the status of a workflow run.
|
||||
* @param credentials Credentials for authenticating to the GitHub API.
|
||||
* @param owner
|
||||
* @param repo
|
||||
* @param owner
|
||||
* @param repo
|
||||
* @param workflowRunId The ID of the workflow run to get the result index for.
|
||||
* @returns The workflow run status.
|
||||
*/
|
||||
@@ -246,18 +267,18 @@ function getWorkflowError(conclusion: string | null): string {
|
||||
}
|
||||
|
||||
if (conclusion === 'cancelled') {
|
||||
return 'The remote query execution was cancelled.';
|
||||
return 'Variant analysis execution was cancelled.';
|
||||
}
|
||||
|
||||
if (conclusion === 'timed_out') {
|
||||
return 'The remote query execution timed out.';
|
||||
return 'Variant analysis execution timed out.';
|
||||
}
|
||||
|
||||
if (conclusion === 'failure') {
|
||||
// TODO: Get the actual error from the workflow or potentially
|
||||
// from an artifact from the action itself.
|
||||
return 'The remote query execution has failed.';
|
||||
return 'Variant analysis execution has failed.';
|
||||
}
|
||||
|
||||
return `Unexpected query execution conclusion: ${conclusion}`;
|
||||
return `Unexpected variant analysis execution conclusion: ${conclusion}`;
|
||||
}
|
||||
|
||||
@@ -4,16 +4,15 @@ import {
|
||||
window as Window,
|
||||
ViewColumn,
|
||||
Uri,
|
||||
workspace,
|
||||
workspace
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { tmpDir } from '../run-queries';
|
||||
import {
|
||||
ToRemoteQueriesMessage,
|
||||
FromRemoteQueriesMessage,
|
||||
RemoteQueryDownloadAnalysisResultsMessage,
|
||||
RemoteQueryDownloadAllAnalysesResultsMessage,
|
||||
RemoteQueryDownloadAllAnalysesResultsMessage
|
||||
} from '../pure/interface-types';
|
||||
import { Logger } from '../logging';
|
||||
import { getHtmlForWebview } from '../interface-utils';
|
||||
@@ -47,16 +46,22 @@ export class RemoteQueriesInterfaceManager {
|
||||
this.getPanel().reveal(undefined, true);
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
const model = this.buildViewModel(query, queryResult);
|
||||
await this.postMessage({
|
||||
t: 'setRemoteQueryResult',
|
||||
queryResult: this.buildViewModel(query, queryResult)
|
||||
queryResult: model
|
||||
});
|
||||
|
||||
// Ensure all pre-downloaded artifacts are loaded into memory
|
||||
await this.analysesResultsManager.loadDownloadedAnalyses(model.analysisSummaries);
|
||||
|
||||
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults(queryResult.queryId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds up a model tailored to the view based on the query and result domain entities.
|
||||
* The data is cleaned up, sorted where necessary, and transformed to a format that
|
||||
* the view model can use.
|
||||
* the view model can use.
|
||||
* @param query Information about the query that was run.
|
||||
* @param queryResult The result of the query.
|
||||
* @returns A fully created view model.
|
||||
@@ -66,6 +71,7 @@ export class RemoteQueriesInterfaceManager {
|
||||
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
|
||||
const executionDuration = this.getDuration(queryResult.executionEndTime, query.executionStartTime);
|
||||
const analysisSummaries = this.buildAnalysisSummaries(queryResult.analysisSummaries);
|
||||
const totalRepositoryCount = queryResult.analysisSummaries.length;
|
||||
const affectedRepositories = queryResult.analysisSummaries.filter(r => r.resultCount > 0);
|
||||
|
||||
return {
|
||||
@@ -73,13 +79,15 @@ export class RemoteQueriesInterfaceManager {
|
||||
queryFileName: queryFileName,
|
||||
queryFilePath: query.queryFilePath,
|
||||
queryText: query.queryText,
|
||||
totalRepositoryCount: query.repositories.length,
|
||||
language: query.language,
|
||||
workflowRunUrl: `https://github.com/${query.controllerRepository.owner}/${query.controllerRepository.name}/actions/runs/${query.actionsWorkflowRunId}`,
|
||||
totalRepositoryCount: totalRepositoryCount,
|
||||
affectedRepositoryCount: affectedRepositories.length,
|
||||
totalResultCount: totalResultCount,
|
||||
executionTimestamp: this.formatDate(query.executionStartTime),
|
||||
executionDuration: executionDuration,
|
||||
downloadLink: queryResult.allResultsDownloadLink,
|
||||
analysisSummaries: analysisSummaries
|
||||
analysisSummaries: analysisSummaries,
|
||||
analysisFailures: queryResult.analysisFailures,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,14 +96,14 @@ export class RemoteQueriesInterfaceManager {
|
||||
const { ctx } = this;
|
||||
const panel = (this.panel = Window.createWebviewPanel(
|
||||
'remoteQueriesView',
|
||||
'Remote Query Results',
|
||||
'CodeQL Query Results',
|
||||
{ viewColumn: ViewColumn.Active, preserveFocus: true },
|
||||
{
|
||||
enableScripts: true,
|
||||
enableFindWidget: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [
|
||||
Uri.file(tmpDir.name),
|
||||
Uri.file(this.analysesResultsManager.storagePath),
|
||||
Uri.file(path.join(this.ctx.extensionPath, 'out')),
|
||||
],
|
||||
}
|
||||
@@ -123,12 +131,15 @@ export class RemoteQueriesInterfaceManager {
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
[baseStylesheetUriOnDisk, stylesheetPathOnDisk]
|
||||
[baseStylesheetUriOnDisk, stylesheetPathOnDisk],
|
||||
true
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
ctx.subscriptions.push(
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.panel;
|
||||
@@ -180,7 +191,7 @@ export class RemoteQueriesInterfaceManager {
|
||||
break;
|
||||
case 'remoteQueryError':
|
||||
void this.logger.log(
|
||||
`Remote query error: ${msg.error}`
|
||||
`Variant analysis error: ${msg.error}`
|
||||
);
|
||||
break;
|
||||
case 'openFile':
|
||||
@@ -201,28 +212,33 @@ export class RemoteQueriesInterfaceManager {
|
||||
}
|
||||
|
||||
private async downloadAnalysisResults(msg: RemoteQueryDownloadAnalysisResultsMessage): Promise<void> {
|
||||
await this.analysesResultsManager.downloadAnalysisResults(msg.analysisSummary);
|
||||
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults());
|
||||
await this.analysesResultsManager.downloadAnalysisResults(
|
||||
msg.analysisSummary,
|
||||
results => this.setAnalysisResults(results));
|
||||
}
|
||||
|
||||
private async downloadAllAnalysesResults(msg: RemoteQueryDownloadAllAnalysesResultsMessage): Promise<void> {
|
||||
await this.analysesResultsManager.downloadAllResults(msg.analysisSummaries);
|
||||
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults());
|
||||
await this.analysesResultsManager.loadAnalysesResults(
|
||||
msg.analysisSummaries,
|
||||
undefined,
|
||||
results => this.setAnalysisResults(results));
|
||||
}
|
||||
|
||||
private async setAnalysisResults(analysesResults: AnalysisResults[]): Promise<void> {
|
||||
await this.postMessage({
|
||||
t: 'setAnalysesResults',
|
||||
analysesResults: analysesResults
|
||||
});
|
||||
public async setAnalysisResults(analysesResults: AnalysisResults[]): Promise<void> {
|
||||
if (this.panel?.active) {
|
||||
await this.postMessage({
|
||||
t: 'setAnalysesResults',
|
||||
analysesResults: analysesResults
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private postMessage(msg: ToRemoteQueriesMessage): Thenable<boolean> {
|
||||
return this.getPanel().webview.postMessage(msg);
|
||||
}
|
||||
|
||||
private getDuration(startTime: Date, endTime: Date): string {
|
||||
const diffInMs = startTime.getTime() - endTime.getTime();
|
||||
private getDuration(startTime: number, endTime: number): string {
|
||||
const diffInMs = startTime - endTime;
|
||||
return this.formatDuration(diffInMs);
|
||||
}
|
||||
|
||||
@@ -242,7 +258,8 @@ export class RemoteQueriesInterfaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
private formatDate = (d: Date): string => {
|
||||
private formatDate = (millis: number): string => {
|
||||
const d = new Date(millis);
|
||||
const datePart = d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
|
||||
const timePart = d.toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric', hour12: true });
|
||||
return `${datePart} at ${timePart}`;
|
||||
@@ -276,6 +293,7 @@ export class RemoteQueriesInterfaceManager {
|
||||
|
||||
return sortedAnalysisSummaries.map((analysisResult) => ({
|
||||
nwo: analysisResult.nwo,
|
||||
databaseSha: analysisResult.databaseSha || 'HEAD',
|
||||
resultCount: analysisResult.resultCount,
|
||||
downloadLink: analysisResult.downloadLink,
|
||||
fileSize: this.formatFileSize(analysisResult.fileSizeInBytes)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { CancellationToken, commands, ExtensionContext, Uri, window } from 'vscode';
|
||||
import { nanoid } from 'nanoid';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { Credentials } from '../authentication';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import { ProgressCallback } from '../commandRunner';
|
||||
import { showAndLogErrorMessage, showInformationMessageWithAction } from '../helpers';
|
||||
import { createTimestampFile, showAndLogErrorMessage, showInformationMessageWithAction } from '../helpers';
|
||||
import { Logger } from '../logging';
|
||||
import { runRemoteQuery } from './run-remote-query';
|
||||
import { RemoteQueriesInterfaceManager } from './remote-queries-interface';
|
||||
@@ -13,18 +17,74 @@ import { RemoteQueryResultIndex } from './remote-query-result-index';
|
||||
import { RemoteQueryResult } from './remote-query-result';
|
||||
import { DownloadLink } from './download-link';
|
||||
import { AnalysesResultsManager } from './analyses-results-manager';
|
||||
import { assertNever } from '../pure/helpers-pure';
|
||||
import { RemoteQueryHistoryItem } from './remote-query-history-item';
|
||||
import { QueryHistoryManager } from '../query-history';
|
||||
import { QueryStatus } from '../query-status';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { QueryHistoryInfo } from '../query-results';
|
||||
|
||||
export class RemoteQueriesManager {
|
||||
const autoDownloadMaxSize = 300 * 1024;
|
||||
const autoDownloadMaxCount = 100;
|
||||
|
||||
const noop = () => { /* do nothing */ };
|
||||
export class RemoteQueriesManager extends DisposableObject {
|
||||
private readonly remoteQueriesMonitor: RemoteQueriesMonitor;
|
||||
private readonly analysesResultsManager: AnalysesResultsManager;
|
||||
private readonly interfaceManager: RemoteQueriesInterfaceManager;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly logger: Logger,
|
||||
private readonly cliServer: CodeQLCliServer
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly qhm: QueryHistoryManager,
|
||||
private readonly storagePath: string,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.analysesResultsManager = new AnalysesResultsManager(ctx, logger);
|
||||
super();
|
||||
this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger);
|
||||
this.interfaceManager = new RemoteQueriesInterfaceManager(ctx, logger, this.analysesResultsManager);
|
||||
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
|
||||
|
||||
// Handle events from the query history manager
|
||||
this.push(this.qhm.onDidAddQueryItem(this.handleAddQueryItem.bind(this)));
|
||||
this.push(this.qhm.onDidRemoveQueryItem(this.handleRemoveQueryItem.bind(this)));
|
||||
this.push(this.qhm.onWillOpenQueryItem(this.handleOpenQueryItem.bind(this)));
|
||||
}
|
||||
|
||||
private async handleAddQueryItem(queryItem: QueryHistoryInfo) {
|
||||
if (queryItem?.t === 'remote') {
|
||||
if (!(await this.queryHistoryItemExists(queryItem))) {
|
||||
// In this case, the query was deleted from disk, most likely because it was purged
|
||||
// by another workspace. We should remove it from the history manager.
|
||||
await this.qhm.handleRemoveHistoryItem(queryItem);
|
||||
} else if (queryItem.status === QueryStatus.InProgress) {
|
||||
// In this case, last time we checked, the query was still in progress.
|
||||
// We need to setup the monitor to check for completion.
|
||||
await commands.executeCommand('codeQL.monitorRemoteQuery', queryItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRemoveQueryItem(queryItem: QueryHistoryInfo) {
|
||||
if (queryItem?.t === 'remote') {
|
||||
this.analysesResultsManager.removeAnalysesResults(queryItem.queryId);
|
||||
await this.removeStorageDirectory(queryItem);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOpenQueryItem(queryItem: QueryHistoryInfo) {
|
||||
if (queryItem?.t === 'remote') {
|
||||
try {
|
||||
const remoteQueryResult = await this.retrieveJsonFile(queryItem, 'query-result.json') as RemoteQueryResult;
|
||||
// open results in the background
|
||||
void this.openResults(queryItem.remoteQuery, remoteQueryResult).then(
|
||||
noop,
|
||||
err => void showAndLogErrorMessage(err)
|
||||
);
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(`Could not open query results. ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async runRemoteQuery(
|
||||
@@ -41,65 +101,179 @@ export class RemoteQueriesManager {
|
||||
progress,
|
||||
token);
|
||||
|
||||
if (querySubmission && querySubmission.query) {
|
||||
void commands.executeCommand('codeQL.monitorRemoteQuery', querySubmission.query);
|
||||
if (querySubmission?.query) {
|
||||
const query = querySubmission.query;
|
||||
const queryId = this.createQueryId(query.queryName);
|
||||
|
||||
const queryHistoryItem: RemoteQueryHistoryItem = {
|
||||
t: 'remote',
|
||||
status: QueryStatus.InProgress,
|
||||
completed: false,
|
||||
queryId,
|
||||
label: query.queryName,
|
||||
remoteQuery: query,
|
||||
};
|
||||
await this.prepareStorageDirectory(queryHistoryItem);
|
||||
await this.storeJsonFile(queryHistoryItem, 'query.json', query);
|
||||
|
||||
this.qhm.addQuery(queryHistoryItem);
|
||||
await this.qhm.refreshTreeView();
|
||||
}
|
||||
}
|
||||
|
||||
public async monitorRemoteQuery(
|
||||
query: RemoteQuery,
|
||||
queryItem: RemoteQueryHistoryItem,
|
||||
cancellationToken: CancellationToken
|
||||
): Promise<void> {
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
|
||||
const queryResult = await this.remoteQueriesMonitor.monitorQuery(query, cancellationToken);
|
||||
const queryWorkflowResult = await this.remoteQueriesMonitor.monitorQuery(queryItem.remoteQuery, cancellationToken);
|
||||
|
||||
const executionEndTime = new Date();
|
||||
const executionEndTime = Date.now();
|
||||
|
||||
if (queryResult.status === 'CompletedSuccessfully') {
|
||||
const resultIndex = await getRemoteQueryIndex(credentials, query);
|
||||
if (!resultIndex) {
|
||||
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${query.queryName}`);
|
||||
return;
|
||||
if (queryWorkflowResult.status === 'CompletedSuccessfully') {
|
||||
const resultIndex = await getRemoteQueryIndex(credentials, queryItem.remoteQuery);
|
||||
queryItem.completed = true;
|
||||
if (resultIndex) {
|
||||
queryItem.status = QueryStatus.Completed;
|
||||
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId);
|
||||
|
||||
await this.storeJsonFile(queryItem, 'query-result.json', queryResult);
|
||||
|
||||
// Kick off auto-download of results in the background.
|
||||
void commands.executeCommand('codeQL.autoDownloadRemoteQueryResults', queryResult);
|
||||
|
||||
// Ask if the user wants to open the results in the background.
|
||||
void this.askToOpenResults(queryItem.remoteQuery, queryResult).then(
|
||||
noop,
|
||||
err => {
|
||||
void showAndLogErrorMessage(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${queryItem.label}`);
|
||||
queryItem.status = QueryStatus.Failed;
|
||||
}
|
||||
|
||||
const queryResult = this.mapQueryResult(executionEndTime, resultIndex);
|
||||
|
||||
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
|
||||
const message = `Query "${query.queryName}" run on ${query.repositories.length} repositories and returned ${totalResultCount} results`;
|
||||
|
||||
const shouldOpenView = await showInformationMessageWithAction(message, 'View');
|
||||
if (shouldOpenView) {
|
||||
const rqim = new RemoteQueriesInterfaceManager(this.ctx, this.logger, this.analysesResultsManager);
|
||||
await rqim.showResults(query, queryResult);
|
||||
}
|
||||
} else if (queryResult.status === 'CompletedUnsuccessfully') {
|
||||
await showAndLogErrorMessage(`Remote query execution failed. Error: ${queryResult.error}`);
|
||||
return;
|
||||
} else if (queryResult.status === 'Cancelled') {
|
||||
await showAndLogErrorMessage('Remote query monitoring was cancelled');
|
||||
} else if (queryWorkflowResult.status === 'CompletedUnsuccessfully') {
|
||||
queryItem.failureReason = queryWorkflowResult.error;
|
||||
queryItem.status = QueryStatus.Failed;
|
||||
void showAndLogErrorMessage(`Variant analysis execution failed. Error: ${queryWorkflowResult.error}`);
|
||||
} else if (queryWorkflowResult.status === 'Cancelled') {
|
||||
queryItem.failureReason = 'Cancelled';
|
||||
queryItem.status = QueryStatus.Failed;
|
||||
void showAndLogErrorMessage('Variant analysis monitoring was cancelled');
|
||||
} else if (queryWorkflowResult.status === 'InProgress') {
|
||||
// Should not get here. Only including this to ensure `assertNever` uses proper type checking.
|
||||
void showAndLogErrorMessage(`Unexpected status: ${queryWorkflowResult.status}`);
|
||||
} else {
|
||||
// Ensure all cases are covered
|
||||
assertNever(queryWorkflowResult.status);
|
||||
}
|
||||
await this.qhm.refreshTreeView();
|
||||
}
|
||||
|
||||
private mapQueryResult(executionEndTime: Date, resultIndex: RemoteQueryResultIndex): RemoteQueryResult {
|
||||
const analysisSummaries = resultIndex.items.map(item => ({
|
||||
public async autoDownloadRemoteQueryResults(
|
||||
queryResult: RemoteQueryResult,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
const analysesToDownload = queryResult.analysisSummaries
|
||||
.filter(a => a.fileSizeInBytes < autoDownloadMaxSize)
|
||||
.slice(0, autoDownloadMaxCount)
|
||||
.map(a => ({
|
||||
nwo: a.nwo,
|
||||
databaseSha: a.databaseSha,
|
||||
resultCount: a.resultCount,
|
||||
downloadLink: a.downloadLink,
|
||||
fileSize: String(a.fileSizeInBytes)
|
||||
}));
|
||||
|
||||
await this.analysesResultsManager.loadAnalysesResults(
|
||||
analysesToDownload,
|
||||
token,
|
||||
results => this.interfaceManager.setAnalysisResults(results));
|
||||
}
|
||||
|
||||
private mapQueryResult(executionEndTime: number, resultIndex: RemoteQueryResultIndex, queryId: string): RemoteQueryResult {
|
||||
|
||||
const analysisSummaries = resultIndex.successes.map(item => ({
|
||||
nwo: item.nwo,
|
||||
databaseSha: item.sha || 'HEAD',
|
||||
resultCount: item.resultCount,
|
||||
fileSizeInBytes: item.sarifFileSize ? item.sarifFileSize : item.bqrsFileSize,
|
||||
downloadLink: {
|
||||
id: item.artifactId.toString(),
|
||||
urlPath: `${resultIndex.artifactsUrlPath}/${item.artifactId}`,
|
||||
innerFilePath: item.sarifFileSize ? 'results.sarif' : 'results.bqrs'
|
||||
innerFilePath: item.sarifFileSize ? 'results.sarif' : 'results.bqrs',
|
||||
queryId,
|
||||
} as DownloadLink
|
||||
}));
|
||||
const analysisFailures = resultIndex.failures.map(item => ({
|
||||
nwo: item.nwo,
|
||||
error: item.error
|
||||
}));
|
||||
|
||||
return {
|
||||
executionEndTime,
|
||||
analysisSummaries,
|
||||
allResultsDownloadLink: {
|
||||
id: resultIndex.allResultsArtifactId.toString(),
|
||||
urlPath: `${resultIndex.artifactsUrlPath}/${resultIndex.allResultsArtifactId}`
|
||||
}
|
||||
analysisFailures,
|
||||
queryId
|
||||
};
|
||||
}
|
||||
|
||||
public async openResults(query: RemoteQuery, queryResult: RemoteQueryResult) {
|
||||
await this.interfaceManager.showResults(query, queryResult);
|
||||
}
|
||||
|
||||
private async askToOpenResults(query: RemoteQuery, queryResult: RemoteQueryResult): Promise<void> {
|
||||
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
|
||||
const totalRepoCount = queryResult.analysisSummaries.length;
|
||||
const message = `Query "${query.queryName}" run on ${totalRepoCount} repositories and returned ${totalResultCount} results`;
|
||||
|
||||
const shouldOpenView = await showInformationMessageWithAction(message, 'View');
|
||||
if (shouldOpenView) {
|
||||
await this.openResults(query, queryResult);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique id for this query, suitable for determining the storage location for the downloaded query artifacts.
|
||||
* @param queryName
|
||||
* @returns
|
||||
*/
|
||||
private createQueryId(queryName: string): string {
|
||||
return `${queryName}-${nanoid()}`;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a directory for storing analysis results for a single query run.
|
||||
* This directory contains a timestamp file, which will be
|
||||
* used by the query history manager to determine when the directory
|
||||
* should be deleted.
|
||||
*
|
||||
* @param queryName The name of the query that was run.
|
||||
*/
|
||||
private async prepareStorageDirectory(queryHistoryItem: RemoteQueryHistoryItem): Promise<void> {
|
||||
await createTimestampFile(path.join(this.storagePath, queryHistoryItem.queryId));
|
||||
}
|
||||
|
||||
private async storeJsonFile<T>(queryHistoryItem: RemoteQueryHistoryItem, fileName: string, obj: T): Promise<void> {
|
||||
const filePath = path.join(this.storagePath, queryHistoryItem.queryId, fileName);
|
||||
await fs.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
private async retrieveJsonFile<T>(queryHistoryItem: RemoteQueryHistoryItem, fileName: string): Promise<T> {
|
||||
const filePath = path.join(this.storagePath, queryHistoryItem.queryId, fileName);
|
||||
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
private async removeStorageDirectory(queryItem: RemoteQueryHistoryItem): Promise<void> {
|
||||
const filePath = path.join(this.storagePath, queryItem.queryId);
|
||||
await fs.remove(filePath);
|
||||
}
|
||||
|
||||
private async queryHistoryItemExists(queryItem: RemoteQueryHistoryItem): Promise<boolean> {
|
||||
const filePath = path.join(this.storagePath, queryItem.queryId);
|
||||
return await fs.pathExists(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export class RemoteQueriesMonitor {
|
||||
attemptCount++;
|
||||
}
|
||||
|
||||
void this.logger.log('Remote query monitoring timed out after 2 days');
|
||||
void this.logger.log('Variant analysis monitoring timed out after 2 days');
|
||||
return { status: 'Cancelled' };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { QueryStatus } from '../query-status';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
|
||||
/**
|
||||
* Information about a remote query.
|
||||
*/
|
||||
export interface RemoteQueryHistoryItem {
|
||||
readonly t: 'remote';
|
||||
failureReason?: string;
|
||||
status: QueryStatus;
|
||||
completed: boolean;
|
||||
readonly queryId: string,
|
||||
label: string; // TODO, the query label should have interpolation like local queries
|
||||
remoteQuery: RemoteQuery;
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
export interface RemoteQueryResultIndex {
|
||||
artifactsUrlPath: string;
|
||||
allResultsArtifactId: number;
|
||||
items: RemoteQueryResultIndexItem[];
|
||||
successes: RemoteQuerySuccessIndexItem[];
|
||||
failures: RemoteQueryFailureIndexItem[];
|
||||
}
|
||||
|
||||
export interface RemoteQueryResultIndexItem {
|
||||
export interface RemoteQuerySuccessIndexItem {
|
||||
id: string;
|
||||
artifactId: number;
|
||||
nwo: string;
|
||||
sha?: string;
|
||||
resultCount: number;
|
||||
bqrsFileSize: number;
|
||||
sarifFileSize?: number;
|
||||
}
|
||||
|
||||
export interface RemoteQueryFailureIndexItem {
|
||||
id: string;
|
||||
artifactId: number;
|
||||
nwo: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { DownloadLink } from './download-link';
|
||||
import { AnalysisFailure } from './shared/analysis-failure';
|
||||
|
||||
export interface RemoteQueryResult {
|
||||
executionEndTime: Date;
|
||||
executionEndTime: number; // Can't use a Date here since it needs to be serialized and desserialized.
|
||||
analysisSummaries: AnalysisSummary[];
|
||||
allResultsDownloadLink: DownloadLink;
|
||||
analysisFailures: AnalysisFailure[];
|
||||
queryId: string;
|
||||
}
|
||||
|
||||
export interface AnalysisSummary {
|
||||
nwo: string,
|
||||
databaseSha: string,
|
||||
resultCount: number,
|
||||
downloadLink: DownloadLink,
|
||||
fileSizeInBytes: number
|
||||
|
||||
@@ -4,8 +4,8 @@ export interface RemoteQuery {
|
||||
queryName: string;
|
||||
queryFilePath: string;
|
||||
queryText: string;
|
||||
language: string;
|
||||
controllerRepository: Repository;
|
||||
repositories: Repository[];
|
||||
executionStartTime: Date;
|
||||
executionStartTime: number; // Use number here since it needs to be serialized and desserialized.
|
||||
actionsWorkflowRunId: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { QuickPickItem, window } from 'vscode';
|
||||
import { showAndLogErrorMessage } from '../helpers';
|
||||
import { getRemoteRepositoryLists } from '../config';
|
||||
import { logger } from '../logging';
|
||||
import { REPO_REGEX } from '../pure/helpers-pure';
|
||||
|
||||
interface RepoListQuickPickItem extends QuickPickItem {
|
||||
repoList: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the repositories to run the query against.
|
||||
*/
|
||||
export async function getRepositories(): Promise<string[] | undefined> {
|
||||
const repoLists = getRemoteRepositoryLists();
|
||||
if (repoLists && Object.keys(repoLists).length) {
|
||||
const quickPickItems = Object.entries(repoLists).map<RepoListQuickPickItem>(([key, value]) => (
|
||||
{
|
||||
label: key, // the name of the repository list
|
||||
repoList: value, // the actual array of repositories
|
||||
}
|
||||
));
|
||||
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
|
||||
quickPickItems,
|
||||
{
|
||||
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.variantAnalysis.repositoryLists` setting.',
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (quickpick?.repoList.length) {
|
||||
void logger.log(`Selected repositories: ${quickpick.repoList.join(', ')}`);
|
||||
return quickpick.repoList;
|
||||
} else {
|
||||
void showAndLogErrorMessage('No repositories selected.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
void logger.log('No repository lists defined. Displaying text input box.');
|
||||
const remoteRepo = await window.showInputBox({
|
||||
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
|
||||
placeHolder: '<owner>/<repo>',
|
||||
prompt: 'Tip: you can save frequently used repositories in the `codeQL.variantAnalysis.repositoryLists` setting',
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (!remoteRepo) {
|
||||
void showAndLogErrorMessage('No repositories entered.');
|
||||
return;
|
||||
} else if (!REPO_REGEX.test(remoteRepo)) { // Check if user entered invalid input
|
||||
void showAndLogErrorMessage('Invalid repository format. Must be in the format <owner>/<repo> (e.g. github/codeql)');
|
||||
return;
|
||||
}
|
||||
void logger.log(`Entered repository: ${remoteRepo}`);
|
||||
return [remoteRepo];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CancellationToken, QuickPickItem, Uri, window } from 'vscode';
|
||||
import { CancellationToken, Uri, window } from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as fs from 'fs-extra';
|
||||
@@ -9,25 +9,20 @@ import {
|
||||
getOnDiskWorkspaceFolders,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogInformationMessage,
|
||||
showInformationMessageWithAction,
|
||||
tryGetQueryMetadata
|
||||
tryGetQueryMetadata,
|
||||
tmpDir
|
||||
} from '../helpers';
|
||||
import { Credentials } from '../authentication';
|
||||
import * as cli from '../cli';
|
||||
import { logger } from '../logging';
|
||||
import { getRemoteControllerRepo, getRemoteRepositoryLists, setRemoteControllerRepo } from '../config';
|
||||
import { tmpDir } from '../run-queries';
|
||||
import { getActionBranch, getRemoteControllerRepo, setRemoteControllerRepo } from '../config';
|
||||
import { ProgressCallback, UserCancellationException } from '../commandRunner';
|
||||
import { OctokitResponse } from '@octokit/types/dist-types';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { RemoteQuerySubmissionResult } from './remote-query-submission-result';
|
||||
import { QueryMetadata } from '../pure/interface-types';
|
||||
|
||||
interface Config {
|
||||
repositories: string[];
|
||||
ref?: string;
|
||||
language?: string;
|
||||
}
|
||||
import { getErrorMessage, REPO_REGEX } from '../pure/helpers-pure';
|
||||
import { getRepositories } from './repository-selection';
|
||||
|
||||
export interface QlPack {
|
||||
name: string;
|
||||
@@ -36,71 +31,16 @@ export interface QlPack {
|
||||
defaultSuite?: Record<string, unknown>[];
|
||||
defaultSuiteFile?: string;
|
||||
}
|
||||
interface RepoListQuickPickItem extends QuickPickItem {
|
||||
repoList: string[];
|
||||
}
|
||||
|
||||
interface QueriesResponse {
|
||||
workflow_run_id: number
|
||||
}
|
||||
|
||||
/**
|
||||
* This regex matches strings of the form `owner/repo` where:
|
||||
* - `owner` is made up of alphanumeric characters or single hyphens, starting and ending in an alphanumeric character
|
||||
* - `repo` is made up of alphanumeric characters, hyphens, or underscores
|
||||
*/
|
||||
const REPO_REGEX = /^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+\/[a-zA-Z0-9-_]+$/;
|
||||
|
||||
/**
|
||||
* Well-known names for the query pack used by the server.
|
||||
*/
|
||||
const QUERY_PACK_NAME = 'codeql-remote/query';
|
||||
|
||||
/**
|
||||
* Gets the repositories to run the query against.
|
||||
*/
|
||||
export async function getRepositories(): Promise<string[] | undefined> {
|
||||
const repoLists = getRemoteRepositoryLists();
|
||||
if (repoLists && Object.keys(repoLists).length) {
|
||||
const quickPickItems = Object.entries(repoLists).map<RepoListQuickPickItem>(([key, value]) => (
|
||||
{
|
||||
label: key, // the name of the repository list
|
||||
repoList: value, // the actual array of repositories
|
||||
}
|
||||
));
|
||||
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
|
||||
quickPickItems,
|
||||
{
|
||||
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.remoteQueries.repositoryLists` setting.',
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (quickpick?.repoList.length) {
|
||||
void logger.log(`Selected repositories: ${quickpick.repoList.join(', ')}`);
|
||||
return quickpick.repoList;
|
||||
} else {
|
||||
void showAndLogErrorMessage('No repositories selected.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
void logger.log('No repository lists defined. Displaying text input box.');
|
||||
const remoteRepo = await window.showInputBox({
|
||||
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
|
||||
placeHolder: '<owner>/<repo>',
|
||||
prompt: 'Tip: you can save frequently used repositories in the `codeQL.remoteQueries.repositoryLists` setting',
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (!remoteRepo) {
|
||||
void showAndLogErrorMessage('No repositories entered.');
|
||||
return;
|
||||
} else if (!REPO_REGEX.test(remoteRepo)) { // Check if user entered invalid input
|
||||
void showAndLogErrorMessage('Invalid repository format. Must be in the format <owner>/<repo> (e.g. github/codeql)');
|
||||
return;
|
||||
}
|
||||
void logger.log(`Entered repository: ${remoteRepo}`);
|
||||
return [remoteRepo];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Two possibilities:
|
||||
* 1. There is no qlpack.yml in this directory. Assume this is a lone query and generate a synthetic qlpack for it.
|
||||
@@ -108,7 +48,7 @@ export async function getRepositories(): Promise<string[] | undefined> {
|
||||
*
|
||||
* @returns the entire qlpack as a base64 string.
|
||||
*/
|
||||
async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: string, queryPackDir: string, fallbackLanguage?: string): Promise<{
|
||||
async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: string, queryPackDir: string): Promise<{
|
||||
base64Pack: string,
|
||||
language: string
|
||||
}> {
|
||||
@@ -150,7 +90,7 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
|
||||
|
||||
} else {
|
||||
// open popup to ask for language if not already hardcoded
|
||||
language = fallbackLanguage || await askForLanguage(cliServer);
|
||||
language = await askForLanguage(cliServer);
|
||||
|
||||
// copy only the query file to the query pack directory
|
||||
// and generate a synthetic query pack
|
||||
@@ -172,6 +112,9 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
|
||||
|
||||
await ensureNameAndSuite(queryPackDir, packRelativePath);
|
||||
|
||||
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
|
||||
await cliServer.clearCache();
|
||||
|
||||
const bundlePath = await getPackedBundlePath(queryPackDir);
|
||||
void logger.log(`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`);
|
||||
await cliServer.packInstall(queryPackDir);
|
||||
@@ -238,47 +181,22 @@ export async function runRemoteQuery(
|
||||
throw new UserCancellationException('Not a CodeQL query file.');
|
||||
}
|
||||
|
||||
progress({
|
||||
maxStep: 5,
|
||||
step: 1,
|
||||
message: 'Determining project list'
|
||||
});
|
||||
|
||||
const queryFile = uri.fsPath;
|
||||
const repositoriesFile = queryFile.substring(0, queryFile.length - '.ql'.length) + '.repositories';
|
||||
let ref: string | undefined;
|
||||
// For the case of single file remote queries, use the language from the config in order to avoid the user having to select it.
|
||||
let fallbackLanguage: string | undefined;
|
||||
let repositories: string[] | undefined;
|
||||
|
||||
progress({
|
||||
maxStep: 5,
|
||||
step: 2,
|
||||
maxStep: 4,
|
||||
step: 1,
|
||||
message: 'Determining query target language'
|
||||
});
|
||||
|
||||
// If the user has an explicit `.repositories` file, use that.
|
||||
// Otherwise, prompt user to select repositories from the `codeQL.remoteQueries.repositoryLists` setting.
|
||||
if (await fs.pathExists(repositoriesFile)) {
|
||||
void logger.log(`Found '${repositoriesFile}'. Using information from that file to run ${queryFile}.`);
|
||||
|
||||
const config = yaml.safeLoad(await fs.readFile(repositoriesFile, 'utf8')) as Config;
|
||||
|
||||
ref = config.ref || 'main';
|
||||
fallbackLanguage = config.language;
|
||||
repositories = config.repositories;
|
||||
} else {
|
||||
ref = 'main';
|
||||
repositories = await getRepositories();
|
||||
}
|
||||
|
||||
const repositories = await getRepositories();
|
||||
if (!repositories || repositories.length === 0) {
|
||||
throw new UserCancellationException('No repositories to query.');
|
||||
}
|
||||
|
||||
progress({
|
||||
maxStep: 5,
|
||||
step: 3,
|
||||
maxStep: 4,
|
||||
step: 2,
|
||||
message: 'Determining controller repo'
|
||||
});
|
||||
|
||||
@@ -309,8 +227,8 @@ export async function runRemoteQuery(
|
||||
const [owner, repo] = controllerRepo.split('/');
|
||||
|
||||
progress({
|
||||
maxStep: 5,
|
||||
step: 4,
|
||||
maxStep: 4,
|
||||
step: 3,
|
||||
message: 'Bundling the query pack'
|
||||
});
|
||||
|
||||
@@ -318,20 +236,21 @@ export async function runRemoteQuery(
|
||||
throw new UserCancellationException('Cancelled');
|
||||
}
|
||||
|
||||
const { base64Pack, language } = await generateQueryPack(cliServer, queryFile, queryPackDir, fallbackLanguage);
|
||||
const { base64Pack, language } = await generateQueryPack(cliServer, queryFile, queryPackDir);
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException('Cancelled');
|
||||
}
|
||||
|
||||
progress({
|
||||
maxStep: 5,
|
||||
step: 5,
|
||||
maxStep: 4,
|
||||
step: 4,
|
||||
message: 'Sending request'
|
||||
});
|
||||
|
||||
const workflowRunId = await runRemoteQueriesApiRequest(credentials, ref, language, repositories, owner, repo, base64Pack, dryRun);
|
||||
const queryStartTime = new Date();
|
||||
const actionBranch = getActionBranch();
|
||||
const workflowRunId = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repositories, owner, repo, base64Pack, dryRun);
|
||||
const queryStartTime = Date.now();
|
||||
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
|
||||
|
||||
if (dryRun) {
|
||||
@@ -341,7 +260,14 @@ export async function runRemoteQuery(
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteQuery = await buildRemoteQueryEntity(repositories, queryFile, queryMetadata, owner, repo, queryStartTime, workflowRunId);
|
||||
const remoteQuery = await buildRemoteQueryEntity(
|
||||
queryFile,
|
||||
queryMetadata,
|
||||
owner,
|
||||
repo,
|
||||
queryStartTime,
|
||||
workflowRunId,
|
||||
language);
|
||||
|
||||
// don't return the path because it has been deleted
|
||||
return { query: remoteQuery };
|
||||
@@ -392,48 +318,7 @@ async function runRemoteQueriesApiRequest(
|
||||
void showAndLogInformationMessage(`Successfully scheduled runs. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${workflowRunId}).`);
|
||||
return workflowRunId;
|
||||
} catch (error) {
|
||||
return await attemptRerun(error, credentials, ref, language, repositories, owner, repo, queryPackBase64, dryRun);
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempts to rerun the query on only the valid repositories */
|
||||
export async function attemptRerun(
|
||||
error: any,
|
||||
credentials: Credentials,
|
||||
ref: string,
|
||||
language: string,
|
||||
repositories: string[],
|
||||
owner: string,
|
||||
repo: string,
|
||||
queryPackBase64: string,
|
||||
dryRun = false
|
||||
) {
|
||||
if (typeof error.message === 'string' && error.message.includes('Some repositories were invalid')) {
|
||||
const invalidRepos = error?.response?.data?.invalid_repos || [];
|
||||
const reposWithoutDbUploads = error?.response?.data?.repos_without_db_uploads || [];
|
||||
void logger.log('Unable to run query on some of the specified repositories');
|
||||
if (invalidRepos.length > 0) {
|
||||
void logger.log(`Invalid repos: ${invalidRepos.join(', ')}`);
|
||||
}
|
||||
if (reposWithoutDbUploads.length > 0) {
|
||||
void logger.log(`Repos without DB uploads: ${reposWithoutDbUploads.join(', ')}`);
|
||||
}
|
||||
|
||||
if (invalidRepos.length + reposWithoutDbUploads.length === repositories.length) {
|
||||
// Every repo is invalid in some way
|
||||
void showAndLogErrorMessage('Unable to run query on any of the specified repositories.');
|
||||
return;
|
||||
}
|
||||
|
||||
const popupMessage = 'Unable to run query on some of the specified repositories. [See logs for more details](command:codeQL.showLogs).';
|
||||
const rerunQuery = await showInformationMessageWithAction(popupMessage, 'Rerun on the valid repositories only');
|
||||
if (rerunQuery) {
|
||||
const validRepositories = repositories.filter(r => !invalidRepos.includes(r) && !reposWithoutDbUploads.includes(r));
|
||||
void logger.log(`Rerunning query on set of valid repositories: ${JSON.stringify(validRepositories)}`);
|
||||
return await runRemoteQueriesApiRequest(credentials, ref, language, validRepositories, owner, repo, queryPackBase64, dryRun);
|
||||
}
|
||||
} else {
|
||||
void showAndLogErrorMessage(error);
|
||||
void showAndLogErrorMessage(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +339,7 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string
|
||||
qlpack.name = QUERY_PACK_NAME;
|
||||
|
||||
qlpack.defaultSuite = [{
|
||||
description: 'Query suite for remote query'
|
||||
description: 'Query suite for variant analysis'
|
||||
}, {
|
||||
query: packRelativePath.replace(/\\/g, '/')
|
||||
}];
|
||||
@@ -462,33 +347,28 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string
|
||||
}
|
||||
|
||||
async function buildRemoteQueryEntity(
|
||||
repositories: string[],
|
||||
queryFilePath: string,
|
||||
queryMetadata: QueryMetadata | undefined,
|
||||
controllerRepoOwner: string,
|
||||
controllerRepoName: string,
|
||||
queryStartTime: Date,
|
||||
workflowRunId: number
|
||||
queryStartTime: number,
|
||||
workflowRunId: number,
|
||||
language: string
|
||||
): Promise<RemoteQuery> {
|
||||
// The query name is either the name as specified in the query metadata, or the file name.
|
||||
// The query name is either the name as specified in the query metadata, or the file name.
|
||||
const queryName = queryMetadata?.name ?? path.basename(queryFilePath);
|
||||
|
||||
const queryRepos = repositories.map(r => {
|
||||
const [owner, repo] = r.split('/');
|
||||
return { owner: owner, name: repo };
|
||||
});
|
||||
|
||||
const queryText = await fs.readFile(queryFilePath, 'utf8');
|
||||
|
||||
return {
|
||||
queryName,
|
||||
queryFilePath,
|
||||
queryText,
|
||||
language,
|
||||
controllerRepository: {
|
||||
owner: controllerRepoOwner,
|
||||
name: controllerRepoName,
|
||||
},
|
||||
repositories: queryRepos,
|
||||
executionStartTime: queryStartTime,
|
||||
actionsWorkflowRunId: workflowRunId
|
||||
};
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { RemoteQueryResult } from './remote-query-result';
|
||||
|
||||
export const sampleRemoteQuery: RemoteQuery = {
|
||||
queryName: 'Inefficient regular expression',
|
||||
queryFilePath: '/Users/foo/dev/vscode-codeql-starter/ql/javascript/ql/src/Performance/ReDoS.ql',
|
||||
queryText: '/**\n * @name Inefficient regular expression\n * @description A regular expression that requires exponential time to match certain inputs\n * can be a performance bottleneck, and may be vulnerable to denial-of-service\n * attacks.\n * @kind problem\n * @problem.severity error\n * @security-severity 7.5\n * @precision high\n * @id js/redos\n * @tags security\n * external/cwe/cwe-1333\n * external/cwe/cwe-730\n * external/cwe/cwe-400\n */\n\nimport javascript\nimport semmle.javascript.security.performance.ReDoSUtil\nimport semmle.javascript.security.performance.ExponentialBackTracking\n\nfrom RegExpTerm t, string pump, State s, string prefixMsg\nwhere hasReDoSResult(t, pump, s, prefixMsg)\nselect t,\n "This part of the regular expression may cause exponential backtracking on strings " + prefixMsg +\n "containing many repetitions of \'" + pump + "\'."\n',
|
||||
controllerRepository: {
|
||||
owner: 'big-corp',
|
||||
name: 'controller-repo'
|
||||
},
|
||||
repositories: [
|
||||
{
|
||||
owner: 'big-corp',
|
||||
name: 'repo1'
|
||||
},
|
||||
{
|
||||
owner: 'big-corp',
|
||||
name: 'repo2'
|
||||
},
|
||||
{
|
||||
owner: 'big-corp',
|
||||
name: 'repo3'
|
||||
},
|
||||
{
|
||||
owner: 'big-corp',
|
||||
name: 'repo4'
|
||||
},
|
||||
{
|
||||
owner: 'big-corp',
|
||||
name: 'repo5'
|
||||
}
|
||||
],
|
||||
executionStartTime: new Date('2022-01-06T17:02:15.026Z'),
|
||||
actionsWorkflowRunId: 1662757118
|
||||
};
|
||||
|
||||
export const sampleRemoteQueryResult: RemoteQueryResult = {
|
||||
executionEndTime: new Date('2022-01-06T17:04:37.026Z'),
|
||||
allResultsDownloadLink: {
|
||||
id: '137697018',
|
||||
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697018'
|
||||
},
|
||||
analysisSummaries: [
|
||||
{
|
||||
nwo: 'big-corp/repo1',
|
||||
resultCount: 85,
|
||||
fileSizeInBytes: 14123,
|
||||
downloadLink: {
|
||||
id: '137697017',
|
||||
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697017',
|
||||
innerFilePath: 'results.sarif'
|
||||
}
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo2',
|
||||
resultCount: 20,
|
||||
fileSizeInBytes: 8698,
|
||||
downloadLink: {
|
||||
id: '137697018',
|
||||
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697018',
|
||||
innerFilePath: 'results.sarif'
|
||||
}
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo3',
|
||||
resultCount: 8,
|
||||
fileSizeInBytes: 4123,
|
||||
downloadLink: {
|
||||
id: '137697019',
|
||||
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697019',
|
||||
innerFilePath: 'results.sarif'
|
||||
}
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo4',
|
||||
resultCount: 3,
|
||||
fileSizeInBytes: 3313,
|
||||
downloadLink: {
|
||||
id: '137697020',
|
||||
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697020',
|
||||
innerFilePath: 'results.sarif'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
253
extensions/ql-vscode/src/remote-queries/sarif-processing.ts
Normal file
253
extensions/ql-vscode/src/remote-queries/sarif-processing.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import * as sarif from 'sarif';
|
||||
import { parseSarifPlainTextMessage, parseSarifRegion } from '../pure/sarif-utils';
|
||||
|
||||
import {
|
||||
AnalysisAlert,
|
||||
CodeFlow,
|
||||
AnalysisMessage,
|
||||
AnalysisMessageToken,
|
||||
ResultSeverity,
|
||||
ThreadFlow,
|
||||
CodeSnippet,
|
||||
HighlightedRegion
|
||||
} from './shared/analysis-result';
|
||||
|
||||
const defaultSeverity = 'Warning';
|
||||
|
||||
export function extractAnalysisAlerts(
|
||||
sarifLog: sarif.Log,
|
||||
fileLinkPrefix: string
|
||||
): {
|
||||
alerts: AnalysisAlert[],
|
||||
errors: string[]
|
||||
} {
|
||||
const alerts: AnalysisAlert[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const run of sarifLog.runs ?? []) {
|
||||
for (const result of run.results ?? []) {
|
||||
try {
|
||||
alerts.push(...extractResultAlerts(run, result, fileLinkPrefix));
|
||||
} catch (e) {
|
||||
errors.push(`Error when processing SARIF result: ${e}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { alerts, errors };
|
||||
}
|
||||
|
||||
function extractResultAlerts(
|
||||
run: sarif.Run,
|
||||
result: sarif.Result,
|
||||
fileLinkPrefix: string
|
||||
): AnalysisAlert[] {
|
||||
const alerts: AnalysisAlert[] = [];
|
||||
|
||||
const message = getMessage(result, fileLinkPrefix);
|
||||
const rule = tryGetRule(run, result);
|
||||
const severity = tryGetSeverity(run, result, rule) || defaultSeverity;
|
||||
const codeFlows = getCodeFlows(result, fileLinkPrefix);
|
||||
const shortDescription = getShortDescription(rule, message!);
|
||||
|
||||
for (const location of result.locations ?? []) {
|
||||
const physicalLocation = location.physicalLocation!;
|
||||
const filePath = physicalLocation.artifactLocation!.uri!;
|
||||
const codeSnippet = getCodeSnippet(physicalLocation.contextRegion, physicalLocation.region);
|
||||
const highlightedRegion = physicalLocation.region
|
||||
? getHighlightedRegion(physicalLocation.region)
|
||||
: undefined;
|
||||
|
||||
const analysisAlert: AnalysisAlert = {
|
||||
message,
|
||||
shortDescription,
|
||||
fileLink: {
|
||||
fileLinkPrefix,
|
||||
filePath,
|
||||
},
|
||||
severity,
|
||||
codeSnippet,
|
||||
highlightedRegion,
|
||||
codeFlows: codeFlows
|
||||
};
|
||||
|
||||
alerts.push(analysisAlert);
|
||||
}
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
function getShortDescription(
|
||||
rule: sarif.ReportingDescriptor | undefined,
|
||||
message: AnalysisMessage,
|
||||
): string {
|
||||
if (rule?.shortDescription?.text) {
|
||||
return rule.shortDescription.text;
|
||||
}
|
||||
|
||||
return message.tokens.map(token => token.text).join();
|
||||
}
|
||||
|
||||
export function tryGetSeverity(
|
||||
sarifRun: sarif.Run,
|
||||
result: sarif.Result,
|
||||
rule: sarif.ReportingDescriptor | undefined
|
||||
): ResultSeverity | undefined {
|
||||
if (!sarifRun || !result || !rule) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const severity = rule.properties?.['problem.severity'];
|
||||
if (!severity) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (severity.toLowerCase()) {
|
||||
case 'recommendation':
|
||||
return 'Recommendation';
|
||||
case 'warning':
|
||||
return 'Warning';
|
||||
case 'error':
|
||||
return 'Error';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function tryGetRule(
|
||||
sarifRun: sarif.Run,
|
||||
result: sarif.Result
|
||||
): sarif.ReportingDescriptor | undefined {
|
||||
if (!sarifRun || !result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resultRule = result.rule;
|
||||
if (!resultRule) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// The rule can found in two places:
|
||||
// - Either in the run's tool driver tool component
|
||||
// - Or in the run's tool extensions tool component
|
||||
|
||||
const ruleId = resultRule.id;
|
||||
if (ruleId) {
|
||||
const rule = sarifRun.tool.driver.rules?.find(r => r.id === ruleId);
|
||||
if (rule) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
const ruleIndex = resultRule.index;
|
||||
if (ruleIndex != undefined) {
|
||||
const toolComponentIndex = result.rule?.toolComponent?.index;
|
||||
const toolExtensions = sarifRun.tool.extensions;
|
||||
if (toolComponentIndex !== undefined && toolExtensions !== undefined) {
|
||||
const toolComponent = toolExtensions[toolComponentIndex];
|
||||
if (toolComponent?.rules !== undefined) {
|
||||
return toolComponent.rules[ruleIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find the rule.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getCodeSnippet(region?: sarif.Region, alternateRegion?: sarif.Region): CodeSnippet | undefined {
|
||||
region = region ?? alternateRegion;
|
||||
|
||||
if (!region) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const text = region.snippet?.text || '';
|
||||
const { startLine, endLine } = parseSarifRegion(region);
|
||||
|
||||
return {
|
||||
startLine,
|
||||
endLine,
|
||||
text
|
||||
};
|
||||
}
|
||||
|
||||
function getHighlightedRegion(region: sarif.Region): HighlightedRegion {
|
||||
const { startLine, startColumn, endLine, endColumn } = parseSarifRegion(region);
|
||||
|
||||
return {
|
||||
startLine,
|
||||
startColumn,
|
||||
endLine,
|
||||
|
||||
// parseSarifRegion currently shifts the end column by 1 to account
|
||||
// for the way vscode counts columns so we need to shift it back.
|
||||
endColumn: endColumn + 1
|
||||
};
|
||||
}
|
||||
|
||||
function getCodeFlows(
|
||||
result: sarif.Result,
|
||||
fileLinkPrefix: string
|
||||
): CodeFlow[] {
|
||||
const codeFlows = [];
|
||||
|
||||
if (result.codeFlows) {
|
||||
for (const codeFlow of result.codeFlows) {
|
||||
const threadFlows = [];
|
||||
|
||||
for (const threadFlow of codeFlow.threadFlows) {
|
||||
for (const threadFlowLocation of threadFlow.locations) {
|
||||
const physicalLocation = threadFlowLocation!.location!.physicalLocation!;
|
||||
const filePath = physicalLocation!.artifactLocation!.uri!;
|
||||
const codeSnippet = getCodeSnippet(physicalLocation.contextRegion, physicalLocation.region);
|
||||
const highlightedRegion = physicalLocation.region
|
||||
? getHighlightedRegion(physicalLocation.region)
|
||||
: undefined;
|
||||
|
||||
threadFlows.push({
|
||||
fileLink: {
|
||||
fileLinkPrefix,
|
||||
filePath,
|
||||
},
|
||||
codeSnippet,
|
||||
highlightedRegion
|
||||
} as ThreadFlow);
|
||||
}
|
||||
}
|
||||
|
||||
codeFlows.push({ threadFlows } as CodeFlow);
|
||||
}
|
||||
}
|
||||
|
||||
return codeFlows;
|
||||
}
|
||||
|
||||
function getMessage(result: sarif.Result, fileLinkPrefix: string): AnalysisMessage {
|
||||
const tokens: AnalysisMessageToken[] = [];
|
||||
|
||||
const messageText = result.message!.text!;
|
||||
const messageParts = parseSarifPlainTextMessage(messageText);
|
||||
|
||||
for (const messagePart of messageParts) {
|
||||
if (typeof messagePart === 'string') {
|
||||
tokens.push({ t: 'text', text: messagePart });
|
||||
} else {
|
||||
const relatedLocation = result.relatedLocations!.find(rl => rl.id === messagePart.dest);
|
||||
tokens.push({
|
||||
t: 'location',
|
||||
text: messagePart.text,
|
||||
location: {
|
||||
fileLink: {
|
||||
fileLinkPrefix: fileLinkPrefix,
|
||||
filePath: relatedLocation!.physicalLocation!.artifactLocation!.uri!,
|
||||
},
|
||||
highlightedRegion: getHighlightedRegion(relatedLocation!.physicalLocation!.region!),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { tokens };
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface AnalysisFailure {
|
||||
nwo: string,
|
||||
error: string
|
||||
}
|
||||
@@ -1,8 +1,80 @@
|
||||
import { RawResultSet, ResultSetSchema } from '../../pure/bqrs-cli-types';
|
||||
|
||||
export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
|
||||
|
||||
export interface AnalysisResults {
|
||||
nwo: string;
|
||||
results: QueryResult[];
|
||||
status: AnalysisResultStatus;
|
||||
interpretedResults: AnalysisAlert[];
|
||||
rawResults?: AnalysisRawResults;
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
message?: string;
|
||||
export interface AnalysisRawResults {
|
||||
schema: ResultSetSchema;
|
||||
resultSet: RawResultSet;
|
||||
fileLinkPrefix: string;
|
||||
capped: boolean;
|
||||
}
|
||||
|
||||
export interface AnalysisAlert {
|
||||
message: AnalysisMessage;
|
||||
shortDescription: string;
|
||||
severity: ResultSeverity;
|
||||
fileLink: FileLink;
|
||||
codeSnippet?: CodeSnippet;
|
||||
highlightedRegion?: HighlightedRegion;
|
||||
codeFlows: CodeFlow[];
|
||||
}
|
||||
|
||||
export interface FileLink {
|
||||
fileLinkPrefix: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export interface CodeSnippet {
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface HighlightedRegion {
|
||||
startLine: number;
|
||||
startColumn: number;
|
||||
endLine: number;
|
||||
endColumn: number;
|
||||
}
|
||||
|
||||
export interface CodeFlow {
|
||||
threadFlows: ThreadFlow[];
|
||||
}
|
||||
|
||||
export interface ThreadFlow {
|
||||
fileLink: FileLink;
|
||||
codeSnippet: CodeSnippet;
|
||||
highlightedRegion?: HighlightedRegion;
|
||||
message?: AnalysisMessage;
|
||||
}
|
||||
|
||||
export interface AnalysisMessage {
|
||||
tokens: AnalysisMessageToken[]
|
||||
}
|
||||
|
||||
export type AnalysisMessageToken =
|
||||
| AnalysisMessageTextToken
|
||||
| AnalysisMessageLocationToken;
|
||||
|
||||
export interface AnalysisMessageTextToken {
|
||||
t: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface AnalysisMessageLocationToken {
|
||||
t: 'location';
|
||||
text: string;
|
||||
location: {
|
||||
fileLink: FileLink;
|
||||
highlightedRegion?: HighlightedRegion;
|
||||
};
|
||||
}
|
||||
|
||||
export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error';
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { DownloadLink } from '../download-link';
|
||||
import { AnalysisFailure } from './analysis-failure';
|
||||
|
||||
export interface RemoteQueryResult {
|
||||
queryTitle: string;
|
||||
queryFileName: string;
|
||||
queryFilePath: string;
|
||||
queryText: string;
|
||||
language: string;
|
||||
workflowRunUrl: string;
|
||||
totalRepositoryCount: number;
|
||||
affectedRepositoryCount: number;
|
||||
totalResultCount: number;
|
||||
executionTimestamp: string;
|
||||
executionDuration: string;
|
||||
downloadLink: DownloadLink;
|
||||
analysisSummaries: AnalysisSummary[]
|
||||
analysisSummaries: AnalysisSummary[];
|
||||
analysisFailures: AnalysisFailure[];
|
||||
}
|
||||
|
||||
export interface AnalysisSummary {
|
||||
nwo: string,
|
||||
databaseSha: string,
|
||||
resultCount: number,
|
||||
downloadLink: DownloadLink,
|
||||
fileSize: string,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// The maximum number of raw results to read from a BQRS file.
|
||||
// This is used to avoid reading the entire result set into memory
|
||||
// and trying to render it on screen. Users will be warned if the
|
||||
// results are capped.
|
||||
export const MAX_RAW_RESULTS = 500;
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { AnalysisAlert } from '../shared/analysis-result';
|
||||
import CodePaths from './CodePaths';
|
||||
import FileCodeSnippet from './FileCodeSnippet';
|
||||
|
||||
const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => {
|
||||
const showPathsLink = alert.codeFlows.length > 0;
|
||||
|
||||
return <FileCodeSnippet
|
||||
fileLink={alert.fileLink}
|
||||
codeSnippet={alert.codeSnippet}
|
||||
highlightedRegion={alert.highlightedRegion}
|
||||
severity={alert.severity}
|
||||
message={alert.message}
|
||||
messageChildren={
|
||||
showPathsLink && <CodePaths
|
||||
codeFlows={alert.codeFlows}
|
||||
ruleDescription={alert.shortDescription}
|
||||
severity={alert.severity}
|
||||
message={alert.message}
|
||||
/>
|
||||
}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default AnalysisAlertResult;
|
||||
@@ -1,9 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const BadgeContainer = styled.span`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding-left: 0.2em;
|
||||
`;
|
||||
|
||||
const BadgeText = styled.span`
|
||||
display: inline-block;
|
||||
min-width: 1.5em;
|
||||
padding: 0.3em;
|
||||
border-radius: 35%;
|
||||
font-size: x-small;
|
||||
text-align: center;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
border-color: var(--vscode-badge-background);
|
||||
`;
|
||||
|
||||
const Badge = ({ text }: { text: string }) => (
|
||||
<span className="vscode-codeql__badge-container">
|
||||
<span className="vscode-codeql__badge">{text}</span>
|
||||
</span>
|
||||
<BadgeContainer>
|
||||
<BadgeText>{text}</BadgeText>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
export default Badge;
|
||||
|
||||
180
extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx
Normal file
180
extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { TriangleDownIcon, XCircleIcon } from '@primer/octicons-react';
|
||||
import { ActionList, ActionMenu, Box, Button, Label, Link, Overlay } from '@primer/react';
|
||||
import * as React from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { CodeFlow, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
|
||||
import FileCodeSnippet from './FileCodeSnippet';
|
||||
import SectionTitle from './SectionTitle';
|
||||
import VerticalSpace from './VerticalSpace';
|
||||
|
||||
const StyledCloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 4em;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
border: none;
|
||||
&:focus-visible {
|
||||
outline: none
|
||||
}
|
||||
`;
|
||||
|
||||
const OverlayContainer = styled.div`
|
||||
padding: 1em;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 2em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const CloseButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<StyledCloseButton onClick={onClick} tabIndex={-1} >
|
||||
<XCircleIcon size={24} />
|
||||
</StyledCloseButton>
|
||||
);
|
||||
|
||||
const CodePath = ({
|
||||
codeFlow,
|
||||
message,
|
||||
severity
|
||||
}: {
|
||||
codeFlow: CodeFlow;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
}) => {
|
||||
return <>
|
||||
{codeFlow.threadFlows.map((threadFlow, index) =>
|
||||
<div key={`thread-flow-${index}`}>
|
||||
{index !== 0 && <VerticalSpace size={3} />}
|
||||
|
||||
<Box display="flex" justifyContent="center" alignItems="center" width="42.5em">
|
||||
<Box flexGrow={1} p={0} border="none">
|
||||
<SectionTitle>Step {index + 1}</SectionTitle>
|
||||
</Box>
|
||||
{index === 0 &&
|
||||
<Box p={0} border="none">
|
||||
<Label>Source</Label>
|
||||
</Box>
|
||||
}
|
||||
{index === codeFlow.threadFlows.length - 1 &&
|
||||
<Box p={0} border="none">
|
||||
<Label>Sink</Label>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<FileCodeSnippet
|
||||
fileLink={threadFlow.fileLink}
|
||||
codeSnippet={threadFlow.codeSnippet}
|
||||
highlightedRegion={threadFlow.highlightedRegion}
|
||||
severity={severity}
|
||||
message={index === codeFlow.threadFlows.length - 1 ? message : threadFlow.message} />
|
||||
</div>
|
||||
)}
|
||||
</>;
|
||||
};
|
||||
|
||||
const getCodeFlowName = (codeFlow: CodeFlow) => {
|
||||
const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].fileLink.filePath;
|
||||
return filePath.substring(filePath.lastIndexOf('/') + 1);
|
||||
};
|
||||
|
||||
const Menu = ({
|
||||
codeFlows,
|
||||
setSelectedCodeFlow
|
||||
}: {
|
||||
codeFlows: CodeFlow[],
|
||||
setSelectedCodeFlow: (value: React.SetStateAction<CodeFlow>) => void
|
||||
}) => {
|
||||
return <ActionMenu>
|
||||
<ActionMenu.Anchor>
|
||||
<Button variant="invisible" sx={{ fontWeight: 'normal', color: 'var(--vscode-editor-foreground);', padding: 0 }} >
|
||||
{getCodeFlowName(codeFlows[0])}
|
||||
<TriangleDownIcon size={16} />
|
||||
</Button>
|
||||
</ActionMenu.Anchor>
|
||||
<ActionMenu.Overlay sx={{ backgroundColor: 'var(--vscode-editor-background)' }}>
|
||||
<ActionList>
|
||||
{codeFlows.map((codeFlow, index) =>
|
||||
<ActionList.Item
|
||||
key={`codeflow-${index}'`}
|
||||
onSelect={(e: React.MouseEvent) => { setSelectedCodeFlow(codeFlow); }}>
|
||||
{getCodeFlowName(codeFlow)}
|
||||
</ActionList.Item>
|
||||
)}
|
||||
</ActionList>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>;
|
||||
};
|
||||
|
||||
const CodePaths = ({
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity
|
||||
}: {
|
||||
codeFlows: CodeFlow[],
|
||||
ruleDescription: string,
|
||||
message: AnalysisMessage,
|
||||
severity: ResultSeverity
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const closeOverlay = () => setIsOpen(false);
|
||||
|
||||
return (
|
||||
<Box ref={anchorRef}>
|
||||
<Link
|
||||
onClick={() => setIsOpen(true)}
|
||||
ref={linkRef}
|
||||
sx={{ cursor: 'pointer' }}>
|
||||
Show paths
|
||||
</Link>
|
||||
{isOpen && (
|
||||
<Overlay
|
||||
returnFocusRef={linkRef}
|
||||
onEscape={closeOverlay}
|
||||
onClickOutside={closeOverlay}
|
||||
anchorSide="outside-top">
|
||||
<OverlayContainer>
|
||||
<CloseButton onClick={closeOverlay} />
|
||||
|
||||
<SectionTitle>{ruleDescription}</SectionTitle>
|
||||
<VerticalSpace size={2} />
|
||||
|
||||
<Box display="flex" justifyContent="center" alignItems="center">
|
||||
<Box p={0} border="none">
|
||||
{codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in
|
||||
</Box>
|
||||
<Box flexGrow={1} p={0} paddingLeft="0.2em" border="none">
|
||||
<Menu codeFlows={codeFlows} setSelectedCodeFlow={setSelectedCodeFlow} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<CodePath
|
||||
codeFlow={selectedCodeFlow}
|
||||
severity={severity}
|
||||
message={message} />
|
||||
|
||||
<VerticalSpace size={3} />
|
||||
|
||||
</OverlayContainer>
|
||||
</Overlay>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodePaths;
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.span`
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
color: var(--vscode-editor-foreground);
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
padding-right: 0.1em;
|
||||
`;
|
||||
|
||||
const CollapsibleItem = ({
|
||||
title,
|
||||
children
|
||||
}: {
|
||||
title: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Container onClick={() => setExpanded(!isExpanded)}>
|
||||
<Button>
|
||||
{isExpanded
|
||||
? <ChevronDownIcon size={16} />
|
||||
: <ChevronRightIcon size={16} />
|
||||
}
|
||||
</Button>
|
||||
<TitleContainer>{title}</TitleContainer>
|
||||
</Container>
|
||||
{isExpanded && children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollapsibleItem;
|
||||
@@ -1,11 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import * as octicons from '../../view/octicons';
|
||||
import styled from 'styled-components';
|
||||
import { DownloadIcon } from '@primer/octicons-react';
|
||||
|
||||
const ButtonLink = styled.a`
|
||||
display: inline-block;
|
||||
font-size: x-small;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
fill: var(--vscode-textLink-foreground);
|
||||
}
|
||||
`;
|
||||
|
||||
const DownloadButton = ({ text, onClick }: { text: string, onClick: () => void }) => (
|
||||
<a className="vscode-codeql__download-button"
|
||||
onClick={onClick}>
|
||||
{octicons.download}{text}
|
||||
</a>
|
||||
<ButtonLink onClick={onClick}>
|
||||
<DownloadIcon size={16} />{text}
|
||||
</ButtonLink>
|
||||
);
|
||||
|
||||
export default DownloadButton;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Spinner } from '@primer/react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SpinnerContainer = styled.span`
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
}
|
||||
`;
|
||||
|
||||
const DownloadSpinner = () => (
|
||||
<SpinnerContainer>
|
||||
<Spinner size="small" />
|
||||
</SpinnerContainer>
|
||||
);
|
||||
|
||||
export default DownloadSpinner;
|
||||
257
extensions/ql-vscode/src/remote-queries/view/FileCodeSnippet.tsx
Normal file
257
extensions/ql-vscode/src/remote-queries/view/FileCodeSnippet.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { CodeSnippet, FileLink, HighlightedRegion, AnalysisMessage, ResultSeverity } from '../shared/analysis-result';
|
||||
import { Box, Link } from '@primer/react';
|
||||
import VerticalSpace from './VerticalSpace';
|
||||
import { createRemoteFileRef } from '../../pure/location-link-utils';
|
||||
|
||||
const borderColor = 'var(--vscode-editor-snippetFinalTabstopHighlightBorder)';
|
||||
const warningColor = '#966C23';
|
||||
const highlightColor = 'var(--vscode-editor-findMatchHighlightBackground)';
|
||||
|
||||
const getSeverityColor = (severity: ResultSeverity) => {
|
||||
switch (severity) {
|
||||
case 'Recommendation':
|
||||
return 'blue';
|
||||
case 'Warning':
|
||||
return warningColor;
|
||||
case 'Error':
|
||||
return 'red';
|
||||
}
|
||||
};
|
||||
|
||||
const replaceSpaceChar = (text: string) => text.replaceAll(' ', '\u00a0');
|
||||
|
||||
const shouldHighlightLine = (lineNumber: number, highlightedRegion: HighlightedRegion) => {
|
||||
if (lineNumber < highlightedRegion.startLine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (highlightedRegion.endLine == undefined) {
|
||||
return lineNumber == highlightedRegion.startLine;
|
||||
}
|
||||
|
||||
return lineNumber <= highlightedRegion.endLine;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
font-size: x-small;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
border: 0.1em solid ${borderColor};
|
||||
border-top-left-radius: 0.2em;
|
||||
border-top-right-radius: 0.2em;
|
||||
padding: 0.5em;
|
||||
`;
|
||||
|
||||
const CodeContainer = styled.div`
|
||||
border-left: 0.1em solid ${borderColor};
|
||||
border-right: 0.1em solid ${borderColor};
|
||||
border-bottom: 0.1em solid ${borderColor};
|
||||
border-bottom-left-radius: 0.2em;
|
||||
border-bottom-right-radius: 0.2em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
`;
|
||||
|
||||
const MessageText = styled.div`
|
||||
font-size: x-small;
|
||||
padding-left: 0.5em;
|
||||
`;
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
`;
|
||||
|
||||
const PlainLine = ({ text }: { text: string }) => {
|
||||
return <span>{replaceSpaceChar(text)}</span>;
|
||||
};
|
||||
|
||||
const HighlightedLine = ({ text }: { text: string }) => {
|
||||
return <span style={{ backgroundColor: highlightColor }}>{replaceSpaceChar(text)}</span>;
|
||||
};
|
||||
|
||||
const Message = ({
|
||||
message,
|
||||
currentLineNumber,
|
||||
highlightedRegion,
|
||||
borderColor,
|
||||
children
|
||||
}: {
|
||||
message: AnalysisMessage,
|
||||
currentLineNumber: number,
|
||||
highlightedRegion?: HighlightedRegion,
|
||||
borderColor: string,
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
if (!highlightedRegion || highlightedRegion.endLine !== currentLineNumber) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <MessageContainer>
|
||||
<Box
|
||||
borderColor="border.default"
|
||||
borderWidth={1}
|
||||
borderStyle="solid"
|
||||
borderLeftColor={borderColor}
|
||||
borderLeftWidth={3}
|
||||
paddingTop="1em"
|
||||
paddingBottom="1em">
|
||||
<MessageText>
|
||||
{message.tokens.map((token, index) => {
|
||||
switch (token.t) {
|
||||
case 'text':
|
||||
return <span key={`token-${index}`}>{token.text}</span>;
|
||||
case 'location':
|
||||
return <Link
|
||||
key={`token-${index}`}
|
||||
href={createRemoteFileRef(
|
||||
token.location.fileLink,
|
||||
token.location.highlightedRegion?.startLine,
|
||||
token.location.highlightedRegion?.endLine)}>
|
||||
{token.text}
|
||||
</Link>;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
})}
|
||||
{children && <>
|
||||
<VerticalSpace size={2} />
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
</MessageText>
|
||||
</Box>
|
||||
|
||||
</MessageContainer>;
|
||||
};
|
||||
|
||||
const CodeLine = ({
|
||||
line,
|
||||
lineNumber,
|
||||
highlightedRegion
|
||||
}: {
|
||||
line: string,
|
||||
lineNumber: number,
|
||||
highlightedRegion?: HighlightedRegion
|
||||
}) => {
|
||||
if (!highlightedRegion || !shouldHighlightLine(lineNumber, highlightedRegion)) {
|
||||
return <PlainLine text={line} />;
|
||||
}
|
||||
|
||||
const isSingleLineHighlight = highlightedRegion.endLine === undefined;
|
||||
const isFirstHighlightedLine = lineNumber === highlightedRegion.startLine;
|
||||
const isLastHighlightedLine = lineNumber === highlightedRegion.endLine;
|
||||
|
||||
const highlightStartColumn = isSingleLineHighlight
|
||||
? highlightedRegion.startColumn
|
||||
: isFirstHighlightedLine
|
||||
? highlightedRegion.startColumn
|
||||
: 0;
|
||||
|
||||
const highlightEndColumn = isSingleLineHighlight
|
||||
? highlightedRegion.endColumn
|
||||
: isLastHighlightedLine
|
||||
? highlightedRegion.endColumn
|
||||
: line.length + 1;
|
||||
|
||||
const section1 = line.substring(0, highlightStartColumn - 1);
|
||||
const section2 = line.substring(highlightStartColumn - 1, highlightEndColumn - 1);
|
||||
const section3 = line.substring(highlightEndColumn - 1, line.length);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlainLine text={section1} />
|
||||
<HighlightedLine text={section2} />
|
||||
<PlainLine text={section3} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FileCodeSnippet = ({
|
||||
fileLink,
|
||||
codeSnippet,
|
||||
highlightedRegion,
|
||||
severity,
|
||||
message,
|
||||
messageChildren,
|
||||
}: {
|
||||
fileLink: FileLink,
|
||||
codeSnippet?: CodeSnippet,
|
||||
highlightedRegion?: HighlightedRegion,
|
||||
severity?: ResultSeverity,
|
||||
message?: AnalysisMessage,
|
||||
messageChildren?: React.ReactNode,
|
||||
}) => {
|
||||
|
||||
const startingLine = codeSnippet?.startLine || 0;
|
||||
const endingLine = codeSnippet?.endLine || 0;
|
||||
|
||||
const titleFileUri = createRemoteFileRef(
|
||||
fileLink,
|
||||
startingLine,
|
||||
endingLine);
|
||||
|
||||
if (!codeSnippet) {
|
||||
return (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<Link href={titleFileUri}>{fileLink.filePath}</Link>
|
||||
</TitleContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const code = codeSnippet.text.split('\n');
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<Link href={titleFileUri}>{fileLink.filePath}</Link>
|
||||
</TitleContainer>
|
||||
<CodeContainer>
|
||||
{code.map((line, index) => (
|
||||
<div key={index}>
|
||||
<Box display="flex">
|
||||
<Box
|
||||
p={2}
|
||||
borderStyle="none"
|
||||
paddingTop="0.01em"
|
||||
paddingLeft="0.5em"
|
||||
paddingRight="0.5em"
|
||||
paddingBottom="0.2em">
|
||||
{startingLine + index}
|
||||
</Box>
|
||||
<Box
|
||||
flexGrow={1}
|
||||
p={2}
|
||||
borderStyle="none"
|
||||
paddingTop="0.01em"
|
||||
paddingLeft="1.5em"
|
||||
paddingRight="0.5em"
|
||||
paddingBottom="0.2em"
|
||||
sx={{ wordBreak: 'break-word' }}>
|
||||
<CodeLine
|
||||
line={line}
|
||||
lineNumber={startingLine + index}
|
||||
highlightedRegion={highlightedRegion} />
|
||||
</Box>
|
||||
</Box>
|
||||
{message && severity && <Message
|
||||
message={message}
|
||||
currentLineNumber={startingLine + index}
|
||||
highlightedRegion={highlightedRegion}
|
||||
borderColor={getSeverityColor(severity)}>
|
||||
{messageChildren}
|
||||
</Message>}
|
||||
</div>
|
||||
))}
|
||||
</CodeContainer>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileCodeSnippet;
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import styled from 'styled-components';
|
||||
import { XCircleIcon } from '@primer/octicons-react';
|
||||
|
||||
const Container = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
background-color: var(--vscode-editor-background);
|
||||
z-index: 5000;
|
||||
padding-top: 1em;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
background-color: var(--vscode-editor-background);
|
||||
border: none;
|
||||
`;
|
||||
|
||||
const FullScreenModal = ({
|
||||
setOpen,
|
||||
containerElementId,
|
||||
children
|
||||
}: {
|
||||
setOpen: (open: boolean) => void;
|
||||
containerElementId: string;
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const containerElement = document.getElementById(containerElementId);
|
||||
if (!containerElement) {
|
||||
throw Error(`Could not find container element. Id: ${containerElementId}`);
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<>
|
||||
<Container>
|
||||
<CloseButton onClick={() => setOpen(false)}>
|
||||
<XCircleIcon size={24} />
|
||||
</CloseButton>
|
||||
{children}
|
||||
</Container>
|
||||
</>,
|
||||
containerElement
|
||||
);
|
||||
};
|
||||
|
||||
export default FullScreenModal;
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HorizontalSpace = styled.div<{ size: 1 | 2 | 3 }>`
|
||||
flex: 0 0 auto;
|
||||
display: inline-block;
|
||||
width: ${props => 0.2 * props.size}em;
|
||||
`;
|
||||
|
||||
export default HorizontalSpace;
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Link } from '@primer/react';
|
||||
import { CellValue, RawResultSet, ResultSetSchema } from '../../pure/bqrs-cli-types';
|
||||
import { tryGetRemoteLocation } from '../../pure/bqrs-utils';
|
||||
import { useState } from 'react';
|
||||
import TextButton from './TextButton';
|
||||
import { convertNonPrintableChars } from '../../text-utils';
|
||||
|
||||
const numOfResultsInContractedMode = 5;
|
||||
|
||||
const Row = ({
|
||||
row,
|
||||
fileLinkPrefix
|
||||
}: {
|
||||
row: CellValue[],
|
||||
fileLinkPrefix: string
|
||||
}) => (
|
||||
<>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<Box key={cellIndex}
|
||||
borderColor="border.default"
|
||||
borderStyle="solid"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
p={2}
|
||||
sx={{ wordBreak: 'break-word' }}>
|
||||
<Cell value={cell} fileLinkPrefix={fileLinkPrefix} />
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const Cell = ({
|
||||
value,
|
||||
fileLinkPrefix
|
||||
}: {
|
||||
value: CellValue,
|
||||
fileLinkPrefix: string
|
||||
}) => {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return <span>{convertNonPrintableChars(value.toString())}</span>;
|
||||
case 'object': {
|
||||
const url = tryGetRemoteLocation(value.url, fileLinkPrefix);
|
||||
return <Link href={url}>{convertNonPrintableChars(value.label)}</Link>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const RawResultsTable = ({
|
||||
schema,
|
||||
results,
|
||||
fileLinkPrefix
|
||||
}: {
|
||||
schema: ResultSetSchema,
|
||||
results: RawResultSet,
|
||||
fileLinkPrefix: string
|
||||
}) => {
|
||||
const [tableExpanded, setTableExpanded] = useState(false);
|
||||
const numOfResultsToShow = tableExpanded ? results.rows.length : numOfResultsInContractedMode;
|
||||
const showButton = results.rows.length > numOfResultsInContractedMode;
|
||||
|
||||
// Create n equal size columns. We use minmax(0, 1fr) because the
|
||||
// minimum width of 1fr is auto, not 0.
|
||||
// https://css-tricks.com/equal-width-columns-in-css-grid-are-kinda-weird/
|
||||
const gridTemplateColumns = `repeat(${schema.columns.length}, minmax(0, 1fr))`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
maxWidth="45rem"
|
||||
p={2}>
|
||||
{results.rows.slice(0, numOfResultsToShow).map((row, rowIndex) => (
|
||||
<Row key={rowIndex} row={row} fileLinkPrefix={fileLinkPrefix} />
|
||||
))}
|
||||
</Box>
|
||||
{
|
||||
showButton &&
|
||||
<TextButton size='x-small' onClick={() => setTableExpanded(!tableExpanded)}>
|
||||
{tableExpanded ? (<span>View less</span>) : (<span>View all</span>)}
|
||||
</TextButton>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RawResultsTable;
|
||||
@@ -1,18 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as Rdom from 'react-dom';
|
||||
import { Flash, ThemeProvider } from '@primer/react';
|
||||
import { ToRemoteQueriesMessage } from '../../pure/interface-types';
|
||||
import { AnalysisSummary, RemoteQueryResult } from '../shared/remote-query-result';
|
||||
import * as octicons from '../../view/octicons';
|
||||
|
||||
import { MAX_RAW_RESULTS } from '../shared/result-limits';
|
||||
import { vscode } from '../../view/vscode-api';
|
||||
|
||||
import SectionTitle from './SectionTitle';
|
||||
import VerticalSpace from './VerticalSpace';
|
||||
import HorizontalSpace from './HorizontalSpace';
|
||||
import Badge from './Badge';
|
||||
import ViewTitle from './ViewTitle';
|
||||
import DownloadButton from './DownloadButton';
|
||||
import { AnalysisResults } from '../shared/analysis-result';
|
||||
import DownloadSpinner from './DownloadSpinner';
|
||||
import CollapsibleItem from './CollapsibleItem';
|
||||
import { AlertIcon, CodeSquareIcon, FileCodeIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
|
||||
import AnalysisAlertResult from './AnalysisAlertResult';
|
||||
import RawResultsTable from './RawResultsTable';
|
||||
import RepositoriesSearch from './RepositoriesSearch';
|
||||
|
||||
const numOfReposInContractedMode = 10;
|
||||
|
||||
@@ -21,16 +28,15 @@ const emptyQueryResult: RemoteQueryResult = {
|
||||
queryFileName: '',
|
||||
queryFilePath: '',
|
||||
queryText: '',
|
||||
language: '',
|
||||
workflowRunUrl: '',
|
||||
totalRepositoryCount: 0,
|
||||
affectedRepositoryCount: 0,
|
||||
totalResultCount: 0,
|
||||
executionTimestamp: '',
|
||||
executionDuration: '',
|
||||
downloadLink: {
|
||||
id: '',
|
||||
urlPath: '',
|
||||
},
|
||||
analysisSummaries: []
|
||||
analysisSummaries: [],
|
||||
analysisFailures: [],
|
||||
};
|
||||
|
||||
const downloadAnalysisResults = (analysisSummary: AnalysisSummary) => {
|
||||
@@ -61,53 +67,142 @@ const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getAnalysisResultCount = (analysisResults: AnalysisResults): number => {
|
||||
const rawResultCount = analysisResults.rawResults?.resultSet.rows.length || 0;
|
||||
return analysisResults.interpretedResults.length + rawResultCount;
|
||||
};
|
||||
|
||||
const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
|
||||
analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0);
|
||||
|
||||
const QueryInfo = (queryResult: RemoteQueryResult) => (
|
||||
<>
|
||||
<VerticalSpace />
|
||||
{queryResult.totalResultCount} results in {queryResult.totalRepositoryCount} repositories
|
||||
<VerticalSpace size={1} />
|
||||
{queryResult.totalResultCount} results from running against {queryResult.totalRepositoryCount} repositories
|
||||
({queryResult.executionDuration}), {queryResult.executionTimestamp}
|
||||
<VerticalSpace />
|
||||
<span className="vscode-codeql__query-file">{octicons.file}
|
||||
<a className="vscode-codeql__query-file-link" href="#" onClick={() => openQueryFile(queryResult)}>
|
||||
<VerticalSpace size={1} />
|
||||
<span>
|
||||
<a className="vscode-codeql__query-info-link" href="#" onClick={() => openQueryFile(queryResult)}>
|
||||
<span> <FileCodeIcon size={16} /> </span>
|
||||
{queryResult.queryFileName}
|
||||
</a>
|
||||
</span>
|
||||
<span>{octicons.codeSquare}
|
||||
<a className="vscode-codeql__query-file-link" href="#" onClick={() => openQueryTextVirtualFile(queryResult)}>
|
||||
query
|
||||
<span>
|
||||
<a className="vscode-codeql__query-info-link" href="#" onClick={() => openQueryTextVirtualFile(queryResult)}>
|
||||
<span> <CodeSquareIcon size={16} /> </span>
|
||||
Query
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<a className="vscode-codeql__query-info-link" href={queryResult.workflowRunUrl}>
|
||||
<span> <TerminalIcon size={16} /> </span>
|
||||
Logs
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const SummaryTitleWithResults = (queryResult: RemoteQueryResult) => (
|
||||
<div className="vscode-codeql__query-summary-container">
|
||||
<SectionTitle text={`Repositories with results (${queryResult.affectedRepositoryCount}):`} />
|
||||
<DownloadButton
|
||||
text="Download all"
|
||||
onClick={() => downloadAllAnalysesResults(queryResult)} />
|
||||
</div>
|
||||
);
|
||||
const Failures = (queryResult: RemoteQueryResult) => {
|
||||
if (queryResult.analysisFailures.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<VerticalSpace size={3} />
|
||||
<Flash variant="danger">
|
||||
{queryResult.analysisFailures.map((f, i) => (
|
||||
<div key={i}>
|
||||
<p className="vscode-codeql__analysis-failure">
|
||||
<AlertIcon size={16} />
|
||||
<b>{f.nwo}: </b>
|
||||
{f.error}
|
||||
</p>
|
||||
{
|
||||
i === queryResult.analysisFailures.length - 1 ? <></> : <VerticalSpace size={1} />
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</Flash>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SummaryTitleWithResults = ({
|
||||
queryResult,
|
||||
analysesResults
|
||||
}: {
|
||||
queryResult: RemoteQueryResult,
|
||||
analysesResults: AnalysisResults[]
|
||||
}) => {
|
||||
const showDownloadButton = queryResult.totalResultCount !== sumAnalysesResults(analysesResults);
|
||||
|
||||
return (
|
||||
<div className="vscode-codeql__query-summary-container">
|
||||
<SectionTitle>Repositories with results ({queryResult.affectedRepositoryCount}):</SectionTitle>
|
||||
{
|
||||
showDownloadButton && <DownloadButton
|
||||
text="Download all"
|
||||
onClick={() => downloadAllAnalysesResults(queryResult)} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SummaryTitleNoResults = () => (
|
||||
<div className="vscode-codeql__query-summary-container">
|
||||
<SectionTitle text="No results found" />
|
||||
<SectionTitle>No results found</SectionTitle>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SummaryItem = (props: AnalysisSummary) => (
|
||||
const SummaryItemDownload = ({
|
||||
analysisSummary,
|
||||
analysisResults
|
||||
}: {
|
||||
analysisSummary: AnalysisSummary,
|
||||
analysisResults: AnalysisResults | undefined
|
||||
}) => {
|
||||
if (!analysisResults || analysisResults.status === 'Failed') {
|
||||
return <DownloadButton
|
||||
text={analysisSummary.fileSize}
|
||||
onClick={() => downloadAnalysisResults(analysisSummary)} />;
|
||||
}
|
||||
|
||||
if (analysisResults.status === 'InProgress') {
|
||||
return <>
|
||||
<HorizontalSpace size={2} />
|
||||
<DownloadSpinner />
|
||||
</>;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const SummaryItem = ({
|
||||
analysisSummary,
|
||||
analysisResults
|
||||
}: {
|
||||
analysisSummary: AnalysisSummary,
|
||||
analysisResults: AnalysisResults | undefined
|
||||
}) => (
|
||||
<span>
|
||||
<span className="vscode-codeql__analysis-item">{octicons.repo}</span>
|
||||
<span className="vscode-codeql__analysis-item">{props.nwo}</span>
|
||||
<span className="vscode-codeql__analysis-item"><Badge text={props.resultCount.toString()} /></span>
|
||||
<span className="vscode-codeql__analysis-item"><RepoIcon size={16} /></span>
|
||||
<span className="vscode-codeql__analysis-item">{analysisSummary.nwo}</span>
|
||||
<span className="vscode-codeql__analysis-item"><Badge text={analysisSummary.resultCount.toString()} /></span>
|
||||
<span className="vscode-codeql__analysis-item">
|
||||
<DownloadButton
|
||||
text={props.fileSize}
|
||||
onClick={() => downloadAnalysisResults(props)} />
|
||||
<SummaryItemDownload
|
||||
analysisSummary={analysisSummary}
|
||||
analysisResults={analysisResults} />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
const Summary = (queryResult: RemoteQueryResult) => {
|
||||
|
||||
const Summary = ({
|
||||
queryResult,
|
||||
analysesResults
|
||||
}: {
|
||||
queryResult: RemoteQueryResult,
|
||||
analysesResults: AnalysisResults[]
|
||||
}) => {
|
||||
const [repoListExpanded, setRepoListExpanded] = useState(false);
|
||||
const numOfReposToShow = repoListExpanded ? queryResult.analysisSummaries.length : numOfReposInContractedMode;
|
||||
|
||||
@@ -116,13 +211,17 @@ const Summary = (queryResult: RemoteQueryResult) => {
|
||||
{
|
||||
queryResult.affectedRepositoryCount === 0
|
||||
? <SummaryTitleNoResults />
|
||||
: <SummaryTitleWithResults {...queryResult} />
|
||||
: <SummaryTitleWithResults
|
||||
queryResult={queryResult}
|
||||
analysesResults={analysesResults} />
|
||||
}
|
||||
|
||||
<ul className="vscode-codeql__analysis-summaries-list">
|
||||
<ul className="vscode-codeql__flat-list">
|
||||
{queryResult.analysisSummaries.slice(0, numOfReposToShow).map((summary, i) =>
|
||||
<li key={summary.nwo} className="vscode-codeql__analysis-summaries-list-item">
|
||||
<SummaryItem {...summary} />
|
||||
<SummaryItem
|
||||
analysisSummary={summary}
|
||||
analysisResults={analysesResults.find(a => a.nwo === summary.nwo)} />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
@@ -138,26 +237,79 @@ const Summary = (queryResult: RemoteQueryResult) => {
|
||||
|
||||
const AnalysesResultsTitle = ({ totalAnalysesResults, totalResults }: { totalAnalysesResults: number, totalResults: number }) => {
|
||||
if (totalAnalysesResults === totalResults) {
|
||||
return <SectionTitle text={`${totalAnalysesResults} results`} />;
|
||||
return <SectionTitle>{totalAnalysesResults} results</SectionTitle>;
|
||||
}
|
||||
|
||||
return <SectionTitle text={`${totalAnalysesResults}/${totalResults} results`} />;
|
||||
return <SectionTitle>{totalAnalysesResults}/{totalResults} results</SectionTitle>;
|
||||
};
|
||||
|
||||
const AnalysesResultsDescription = ({ totalAnalysesResults, totalResults }: { totalAnalysesResults: number, totalResults: number }) => {
|
||||
if (totalAnalysesResults < totalResults) {
|
||||
return <>
|
||||
<VerticalSpace />
|
||||
Some results haven't been downloaded automatically because of their size or because enough were downloaded already.
|
||||
Download them manually from the list above if you want to see them here.
|
||||
</>;
|
||||
}
|
||||
const AnalysesResultsDescription = ({
|
||||
queryResult,
|
||||
analysesResults,
|
||||
}: {
|
||||
queryResult: RemoteQueryResult
|
||||
analysesResults: AnalysisResults[],
|
||||
}) => {
|
||||
const showDownloadsMessage = queryResult.analysisSummaries.some(
|
||||
s => !analysesResults.some(a => a.nwo === s.nwo && a.status === 'Completed'));
|
||||
const downloadsMessage = <>
|
||||
<VerticalSpace size={1} />
|
||||
Some results haven't been downloaded automatically because of their size or because enough were downloaded already.
|
||||
Download them manually from the list above if you want to see them here.
|
||||
</>;
|
||||
|
||||
return <></>;
|
||||
const showMaxResultsMessage = analysesResults.some(a => a.rawResults?.capped);
|
||||
const maxRawResultsMessage = <>
|
||||
<VerticalSpace size={1} />
|
||||
Some repositories have more than {MAX_RAW_RESULTS} results. We will only show you up to
|
||||
{MAX_RAW_RESULTS} results for each repository.
|
||||
</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showDownloadsMessage && downloadsMessage}
|
||||
{showMaxResultsMessage && maxRawResultsMessage}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: AnalysisResults[], totalResults: number }) => {
|
||||
const totalAnalysesResults = analysesResults.reduce((acc, curr) => acc + curr.results.length, 0);
|
||||
const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
|
||||
const numOfResults = getAnalysisResultCount(analysisResults);
|
||||
const title = <>
|
||||
{analysisResults.nwo}
|
||||
<Badge text={numOfResults.toString()} />
|
||||
</>;
|
||||
|
||||
return (
|
||||
<CollapsibleItem title={title}>
|
||||
<ul className="vscode-codeql__flat-list" >
|
||||
{analysisResults.interpretedResults.map((r, i) =>
|
||||
<li key={i}>
|
||||
<AnalysisAlertResult alert={r} />
|
||||
<VerticalSpace size={2} />
|
||||
</li>)}
|
||||
</ul>
|
||||
{analysisResults.rawResults &&
|
||||
<RawResultsTable
|
||||
schema={analysisResults.rawResults.schema}
|
||||
results={analysisResults.rawResults.resultSet}
|
||||
fileLinkPrefix={analysisResults.rawResults.fileLinkPrefix} />
|
||||
}
|
||||
</CollapsibleItem>
|
||||
);
|
||||
};
|
||||
|
||||
const AnalysesResults = ({
|
||||
queryResult,
|
||||
analysesResults,
|
||||
totalResults
|
||||
}: {
|
||||
queryResult: RemoteQueryResult,
|
||||
analysesResults: AnalysisResults[],
|
||||
totalResults: number
|
||||
}) => {
|
||||
const totalAnalysesResults = sumAnalysesResults(analysesResults);
|
||||
const [filterValue, setFilterValue] = React.useState('');
|
||||
|
||||
if (totalResults === 0) {
|
||||
return <></>;
|
||||
@@ -165,14 +317,28 @@ const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: A
|
||||
|
||||
return (
|
||||
<>
|
||||
<VerticalSpace />
|
||||
<VerticalSpace />
|
||||
<VerticalSpace size={2} />
|
||||
<AnalysesResultsTitle
|
||||
totalAnalysesResults={totalAnalysesResults}
|
||||
totalResults={totalResults} />
|
||||
<AnalysesResultsDescription
|
||||
totalAnalysesResults={totalAnalysesResults}
|
||||
totalResults={totalResults} />
|
||||
queryResult={queryResult}
|
||||
analysesResults={analysesResults} />
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<RepositoriesSearch
|
||||
filterValue={filterValue}
|
||||
setFilterValue={setFilterValue} />
|
||||
|
||||
<ul className="vscode-codeql__flat-list">
|
||||
{analysesResults
|
||||
.filter(a => a.interpretedResults.length > 0 || a.rawResults)
|
||||
.filter(a => a.nwo.toLowerCase().includes(filterValue.toLowerCase()))
|
||||
.map(r =>
|
||||
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
|
||||
<RepoAnalysisResults {...r} />
|
||||
</li>)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -203,12 +369,20 @@ export function RemoteQueries(): JSX.Element {
|
||||
}
|
||||
|
||||
try {
|
||||
return <div>
|
||||
<ViewTitle title={queryResult.queryTitle} />
|
||||
<QueryInfo {...queryResult} />
|
||||
<Summary {...queryResult} />
|
||||
<AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />
|
||||
</div>;
|
||||
return (
|
||||
<div className="vscode-codeql__remote-queries">
|
||||
<ThemeProvider colorMode="auto">
|
||||
<ViewTitle>{queryResult.queryTitle}</ViewTitle>
|
||||
<QueryInfo {...queryResult} />
|
||||
<Failures {...queryResult} />
|
||||
<Summary queryResult={queryResult} analysesResults={analysesResults} />
|
||||
<AnalysesResults
|
||||
queryResult={queryResult}
|
||||
analysesResults={analysesResults}
|
||||
totalResults={queryResult.totalResultCount} />
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return <div>There was an error displaying the view.</div>;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { TextInput } from '@primer/react';
|
||||
import { SearchIcon } from '@primer/octicons-react';
|
||||
|
||||
interface RepositoriesSearchProps {
|
||||
filterValue: string;
|
||||
setFilterValue: (value: string) => void;
|
||||
}
|
||||
|
||||
const RepositoriesSearch = ({ filterValue, setFilterValue }: RepositoriesSearchProps) => {
|
||||
return <>
|
||||
<TextInput
|
||||
block
|
||||
sx={{
|
||||
backgroundColor: 'var(--vscode-editor-background);',
|
||||
color: 'var(--vscode-editor-foreground);',
|
||||
width: 'calc(100% - 14px)',
|
||||
}}
|
||||
leadingVisual={SearchIcon}
|
||||
aria-label="Repository search"
|
||||
name="repository-search"
|
||||
placeholder="Filter by repository owner/name"
|
||||
value={filterValue}
|
||||
onChange={(e: ChangeEvent) => setFilterValue((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default RepositoriesSearch;
|
||||
@@ -1,7 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SectionTitle = ({ text }: { text: string }) => (
|
||||
<h2 className="vscode-codeql__section-title">{text}</h2>
|
||||
);
|
||||
const SectionTitle = styled.h2`
|
||||
font-size: medium;
|
||||
font-weight: 500;
|
||||
padding: 0 0.5em 0 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
export default SectionTitle;
|
||||
|
||||
30
extensions/ql-vscode/src/remote-queries/view/TextButton.tsx
Normal file
30
extensions/ql-vscode/src/remote-queries/view/TextButton.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Size = 'x-small' | 'small' | 'medium' | 'large' | 'x-large';
|
||||
|
||||
const StyledButton = styled.button<{ size: Size }>`
|
||||
background: none;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: ${props => props.size};
|
||||
`;
|
||||
|
||||
const TextButton = ({
|
||||
size,
|
||||
onClick,
|
||||
children
|
||||
}: {
|
||||
size: Size,
|
||||
onClick: () => void,
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<StyledButton
|
||||
size={size}
|
||||
onClick={onClick}>
|
||||
{children}
|
||||
</StyledButton>
|
||||
);
|
||||
|
||||
export default TextButton;
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const VerticalSpace = () => (
|
||||
<div className="vscode-codeql__vertical-space" />
|
||||
);
|
||||
const VerticalSpace = styled.div<{ size: 1 | 2 | 3 }>`
|
||||
flex: 0 0 auto;
|
||||
height: ${props => 0.5 * props.size}em;
|
||||
`;
|
||||
|
||||
export default VerticalSpace;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ViewTitle = ({ title }: { title: string }) => (
|
||||
<h1 className="vscode-codeql__view-title">{title}</h1>
|
||||
);
|
||||
const ViewTitle = styled.h1`
|
||||
font-size: large;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export default ViewTitle;
|
||||
|
||||
@@ -2,74 +2,3 @@ body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
|
||||
sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* SectionTitle component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__section-title {
|
||||
font-size: medium;
|
||||
font-weight: 500;
|
||||
padding: 0 0.5em 0 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ViewTitle component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__view-title {
|
||||
font-size: large;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* VerticalSpace component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__vertical-space {
|
||||
flex: 0 0 auto;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Badge component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__badge-container {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding-left: 0.2em;
|
||||
}
|
||||
|
||||
.vscode-codeql__badge {
|
||||
display: inline-block;
|
||||
min-width: 1.5em;
|
||||
padding: 0.3em;
|
||||
border-radius: 35%;
|
||||
font-size: x-small;
|
||||
text-align: center;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
border-color: var(--vscode-badge-background);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* DownloadButton component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__download-button {
|
||||
display: inline-block;
|
||||
font-size: x-small;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.vscode-codeql__download-button svg {
|
||||
fill: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
.octicon {
|
||||
fill: var(--vscode-editor-foreground);
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
.vscode-codeql__remote-queries {
|
||||
max-width: 55em;
|
||||
}
|
||||
|
||||
.octicon-light {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vscode-codeql__query-file {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.vscode-codeql__query-file-link {
|
||||
.vscode-codeql__query-info-link {
|
||||
text-decoration: none;
|
||||
padding-left: 0.3em;
|
||||
padding-right: 1em;
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
.vscode-codeql__query-file-link:hover {
|
||||
.vscode-codeql__query-info-link:hover {
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
@@ -28,16 +16,14 @@
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
.vscode-codeql__analysis-summaries-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0.5em 0 0 0;
|
||||
}
|
||||
|
||||
.vscode-codeql__analysis-summaries-list-item {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.vscode-codeql__analyses-results-list-item {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
.vscode-codeql__analysis-item {
|
||||
padding-right: 0.1em;
|
||||
}
|
||||
@@ -50,3 +36,16 @@
|
||||
padding-top: 1em;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.vscode-codeql__analysis-failure {
|
||||
margin: 0;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
||||
Liberation Mono, monospace;
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
.vscode-codeql__flat-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0.5em 0 0 0;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": ["es6", "dom"],
|
||||
"lib": ["ES2021", "dom"],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"rootDir": "..",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import * as path from 'path';
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
CancellationToken,
|
||||
ConfigurationTarget,
|
||||
@@ -9,43 +10,50 @@ import {
|
||||
TextDocument,
|
||||
TextEditor,
|
||||
Uri,
|
||||
window,
|
||||
workspace
|
||||
window
|
||||
} from 'vscode';
|
||||
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
|
||||
|
||||
import * as cli from './cli';
|
||||
import * as config from './config';
|
||||
import { DatabaseItem } from './databases';
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage, tryGetQueryMetadata } from './helpers';
|
||||
import { DatabaseItem, DatabaseManager } from './databases';
|
||||
import {
|
||||
createTimestampFile,
|
||||
getOnDiskWorkspaceFolders,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogWarningMessage,
|
||||
tryGetQueryMetadata,
|
||||
upgradesTmpDir
|
||||
} from './helpers';
|
||||
import { ProgressCallback, UserCancellationException } from './commandRunner';
|
||||
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './pure/interface-types';
|
||||
import { DatabaseInfo, QueryMetadata } from './pure/interface-types';
|
||||
import { logger } from './logging';
|
||||
import * as messages from './pure/messages';
|
||||
import { QueryHistoryItemOptions } from './query-history';
|
||||
import { InitialQueryInfo, LocalQueryInfo } from './query-results';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { isQuickQueryPath } from './quick-query';
|
||||
import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades';
|
||||
import { ensureMetadataIsComplete } from './query-results';
|
||||
import { SELECT_QUERY_NAME } from './contextual/locationFinder';
|
||||
import { DecodedBqrsChunk } from './pure/bqrs-cli-types';
|
||||
import { getErrorMessage } from './pure/helpers-pure';
|
||||
|
||||
/**
|
||||
* run-queries.ts
|
||||
* -------------
|
||||
* --------------
|
||||
*
|
||||
* Compiling and running QL queries.
|
||||
*/
|
||||
|
||||
// XXX: Tmp directory should be configuarble.
|
||||
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
|
||||
export const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
|
||||
export const tmpDirDisposal = {
|
||||
dispose: () => {
|
||||
upgradesTmpDir.removeCallback();
|
||||
tmpDir.removeCallback();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Information about which query will be to be run. `quickEvalPosition` and `quickEvalText`
|
||||
* is only filled in if the query is a quick query.
|
||||
*/
|
||||
interface SelectedQuery {
|
||||
queryPath: string;
|
||||
quickEvalPosition?: messages.Position;
|
||||
quickEvalText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of evaluation-time information about a query,
|
||||
@@ -53,48 +61,96 @@ export const tmpDirDisposal = {
|
||||
* temporary files associated with it, such as the compiled query
|
||||
* output and results.
|
||||
*/
|
||||
export class QueryInfo {
|
||||
private static nextQueryId = 0;
|
||||
|
||||
readonly compiledQueryPath: string;
|
||||
readonly dilPath: string;
|
||||
readonly csvPath: string;
|
||||
readonly resultsPaths: ResultsPaths;
|
||||
readonly dataset: Uri; // guarantee the existence of a well-defined dataset dir at this point
|
||||
readonly queryID: number;
|
||||
export class QueryEvaluationInfo {
|
||||
|
||||
/**
|
||||
* Note that in the {@link FullQueryInfo.slurp} method, we create a QueryEvaluationInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(
|
||||
public readonly program: messages.QlProgram,
|
||||
public readonly dbItem: DatabaseItem,
|
||||
public readonly querySaveDir: string,
|
||||
public readonly dbItemPath: string,
|
||||
private readonly databaseHasMetadataFile: boolean,
|
||||
public readonly queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
|
||||
public readonly quickEvalPosition?: messages.Position,
|
||||
public readonly metadata?: QueryMetadata,
|
||||
public readonly templates?: messages.TemplateDefinitions,
|
||||
public readonly templates?: messages.TemplateDefinitions
|
||||
) {
|
||||
this.queryID = QueryInfo.nextQueryId++;
|
||||
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
|
||||
this.dilPath = path.join(tmpDir.name, `results${this.queryID}.dil`);
|
||||
this.csvPath = path.join(tmpDir.name, `results${this.queryID}.csv`);
|
||||
this.resultsPaths = {
|
||||
resultsPath: path.join(tmpDir.name, `results${this.queryID}.bqrs`),
|
||||
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryID}.sarif`)
|
||||
/**/
|
||||
}
|
||||
|
||||
get dilPath() {
|
||||
return path.join(this.querySaveDir, 'results.dil');
|
||||
}
|
||||
|
||||
get csvPath() {
|
||||
return path.join(this.querySaveDir, 'results.csv');
|
||||
}
|
||||
|
||||
get compiledQueryPath() {
|
||||
return path.join(this.querySaveDir, 'compiledQuery.qlo');
|
||||
}
|
||||
|
||||
get logPath() {
|
||||
return qsClient.findQueryLogFile(this.querySaveDir);
|
||||
}
|
||||
|
||||
get evalLogPath() {
|
||||
return qsClient.findQueryEvalLogFile(this.querySaveDir);
|
||||
}
|
||||
|
||||
get evalLogSummaryPath() {
|
||||
return qsClient.findQueryEvalLogSummaryFile(this.querySaveDir);
|
||||
}
|
||||
|
||||
get evalLogEndSummaryPath() {
|
||||
return qsClient.findQueryEvalLogEndSummaryFile(this.querySaveDir);
|
||||
}
|
||||
|
||||
get resultsPaths() {
|
||||
return {
|
||||
resultsPath: path.join(this.querySaveDir, 'results.bqrs'),
|
||||
interpretedResultsPath: path.join(this.querySaveDir,
|
||||
this.metadata?.kind === 'graph'
|
||||
? 'graphResults'
|
||||
: 'interpretedResults.sarif'
|
||||
),
|
||||
};
|
||||
if (dbItem.contents === undefined) {
|
||||
throw new Error('Can\'t run query on invalid database.');
|
||||
}
|
||||
this.dataset = dbItem.contents.datasetUri;
|
||||
}
|
||||
|
||||
getSortedResultSetPath(resultSetName: string) {
|
||||
return path.join(this.querySaveDir, `sortedResults-${resultSetName}.bqrs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file in the query directory that indicates when this query was created.
|
||||
* This is important for keeping track of when queries should be removed.
|
||||
*/
|
||||
async createTimestampFile() {
|
||||
await createTimestampFile(this.querySaveDir);
|
||||
}
|
||||
|
||||
async run(
|
||||
qs: qsClient.QueryServerClient,
|
||||
upgradeQlo: string | undefined,
|
||||
availableMlModels: cli.MlModelInfo[],
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
queryInfo?: LocalQueryInfo,
|
||||
): Promise<messages.EvaluationResult> {
|
||||
if (!dbItem.contents || dbItem.error) {
|
||||
throw new Error('Can\'t run query on invalid database.');
|
||||
}
|
||||
|
||||
let result: messages.EvaluationResult | null = null;
|
||||
|
||||
const callbackId = qs.registerCallback(res => { result = res; });
|
||||
const callbackId = qs.registerCallback(res => {
|
||||
result = {
|
||||
...res,
|
||||
logFileLocation: this.logPath
|
||||
};
|
||||
});
|
||||
|
||||
const availableMlModelUris: messages.MlModel[] = availableMlModels.map(model => ({ uri: Uri.file(model.path).toString(true) }));
|
||||
|
||||
@@ -108,10 +164,18 @@ export class QueryInfo {
|
||||
id: callbackId,
|
||||
timeoutSecs: qs.config.timeoutSecs,
|
||||
};
|
||||
|
||||
const dataset: messages.Dataset = {
|
||||
dbDir: this.dataset.fsPath,
|
||||
dbDir: dbItem.contents.datasetUri.fsPath,
|
||||
workingSet: 'default'
|
||||
};
|
||||
if (queryInfo && await qs.cliServer.cliConstraints.supportsPerQueryEvalLog()) {
|
||||
await qs.sendRequest(messages.startLog, {
|
||||
db: dataset,
|
||||
logPath: this.evalLogPath,
|
||||
});
|
||||
|
||||
}
|
||||
const params: messages.EvaluateQueriesParams = {
|
||||
db: dataset,
|
||||
evaluateId: callbackId,
|
||||
@@ -121,8 +185,33 @@ export class QueryInfo {
|
||||
};
|
||||
try {
|
||||
await qs.sendRequest(messages.runQueries, params, token, progress);
|
||||
if (qs.config.customLogDirectory) {
|
||||
void showAndLogWarningMessage(
|
||||
`Custom log directories are no longer supported. The "codeQL.runningQueries.customLogDirectory" setting is deprecated. Unset the setting to stop seeing this message. Query logs saved to ${this.logPath}.`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
qs.unRegisterCallback(callbackId);
|
||||
if (queryInfo && await qs.cliServer.cliConstraints.supportsPerQueryEvalLog()) {
|
||||
await qs.sendRequest(messages.endLog, {
|
||||
db: dataset,
|
||||
logPath: this.evalLogPath,
|
||||
});
|
||||
if (await this.hasEvalLog()) {
|
||||
queryInfo.evalLogLocation = this.evalLogPath;
|
||||
await qs.cliServer.generateLogSummary(this.evalLogPath, this.evalLogSummaryPath, this.evalLogEndSummaryPath);
|
||||
queryInfo.evalLogSummaryLocation = this.evalLogSummaryPath;
|
||||
fs.readFile(this.evalLogEndSummaryPath, (err, buffer) => {
|
||||
if (err) {
|
||||
throw new Error(`Could not read structured evaluator log end of summary file at ${this.evalLogEndSummaryPath}.`);
|
||||
}
|
||||
void qs.logger.log(' --- Evaluator Log Summary --- ');
|
||||
void qs.logger.log(buffer.toString());
|
||||
});
|
||||
} else {
|
||||
void showAndLogWarningMessage(`Failed to write structured evaluator log to ${this.evalLogPath}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result || {
|
||||
evaluationTime: 0,
|
||||
@@ -135,6 +224,7 @@ export class QueryInfo {
|
||||
|
||||
async compile(
|
||||
qs: qsClient.QueryServerClient,
|
||||
program: messages.QlProgram,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<messages.CompilationMessage[]> {
|
||||
@@ -157,14 +247,14 @@ export class QueryInfo {
|
||||
extraOptions: {
|
||||
timeoutSecs: qs.config.timeoutSecs
|
||||
},
|
||||
queryToCheck: this.program,
|
||||
queryToCheck: program,
|
||||
resultPath: this.compiledQueryPath,
|
||||
target,
|
||||
};
|
||||
|
||||
compiled = await qs.sendRequest(messages.compileQuery, params, token, progress);
|
||||
} finally {
|
||||
void qs.logger.log(' - - - COMPILATION DONE - - - ');
|
||||
void qs.logger.log(' - - - COMPILATION DONE - - - ', { additionalLogLocation: this.logPath });
|
||||
}
|
||||
return (compiled?.messages || []).filter(msg => msg.severity === messages.Severity.ERROR);
|
||||
}
|
||||
@@ -172,20 +262,27 @@ export class QueryInfo {
|
||||
/**
|
||||
* Holds if this query can in principle produce interpreted results.
|
||||
*/
|
||||
async canHaveInterpretedResults(): Promise<boolean> {
|
||||
const hasMetadataFile = await this.dbItem.hasMetadataFile();
|
||||
if (!hasMetadataFile) {
|
||||
canHaveInterpretedResults(): boolean {
|
||||
if (!this.databaseHasMetadataFile) {
|
||||
void logger.log('Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasKind = !!this.metadata?.kind;
|
||||
const kind = this.metadata?.kind;
|
||||
const hasKind = !!kind;
|
||||
if (!hasKind) {
|
||||
void logger.log('Cannot produce interpreted results since the query does not have @kind metadata.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTable = hasKind && this.metadata?.kind === 'table';
|
||||
// Graph queries only return interpreted results if we are in canary mode.
|
||||
if (kind === 'graph') {
|
||||
return config.isCanary();
|
||||
}
|
||||
|
||||
return hasMetadataFile && hasKind && !isTable;
|
||||
// table is the default query kind. It does not produce interpreted results.
|
||||
// any query kind that is not table can, in principle, produce interpreted results.
|
||||
return kind !== 'table';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,6 +306,10 @@ export class QueryInfo {
|
||||
return fs.pathExists(this.csvPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the DIL file produced by this query. If the query has not yet produced DIL,
|
||||
* this will return first create the DIL file and then return the path to the DIL file.
|
||||
*/
|
||||
async ensureDilPath(qs: qsClient.QueryServerClient): Promise<string> {
|
||||
if (await this.hasDil()) {
|
||||
return this.dilPath;
|
||||
@@ -224,6 +325,17 @@ export class QueryInfo {
|
||||
return this.dilPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this query already has a completed structured evaluator log
|
||||
*/
|
||||
async hasEvalLog(): Promise<boolean> {
|
||||
return fs.pathExists(this.evalLogPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the CSV file containing the results of this query. This will only be called if the query
|
||||
* does not have interpreted results and the CSV file does not already exist.
|
||||
*/
|
||||
async exportCsvResults(qs: qsClient.QueryServerClient, csvPath: string, onFinish: () => void): Promise<void> {
|
||||
let stopDecoding = false;
|
||||
const out = fs.createWriteStream(csvPath);
|
||||
@@ -240,23 +352,35 @@ export class QueryInfo {
|
||||
pageSize: 100,
|
||||
offset: nextOffset,
|
||||
});
|
||||
for (const tuple of chunk.tuples)
|
||||
for (const tuple of chunk.tuples) {
|
||||
out.write(tuple.join(',') + '\n');
|
||||
}
|
||||
nextOffset = chunk.next;
|
||||
}
|
||||
out.end();
|
||||
}
|
||||
|
||||
async ensureCsvProduced(qs: qsClient.QueryServerClient): Promise<string> {
|
||||
/**
|
||||
* Returns the path to the CSV alerts interpretation of this query results. If CSV results have
|
||||
* not yet been produced, this will return first create the CSV results and then return the path.
|
||||
*
|
||||
* This method only works for queries with interpreted results.
|
||||
*/
|
||||
async ensureCsvAlerts(qs: qsClient.QueryServerClient, dbm: DatabaseManager): Promise<string> {
|
||||
if (await this.hasCsv()) {
|
||||
return this.csvPath;
|
||||
}
|
||||
|
||||
const dbItem = dbm.findDatabaseItem(Uri.file(this.dbItemPath));
|
||||
if (!dbItem) {
|
||||
throw new Error(`Cannot produce CSV results because database is missing. ${this.dbItemPath}`);
|
||||
}
|
||||
|
||||
let sourceInfo;
|
||||
if (this.dbItem.sourceArchive !== undefined) {
|
||||
if (dbItem.sourceArchive !== undefined) {
|
||||
sourceInfo = {
|
||||
sourceArchive: this.dbItem.sourceArchive.fsPath,
|
||||
sourceLocationPrefix: await this.dbItem.getSourceLocationPrefix(
|
||||
sourceArchive: dbItem.sourceArchive.fsPath,
|
||||
sourceLocationPrefix: await dbItem.getSourceLocationPrefix(
|
||||
qs.cliServer
|
||||
),
|
||||
};
|
||||
@@ -265,14 +389,18 @@ export class QueryInfo {
|
||||
await qs.cliServer.generateResultsCsv(ensureMetadataIsComplete(this.metadata), this.resultsPaths.resultsPath, this.csvPath, sourceInfo);
|
||||
return this.csvPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans this query's results directory.
|
||||
*/
|
||||
async deleteQuery(): Promise<void> {
|
||||
await fs.remove(this.querySaveDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface QueryWithResults {
|
||||
readonly query: QueryInfo;
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly result: messages.EvaluationResult;
|
||||
readonly database: DatabaseInfo;
|
||||
readonly options: QueryHistoryItemOptions;
|
||||
readonly logFileLocation?: string;
|
||||
readonly dispose: () => void;
|
||||
}
|
||||
@@ -301,9 +429,7 @@ export async function clearCacheInDatabase(
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param filePath This needs to be equivalent to java Path.toRealPath(NO_FOLLOW_LINKS)
|
||||
*
|
||||
* @param filePath This needs to be equivalent to Java's `Path.toRealPath(NO_FOLLOW_LINKS)`
|
||||
*/
|
||||
async function convertToQlPath(filePath: string): Promise<string> {
|
||||
if (process.platform === 'win32') {
|
||||
@@ -349,40 +475,40 @@ async function getSelectedPosition(editor: TextEditor, range?: Range): Promise<m
|
||||
|
||||
/**
|
||||
* Compare the dbscheme implied by the query `query` and that of the current database.
|
||||
* If they are compatible, do nothing.
|
||||
* If they are incompatible but the database can be upgraded, suggest that upgrade.
|
||||
* If they are incompatible and the database cannot be upgraded, throw an error.
|
||||
* - If they are compatible, do nothing.
|
||||
* - If they are incompatible but the database can be upgraded, suggest that upgrade.
|
||||
* - If they are incompatible and the database cannot be upgraded, throw an error.
|
||||
*/
|
||||
async function checkDbschemeCompatibility(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
qs: qsClient.QueryServerClient,
|
||||
query: QueryInfo,
|
||||
query: QueryEvaluationInfo,
|
||||
qlProgram: messages.QlProgram,
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
const searchPath = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) {
|
||||
const { finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath, false);
|
||||
if (dbItem.contents?.dbSchemeUri !== undefined) {
|
||||
const { finalDbscheme } = await cliServer.resolveUpgrades(dbItem.contents.dbSchemeUri.fsPath, searchPath, false);
|
||||
const hash = async function(filename: string): Promise<string> {
|
||||
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
|
||||
};
|
||||
|
||||
// At this point, we have learned about three dbschemes:
|
||||
|
||||
// query.program.dbschemePath is the dbscheme of the actual
|
||||
// database we're querying.
|
||||
const dbschemeOfDb = await hash(query.program.dbschemePath);
|
||||
// the dbscheme of the actual database we're querying.
|
||||
const dbschemeOfDb = await hash(dbItem.contents.dbSchemeUri.fsPath);
|
||||
|
||||
// query.queryDbScheme is the dbscheme of the query we're
|
||||
// running, including the library we've resolved it to use.
|
||||
// the dbscheme of the query we're running, including the library we've resolved it to use.
|
||||
const dbschemeOfLib = await hash(query.queryDbscheme);
|
||||
|
||||
// info.finalDbscheme is which database we're able to upgrade to
|
||||
// the database we're able to upgrade to
|
||||
const upgradableTo = await hash(finalDbscheme);
|
||||
|
||||
if (upgradableTo != dbschemeOfLib) {
|
||||
reportNoUpgradePath(query);
|
||||
reportNoUpgradePath(qlProgram, query);
|
||||
}
|
||||
|
||||
if (upgradableTo == dbschemeOfLib &&
|
||||
@@ -390,7 +516,7 @@ async function checkDbschemeCompatibility(
|
||||
// Try to upgrade the database
|
||||
await upgradeDatabaseExplicit(
|
||||
qs,
|
||||
query.dbItem,
|
||||
dbItem,
|
||||
progress,
|
||||
token
|
||||
);
|
||||
@@ -398,8 +524,10 @@ async function checkDbschemeCompatibility(
|
||||
}
|
||||
}
|
||||
|
||||
function reportNoUpgradePath(query: QueryInfo) {
|
||||
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`);
|
||||
function reportNoUpgradePath(qlProgram: messages.QlProgram, query: QueryEvaluationInfo): void {
|
||||
throw new Error(
|
||||
`Query ${qlProgram.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,30 +536,42 @@ function reportNoUpgradePath(query: QueryInfo) {
|
||||
async function compileNonDestructiveUpgrade(
|
||||
qs: qsClient.QueryServerClient,
|
||||
upgradeTemp: tmp.DirectoryResult,
|
||||
query: QueryInfo,
|
||||
query: QueryEvaluationInfo,
|
||||
qlProgram: messages.QlProgram,
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<string> {
|
||||
const searchPath = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (!query.dbItem?.contents?.dbSchemeUri) {
|
||||
if (!dbItem?.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
const { scripts, matchesTarget } = await qs.cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath, true, query.queryDbscheme);
|
||||
|
||||
// When packaging is used, dependencies may exist outside of the workspace and they are always on the resolved search path.
|
||||
// When packaging is not used, all dependencies are in the workspace.
|
||||
const upgradesPath = (await qs.cliServer.cliConstraints.supportsPackaging())
|
||||
? qlProgram.libraryPath
|
||||
: getOnDiskWorkspaceFolders();
|
||||
|
||||
const { scripts, matchesTarget } = await qs.cliServer.resolveUpgrades(
|
||||
dbItem.contents.dbSchemeUri.fsPath,
|
||||
upgradesPath,
|
||||
true,
|
||||
query.queryDbscheme
|
||||
);
|
||||
|
||||
if (!matchesTarget) {
|
||||
reportNoUpgradePath(query);
|
||||
reportNoUpgradePath(qlProgram, query);
|
||||
}
|
||||
const result = await compileDatabaseUpgradeSequence(qs, query.dbItem, scripts, upgradeTemp, progress, token);
|
||||
const result = await compileDatabaseUpgradeSequence(qs, dbItem, scripts, upgradeTemp, progress, token);
|
||||
if (result.compiledUpgrade === undefined) {
|
||||
const error = result.error || '[no error message available]';
|
||||
throw new Error(error);
|
||||
}
|
||||
// We can upgrade to the actual target
|
||||
query.program.dbschemePath = query.queryDbscheme;
|
||||
qlProgram.dbschemePath = query.queryDbscheme;
|
||||
// We are new enough that we will always support single file upgrades.
|
||||
return result.compiledUpgrade;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -476,12 +616,6 @@ async function promptUserToSaveChanges(document: TextDocument): Promise<boolean>
|
||||
return false;
|
||||
}
|
||||
|
||||
type SelectedQuery = {
|
||||
queryPath: string;
|
||||
quickEvalPosition?: messages.Position;
|
||||
quickEvalText?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines which QL file to run during an invocation of `Run Query` or `Quick Evaluation`, as follows:
|
||||
* - If the command was called by clicking on a file, then use that file.
|
||||
@@ -492,12 +626,19 @@ type SelectedQuery = {
|
||||
* @param selectedResourceUri The selected resource when the command was run.
|
||||
* @param quickEval Whether the command being run is `Quick Evaluation`.
|
||||
*/
|
||||
export async function determineSelectedQuery(selectedResourceUri: Uri | undefined, quickEval: boolean, range?: Range): Promise<SelectedQuery> {
|
||||
export async function determineSelectedQuery(
|
||||
selectedResourceUri: Uri | undefined,
|
||||
quickEval: boolean,
|
||||
range?: Range
|
||||
): Promise<SelectedQuery> {
|
||||
const editor = window.activeTextEditor;
|
||||
|
||||
// Choose which QL file to use.
|
||||
let queryUri: Uri;
|
||||
if (selectedResourceUri === undefined) {
|
||||
if (selectedResourceUri) {
|
||||
// A resource was passed to the command handler, so use it.
|
||||
queryUri = selectedResourceUri;
|
||||
} else {
|
||||
// No resource was passed to the command handler, so obtain it from the active editor.
|
||||
// This usually happens when the command is called from the Command Palette.
|
||||
if (editor === undefined) {
|
||||
@@ -505,22 +646,18 @@ export async function determineSelectedQuery(selectedResourceUri: Uri | undefine
|
||||
} else {
|
||||
queryUri = editor.document.uri;
|
||||
}
|
||||
} else {
|
||||
// A resource was passed to the command handler, so use it.
|
||||
queryUri = selectedResourceUri;
|
||||
}
|
||||
|
||||
if (queryUri.scheme !== 'file') {
|
||||
throw new Error('Can only run queries that are on disk.');
|
||||
}
|
||||
const queryPath = queryUri.fsPath || '';
|
||||
const queryPath = queryUri.fsPath;
|
||||
|
||||
if (quickEval) {
|
||||
if (!(queryPath.endsWith('.ql') || queryPath.endsWith('.qll'))) {
|
||||
throw new Error('The selected resource is not a CodeQL file; It should have the extension ".ql" or ".qll".');
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (!(queryPath.endsWith('.ql'))) {
|
||||
throw new Error('The selected resource is not a CodeQL query file; It should have the extension ".ql".');
|
||||
}
|
||||
@@ -547,7 +684,13 @@ export async function determineSelectedQuery(selectedResourceUri: Uri | undefine
|
||||
throw new Error('The selected resource for quick evaluation should match the active editor.');
|
||||
}
|
||||
quickEvalPosition = await getSelectedPosition(editor, range);
|
||||
quickEvalText = editor.document.getText(editor.selection);
|
||||
if (!editor.selection?.isEmpty) {
|
||||
quickEvalText = editor.document.getText(editor.selection);
|
||||
} else {
|
||||
// capture the entire line if the user didn't select anything
|
||||
const line = editor.document.lineAt(editor.selection.active.line);
|
||||
quickEvalText = line.text.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return { queryPath, quickEvalPosition, quickEvalText };
|
||||
@@ -556,33 +699,22 @@ export async function determineSelectedQuery(selectedResourceUri: Uri | undefine
|
||||
export async function compileAndRunQueryAgainstDatabase(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
quickEval: boolean,
|
||||
selectedQueryUri: Uri | undefined,
|
||||
dbItem: DatabaseItem,
|
||||
initialInfo: InitialQueryInfo,
|
||||
queryStorageDir: string,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
templates?: messages.TemplateDefinitions,
|
||||
range?: Range
|
||||
queryInfo?: LocalQueryInfo, // May be omitted for queries not initiated by the user. If omitted we won't create a structured log for the query.
|
||||
): Promise<QueryWithResults> {
|
||||
if (!db.contents || !db.contents.dbSchemeUri) {
|
||||
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
|
||||
}
|
||||
|
||||
// Determine which query to run, based on the selection and the active editor.
|
||||
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval, range);
|
||||
|
||||
const historyItemOptions: QueryHistoryItemOptions = {};
|
||||
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
|
||||
if (quickEval) {
|
||||
historyItemOptions.queryText = quickEvalText;
|
||||
} else {
|
||||
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
|
||||
if (!dbItem.contents || !dbItem.contents.dbSchemeUri) {
|
||||
throw new Error(`Database ${dbItem.databaseUri} does not have a CodeQL database scheme.`);
|
||||
}
|
||||
|
||||
// Get the workspace folder paths.
|
||||
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
|
||||
// Figure out the library path for the query.
|
||||
const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, queryPath);
|
||||
const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, initialInfo.queryPath);
|
||||
|
||||
if (!packConfig.dbscheme) {
|
||||
throw new Error('Could not find a database scheme for this query. Please check that you have a valid qlpack.yml file for this query, which refers to a database scheme either in the `dbscheme` field or through one of its dependencies.');
|
||||
@@ -593,10 +725,10 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// won't trigger this check)
|
||||
// This test will produce confusing results if we ever change the name of the database schema files.
|
||||
const querySchemaName = path.basename(packConfig.dbscheme);
|
||||
const dbSchemaName = path.basename(db.contents.dbSchemeUri.fsPath);
|
||||
const dbSchemaName = path.basename(dbItem.contents.dbSchemeUri.fsPath);
|
||||
if (querySchemaName != dbSchemaName) {
|
||||
void logger.log(`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`);
|
||||
throw new Error(`The query ${path.basename(queryPath)} cannot be run against the selected database (${db.name}): their target languages are different. Please select a different database and try again.`);
|
||||
throw new Error(`The query ${path.basename(initialInfo.queryPath)} cannot be run against the selected database (${dbItem.name}): their target languages are different. Please select a different database and try again.`);
|
||||
}
|
||||
|
||||
const qlProgram: messages.QlProgram = {
|
||||
@@ -607,21 +739,15 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// Since we are compiling and running a query against a database,
|
||||
// we use the database's DB scheme here instead of the DB scheme
|
||||
// from the current document's project.
|
||||
dbschemePath: db.contents.dbSchemeUri.fsPath,
|
||||
queryPath: queryPath
|
||||
dbschemePath: dbItem.contents.dbSchemeUri.fsPath,
|
||||
queryPath: initialInfo.queryPath
|
||||
};
|
||||
|
||||
// Read the query metadata if possible, to use in the UI.
|
||||
const metadata = await tryGetQueryMetadata(cliServer, qlProgram.queryPath);
|
||||
|
||||
let availableMlModels: cli.MlModelInfo[] = [];
|
||||
// The `capabilities.untrustedWorkspaces.restrictedConfigurations` entry in package.json doesn't
|
||||
// work with hidden settings, so we manually check that the workspace is trusted before looking at
|
||||
// whether the `shouldInsecurelyLoadMlModelsFromPacks` setting is enabled.
|
||||
if (workspace.isTrusted &&
|
||||
config.isCanary() &&
|
||||
config.shouldInsecurelyLoadMlModelsFromPacks() &&
|
||||
await cliServer.cliConstraints.supportsResolveMlModels()) {
|
||||
if (await cliServer.cliConstraints.supportsResolveMlModels()) {
|
||||
try {
|
||||
availableMlModels = (await cliServer.resolveMlModels(diskWorkspaceFolders)).models;
|
||||
void logger.log(`Found available ML models at the following paths: ${availableMlModels.map(x => `'${x.path}'`).join(', ')}.`);
|
||||
@@ -632,29 +758,40 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
|
||||
const hasMetadataFile = (await dbItem.hasMetadataFile());
|
||||
const query = new QueryEvaluationInfo(
|
||||
path.join(queryStorageDir, initialInfo.id),
|
||||
dbItem.databaseUri.fsPath,
|
||||
hasMetadataFile,
|
||||
packConfig.dbscheme,
|
||||
initialInfo.quickEvalPosition,
|
||||
metadata,
|
||||
templates
|
||||
);
|
||||
await query.createTimestampFile();
|
||||
|
||||
const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name, unsafeCleanup: true });
|
||||
let upgradeDir: tmp.DirectoryResult | undefined;
|
||||
try {
|
||||
let upgradeQlo;
|
||||
if (await hasNondestructiveUpgradeCapabilities(qs)) {
|
||||
upgradeQlo = await compileNonDestructiveUpgrade(qs, upgradeDir, query, progress, token);
|
||||
upgradeDir = await tmp.dir({ dir: upgradesTmpDir, unsafeCleanup: true });
|
||||
upgradeQlo = await compileNonDestructiveUpgrade(qs, upgradeDir, query, qlProgram, dbItem, progress, token);
|
||||
} else {
|
||||
await checkDbschemeCompatibility(cliServer, qs, query, progress, token);
|
||||
await checkDbschemeCompatibility(cliServer, qs, query, qlProgram, dbItem, progress, token);
|
||||
}
|
||||
let errors;
|
||||
try {
|
||||
errors = await query.compile(qs, progress, token);
|
||||
errors = await query.compile(qs, qlProgram, progress, token);
|
||||
} catch (e) {
|
||||
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
|
||||
return createSyntheticResult(query, 'Query cancelled', messages.QueryResultType.CANCELLATION);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
const result = await query.run(qs, upgradeQlo, availableMlModels, progress, token);
|
||||
const result = await query.run(qs, upgradeQlo, availableMlModels, dbItem, progress, token, queryInfo);
|
||||
if (result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
const message = result.message || 'Failed to run query';
|
||||
void logger.log(message);
|
||||
@@ -663,11 +800,6 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
return {
|
||||
query,
|
||||
result,
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
},
|
||||
options: historyItemOptions,
|
||||
logFileLocation: result.logFileLocation,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
|
||||
@@ -678,7 +810,10 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// so we include a general description of the problem,
|
||||
// and direct the user to the output window for the detailed compilation messages.
|
||||
// However we don't show quick eval errors there so we need to display them anyway.
|
||||
void qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
|
||||
void qs.logger.log(
|
||||
`Failed to compile query ${initialInfo.queryPath} against database scheme ${qlProgram.dbschemePath}:`,
|
||||
{ additionalLogLocation: query.logPath }
|
||||
);
|
||||
|
||||
const formattedMessages: string[] = [];
|
||||
|
||||
@@ -686,40 +821,79 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
const message = error.message || '[no error message available]';
|
||||
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
|
||||
formattedMessages.push(formatted);
|
||||
void qs.logger.log(formatted);
|
||||
void qs.logger.log(formatted, { additionalLogLocation: query.logPath });
|
||||
}
|
||||
if (quickEval && formattedMessages.length <= 2) {
|
||||
if (initialInfo.isQuickEval && formattedMessages.length <= 2) {
|
||||
// If there are more than 2 error messages, they will not be displayed well in a popup
|
||||
// and will be trimmed by the function displaying the error popup. Accordingly, we only
|
||||
// try to show the errors if there are 2 or less, otherwise we direct the user to the log.
|
||||
void showAndLogErrorMessage('Quick evaluation compilation failed: ' + formattedMessages.join('\n'));
|
||||
} else {
|
||||
void showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
|
||||
void showAndLogErrorMessage((initialInfo.isQuickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
|
||||
}
|
||||
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
|
||||
return createSyntheticResult(query, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await upgradeDir.cleanup();
|
||||
await upgradeDir?.cleanup();
|
||||
} catch (e) {
|
||||
void qs.logger.log(`Could not clean up the upgrades dir. Reason: ${e.message || e}`);
|
||||
void qs.logger.log(
|
||||
`Could not clean up the upgrades dir. Reason: ${getErrorMessage(e)}`,
|
||||
{ additionalLogLocation: query.logPath }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the initial information for a query. This is everything of interest
|
||||
* we know about this query that is available before it is run.
|
||||
*
|
||||
* @param selectedQueryUri The Uri of the document containing the query to be run.
|
||||
* @param databaseInfo The database to run the query against.
|
||||
* @param isQuickEval true if this is a quick evaluation.
|
||||
* @param range the selection range of the query to be run. Only used if isQuickEval is true.
|
||||
* @returns The initial information for the query to be run.
|
||||
*/
|
||||
export async function createInitialQueryInfo(
|
||||
selectedQueryUri: Uri | undefined,
|
||||
databaseInfo: DatabaseInfo,
|
||||
isQuickEval: boolean,
|
||||
range?: Range
|
||||
): Promise<InitialQueryInfo> {
|
||||
// Determine which query to run, based on the selection and the active editor.
|
||||
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, isQuickEval, range);
|
||||
|
||||
return {
|
||||
queryPath,
|
||||
isQuickEval,
|
||||
isQuickQuery: isQuickQueryPath(queryPath),
|
||||
databaseInfo,
|
||||
id: `${path.basename(queryPath)}-${nanoid()}`,
|
||||
start: new Date(),
|
||||
... (isQuickEval ? {
|
||||
queryText: quickEvalText!, // if this query is quick eval, it must have quick eval text
|
||||
quickEvalPosition: quickEvalPosition
|
||||
} : {
|
||||
queryText: await fs.readFile(queryPath, 'utf8')
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const compilationFailedErrorTail = ' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
|
||||
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
|
||||
' and choose CodeQL Query Server from the dropdown.';
|
||||
|
||||
/**
|
||||
* Create a synthetic result for a query that failed to compile.
|
||||
*/
|
||||
function createSyntheticResult(
|
||||
query: QueryInfo,
|
||||
db: DatabaseItem,
|
||||
historyItemOptions: QueryHistoryItemOptions,
|
||||
query: QueryEvaluationInfo,
|
||||
message: string,
|
||||
resultType: number
|
||||
): QueryWithResults {
|
||||
|
||||
return {
|
||||
query,
|
||||
result: {
|
||||
@@ -729,11 +903,6 @@ function createSyntheticResult(
|
||||
runId: -1,
|
||||
message
|
||||
},
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
},
|
||||
options: historyItemOptions,
|
||||
dispose: () => { /**/ },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,17 +4,18 @@ import { parser } from 'stream-json';
|
||||
import { pick } from 'stream-json/filters/Pick';
|
||||
import Assembler = require('stream-json/Assembler');
|
||||
import { chain } from 'stream-chain';
|
||||
import { getErrorMessage } from './pure/helpers-pure';
|
||||
|
||||
const DUMMY_TOOL : Sarif.Tool = {driver: {name: ''}};
|
||||
const DUMMY_TOOL: Sarif.Tool = { driver: { name: '' } };
|
||||
|
||||
export async function sarifParser(interpretedResultsPath: string) : Promise<Sarif.Log> {
|
||||
export async function sarifParser(interpretedResultsPath: string): Promise<Sarif.Log> {
|
||||
try {
|
||||
// Parse the SARIF file into token streams, filtering out only the results array.
|
||||
const p = parser();
|
||||
const pipeline = chain([
|
||||
fs.createReadStream(interpretedResultsPath),
|
||||
p,
|
||||
pick({filter: 'runs.0.results'})
|
||||
pick({ filter: 'runs.0.results' })
|
||||
]);
|
||||
|
||||
// Creates JavaScript objects from the token stream
|
||||
@@ -26,23 +27,23 @@ export async function sarifParser(interpretedResultsPath: string) : Promise<Sari
|
||||
pipeline.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
|
||||
asm.on('done', (asm) => {
|
||||
|
||||
const log : Sarif.Log = {
|
||||
version: '2.1.0',
|
||||
const log: Sarif.Log = {
|
||||
version: '2.1.0',
|
||||
runs: [
|
||||
{
|
||||
tool: DUMMY_TOOL,
|
||||
{
|
||||
tool: DUMMY_TOOL,
|
||||
results: asm.current ?? []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
resolve(log);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Parsing output of interpretation failed: ${err.stderr || err}`);
|
||||
} catch (e) {
|
||||
throw new Error(`Parsing output of interpretation failed: ${(e as any).stderr || getErrorMessage(e)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class QLTestAdapterFactory extends DisposableObject {
|
||||
* @param ext The new extension, including the `.`.
|
||||
*/
|
||||
function changeExtension(p: string, ext: string): string {
|
||||
return p.substr(0, p.length - path.extname(p).length) + ext;
|
||||
return p.slice(0, -path.extname(p).length) + ext;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
31
extensions/ql-vscode/src/text-utils.ts
Normal file
31
extensions/ql-vscode/src/text-utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
const CONTROL_CODE = '\u001F'.codePointAt(0)!;
|
||||
const CONTROL_LABEL = '\u2400'.codePointAt(0)!;
|
||||
|
||||
/**
|
||||
* Converts the given text so that any non-printable characters are replaced.
|
||||
* @param label The text to convert.
|
||||
* @returns The converted text.
|
||||
*/
|
||||
export function convertNonPrintableChars(label: string | undefined) {
|
||||
// If the label was empty, use a placeholder instead, so the link is still clickable.
|
||||
if (!label) {
|
||||
return '[empty string]';
|
||||
} else if (label.match(/^\s+$/)) {
|
||||
return `[whitespace: "${label}"]`;
|
||||
} else {
|
||||
/**
|
||||
* If the label contains certain non-printable characters, loop through each
|
||||
* character and replace it with the cooresponding unicode control label.
|
||||
*/
|
||||
const convertedLabelArray: any[] = [];
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
const labelCheck = label.codePointAt(i)!;
|
||||
if (labelCheck <= CONTROL_CODE) {
|
||||
convertedLabelArray[i] = String.fromCodePoint(labelCheck + CONTROL_LABEL);
|
||||
} else {
|
||||
convertedLabelArray[i] = label.charAt(i);
|
||||
}
|
||||
}
|
||||
return convertedLabelArray.join('');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers';
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage, tmpDir } from './helpers';
|
||||
import { ProgressCallback, UserCancellationException } from './commandRunner';
|
||||
import { logger } from './logging';
|
||||
import * as messages from './pure/messages';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { upgradesTmpDir } from './run-queries';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
@@ -35,13 +34,13 @@ export async function hasNondestructiveUpgradeCapabilities(qs: qsClient.QuerySer
|
||||
*/
|
||||
export async function compileDatabaseUpgradeSequence(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
dbItem: DatabaseItem,
|
||||
resolvedSequence: string[],
|
||||
currentUpgradeTmp: tmp.DirectoryResult,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<messages.CompileUpgradeSequenceResult> {
|
||||
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
|
||||
if (dbItem.contents === undefined || dbItem.contents.dbSchemeUri === undefined) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
if (!await hasNondestructiveUpgradeCapabilities(qs)) {
|
||||
@@ -56,14 +55,14 @@ export async function compileDatabaseUpgradeSequence(
|
||||
|
||||
async function compileDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
dbItem: DatabaseItem,
|
||||
targetDbScheme: string,
|
||||
resolvedSequence: string[],
|
||||
currentUpgradeTmp: tmp.DirectoryResult,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<messages.CompileUpgradeResult> {
|
||||
if (!db.contents?.dbSchemeUri) {
|
||||
if (!dbItem.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
// We have the upgrades we want but compileUpgrade
|
||||
@@ -78,7 +77,7 @@ async function compileDatabaseUpgrade(
|
||||
});
|
||||
return qs.sendRequest(messages.compileUpgrade, {
|
||||
upgrade: {
|
||||
fromDbscheme: db.contents.dbSchemeUri.fsPath,
|
||||
fromDbscheme: dbItem.contents.dbSchemeUri.fsPath,
|
||||
toDbscheme: targetDbScheme,
|
||||
additionalUpgrades: Array.from(uniqueParentDirs)
|
||||
},
|
||||
@@ -159,18 +158,18 @@ function getUpgradeDescriptions(compiled: messages.CompiledUpgrades): messages.U
|
||||
*/
|
||||
export async function upgradeDatabaseExplicit(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<messages.RunUpgradeResult | undefined> {
|
||||
|
||||
const searchPath: string[] = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (!db?.contents?.dbSchemeUri) {
|
||||
if (!dbItem?.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
const upgradeInfo = await qs.cliServer.resolveUpgrades(
|
||||
db.contents.dbSchemeUri.fsPath,
|
||||
dbItem.contents.dbSchemeUri.fsPath,
|
||||
searchPath,
|
||||
false
|
||||
);
|
||||
@@ -180,11 +179,11 @@ export async function upgradeDatabaseExplicit(
|
||||
if (finalDbscheme === undefined) {
|
||||
throw new Error('Could not determine target dbscheme to upgrade to.');
|
||||
}
|
||||
const currentUpgradeTmp = await tmp.dir({ dir: upgradesTmpDir.name, prefix: 'upgrade_', keep: false, unsafeCleanup: true });
|
||||
const currentUpgradeTmp = await tmp.dir({ dir: tmpDir.name, prefix: 'upgrade_', keep: false, unsafeCleanup: true });
|
||||
try {
|
||||
let compileUpgradeResult: messages.CompileUpgradeResult;
|
||||
try {
|
||||
compileUpgradeResult = await compileDatabaseUpgrade(qs, db, finalDbscheme, scripts, currentUpgradeTmp, progress, token);
|
||||
compileUpgradeResult = await compileDatabaseUpgrade(qs, dbItem, finalDbscheme, scripts, currentUpgradeTmp, progress, token);
|
||||
}
|
||||
catch (e) {
|
||||
void showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
|
||||
@@ -200,13 +199,13 @@ export async function upgradeDatabaseExplicit(
|
||||
return;
|
||||
}
|
||||
|
||||
await checkAndConfirmDatabaseUpgrade(compileUpgradeResult.compiledUpgrades, db, qs.cliServer.quiet);
|
||||
await checkAndConfirmDatabaseUpgrade(compileUpgradeResult.compiledUpgrades, dbItem, qs.cliServer.quiet);
|
||||
|
||||
try {
|
||||
void qs.logger.log('Running the following database upgrade:');
|
||||
|
||||
getUpgradeDescriptions(compileUpgradeResult.compiledUpgrades).map(s => s.description).join('\n');
|
||||
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades, progress, token);
|
||||
return await runDatabaseUpgrade(qs, dbItem, compileUpgradeResult.compiledUpgrades, progress, token);
|
||||
}
|
||||
catch (e) {
|
||||
void showAndLogErrorMessage(`Database upgrade failed: ${e}`);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user