Merge remote-tracking branch 'origin/main' into dbartol/debug-adapter-backup
This commit is contained in:
5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
@@ -100,6 +100,11 @@ jobs:
|
||||
run: |
|
||||
npm run lint
|
||||
|
||||
- name: Lint Markdown
|
||||
working-directory: extensions/ql-vscode
|
||||
run: |
|
||||
npm run lint:markdown
|
||||
|
||||
- name: Lint scenarios
|
||||
working-directory: extensions/ql-vscode
|
||||
run: |
|
||||
|
||||
4
.markdownlint.json
Normal file
4
.markdownlint.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"MD013": false,
|
||||
"MD041": false
|
||||
}
|
||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -4,6 +4,7 @@
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"amodio.tsl-problem-matcher",
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"firsttris.vscode-jest-runner",
|
||||
|
||||
24
.vscode/settings.json
vendored
24
.vscode/settings.json
vendored
@@ -43,6 +43,30 @@
|
||||
"TZ": "UTC"
|
||||
},
|
||||
|
||||
// These custom rules are read in extensions/ql-vscode/.markdownlint-cli2.cjs
|
||||
// but markdownlint only considers that config when linting files in
|
||||
// extensions/ql-vscode/ or its subfolders. Therefore, we have to explicitly
|
||||
// load the custom rules here too.
|
||||
"markdownlint.customRules": [
|
||||
"./extensions/ql-vscode/node_modules/@github/markdownlint-github/src/rules/no-default-alt-text.js",
|
||||
"./extensions/ql-vscode/node_modules/@github/markdownlint-github/src/rules/no-generic-link-text.js"
|
||||
],
|
||||
|
||||
// This ensures that the accessibility rule enablement done by github-markdownlint is
|
||||
// considered by the extension too.
|
||||
//
|
||||
// Unfortunately, we can only specify a single extends, so the config here isn't
|
||||
// identical since it can't also consider @github/markdownlint-github/style/base.json
|
||||
// That's not as bad as it could be since the full config is considered for anything
|
||||
// in extensions/ql-vscode/ or its subfolders anyway.
|
||||
//
|
||||
// Additional nonfiguration of the default rules is done in .markdownlint.json,
|
||||
// which is picked up by the extension automatically, and read explicitly in
|
||||
// .markdownlint-cli2.cjs
|
||||
"markdownlint.config": {
|
||||
"extends": "./extensions/ql-vscode/node_modules/@github/markdownlint-github/style/accessibility.json"
|
||||
},
|
||||
|
||||
// These options are used by the `jestrunner.debug` command.
|
||||
// They are not used by the `jestrunner.run` command.
|
||||
// After clicking "debug" over a test, continually invoke the
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
**/* @github/codeql-vscode-reviewers
|
||||
**/variant-analysis/ @github/code-scanning-secexp-reviewers
|
||||
**/databases/ @github/code-scanning-secexp-reviewers
|
||||
**/data-extensions-editor/ @github/code-scanning-secexp-reviewers
|
||||
|
||||
@@ -67,10 +67,7 @@ members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
This Code of Conduct is adapted from the [Contributor Covenant, version 1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html).
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
[the Contributor Covenant FAQ](https://www.contributor-covenant.org/faq). For more about Contributor Covenant, see [the Contributor Covenant website](https://www.contributor-covenant.org).
|
||||
|
||||
@@ -23,7 +23,9 @@ Please note that this project is released with a [Contributor Code of Conduct][c
|
||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||
|
||||
* Follow the [style guide][style].
|
||||
* Write tests. Tests that don't require the VS Code API are located [here](extensions/ql-vscode/test). Integration tests that do require the VS Code API are located [here](extensions/ql-vscode/src/vscode-tests).
|
||||
* Write tests:
|
||||
* [Tests that don't require the VS Code API are located here](extensions/ql-vscode/test).
|
||||
* [Integration tests that do require the VS Code API are located here](extensions/ql-vscode/src/vscode-tests).
|
||||
* Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
|
||||
* Write a [good commit message](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
|
||||
@@ -93,7 +95,7 @@ More information about Storybook can be found inside the **Overview** page once
|
||||
|
||||
### Testing
|
||||
|
||||
Information about testing can be found [here](./docs/testing.md).
|
||||
[Information about testing can be found here](./docs/testing.md).
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@@ -25,8 +25,8 @@ This project will track new feature development in CodeQL and, whenever appropri
|
||||
|
||||
This extension depends on the following two extensions for required functionality. They will be installed automatically when you install VS Code CodeQL.
|
||||
|
||||
- [Test Adapter Converter](https://marketplace.visualstudio.com/items?itemName=ms-vscode.test-adapter-converter)
|
||||
- [Test Explorer UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer)
|
||||
* [Test Adapter Converter](https://marketplace.visualstudio.com/items?itemName=ms-vscode.test-adapter-converter)
|
||||
* [Test Explorer UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
* Accumulation of many changes, none of which are individually big enough to warrant a minor bump, but which together are. This does not include changes which are purely internal to the extension, such as refactoring, or which are only available behind a feature flag.
|
||||
1. Double-check that the node version we're using matches the one used for VS Code. If it doesn't, you will then need to update the node version in the following files:
|
||||
* `.nvmrc` - this will enable `nvm` to automatically switch to the correct node version when you're in the project folder
|
||||
* `.github/workflows/main.yml` - all the "node-version: <version>" settings
|
||||
* `.github/workflows/release.yml` - the "node-version: <version>" setting
|
||||
* `.github/workflows/main.yml` - all the "node-version: '[VERSION]'" settings
|
||||
* `.github/workflows/release.yml` - the "node-version: '[VERSION]'" setting
|
||||
1. Double-check that the extension `package.json` and `package-lock.json` have the version you intend to release. If you are doing a patch release (as opposed to minor or major version) this should already be correct.
|
||||
1. Create a PR for this release:
|
||||
* This PR will contain any missing bits from steps 1, 2 and 3. Most of the time, this will just be updating `CHANGELOG.md` with today's date.
|
||||
@@ -22,7 +22,7 @@
|
||||
* Create a PR for this branch.
|
||||
* Wait for the PR to be merged into `main`
|
||||
1. Switch to `main` branch and pull latest changes
|
||||
1. Lock the `main` branch.
|
||||
1. Lock the `main` branch.
|
||||
* Go to the [branch protection rules for the `main` branch](https://github.com/github/vscode-codeql/settings/branch_protection_rules/16447115)
|
||||
* Select "Lock branch"
|
||||
* Click "Save changes"
|
||||
@@ -30,15 +30,18 @@
|
||||
1. Build the extension `npm run build` and install it on your VS Code using "Install from VSIX".
|
||||
1. Go through [our test plan](./test-plan.md) to ensure that the extension is working as expected.
|
||||
1. Switch to `main` and add a new tag on the `main` branch with your new version (named after the release), e.g.
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git tag v1.3.6
|
||||
```
|
||||
|
||||
If you've accidentally created a badly named tag, you can delete it via
|
||||
```bash
|
||||
git tag -d badly-named-tag
|
||||
```
|
||||
|
||||
```bash
|
||||
git tag -d badly-named-tag
|
||||
```
|
||||
|
||||
1. Unlock the main branch
|
||||
* Go to the [branch protection rules for the `main` branch](https://github.com/github/vscode-codeql/settings/branch_protection_rules/16447115)
|
||||
* Deselect "Lock branch"
|
||||
@@ -68,7 +71,7 @@
|
||||
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.
|
||||
* 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.
|
||||
1. Go to the draft GitHub release in [the releases tab of the repository](https://github.com/github/vscode-codeql/releases), click 'Edit', add some summary description, and publish it.
|
||||
1. Confirm the new release is marked as the latest release at <https://github.com/github/vscode-codeql/releases>.
|
||||
|
||||
@@ -83,6 +83,7 @@ choose to go through some of the Optional Test Cases.
|
||||
### Test Case 5: MRVA - Canceling a variant analysis run
|
||||
|
||||
Run one of the above MRVAs, but cancel it from within VS Code:
|
||||
|
||||
- Check that the query is canceled and the query history item is updated.
|
||||
- Check that the workflow run is also canceled.
|
||||
- Check that any available results are visible in VS Code.
|
||||
@@ -231,6 +232,7 @@ with this since it has quite a limited number of actions you can do.
|
||||
|
||||
This requires running a MRVA query and seeing the results view.
|
||||
|
||||
<!-- markdownlint-disable-next-line MD024 -->
|
||||
#### Test case 1: When variant analysis state is "pending"
|
||||
|
||||
1. Can open a results view
|
||||
|
||||
@@ -18,6 +18,7 @@ Any test data you're using (sample projects, config files, etc.) must go in a `t
|
||||
## Running the tests
|
||||
|
||||
Pre-requisites:
|
||||
|
||||
1. Run `npm run build`.
|
||||
2. You will need to have `npm run watch` running in the background.
|
||||
|
||||
@@ -29,7 +30,7 @@ Then, from the `extensions/ql-vscode` directory, use the appropriate command to
|
||||
* View Tests: `npm test:view`
|
||||
* VSCode integration tests: `npm run test:vscode-integration`
|
||||
|
||||
#### CLI integration tests
|
||||
#### Running CLI integration tests from the terminal
|
||||
|
||||
The CLI integration tests require the CodeQL standard libraries in order to run so you will need to clone a local copy of the `github/codeql` repository.
|
||||
|
||||
@@ -51,7 +52,7 @@ You will need to run tests using a task from inside of VS Code, under the "Run a
|
||||
* View Tests: run the _Launch Unit Tests_ task
|
||||
* VSCode integration tests: run the _Launch Unit Tests - No Workspace_ and _Launch Unit Tests - Minimal Workspace_ tasks
|
||||
|
||||
#### CLI integration tests
|
||||
#### Running CLI integration tests from VSCode
|
||||
|
||||
The CLI integration tests require the CodeQL standard libraries in order to run so you will need to clone a local copy of the `github/codeql` repository.
|
||||
|
||||
@@ -63,7 +64,7 @@ The CLI integration tests require the CodeQL standard libraries in order to run
|
||||
|
||||
## Running a single test
|
||||
|
||||
### 1. From the terminal
|
||||
### 1. Running a single test from the terminal
|
||||
|
||||
The easiest way to run a single test is to change the `it` of the test to `it.only` and then run the test command with some additional options
|
||||
to only run tests for this specific file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
@@ -78,7 +79,7 @@ You can also use the `--testNamePattern` option to run a specific test within a
|
||||
npm run test:cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts --testNamePattern "should create a QueryEvaluationInfo"
|
||||
```
|
||||
|
||||
### 2. From VSCode
|
||||
### 2. Running a single test from VSCode
|
||||
|
||||
Alternatively, you can run a single test inside VSCode. To do so, install the [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) extension. Then,
|
||||
you will have quicklinks to run a single test from within test files. To run a single unit or integration test, click the "Run" button. Debugging a single test is currently only supported
|
||||
@@ -94,7 +95,9 @@ Multi-Repo Variant Analyses (MRVA) rely on the GitHub API. In order to make deve
|
||||
### Using a pre-recorded test scenario
|
||||
|
||||
To run a mock MRVA scenario, follow these steps:
|
||||
|
||||
1. Enable the mock GitHub API server by adding the following in your VS Code user settings (which can be found by running the `Preferences: Open User Settings (JSON)` VS Code command):
|
||||
|
||||
```json
|
||||
"codeQL.mockGitHubApiServer": {
|
||||
"enabled": true
|
||||
@@ -108,9 +111,11 @@ To run a mock MRVA scenario, follow these steps:
|
||||
If you want to replay the same scenario you should unload and reload it so requests are replayed from the start.
|
||||
|
||||
### Recording a new test scenario
|
||||
|
||||
To record a new mock MRVA scenario, follow these steps:
|
||||
|
||||
1. Enable the mock GitHub API server by adding the following in your VS Code user settings (which can be found by running the `Preferences: Open User Settings (JSON)` VS Code command):
|
||||
|
||||
```json
|
||||
"codeQL.mockGitHubApiServer": {
|
||||
"enabled": true
|
||||
|
||||
16
extensions/ql-vscode/.markdownlint-cli2.cjs
Normal file
16
extensions/ql-vscode/.markdownlint-cli2.cjs
Normal file
@@ -0,0 +1,16 @@
|
||||
// Having the base options in a top-level config file means
|
||||
// that the VS Code markdownlint extension can pick them up
|
||||
// too, since that only considers _this_ file when looking
|
||||
// at files in this directory or below.
|
||||
base_options = require('../../.markdownlint.json')
|
||||
|
||||
const options = require('@github/markdownlint-github').init(
|
||||
base_options
|
||||
)
|
||||
module.exports = {
|
||||
config: options,
|
||||
customRules: ["@github/markdownlint-github"],
|
||||
outputFormatters: [
|
||||
[ "markdownlint-cli2-formatter-pretty", { "appendLink": true } ] // ensures the error message includes a link to the rule documentation
|
||||
]
|
||||
}
|
||||
@@ -28,7 +28,7 @@ For information about other configurations, see the separate [CodeQL help](https
|
||||
1. [Run a query](#running-a-query).
|
||||
|
||||
---
|
||||
|
||||
<!-- markdownlint-disable-next-line MD024 -->
|
||||
## Quick start: Installing and configuring the extension
|
||||
|
||||
### Installing the extension
|
||||
@@ -71,6 +71,7 @@ in the starter workspace directory.
|
||||
|
||||
If you're using your own clone of the CodeQL standard libraries, you can do a `git pull` from where you have the libraries checked out.
|
||||
|
||||
<!-- markdownlint-disable-next-line MD024 -->
|
||||
## Quick start: Using CodeQL
|
||||
|
||||
You can find all the commands contributed by the extension in the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**) by typing `CodeQL`, many of them are also accessible through the interface, and via keyboard shortcuts.
|
||||
|
||||
10517
extensions/ql-vscode/package-lock.json
generated
10517
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -785,6 +785,10 @@
|
||||
"title": "CodeQL: Go to QL Code",
|
||||
"enablement": "codeql.hasQLSource"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openDataExtensionsEditor",
|
||||
"title": "CodeQL: Open Data Extensions Editor"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockGitHubApiServer.startRecording",
|
||||
"title": "CodeQL: Mock GitHub API Server: Start Scenario Recording"
|
||||
@@ -1208,6 +1212,10 @@
|
||||
"command": "codeQL.viewCfgContextEditor",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openDataExtensionsEditor",
|
||||
"when": "config.codeQL.canary && config.codeQL.dataExtensions.editor"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"when": "false"
|
||||
@@ -1556,6 +1564,7 @@
|
||||
"update-vscode": "node ./node_modules/vscode/bin/install",
|
||||
"format": "prettier --write **/*.{ts,tsx} && eslint . --ext .ts,.tsx --fix",
|
||||
"lint": "eslint . --ext .js,.ts,.tsx --max-warnings=0",
|
||||
"lint:markdown": "markdownlint-cli2 \"../../**/*.{md,mdx}\" \"!**/node_modules/**\" \"!**/.vscode-test/**\" \"!**/build/cli/v*/**\"",
|
||||
"format-staged": "lint-staged",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook",
|
||||
@@ -1586,8 +1595,8 @@
|
||||
"node-fetch": "~2.6.7",
|
||||
"p-queue": "^6.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"semver": "~7.3.2",
|
||||
"source-map": "^0.7.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
@@ -1610,17 +1619,18 @@
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
|
||||
"@faker-js/faker": "^7.5.0",
|
||||
"@github/markdownlint-github": "^0.3.0",
|
||||
"@octokit/plugin-throttling": "^5.0.1",
|
||||
"@storybook/addon-actions": "^6.5.10",
|
||||
"@storybook/addon-essentials": "^6.5.10",
|
||||
"@storybook/addon-interactions": "^6.5.10",
|
||||
"@storybook/addon-links": "^6.5.10",
|
||||
"@storybook/builder-webpack5": "^6.5.10",
|
||||
"@storybook/manager-webpack5": "^6.5.10",
|
||||
"@storybook/react": "^6.5.10",
|
||||
"@storybook/addon-actions": "^6.5.17-alpha.0",
|
||||
"@storybook/addon-essentials": "^6.5.17-alpha.0",
|
||||
"@storybook/addon-interactions": "^6.5.17-alpha.0",
|
||||
"@storybook/addon-links": "^6.5.17-alpha.0",
|
||||
"@storybook/builder-webpack5": "^6.5.17-alpha.0",
|
||||
"@storybook/manager-webpack5": "^6.5.17-alpha.0",
|
||||
"@storybook/react": "^6.5.17-alpha.0",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/child-process-promise": "^2.2.1",
|
||||
"@types/classnames": "~2.2.9",
|
||||
@@ -1638,8 +1648,8 @@
|
||||
"@types/nanoid": "^3.0.0",
|
||||
"@types/node": "^16.11.25",
|
||||
"@types/node-fetch": "~2.5.2",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/sarif": "~2.1.2",
|
||||
"@types/semver": "~7.2.0",
|
||||
"@types/stream-chain": "~2.0.1",
|
||||
@@ -1684,6 +1694,8 @@
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
"jest-runner-vscode": "^3.0.1",
|
||||
"lint-staged": "~13.2.0",
|
||||
"markdownlint-cli2": "^0.6.0",
|
||||
"markdownlint-cli2-formatter-pretty": "^0.0.4",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"patch-package": "^6.5.0",
|
||||
@@ -1692,10 +1704,10 @@
|
||||
"through2": "^4.0.2",
|
||||
"ts-jest": "^29.0.1",
|
||||
"ts-json-schema-generator": "^1.1.2",
|
||||
"ts-loader": "^8.1.0",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.7.0",
|
||||
"ts-protoc-gen": "^0.9.0",
|
||||
"typescript": "^4.5.5",
|
||||
"typescript": "^5.0.2",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
|
||||
@@ -69,6 +69,8 @@ export type BaseCommands = {
|
||||
|
||||
"codeQL.copyVersion": () => Promise<void>;
|
||||
"codeQL.restartQueryServer": () => Promise<void>;
|
||||
"codeQL.restartQueryServerOnConfigChange": () => Promise<void>;
|
||||
"codeQL.restartLegacyQueryServerOnConfigChange": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used when working with queries in the editor
|
||||
@@ -208,7 +210,10 @@ export type VariantAnalysisCommands = {
|
||||
variantAnalysisId: number,
|
||||
repositoryFullName: string,
|
||||
) => Promise<VariantAnalysisScannedRepositoryResult>;
|
||||
"codeQL.monitorVariantAnalysis": (
|
||||
"codeQL.monitorNewVariantAnalysis": (
|
||||
variantAnalysis: VariantAnalysis,
|
||||
) => Promise<void>;
|
||||
"codeQL.monitorRehydratedVariantAnalysis": (
|
||||
variantAnalysis: VariantAnalysis,
|
||||
) => Promise<void>;
|
||||
"codeQL.openVariantAnalysisLogs": (
|
||||
@@ -253,6 +258,10 @@ export type PackagingCommands = {
|
||||
"codeQL.downloadPacks": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type DataExtensionsEditorCommands = {
|
||||
"codeQL.openDataExtensionsEditor": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type EvalLogViewerCommands = {
|
||||
"codeQLEvalLogViewer.clear": () => Promise<void>;
|
||||
};
|
||||
@@ -286,6 +295,7 @@ export type AllExtensionCommands = BaseCommands &
|
||||
AstCfgCommands &
|
||||
AstViewerCommands &
|
||||
PackagingCommands &
|
||||
DataExtensionsEditorCommands &
|
||||
EvalLogViewerCommands &
|
||||
SummaryLanguageSupportCommands &
|
||||
Partial<TestUICommands> &
|
||||
|
||||
54
extensions/ql-vscode/src/data-extensions-editor/bqrs.ts
Normal file
54
extensions/ql-vscode/src/data-extensions-editor/bqrs.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { DecodedBqrsChunk } from "../pure/bqrs-cli-types";
|
||||
import { Call, ExternalApiUsage } from "./external-api-usage";
|
||||
|
||||
export function decodeBqrsToExternalApiUsages(
|
||||
chunk: DecodedBqrsChunk,
|
||||
): ExternalApiUsage[] {
|
||||
const methodsByApiName = new Map<string, ExternalApiUsage>();
|
||||
|
||||
chunk?.tuples.forEach((tuple) => {
|
||||
const signature = tuple[0] as string;
|
||||
const supported = tuple[1] as boolean;
|
||||
const usage = tuple[2] as Call;
|
||||
|
||||
const [packageWithType, methodDeclaration] = signature.split("#");
|
||||
|
||||
const packageName = packageWithType.substring(
|
||||
0,
|
||||
packageWithType.lastIndexOf("."),
|
||||
);
|
||||
const typeName = packageWithType.substring(
|
||||
packageWithType.lastIndexOf(".") + 1,
|
||||
);
|
||||
|
||||
const methodName = methodDeclaration.substring(
|
||||
0,
|
||||
methodDeclaration.indexOf("("),
|
||||
);
|
||||
const methodParameters = methodDeclaration.substring(
|
||||
methodDeclaration.indexOf("("),
|
||||
);
|
||||
|
||||
if (!methodsByApiName.has(signature)) {
|
||||
methodsByApiName.set(signature, {
|
||||
signature,
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters,
|
||||
supported,
|
||||
usages: [],
|
||||
});
|
||||
}
|
||||
|
||||
const method = methodsByApiName.get(signature)!;
|
||||
method.usages.push(usage);
|
||||
});
|
||||
|
||||
const externalApiUsages = Array.from(methodsByApiName.values());
|
||||
externalApiUsages.sort((a, b) => {
|
||||
// Sort by number of usages descending
|
||||
return b.usages.length - a.usages.length;
|
||||
});
|
||||
return externalApiUsages;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ExtensionContext } from "vscode";
|
||||
import { DataExtensionsEditorView } from "./data-extensions-editor-view";
|
||||
import { DataExtensionsEditorCommands } from "../common/commands";
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import { QueryRunner } from "../queryRunner";
|
||||
import { DatabaseManager } from "../local-databases";
|
||||
import { extLogger } from "../common";
|
||||
import { ensureDir } from "fs-extra";
|
||||
import { join } from "path";
|
||||
|
||||
export class DataExtensionsEditorModule {
|
||||
private readonly queryStorageDir: string;
|
||||
|
||||
private constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly databaseManager: DatabaseManager,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly queryRunner: QueryRunner,
|
||||
baseQueryStorageDir: string,
|
||||
) {
|
||||
this.queryStorageDir = join(
|
||||
baseQueryStorageDir,
|
||||
"data-extensions-editor-results",
|
||||
);
|
||||
}
|
||||
|
||||
public static async initialize(
|
||||
ctx: ExtensionContext,
|
||||
databaseManager: DatabaseManager,
|
||||
cliServer: CodeQLCliServer,
|
||||
queryRunner: QueryRunner,
|
||||
queryStorageDir: string,
|
||||
): Promise<DataExtensionsEditorModule> {
|
||||
const dataExtensionsEditorModule = new DataExtensionsEditorModule(
|
||||
ctx,
|
||||
databaseManager,
|
||||
cliServer,
|
||||
queryRunner,
|
||||
queryStorageDir,
|
||||
);
|
||||
|
||||
await dataExtensionsEditorModule.initialize();
|
||||
return dataExtensionsEditorModule;
|
||||
}
|
||||
|
||||
public getCommands(): DataExtensionsEditorCommands {
|
||||
return {
|
||||
"codeQL.openDataExtensionsEditor": async () => {
|
||||
const db = this.databaseManager.currentDatabaseItem;
|
||||
if (!db) {
|
||||
void extLogger.log("No database selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const view = new DataExtensionsEditorView(
|
||||
this.ctx,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
this.queryStorageDir,
|
||||
db,
|
||||
);
|
||||
await view.openView();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
await ensureDir(this.queryStorageDir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import {
|
||||
CancellationTokenSource,
|
||||
ExtensionContext,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||
import {
|
||||
FromDataExtensionsEditorMessage,
|
||||
ToDataExtensionsEditorMessage,
|
||||
} from "../pure/interface-types";
|
||||
import { ProgressUpdate } from "../progress";
|
||||
import { extLogger, TeeLogger } from "../common";
|
||||
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
|
||||
import { qlpackOfDatabase } from "../contextual/queryResolver";
|
||||
import { file } from "tmp-promise";
|
||||
import { readFile, writeFile } from "fs-extra";
|
||||
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
||||
import {
|
||||
getOnDiskWorkspaceFolders,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogWarningMessage,
|
||||
} from "../helpers";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
|
||||
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
|
||||
import { showResolvableLocation } from "../interface-utils";
|
||||
import { decodeBqrsToExternalApiUsages } from "./bqrs";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
|
||||
import { ExternalApiUsage } from "./external-api-usage";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
|
||||
export class DataExtensionsEditorView extends AbstractWebview<
|
||||
ToDataExtensionsEditorMessage,
|
||||
FromDataExtensionsEditorMessage
|
||||
> {
|
||||
public constructor(
|
||||
ctx: ExtensionContext,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly queryRunner: QueryRunner,
|
||||
private readonly queryStorageDir: string,
|
||||
private readonly databaseItem: DatabaseItem,
|
||||
) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
public async openView() {
|
||||
const panel = await this.getPanel();
|
||||
panel.reveal(undefined, true);
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
}
|
||||
|
||||
protected async getPanelConfig(): Promise<WebviewPanelConfig> {
|
||||
return {
|
||||
viewId: "data-extensions-editor",
|
||||
title: "Data Extensions Editor",
|
||||
viewColumn: ViewColumn.Active,
|
||||
preserveFocus: true,
|
||||
view: "data-extensions-editor",
|
||||
};
|
||||
}
|
||||
|
||||
protected onPanelDispose(): void {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
protected async onMessage(
|
||||
msg: FromDataExtensionsEditorMessage,
|
||||
): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case "viewLoaded":
|
||||
await this.onWebViewLoaded();
|
||||
|
||||
break;
|
||||
case "jumpToUsage":
|
||||
await this.jumpToUsage(msg.location);
|
||||
|
||||
break;
|
||||
case "saveModeledMethods":
|
||||
await this.saveModeledMethods(
|
||||
msg.externalApiUsages,
|
||||
msg.modeledMethods,
|
||||
);
|
||||
await this.loadExternalApiUsages();
|
||||
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onWebViewLoaded() {
|
||||
super.onWebViewLoaded();
|
||||
|
||||
await Promise.all([
|
||||
this.loadExternalApiUsages(),
|
||||
this.loadExistingModeledMethods(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected async jumpToUsage(
|
||||
location: ResolvableLocationValue,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await showResolvableLocation(location, this.databaseItem);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.match(/File not found/)) {
|
||||
void window.showErrorMessage(
|
||||
"Original file of this result is not in the database's source archive.",
|
||||
);
|
||||
} else {
|
||||
void extLogger.log(`Unable to handleMsgFromView: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
void extLogger.log(`Unable to handleMsgFromView: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async saveModeledMethods(
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
): Promise<void> {
|
||||
const modelFilename = this.calculateModelFilename();
|
||||
if (!modelFilename) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yaml = createDataExtensionYaml(externalApiUsages, modeledMethods);
|
||||
|
||||
await writeFile(modelFilename, yaml);
|
||||
|
||||
void extLogger.log(`Saved data extension YAML to ${modelFilename}`);
|
||||
}
|
||||
|
||||
protected async loadExistingModeledMethods(): Promise<void> {
|
||||
const modelFilename = this.calculateModelFilename();
|
||||
if (!modelFilename) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const yaml = await readFile(modelFilename, "utf8");
|
||||
|
||||
const data = loadYaml(yaml, {
|
||||
filename: modelFilename,
|
||||
});
|
||||
|
||||
const existingModeledMethods = loadDataExtensionYaml(data);
|
||||
|
||||
if (!existingModeledMethods) {
|
||||
void showAndLogWarningMessage("Failed to parse data extension YAML.");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: "setExistingModeledMethods",
|
||||
existingModeledMethods,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
void extLogger.log(`Unable to read data extension YAML: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadExternalApiUsages(): Promise<void> {
|
||||
try {
|
||||
const queryResult = await this.runQuery();
|
||||
if (!queryResult) {
|
||||
await this.clearProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.showProgress({
|
||||
message: "Loading results",
|
||||
step: 1100,
|
||||
maxStep: 1500,
|
||||
});
|
||||
|
||||
const bqrsPath = queryResult.outputDir.bqrsPath;
|
||||
|
||||
const bqrsChunk = await this.getResults(bqrsPath);
|
||||
if (!bqrsChunk) {
|
||||
await this.clearProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.showProgress({
|
||||
message: "Finalizing results",
|
||||
step: 1450,
|
||||
maxStep: 1500,
|
||||
});
|
||||
|
||||
const externalApiUsages = decodeBqrsToExternalApiUsages(bqrsChunk);
|
||||
|
||||
await this.postMessage({
|
||||
t: "setExternalApiUsages",
|
||||
externalApiUsages,
|
||||
});
|
||||
|
||||
await this.clearProgress();
|
||||
} catch (err) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(err),
|
||||
)`Failed to load external APi usages: ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async runQuery(): Promise<CoreCompletedQuery | undefined> {
|
||||
const qlpacks = await qlpackOfDatabase(this.cliServer, this.databaseItem);
|
||||
|
||||
const packsToSearch = [qlpacks.dbschemePack];
|
||||
if (qlpacks.queryPack) {
|
||||
packsToSearch.push(qlpacks.queryPack);
|
||||
}
|
||||
|
||||
const suiteFile = (
|
||||
await file({
|
||||
postfix: ".qls",
|
||||
})
|
||||
).path;
|
||||
const suiteYaml = [];
|
||||
for (const qlpack of packsToSearch) {
|
||||
suiteYaml.push({
|
||||
from: qlpack,
|
||||
queries: ".",
|
||||
include: {
|
||||
id: `${this.databaseItem.language}/telemetry/fetch-external-apis`,
|
||||
},
|
||||
});
|
||||
}
|
||||
await writeFile(suiteFile, dumpYaml(suiteYaml), "utf8");
|
||||
|
||||
const queries = await this.cliServer.resolveQueriesInSuite(
|
||||
suiteFile,
|
||||
getOnDiskWorkspaceFolders(),
|
||||
);
|
||||
|
||||
if (queries.length !== 1) {
|
||||
void extLogger.log(`Expected exactly one query, got ${queries.length}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const query = queries[0];
|
||||
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
|
||||
const queryRun = this.queryRunner.createQueryRun(
|
||||
this.databaseItem.databaseUri.fsPath,
|
||||
{ queryPath: query, quickEvalPosition: undefined },
|
||||
false,
|
||||
getOnDiskWorkspaceFolders(),
|
||||
undefined,
|
||||
this.queryStorageDir,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
return queryRun.evaluate(
|
||||
(update) => this.showProgress(update, 1500),
|
||||
tokenSource.token,
|
||||
new TeeLogger(this.queryRunner.logger, queryRun.outputDir.logPath),
|
||||
);
|
||||
}
|
||||
|
||||
private async getResults(bqrsPath: string) {
|
||||
const bqrsInfo = await this.cliServer.bqrsInfo(bqrsPath);
|
||||
if (bqrsInfo["result-sets"].length !== 1) {
|
||||
void extLogger.log(
|
||||
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resultSet = bqrsInfo["result-sets"][0];
|
||||
|
||||
await this.showProgress({
|
||||
message: "Decoding results",
|
||||
step: 1200,
|
||||
maxStep: 1500,
|
||||
});
|
||||
|
||||
return this.cliServer.bqrsDecode(bqrsPath, resultSet.name);
|
||||
}
|
||||
|
||||
/*
|
||||
* Progress in this class is a bit weird. Most of the progress is based on running the query.
|
||||
* Query progress is always between 0 and 1000. However, we still have some steps that need
|
||||
* to be done after the query has finished. Therefore, the maximum step is 1500. This captures
|
||||
* that there's 1000 steps of the query progress since that takes the most time, and then
|
||||
* an additional 500 steps for the rest of the work. The progress doesn't need to be 100%
|
||||
* accurate, so this is just a rough estimate.
|
||||
*/
|
||||
private async showProgress(update: ProgressUpdate, maxStep?: number) {
|
||||
await this.postMessage({
|
||||
t: "showProgress",
|
||||
step: update.step,
|
||||
maxStep: maxStep ?? update.maxStep,
|
||||
message: update.message,
|
||||
});
|
||||
}
|
||||
|
||||
private async clearProgress() {
|
||||
await this.showProgress({
|
||||
step: 0,
|
||||
maxStep: 0,
|
||||
message: "",
|
||||
});
|
||||
}
|
||||
|
||||
private calculateModelFilename(): string | undefined {
|
||||
const workspaceFolder = workspace.workspaceFolders?.find(
|
||||
(folder) => folder.name === "ql",
|
||||
);
|
||||
if (!workspaceFolder) {
|
||||
void extLogger.log("No workspace folder 'ql' found");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return Uri.joinPath(
|
||||
workspaceFolder.uri,
|
||||
"java/ql/lib/ext",
|
||||
`${this.databaseItem.name.replaceAll("/", ".")}.model.yml`,
|
||||
).fsPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
|
||||
|
||||
export type Call = {
|
||||
label: string;
|
||||
url: ResolvableLocationValue;
|
||||
};
|
||||
|
||||
export type ExternalApiUsage = {
|
||||
/**
|
||||
* Contains the full method signature, e.g. `org.sql2o.Connection#createQuery(String)`
|
||||
*/
|
||||
signature: string;
|
||||
packageName: string;
|
||||
typeName: string;
|
||||
methodName: string;
|
||||
methodParameters: string;
|
||||
supported: boolean;
|
||||
usages: Call[];
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export type ModeledMethodType =
|
||||
| "none"
|
||||
| "source"
|
||||
| "sink"
|
||||
| "summary"
|
||||
| "neutral";
|
||||
|
||||
export type ModeledMethod = {
|
||||
type: ModeledMethodType;
|
||||
input: string;
|
||||
output: string;
|
||||
kind: string;
|
||||
};
|
||||
237
extensions/ql-vscode/src/data-extensions-editor/yaml.ts
Normal file
237
extensions/ql-vscode/src/data-extensions-editor/yaml.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { ExternalApiUsage } from "./external-api-usage";
|
||||
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
|
||||
|
||||
type ExternalApiUsageByType = {
|
||||
externalApiUsage: ExternalApiUsage;
|
||||
modeledMethod: ModeledMethod;
|
||||
};
|
||||
|
||||
type DataExtensionDefinition = {
|
||||
extensible: string;
|
||||
generateMethodDefinition: (method: ExternalApiUsageByType) => any[];
|
||||
readModeledMethod: (row: any[]) => [string, ModeledMethod] | undefined;
|
||||
};
|
||||
|
||||
function readRowToMethod(row: any[]): string {
|
||||
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
|
||||
}
|
||||
|
||||
const definitions: Record<
|
||||
Exclude<ModeledMethodType, "none">,
|
||||
DataExtensionDefinition
|
||||
> = {
|
||||
source: {
|
||||
extensible: "sourceModel",
|
||||
// extensible predicate sourceModel(
|
||||
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
||||
// string output, string kind, string provenance
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.externalApiUsage.packageName,
|
||||
method.externalApiUsage.typeName,
|
||||
true,
|
||||
method.externalApiUsage.methodName,
|
||||
method.externalApiUsage.methodParameters,
|
||||
"",
|
||||
method.modeledMethod.output,
|
||||
method.modeledMethod.kind,
|
||||
"manual",
|
||||
],
|
||||
readModeledMethod: (row) => [
|
||||
readRowToMethod(row),
|
||||
{
|
||||
type: "source",
|
||||
input: "",
|
||||
output: row[6],
|
||||
kind: row[7],
|
||||
},
|
||||
],
|
||||
},
|
||||
sink: {
|
||||
extensible: "sinkModel",
|
||||
// extensible predicate sinkModel(
|
||||
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
||||
// string input, string kind, string provenance
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.externalApiUsage.packageName,
|
||||
method.externalApiUsage.typeName,
|
||||
true,
|
||||
method.externalApiUsage.methodName,
|
||||
method.externalApiUsage.methodParameters,
|
||||
"",
|
||||
method.modeledMethod.input,
|
||||
method.modeledMethod.kind,
|
||||
"manual",
|
||||
],
|
||||
readModeledMethod: (row) => [
|
||||
readRowToMethod(row),
|
||||
{
|
||||
type: "sink",
|
||||
input: row[6],
|
||||
output: "",
|
||||
kind: row[7],
|
||||
},
|
||||
],
|
||||
},
|
||||
summary: {
|
||||
extensible: "summaryModel",
|
||||
// extensible predicate summaryModel(
|
||||
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
||||
// string input, string output, string kind, string provenance
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.externalApiUsage.packageName,
|
||||
method.externalApiUsage.typeName,
|
||||
true,
|
||||
method.externalApiUsage.methodName,
|
||||
method.externalApiUsage.methodParameters,
|
||||
"",
|
||||
method.modeledMethod.input,
|
||||
method.modeledMethod.output,
|
||||
method.modeledMethod.kind,
|
||||
"manual",
|
||||
],
|
||||
readModeledMethod: (row) => [
|
||||
readRowToMethod(row),
|
||||
{
|
||||
type: "summary",
|
||||
input: row[6],
|
||||
output: row[7],
|
||||
kind: row[8],
|
||||
},
|
||||
],
|
||||
},
|
||||
neutral: {
|
||||
extensible: "neutralModel",
|
||||
// extensible predicate neutralModel(
|
||||
// string package, string type, string name, string signature, string provenance
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.externalApiUsage.packageName,
|
||||
method.externalApiUsage.typeName,
|
||||
method.externalApiUsage.methodName,
|
||||
method.externalApiUsage.methodParameters,
|
||||
"manual",
|
||||
],
|
||||
readModeledMethod: (row) => [
|
||||
`${row[0]}.${row[1]}#${row[2]}${row[3]}`,
|
||||
{
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function createDataProperty(
|
||||
methods: ExternalApiUsageByType[],
|
||||
definition: DataExtensionDefinition,
|
||||
) {
|
||||
if (methods.length === 0) {
|
||||
return " []";
|
||||
}
|
||||
|
||||
return `\n${methods
|
||||
.map(
|
||||
(method) =>
|
||||
` - ${JSON.stringify(
|
||||
definition.generateMethodDefinition(method),
|
||||
)}`,
|
||||
)
|
||||
.join("\n")}`;
|
||||
}
|
||||
|
||||
export function createDataExtensionYaml(
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
) {
|
||||
const methodsByType: Record<
|
||||
Exclude<ModeledMethodType, "none">,
|
||||
ExternalApiUsageByType[]
|
||||
> = {
|
||||
source: [],
|
||||
sink: [],
|
||||
summary: [],
|
||||
neutral: [],
|
||||
};
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
const modeledMethod = modeledMethods[externalApiUsage.signature];
|
||||
|
||||
if (modeledMethod?.type && modeledMethod.type !== "none") {
|
||||
methodsByType[modeledMethod.type].push({
|
||||
externalApiUsage,
|
||||
modeledMethod,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const extensions = Object.entries(definitions).map(
|
||||
([type, definition]) => ` - addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: ${definition.extensible}
|
||||
data:${createDataProperty(
|
||||
methodsByType[type as Exclude<ModeledMethodType, "none">],
|
||||
definition,
|
||||
)}
|
||||
`,
|
||||
);
|
||||
|
||||
return `extensions:
|
||||
${extensions.join("\n")}`;
|
||||
}
|
||||
|
||||
export function loadDataExtensionYaml(
|
||||
data: any,
|
||||
): Record<string, ModeledMethod> | undefined {
|
||||
if (typeof data !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const extensions = data.extensions;
|
||||
if (!Array.isArray(extensions)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modeledMethods: Record<string, ModeledMethod> = {};
|
||||
|
||||
for (const extension of extensions) {
|
||||
const addsTo = extension.addsTo;
|
||||
if (typeof addsTo !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensible = addsTo.extensible;
|
||||
if (typeof extensible !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = extension.data;
|
||||
if (!Array.isArray(data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const definition = Object.values(definitions).find(
|
||||
(definition) => definition.extensible === extensible,
|
||||
);
|
||||
if (!definition) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const row of data) {
|
||||
const result = definition.readModeledMethod(row);
|
||||
if (!result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [apiInfo, modeledMethod] = result;
|
||||
|
||||
modeledMethods[apiInfo] = modeledMethod;
|
||||
}
|
||||
}
|
||||
|
||||
return modeledMethods;
|
||||
}
|
||||
@@ -122,6 +122,7 @@ import { getQueryEditorCommands } from "./query-editor";
|
||||
import { App } from "./common/app";
|
||||
import { registerCommandWithErrorHandling } from "./common/vscode/commands";
|
||||
import { DebuggerUI } from "./debugger/debugger-ui";
|
||||
import { DataExtensionsEditorModule } from "./data-extensions-editor/data-extensions-editor-module";
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -170,27 +171,31 @@ function getCommands(
|
||||
}
|
||||
};
|
||||
|
||||
const restartQueryServer = async () =>
|
||||
withProgress(
|
||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||
// Restart all of the spawned servers: cli, query, and language.
|
||||
cliServer.restartCliServer();
|
||||
await Promise.all([
|
||||
queryRunner.restartQueryServer(progress, token),
|
||||
ideServer.restart(),
|
||||
]);
|
||||
void showAndLogInformationMessage("CodeQL Query Server restarted.", {
|
||||
outputLogger: queryServerLogger,
|
||||
});
|
||||
},
|
||||
{
|
||||
title: "Restarting Query Server",
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
"codeQL.openDocumentation": async () => {
|
||||
await env.openExternal(Uri.parse("https://codeql.github.com/docs/"));
|
||||
},
|
||||
"codeQL.restartQueryServer": async () =>
|
||||
withProgress(
|
||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||
// Restart all of the spawned servers: cli, query, and language.
|
||||
cliServer.restartCliServer();
|
||||
await Promise.all([
|
||||
queryRunner.restartQueryServer(progress, token),
|
||||
ideServer.restart(),
|
||||
]);
|
||||
void showAndLogInformationMessage("CodeQL Query Server restarted.", {
|
||||
outputLogger: queryServerLogger,
|
||||
});
|
||||
},
|
||||
{
|
||||
title: "Restarting Query Server",
|
||||
},
|
||||
),
|
||||
"codeQL.restartQueryServer": restartQueryServer,
|
||||
"codeQL.restartQueryServerOnConfigChange": restartQueryServer,
|
||||
"codeQL.restartLegacyQueryServerOnConfigChange": restartQueryServer,
|
||||
"codeQL.copyVersion": async () => {
|
||||
const text = `CodeQL extension version: ${
|
||||
extension?.packageJSON.version
|
||||
@@ -871,6 +876,24 @@ async function activateWithInstalledDistribution(
|
||||
const debuggerUI = new DebuggerUI(localQueryResultsView, localQueries, dbm);
|
||||
ctx.subscriptions.push(debuggerUI);
|
||||
|
||||
void extLogger.log("Initializing debugger factory.");
|
||||
ctx.subscriptions.push(
|
||||
new QLDebugAdapterDescriptorFactory(queryStorageDir, qs, localQueries),
|
||||
);
|
||||
|
||||
void extLogger.log("Initializing debugger UI.");
|
||||
const debuggerUI = new DebuggerUI(localQueryResultsView, localQueries, dbm);
|
||||
ctx.subscriptions.push(debuggerUI);
|
||||
|
||||
const dataExtensionsEditorModule =
|
||||
await DataExtensionsEditorModule.initialize(
|
||||
ctx,
|
||||
dbm,
|
||||
cliServer,
|
||||
qs,
|
||||
tmpDir.name,
|
||||
);
|
||||
|
||||
void extLogger.log("Initializing QLTest interface.");
|
||||
const testExplorerExtension = extensions.getExtension<TestHub>(
|
||||
testExplorerExtensionId,
|
||||
@@ -933,6 +956,7 @@ async function activateWithInstalledDistribution(
|
||||
...getPackagingCommands({
|
||||
cliServer,
|
||||
}),
|
||||
...dataExtensionsEditorModule.getCommands(),
|
||||
...evalLogViewer.getCommands(),
|
||||
...summaryLanguageSupport.getCommands(),
|
||||
...testUiCommands,
|
||||
|
||||
@@ -113,7 +113,8 @@ export type WebviewView =
|
||||
| "results"
|
||||
| "compare"
|
||||
| "variant-analysis"
|
||||
| "data-flow-paths";
|
||||
| "data-flow-paths"
|
||||
| "data-extensions-editor";
|
||||
|
||||
export interface WebviewMessage {
|
||||
t: string;
|
||||
|
||||
@@ -71,7 +71,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
if (config.onDidChangeConfiguration !== undefined) {
|
||||
this.push(
|
||||
config.onDidChangeConfiguration(() =>
|
||||
app.commands.execute("codeQL.restartQueryServer"),
|
||||
app.commands.execute("codeQL.restartLegacyQueryServerOnConfigChange"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,10 +172,6 @@ async function runQuery(
|
||||
*/
|
||||
export class QueryInProgress {
|
||||
public queryEvalInfo: QueryEvaluationInfo;
|
||||
/**
|
||||
* Note that in the {@link readQueryHistoryFromFile} method, we create a QueryEvaluationInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(
|
||||
readonly querySaveDir: string,
|
||||
readonly dbItemPath: string,
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
import { RepositoriesFilterSortStateWithIds } from "./variant-analysis-filter-sort";
|
||||
import { ErrorLike } from "./errors";
|
||||
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
|
||||
import { ExternalApiUsage } from "../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../data-extensions-editor/modeled-method";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -478,3 +480,41 @@ export interface SetDataFlowPathsMessage {
|
||||
export type ToDataFlowPathsMessage = SetDataFlowPathsMessage;
|
||||
|
||||
export type FromDataFlowPathsMessage = CommonFromViewMessages;
|
||||
|
||||
export interface SetExternalApiUsagesMessage {
|
||||
t: "setExternalApiUsages";
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
}
|
||||
|
||||
export interface ShowProgressMessage {
|
||||
t: "showProgress";
|
||||
step: number;
|
||||
maxStep: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface JumpToUsageMessage {
|
||||
t: "jumpToUsage";
|
||||
location: ResolvableLocationValue;
|
||||
}
|
||||
|
||||
export interface SetExistingModeledMethods {
|
||||
t: "setExistingModeledMethods";
|
||||
existingModeledMethods: Record<string, ModeledMethod>;
|
||||
}
|
||||
|
||||
export interface SaveModeledMethods {
|
||||
t: "saveModeledMethods";
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
}
|
||||
|
||||
export type ToDataExtensionsEditorMessage =
|
||||
| SetExternalApiUsagesMessage
|
||||
| ShowProgressMessage
|
||||
| SetExistingModeledMethods;
|
||||
|
||||
export type FromDataExtensionsEditorMessage =
|
||||
| ViewLoadedMsg
|
||||
| JumpToUsageMessage
|
||||
| SaveModeledMethods;
|
||||
|
||||
105
extensions/ql-vscode/src/query-history/store/data-mapper.ts
Normal file
105
extensions/ql-vscode/src/query-history/store/data-mapper.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
LocalQueryInfo,
|
||||
CompletedQueryInfo,
|
||||
InitialQueryInfo,
|
||||
} from "../../query-results";
|
||||
import { QueryEvaluationInfo } from "../../run-queries-shared";
|
||||
import { QueryHistoryInfo } from "../query-history-info";
|
||||
import { VariantAnalysisHistoryItem } from "../variant-analysis-history-item";
|
||||
import {
|
||||
CompletedQueryInfoData,
|
||||
QueryEvaluationInfoData,
|
||||
InitialQueryInfoData,
|
||||
LocalQueryDataItem,
|
||||
} from "./local-query-data-item";
|
||||
import { QueryHistoryDataItem } from "./query-history-data";
|
||||
|
||||
// Maps Query History Data Models to Domain Models
|
||||
|
||||
export function mapQueryHistoryToDomainModels(
|
||||
queries: QueryHistoryDataItem[],
|
||||
): QueryHistoryInfo[] {
|
||||
return queries.map((d) => {
|
||||
if (d.t === "variant-analysis") {
|
||||
const query: VariantAnalysisHistoryItem = d;
|
||||
return query;
|
||||
} else if (d.t === "local") {
|
||||
return mapLocalQueryDataItemToDomainModel(d);
|
||||
}
|
||||
|
||||
throw Error(
|
||||
`Unexpected or corrupted query history file. Unknown query history item: ${JSON.stringify(
|
||||
d,
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function mapLocalQueryDataItemToDomainModel(
|
||||
localQuery: LocalQueryDataItem,
|
||||
): LocalQueryInfo {
|
||||
return new LocalQueryInfo(
|
||||
mapInitialQueryInfoDataToDomainModel(localQuery.initialInfo),
|
||||
undefined,
|
||||
localQuery.failureReason,
|
||||
localQuery.completedQuery &&
|
||||
mapCompletedQueryInfoDataToDomainModel(localQuery.completedQuery),
|
||||
localQuery.evalLogLocation,
|
||||
localQuery.evalLogSummaryLocation,
|
||||
localQuery.jsonEvalLogSummaryLocation,
|
||||
localQuery.evalLogSummarySymbolsLocation,
|
||||
);
|
||||
}
|
||||
|
||||
function mapCompletedQueryInfoDataToDomainModel(
|
||||
completedQuery: CompletedQueryInfoData,
|
||||
): CompletedQueryInfo {
|
||||
return new CompletedQueryInfo(
|
||||
mapQueryEvaluationInfoDataToDomainModel(completedQuery.query),
|
||||
{
|
||||
runId: completedQuery.result.runId,
|
||||
queryId: completedQuery.result.queryId,
|
||||
resultType: completedQuery.result.resultType,
|
||||
evaluationTime: completedQuery.result.evaluationTime,
|
||||
message: completedQuery.result.message,
|
||||
logFileLocation: completedQuery.result.logFileLocation,
|
||||
},
|
||||
completedQuery.logFileLocation,
|
||||
completedQuery.successful ?? completedQuery.sucessful,
|
||||
completedQuery.message,
|
||||
completedQuery.interpretedResultsSortState,
|
||||
completedQuery.resultCount,
|
||||
completedQuery.sortedResultsInfo,
|
||||
);
|
||||
}
|
||||
|
||||
function mapInitialQueryInfoDataToDomainModel(
|
||||
initialInfo: InitialQueryInfoData,
|
||||
): InitialQueryInfo {
|
||||
return {
|
||||
userSpecifiedLabel: initialInfo.userSpecifiedLabel,
|
||||
queryText: initialInfo.queryText,
|
||||
isQuickQuery: initialInfo.isQuickQuery,
|
||||
isQuickEval: initialInfo.isQuickEval,
|
||||
quickEvalPosition: initialInfo.quickEvalPosition,
|
||||
queryPath: initialInfo.queryPath,
|
||||
databaseInfo: {
|
||||
databaseUri: initialInfo.databaseInfo.databaseUri,
|
||||
name: initialInfo.databaseInfo.name,
|
||||
},
|
||||
start: new Date(initialInfo.start),
|
||||
id: initialInfo.id,
|
||||
};
|
||||
}
|
||||
|
||||
function mapQueryEvaluationInfoDataToDomainModel(
|
||||
evaluationInfo: QueryEvaluationInfoData,
|
||||
): QueryEvaluationInfo {
|
||||
return new QueryEvaluationInfo(
|
||||
evaluationInfo.querySaveDir,
|
||||
evaluationInfo.dbItemPath,
|
||||
evaluationInfo.databaseHasMetadataFile,
|
||||
evaluationInfo.quickEvalPosition,
|
||||
evaluationInfo.metadata,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import { LocalQueryInfo, InitialQueryInfo } from "../../query-results";
|
||||
import { QueryEvaluationInfo } from "../../run-queries-shared";
|
||||
import { QueryHistoryInfo } from "../query-history-info";
|
||||
import {
|
||||
LocalQueryDataItem,
|
||||
InitialQueryInfoData,
|
||||
QueryEvaluationInfoData,
|
||||
} from "./local-query-data-item";
|
||||
import { QueryHistoryDataItem } from "./query-history-data";
|
||||
import { VariantAnalysisDataItem } from "./variant-analysis-data-item";
|
||||
|
||||
// Maps Query History Domain Models to Data Models
|
||||
|
||||
export function mapQueryHistoryToDataModels(
|
||||
queries: QueryHistoryInfo[],
|
||||
): QueryHistoryDataItem[] {
|
||||
return queries.map((q) => {
|
||||
if (q.t === "variant-analysis") {
|
||||
const query: VariantAnalysisDataItem = q;
|
||||
return query;
|
||||
} else if (q.t === "local") {
|
||||
return mapLocalQueryInfoToDataModel(q);
|
||||
} else {
|
||||
assertNever(q);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mapLocalQueryInfoToDataModel(
|
||||
query: LocalQueryInfo,
|
||||
): LocalQueryDataItem {
|
||||
return {
|
||||
initialInfo: mapInitialQueryInfoToDataModel(query.initialInfo),
|
||||
t: "local",
|
||||
evalLogLocation: query.evalLogLocation,
|
||||
evalLogSummaryLocation: query.evalLogSummaryLocation,
|
||||
jsonEvalLogSummaryLocation: query.jsonEvalLogSummaryLocation,
|
||||
evalLogSummarySymbolsLocation: query.evalLogSummarySymbolsLocation,
|
||||
failureReason: query.failureReason,
|
||||
completedQuery: query.completedQuery && {
|
||||
query: mapQueryEvaluationInfoToDataModel(query.completedQuery.query),
|
||||
result: {
|
||||
runId: query.completedQuery.result.runId,
|
||||
queryId: query.completedQuery.result.queryId,
|
||||
resultType: query.completedQuery.result.resultType,
|
||||
evaluationTime: query.completedQuery.result.evaluationTime,
|
||||
message: query.completedQuery.result.message,
|
||||
logFileLocation: query.completedQuery.result.logFileLocation,
|
||||
},
|
||||
logFileLocation: query.completedQuery.logFileLocation,
|
||||
successful: query.completedQuery.successful,
|
||||
message: query.completedQuery.message,
|
||||
resultCount: query.completedQuery.resultCount,
|
||||
sortedResultsInfo: query.completedQuery.sortedResultsInfo,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mapInitialQueryInfoToDataModel(
|
||||
localQueryInitialInfo: InitialQueryInfo,
|
||||
): InitialQueryInfoData {
|
||||
return {
|
||||
userSpecifiedLabel: localQueryInitialInfo.userSpecifiedLabel,
|
||||
queryText: localQueryInitialInfo.queryText,
|
||||
isQuickQuery: localQueryInitialInfo.isQuickQuery,
|
||||
isQuickEval: localQueryInitialInfo.isQuickEval,
|
||||
quickEvalPosition: localQueryInitialInfo.quickEvalPosition,
|
||||
queryPath: localQueryInitialInfo.queryPath,
|
||||
databaseInfo: {
|
||||
databaseUri: localQueryInitialInfo.databaseInfo.databaseUri,
|
||||
name: localQueryInitialInfo.databaseInfo.name,
|
||||
},
|
||||
start: localQueryInitialInfo.start,
|
||||
id: localQueryInitialInfo.id,
|
||||
};
|
||||
}
|
||||
|
||||
function mapQueryEvaluationInfoToDataModel(
|
||||
queryEvaluationInfo: QueryEvaluationInfo,
|
||||
): QueryEvaluationInfoData {
|
||||
return {
|
||||
querySaveDir: queryEvaluationInfo.querySaveDir,
|
||||
dbItemPath: queryEvaluationInfo.dbItemPath,
|
||||
databaseHasMetadataFile: queryEvaluationInfo.databaseHasMetadataFile,
|
||||
quickEvalPosition: queryEvaluationInfo.quickEvalPosition,
|
||||
metadata: queryEvaluationInfo.metadata,
|
||||
resultsPaths: queryEvaluationInfo.resultsPaths,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
export interface LocalQueryDataItem {
|
||||
initialInfo: InitialQueryInfoData;
|
||||
t: "local";
|
||||
evalLogLocation?: string;
|
||||
evalLogSummaryLocation?: string;
|
||||
jsonEvalLogSummaryLocation?: string;
|
||||
evalLogSummarySymbolsLocation?: string;
|
||||
completedQuery?: CompletedQueryInfoData;
|
||||
failureReason?: string;
|
||||
}
|
||||
|
||||
export interface InitialQueryInfoData {
|
||||
userSpecifiedLabel?: string;
|
||||
queryText: string;
|
||||
isQuickQuery: boolean;
|
||||
isQuickEval: boolean;
|
||||
quickEvalPosition?: PositionData;
|
||||
queryPath: string;
|
||||
databaseInfo: DatabaseInfoData;
|
||||
start: Date;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface DatabaseInfoData {
|
||||
name: string;
|
||||
databaseUri: string;
|
||||
}
|
||||
|
||||
interface PositionData {
|
||||
line: number;
|
||||
column: number;
|
||||
endLine: number;
|
||||
endColumn: number;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface CompletedQueryInfoData {
|
||||
query: QueryEvaluationInfoData;
|
||||
message?: string;
|
||||
successful?: boolean;
|
||||
|
||||
// There once was a typo in the data model, which is why we need to support both
|
||||
sucessful?: boolean;
|
||||
result: EvaluationResultData;
|
||||
logFileLocation?: string;
|
||||
resultCount: number;
|
||||
sortedResultsInfo: Record<string, SortedResultSetInfo>;
|
||||
interpretedResultsSortState?: InterpretedResultsSortState;
|
||||
}
|
||||
|
||||
interface InterpretedResultsSortState {
|
||||
sortBy: InterpretedResultsSortColumn;
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
type InterpretedResultsSortColumn = "alert-message";
|
||||
|
||||
interface SortedResultSetInfo {
|
||||
resultsPath: string;
|
||||
sortState: RawResultsSortState;
|
||||
}
|
||||
|
||||
interface RawResultsSortState {
|
||||
columnIndex: number;
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
enum SortDirection {
|
||||
asc,
|
||||
desc,
|
||||
}
|
||||
|
||||
interface EvaluationResultData {
|
||||
runId: number;
|
||||
queryId: number;
|
||||
resultType: number;
|
||||
evaluationTime: number;
|
||||
message?: string;
|
||||
logFileLocation?: string;
|
||||
}
|
||||
|
||||
export interface QueryEvaluationInfoData {
|
||||
querySaveDir: string;
|
||||
dbItemPath: string;
|
||||
databaseHasMetadataFile: boolean;
|
||||
quickEvalPosition?: PositionData;
|
||||
metadata?: QueryMetadataData;
|
||||
resultsPaths: {
|
||||
resultsPath: string;
|
||||
interpretedResultsPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface QueryMetadataData {
|
||||
name?: string;
|
||||
description?: string;
|
||||
id?: string;
|
||||
kind?: string;
|
||||
scored?: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Contains models and consts for the data we want to store in the query history store.
|
||||
// Changes to these models should be done carefully and account for backwards compatibility of data.
|
||||
|
||||
import { LocalQueryDataItem } from "./local-query-data-item";
|
||||
import { VariantAnalysisDataItem } from "./variant-analysis-data-item";
|
||||
|
||||
export const ALLOWED_QUERY_HISTORY_VERSIONS = [1, 2];
|
||||
|
||||
export interface QueryHistoryData {
|
||||
version: number;
|
||||
queries: QueryHistoryDataItem[];
|
||||
}
|
||||
|
||||
export type QueryHistoryDataItem = LocalQueryDataItem | VariantAnalysisDataItem;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pathExists, readFile, remove, mkdir, writeFile } from "fs-extra";
|
||||
import { pathExists, remove, mkdir, writeFile, readJson } from "fs-extra";
|
||||
import { dirname } from "path";
|
||||
|
||||
import { showAndLogExceptionWithTelemetry } from "../../helpers";
|
||||
@@ -8,11 +8,15 @@ import {
|
||||
getErrorMessage,
|
||||
getErrorStack,
|
||||
} from "../../pure/helpers-pure";
|
||||
import { CompletedQueryInfo, LocalQueryInfo } from "../../query-results";
|
||||
import { QueryHistoryInfo } from "../query-history-info";
|
||||
import { QueryEvaluationInfo } from "../../run-queries-shared";
|
||||
import { QueryResultType } from "../../pure/legacy-messages";
|
||||
import { redactableError } from "../../pure/errors";
|
||||
import {
|
||||
ALLOWED_QUERY_HISTORY_VERSIONS,
|
||||
QueryHistoryData,
|
||||
QueryHistoryDataItem,
|
||||
} from "./query-history-data";
|
||||
import { mapQueryHistoryToDomainModels } from "./data-mapper";
|
||||
import { mapQueryHistoryToDataModels } from "./domain-mapper";
|
||||
|
||||
export async function readQueryHistoryFromFile(
|
||||
fsPath: string,
|
||||
@@ -22,9 +26,11 @@ export async function readQueryHistoryFromFile(
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await readFile(fsPath, "utf8");
|
||||
const obj = JSON.parse(data);
|
||||
if (![1, 2].includes(obj.version)) {
|
||||
const obj: QueryHistoryData = await readJson(fsPath, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
if (!ALLOWED_QUERY_HISTORY_VERSIONS.includes(obj.version)) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError`Can't parse query history. Unsupported query history format: v${obj.version}.`,
|
||||
);
|
||||
@@ -32,61 +38,33 @@ export async function readQueryHistoryFromFile(
|
||||
}
|
||||
|
||||
const queries = obj.queries;
|
||||
const parsedQueries = queries
|
||||
// Remove remote queries, which are not supported anymore.
|
||||
.filter((q: QueryHistoryInfo | { t: "remote" }) => q.t !== "remote")
|
||||
.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);
|
||||
// Remove remote queries, which are not supported anymore.
|
||||
const parsedQueries = queries.filter(
|
||||
(q: QueryHistoryDataItem | { t: "remote" }) => q.t !== "remote",
|
||||
);
|
||||
|
||||
// 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,
|
||||
);
|
||||
|
||||
// Previously, there was a typo in the completedQuery type. There was a field
|
||||
// `sucessful` and it was renamed to `successful`. We need to handle this case.
|
||||
if ("sucessful" in q.completedQuery) {
|
||||
(q.completedQuery as any).successful = (
|
||||
q.completedQuery as any
|
||||
).sucessful;
|
||||
delete (q.completedQuery as any).sucessful;
|
||||
}
|
||||
|
||||
if (!("successful" in q.completedQuery)) {
|
||||
(q.completedQuery as any).successful =
|
||||
q.completedQuery.result?.resultType === QueryResultType.SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
return q;
|
||||
});
|
||||
// Map the data models to the domain models.
|
||||
const domainModels: QueryHistoryInfo[] =
|
||||
mapQueryHistoryToDomainModels(parsedQueries);
|
||||
|
||||
// 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 === "variant-analysis") {
|
||||
// the deserializer 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 pathExists(resultsPath));
|
||||
});
|
||||
const filteredDomainModels: Promise<QueryHistoryInfo[]> = asyncFilter(
|
||||
domainModels,
|
||||
async (q) => {
|
||||
if (q.t === "variant-analysis") {
|
||||
// the query history store doesn't know where variant analysises are
|
||||
// stored so we need to assume here that they exist. We check later
|
||||
// to see if they exist on disk.
|
||||
return true;
|
||||
}
|
||||
const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath;
|
||||
return !!resultsPath && (await pathExists(resultsPath));
|
||||
},
|
||||
);
|
||||
|
||||
return filteredDomainModels;
|
||||
} catch (e) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(asError(e))`Error loading query history.`,
|
||||
@@ -121,13 +99,17 @@ export async function writeQueryHistoryToFile(
|
||||
const filteredQueries = queries.filter((q) =>
|
||||
q.t === "local" ? q.completedQuery !== undefined : true,
|
||||
);
|
||||
|
||||
// map domain model queries to data model
|
||||
const queryHistoryData = mapQueryHistoryToDataModels(filteredQueries);
|
||||
|
||||
const data = JSON.stringify(
|
||||
{
|
||||
// version 2:
|
||||
// - adds the `variant-analysis` type
|
||||
// - ensures a `successful` property exists on completedQuery
|
||||
version: 2,
|
||||
queries: filteredQueries,
|
||||
queries: queryHistoryData,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { QueryLanguage } from "../../common/query-language";
|
||||
import { QueryStatus } from "../../query-status";
|
||||
import {
|
||||
VariantAnalysisFailureReason,
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisStatus,
|
||||
} from "../../variant-analysis/shared/variant-analysis";
|
||||
|
||||
// Data Model for Variant Analysis Query History Items
|
||||
// All data points are modelled, except enums.
|
||||
|
||||
export interface VariantAnalysisDataItem {
|
||||
readonly t: "variant-analysis";
|
||||
failureReason?: string;
|
||||
resultCount?: number;
|
||||
status: QueryStatus;
|
||||
completed: boolean;
|
||||
variantAnalysis: VariantAnalysisQueryHistoryData;
|
||||
userSpecifiedLabel?: string;
|
||||
}
|
||||
|
||||
export interface VariantAnalysisQueryHistoryData {
|
||||
id: number;
|
||||
controllerRepo: {
|
||||
id: number;
|
||||
fullName: string;
|
||||
private: boolean;
|
||||
};
|
||||
query: {
|
||||
name: string;
|
||||
filePath: string;
|
||||
language: QueryLanguage;
|
||||
text: string;
|
||||
};
|
||||
databases: {
|
||||
repositories?: string[];
|
||||
repositoryLists?: string[];
|
||||
repositoryOwners?: string[];
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
executionStartTime: number;
|
||||
status: VariantAnalysisStatus;
|
||||
completedAt?: string;
|
||||
actionsWorkflowRunId?: number;
|
||||
failureReason?: VariantAnalysisFailureReason;
|
||||
scannedRepos?: VariantAnalysisScannedRepositoryData[];
|
||||
skippedRepos?: VariantAnalysisSkippedRepositoriesData;
|
||||
}
|
||||
|
||||
export interface VariantAnalysisScannedRepositoryData {
|
||||
repository: {
|
||||
id: number;
|
||||
fullName: string;
|
||||
private: boolean;
|
||||
stargazersCount: number;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
analysisStatus: VariantAnalysisRepoStatus;
|
||||
resultCount?: number;
|
||||
artifactSizeInBytes?: number;
|
||||
failureMessage?: string;
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepositoriesData {
|
||||
accessMismatchRepos?: VariantAnalysisSkippedRepositoryGroupData;
|
||||
notFoundRepos?: VariantAnalysisSkippedRepositoryGroupData;
|
||||
noCodeqlDbRepos?: VariantAnalysisSkippedRepositoryGroupData;
|
||||
overLimitRepos?: VariantAnalysisSkippedRepositoryGroupData;
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepositoryGroupData {
|
||||
repositoryCount: number;
|
||||
repositories: VariantAnalysisSkippedRepositoryData[];
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepositoryData {
|
||||
id?: number;
|
||||
fullName: string;
|
||||
private?: boolean;
|
||||
stargazersCount?: number;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
@@ -49,45 +49,31 @@ export interface InitialQueryInfo {
|
||||
}
|
||||
|
||||
export class CompletedQueryInfo implements QueryWithResults {
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly message?: string;
|
||||
readonly successful?: boolean;
|
||||
/**
|
||||
* The legacy result. This is only set when loading from the query history.
|
||||
*/
|
||||
readonly result: legacyMessages.EvaluationResult;
|
||||
readonly logFileLocation?: string;
|
||||
resultCount: number;
|
||||
constructor(
|
||||
public readonly query: QueryEvaluationInfo,
|
||||
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
*/
|
||||
sortedResultsInfo: Record<string, SortedResultSetInfo>;
|
||||
/**
|
||||
* The legacy result. This is only set when loading from the query history.
|
||||
*/
|
||||
public readonly result: legacyMessages.EvaluationResult,
|
||||
public readonly logFileLocation: string | undefined,
|
||||
public readonly successful: boolean | undefined,
|
||||
public readonly message: string | undefined,
|
||||
/**
|
||||
* How we're currently sorting alerts. This is not mere interface
|
||||
* state due to truncation; on re-sort, we want to read in the file
|
||||
* again, sort it, and only ship off a reasonable number of results
|
||||
* to the webview. Undefined means to use whatever order is in the
|
||||
* sarif file.
|
||||
*/
|
||||
public interpretedResultsSortState: InterpretedResultsSortState | undefined,
|
||||
public resultCount: number = 0,
|
||||
|
||||
/**
|
||||
* How we're currently sorting alerts. This is not mere interface
|
||||
* state due to truncation; on re-sort, we want to read in the file
|
||||
* again, sort it, and only ship off a reasonable number of results
|
||||
* to the webview. Undefined means to use whatever order is in the
|
||||
* sarif file.
|
||||
*/
|
||||
interpretedResultsSortState: InterpretedResultsSortState | undefined;
|
||||
|
||||
/**
|
||||
* Note that in the {@link readQueryHistoryFromFile} method, we create a CompletedQueryInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(evaluation: QueryWithResults) {
|
||||
this.query = evaluation.query;
|
||||
this.logFileLocation = evaluation.logFileLocation;
|
||||
this.result = evaluation.result;
|
||||
|
||||
this.message = evaluation.message;
|
||||
this.successful = evaluation.successful;
|
||||
|
||||
this.sortedResultsInfo = {};
|
||||
this.resultCount = 0;
|
||||
}
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
*/
|
||||
public sortedResultsInfo: Record<string, SortedResultSetInfo> = {},
|
||||
) {}
|
||||
|
||||
setResultCount(value: number) {
|
||||
this.resultCount = value;
|
||||
@@ -220,20 +206,15 @@ export type CompletedLocalQueryInfo = LocalQueryInfo & {
|
||||
export class LocalQueryInfo {
|
||||
readonly t = "local";
|
||||
|
||||
public failureReason: string | undefined;
|
||||
public completedQuery: CompletedQueryInfo | undefined;
|
||||
public evalLogLocation: string | undefined;
|
||||
public evalLogSummaryLocation: string | undefined;
|
||||
public jsonEvalLogSummaryLocation: string | undefined;
|
||||
public evalLogSummarySymbolsLocation: string | undefined;
|
||||
|
||||
/**
|
||||
* Note that in the {@link readQueryHistoryFromFile} method, we create a FullQueryInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(
|
||||
public readonly initialInfo: InitialQueryInfo,
|
||||
private cancellationSource?: CancellationTokenSource, // used to cancel in progress queries
|
||||
public failureReason?: string,
|
||||
public completedQuery?: CompletedQueryInfo,
|
||||
public evalLogLocation?: string,
|
||||
public evalLogSummaryLocation?: string,
|
||||
public jsonEvalLogSummaryLocation?: string,
|
||||
public evalLogSummarySymbolsLocation?: string,
|
||||
) {
|
||||
/**/
|
||||
}
|
||||
@@ -301,7 +282,14 @@ export class LocalQueryInfo {
|
||||
}
|
||||
|
||||
completeThisQuery(info: QueryWithResults): void {
|
||||
this.completedQuery = new CompletedQueryInfo(info);
|
||||
this.completedQuery = new CompletedQueryInfo(
|
||||
info.query,
|
||||
info.result,
|
||||
info.logFileLocation,
|
||||
info.successful,
|
||||
info.message,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// dispose of the cancellation token source and also ensure the source is not serialized as JSON
|
||||
this.cancellationSource?.dispose();
|
||||
|
||||
@@ -68,7 +68,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
if (config.onDidChangeConfiguration !== undefined) {
|
||||
this.push(
|
||||
config.onDidChangeConfiguration(() =>
|
||||
app.commands.execute("codeQL.restartQueryServer"),
|
||||
app.commands.execute("codeQL.restartQueryServerOnConfigChange"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export class QueryEvaluationInfo extends QueryOutputDir {
|
||||
constructor(
|
||||
querySaveDir: string,
|
||||
public readonly dbItemPath: string,
|
||||
private readonly databaseHasMetadataFile: boolean,
|
||||
public readonly databaseHasMetadataFile: boolean,
|
||||
public readonly quickEvalPosition?: messages.Position,
|
||||
public readonly metadata?: QueryMetadata,
|
||||
) {
|
||||
@@ -382,10 +382,10 @@ export class QueryEvaluationInfo extends QueryOutputDir {
|
||||
|
||||
export interface QueryWithResults {
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly result: legacyMessages.EvaluationResult;
|
||||
readonly logFileLocation?: string;
|
||||
readonly successful?: boolean;
|
||||
readonly message?: string;
|
||||
readonly result: legacyMessages.EvaluationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,8 +29,8 @@ For more information about how to write stories and how to add controls, please
|
||||
|
||||
### WebView UI Toolkit
|
||||
|
||||
As much as possible, we try to make use of the [WebView UI Toolkit](https://github.com/microsoft/vscode-webview-ui-toolkit). The Storybook
|
||||
for the WebView UI Toolkit can be found [here](https://microsoft.github.io/vscode-webview-ui-toolkit/).
|
||||
As much as possible, we try to make use of the [WebView UI Toolkit](https://github.com/microsoft/vscode-webview-ui-toolkit): See the
|
||||
[WebView UI Toolkit Storybook here](https://microsoft.github.io/vscode-webview-ui-toolkit/).
|
||||
|
||||
### Updating VSCode CSS variables
|
||||
|
||||
@@ -45,12 +45,12 @@ select the **Dark+** theme. You can use **Preferences: Color Theme** in the *Com
|
||||
4. Select **Developer: Open WebView Developer Tools**
|
||||
5. Now, you will need to find the `<html>` element in the lowest-level `<iframe>`. See the image below:
|
||||
|
||||
<img src={iframeImage} />
|
||||
<img src={iframeImage} />
|
||||
|
||||
6. Once you have selected the `<html>` element as in the image above, click on **Show All Properties (... more)** (see image below). This will
|
||||
expand all CSS variables.
|
||||
|
||||
<img src={stylesImage} />
|
||||
<img src={stylesImage} />
|
||||
|
||||
7. Copy all variables to the `src/stories/vscode-theme-dark.css` file.
|
||||
8. Now, select the `<body>` element which is a direct child of the `<html>` element.
|
||||
|
||||
@@ -112,7 +112,7 @@ export function generateVariantAnalysisMarkdownSummary(
|
||||
lines.push(`### Results for "${query.name}"`, "");
|
||||
|
||||
// Expandable section containing query text
|
||||
const queryCodeBlock = ["```ql", ...query.text.split("\n"), "```"];
|
||||
const queryCodeBlock = ["```ql", ...query.text.split("\n"), "```", ""];
|
||||
lines.push(...buildExpandableMarkdownSection("Query", queryCodeBlock));
|
||||
|
||||
// Padding between sections
|
||||
@@ -128,6 +128,9 @@ export function generateVariantAnalysisMarkdownSummary(
|
||||
lines.push(`| ${fullName} | [${summary.resultCount} result(s)](${link}) |`);
|
||||
}
|
||||
|
||||
// Add a trailing newline
|
||||
lines.push("");
|
||||
|
||||
return {
|
||||
fileName: "_summary",
|
||||
content: lines,
|
||||
@@ -279,8 +282,8 @@ function generateMarkdownForPathResults(
|
||||
);
|
||||
// Indent the snippet to fit with the numbered list.
|
||||
// The indentation is "n + 2" where the list number is an n-digit number.
|
||||
const codeSnippetIndented = codeSnippet.map(
|
||||
(line) => " ".repeat(listNumber.toString().length + 2) + line,
|
||||
const codeSnippetIndented = codeSnippet.map((line) =>
|
||||
(" ".repeat(listNumber.toString().length + 2) + line).trimEnd(),
|
||||
);
|
||||
pathLines.push(`${listNumber}. ${link}`, ...codeSnippetIndented);
|
||||
}
|
||||
@@ -382,7 +385,6 @@ function buildExpandableMarkdownSection(
|
||||
`<summary>${title}</summary>`,
|
||||
"",
|
||||
...contents,
|
||||
"",
|
||||
"</details>",
|
||||
"",
|
||||
);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface VariantAnalysisScannedRepositoryStateData {
|
||||
repositoryId: number;
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadData;
|
||||
downloadPercentage?: number;
|
||||
}
|
||||
|
||||
export enum VariantAnalysisScannedRepositoryDownloadData {
|
||||
Pending = "pending",
|
||||
InProgress = "inProgress",
|
||||
Succeeded = "succeeded",
|
||||
Failed = "failed",
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { outputJson, readJson } from "fs-extra";
|
||||
import { VariantAnalysisScannedRepositoryState } from "../shared/variant-analysis";
|
||||
import { VariantAnalysisScannedRepositoryStateData } from "./repo-states-data-types";
|
||||
import { mapRepoStateToData } from "./repo-states-to-data-mapper";
|
||||
import { mapRepoStateToDomain } from "./repo-states-to-domain-mapper";
|
||||
|
||||
export const REPO_STATES_FILENAME = "repo_states.json";
|
||||
|
||||
export async function writeRepoStates(
|
||||
storagePath: string,
|
||||
repoStates: Record<number, VariantAnalysisScannedRepositoryState>,
|
||||
): Promise<void> {
|
||||
// Map from repoStates Domain type to the repoStates Data type
|
||||
const repoStatesData = Object.fromEntries(
|
||||
Object.entries(repoStates).map(([key, value]) => {
|
||||
return [key, mapRepoStateToData(value)];
|
||||
}),
|
||||
);
|
||||
|
||||
await outputJson(storagePath, repoStatesData);
|
||||
}
|
||||
|
||||
export async function readRepoStates(
|
||||
storagePath: string,
|
||||
): Promise<Record<number, VariantAnalysisScannedRepositoryState> | undefined> {
|
||||
try {
|
||||
const repoStatesData: Record<
|
||||
number,
|
||||
VariantAnalysisScannedRepositoryStateData
|
||||
> = await readJson(storagePath);
|
||||
|
||||
// Map from repoStates Data type to the repoStates Domain type
|
||||
const repoStates = Object.fromEntries(
|
||||
Object.entries(repoStatesData).map(([key, value]) => {
|
||||
return [key, mapRepoStateToDomain(value)];
|
||||
}),
|
||||
);
|
||||
|
||||
return repoStates;
|
||||
} catch (e) {
|
||||
// Ignore this error, we simply might not have downloaded anything yet
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import {
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from "../shared/variant-analysis";
|
||||
import {
|
||||
VariantAnalysisScannedRepositoryDownloadData,
|
||||
VariantAnalysisScannedRepositoryStateData,
|
||||
} from "./repo-states-data-types";
|
||||
|
||||
export function mapRepoStateToData(
|
||||
repoState: VariantAnalysisScannedRepositoryState,
|
||||
): VariantAnalysisScannedRepositoryStateData {
|
||||
return {
|
||||
repositoryId: repoState.repositoryId,
|
||||
downloadStatus: processDownloadStatus(repoState.downloadStatus),
|
||||
downloadPercentage: repoState.downloadPercentage,
|
||||
};
|
||||
}
|
||||
|
||||
function processDownloadStatus(
|
||||
downloadedStatus: VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
) {
|
||||
switch (downloadedStatus) {
|
||||
case VariantAnalysisScannedRepositoryDownloadStatus.Pending:
|
||||
return VariantAnalysisScannedRepositoryDownloadData.Pending;
|
||||
case VariantAnalysisScannedRepositoryDownloadStatus.InProgress:
|
||||
return VariantAnalysisScannedRepositoryDownloadData.InProgress;
|
||||
case VariantAnalysisScannedRepositoryDownloadStatus.Succeeded:
|
||||
return VariantAnalysisScannedRepositoryDownloadData.Succeeded;
|
||||
case VariantAnalysisScannedRepositoryDownloadStatus.Failed:
|
||||
return VariantAnalysisScannedRepositoryDownloadData.Failed;
|
||||
default:
|
||||
assertNever(downloadedStatus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import {
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
} from "../shared/variant-analysis";
|
||||
import {
|
||||
VariantAnalysisScannedRepositoryStateData,
|
||||
VariantAnalysisScannedRepositoryDownloadData,
|
||||
} from "./repo-states-data-types";
|
||||
|
||||
export function mapRepoStateToDomain(
|
||||
repoState: VariantAnalysisScannedRepositoryStateData,
|
||||
): VariantAnalysisScannedRepositoryState {
|
||||
return {
|
||||
repositoryId: repoState.repositoryId,
|
||||
downloadStatus: processDownloadStatus(repoState.downloadStatus),
|
||||
downloadPercentage: repoState.downloadPercentage,
|
||||
};
|
||||
}
|
||||
|
||||
function processDownloadStatus(
|
||||
downloadedStatus: VariantAnalysisScannedRepositoryDownloadData,
|
||||
) {
|
||||
switch (downloadedStatus) {
|
||||
case VariantAnalysisScannedRepositoryDownloadData.Pending:
|
||||
return VariantAnalysisScannedRepositoryDownloadStatus.Pending;
|
||||
case VariantAnalysisScannedRepositoryDownloadData.InProgress:
|
||||
return VariantAnalysisScannedRepositoryDownloadStatus.InProgress;
|
||||
case VariantAnalysisScannedRepositoryDownloadData.Succeeded:
|
||||
return VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
|
||||
case VariantAnalysisScannedRepositoryDownloadData.Failed:
|
||||
return VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
default:
|
||||
assertNever(downloadedStatus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface VariantAnalysisRepositoryTaskData {
|
||||
repository: RepositoryData;
|
||||
analysisStatus: VariantAnalysisRepoStatusData;
|
||||
resultCount?: number;
|
||||
artifactSizeInBytes?: number;
|
||||
failureMessage?: string;
|
||||
databaseCommitSha?: string;
|
||||
sourceLocationPrefix?: string;
|
||||
artifactUrl?: string;
|
||||
}
|
||||
|
||||
interface RepositoryData {
|
||||
id: number;
|
||||
fullName: string;
|
||||
private: boolean;
|
||||
}
|
||||
|
||||
export enum VariantAnalysisRepoStatusData {
|
||||
Pending = "pending",
|
||||
InProgress = "inProgress",
|
||||
Succeeded = "succeeded",
|
||||
Failed = "failed",
|
||||
Canceled = "canceled",
|
||||
TimedOut = "timedOut",
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { outputJson, readJson } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import { VariantAnalysisRepositoryTask } from "../shared/variant-analysis";
|
||||
import { mapRepoTaskToData } from "./repo-task-to-data-mapper";
|
||||
import { mapRepoTaskToDomain } from "./repo-task-to-domain-mapper";
|
||||
|
||||
export const REPO_TASK_FILENAME = "repo_task.json";
|
||||
|
||||
export async function writeRepoTask(
|
||||
storageDirectory: string,
|
||||
repoTask: VariantAnalysisRepositoryTask,
|
||||
): Promise<void> {
|
||||
const repoTaskData = mapRepoTaskToData(repoTask);
|
||||
await outputJson(join(storageDirectory, REPO_TASK_FILENAME), repoTaskData);
|
||||
}
|
||||
|
||||
export async function readRepoTask(
|
||||
storageDirectory: string,
|
||||
): Promise<VariantAnalysisRepositoryTask> {
|
||||
const repoTaskData = await readJson(
|
||||
join(storageDirectory, REPO_TASK_FILENAME),
|
||||
);
|
||||
return mapRepoTaskToDomain(repoTaskData);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import {
|
||||
VariantAnalysisRepositoryTask,
|
||||
VariantAnalysisRepoStatus,
|
||||
} from "../shared/variant-analysis";
|
||||
import {
|
||||
VariantAnalysisRepositoryTaskData,
|
||||
VariantAnalysisRepoStatusData,
|
||||
} from "./repo-task-data-types";
|
||||
|
||||
export function mapRepoTaskToData(
|
||||
repoTask: VariantAnalysisRepositoryTask,
|
||||
): VariantAnalysisRepositoryTaskData {
|
||||
return {
|
||||
repository: {
|
||||
id: repoTask.repository.id,
|
||||
fullName: repoTask.repository.fullName,
|
||||
private: repoTask.repository.private,
|
||||
},
|
||||
analysisStatus: mapRepoTaskAnalysisStatusToData(repoTask.analysisStatus),
|
||||
resultCount: repoTask.resultCount,
|
||||
artifactSizeInBytes: repoTask.artifactSizeInBytes,
|
||||
failureMessage: repoTask.failureMessage,
|
||||
databaseCommitSha: repoTask.databaseCommitSha,
|
||||
sourceLocationPrefix: repoTask.sourceLocationPrefix,
|
||||
artifactUrl: repoTask.artifactUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function mapRepoTaskAnalysisStatusToData(
|
||||
analysisStatus: VariantAnalysisRepoStatus,
|
||||
): VariantAnalysisRepoStatusData {
|
||||
switch (analysisStatus) {
|
||||
case VariantAnalysisRepoStatus.Pending:
|
||||
return VariantAnalysisRepoStatusData.Pending;
|
||||
case VariantAnalysisRepoStatus.InProgress:
|
||||
return VariantAnalysisRepoStatusData.InProgress;
|
||||
case VariantAnalysisRepoStatus.Succeeded:
|
||||
return VariantAnalysisRepoStatusData.Succeeded;
|
||||
case VariantAnalysisRepoStatus.Failed:
|
||||
return VariantAnalysisRepoStatusData.Failed;
|
||||
case VariantAnalysisRepoStatus.Canceled:
|
||||
return VariantAnalysisRepoStatusData.Canceled;
|
||||
case VariantAnalysisRepoStatus.TimedOut:
|
||||
return VariantAnalysisRepoStatusData.TimedOut;
|
||||
default:
|
||||
assertNever(analysisStatus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import {
|
||||
VariantAnalysisRepositoryTask,
|
||||
VariantAnalysisRepoStatus,
|
||||
} from "../shared/variant-analysis";
|
||||
import {
|
||||
VariantAnalysisRepositoryTaskData,
|
||||
VariantAnalysisRepoStatusData,
|
||||
} from "./repo-task-data-types";
|
||||
|
||||
export function mapRepoTaskToDomain(
|
||||
repoTask: VariantAnalysisRepositoryTaskData,
|
||||
): VariantAnalysisRepositoryTask {
|
||||
return {
|
||||
repository: {
|
||||
id: repoTask.repository.id,
|
||||
fullName: repoTask.repository.fullName,
|
||||
private: repoTask.repository.private,
|
||||
},
|
||||
analysisStatus: mapRepoTaskAnalysisStatusToDomain(repoTask.analysisStatus),
|
||||
resultCount: repoTask.resultCount,
|
||||
artifactSizeInBytes: repoTask.artifactSizeInBytes,
|
||||
failureMessage: repoTask.failureMessage,
|
||||
databaseCommitSha: repoTask.databaseCommitSha,
|
||||
sourceLocationPrefix: repoTask.sourceLocationPrefix,
|
||||
artifactUrl: repoTask.artifactUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function mapRepoTaskAnalysisStatusToDomain(
|
||||
analysisStatus: VariantAnalysisRepoStatusData,
|
||||
): VariantAnalysisRepoStatus {
|
||||
switch (analysisStatus) {
|
||||
case VariantAnalysisRepoStatusData.Pending:
|
||||
return VariantAnalysisRepoStatus.Pending;
|
||||
case VariantAnalysisRepoStatusData.InProgress:
|
||||
return VariantAnalysisRepoStatus.InProgress;
|
||||
case VariantAnalysisRepoStatusData.Succeeded:
|
||||
return VariantAnalysisRepoStatus.Succeeded;
|
||||
case VariantAnalysisRepoStatusData.Failed:
|
||||
return VariantAnalysisRepoStatus.Failed;
|
||||
case VariantAnalysisRepoStatusData.Canceled:
|
||||
return VariantAnalysisRepoStatus.Canceled;
|
||||
case VariantAnalysisRepoStatusData.TimedOut:
|
||||
return VariantAnalysisRepoStatus.TimedOut;
|
||||
default:
|
||||
assertNever(analysisStatus);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
showAndLogInformationMessage,
|
||||
showAndLogWarningMessage,
|
||||
} from "../helpers";
|
||||
import { readFile, readJson, remove, pathExists, outputJson } from "fs-extra";
|
||||
import { readFile, remove, pathExists } from "fs-extra";
|
||||
import { EOL } from "os";
|
||||
import { cancelVariantAnalysis } from "./gh-api/gh-actions-api-client";
|
||||
import {
|
||||
@@ -67,12 +67,16 @@ import { App } from "../common/app";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { AppCommandManager, VariantAnalysisCommands } from "../common/commands";
|
||||
import { exportVariantAnalysisResults } from "./export-results";
|
||||
import {
|
||||
readRepoStates,
|
||||
REPO_STATES_FILENAME,
|
||||
writeRepoStates,
|
||||
} from "./store/repo-states-store";
|
||||
|
||||
export class VariantAnalysisManager
|
||||
extends DisposableObject
|
||||
implements VariantAnalysisViewManager<VariantAnalysisView>
|
||||
{
|
||||
private static readonly REPO_STATES_FILENAME = "repo_states.json";
|
||||
private static readonly DOWNLOAD_PERCENTAGE_UPDATE_DELAY_MS = 500;
|
||||
|
||||
private readonly _onVariantAnalysisAdded = this.push(
|
||||
@@ -136,7 +140,10 @@ export class VariantAnalysisManager
|
||||
"codeQL.copyVariantAnalysisRepoList":
|
||||
this.copyRepoListToClipboard.bind(this),
|
||||
"codeQL.loadVariantAnalysisRepoResults": this.loadResults.bind(this),
|
||||
"codeQL.monitorVariantAnalysis": this.monitorVariantAnalysis.bind(this),
|
||||
"codeQL.monitorNewVariantAnalysis":
|
||||
this.monitorVariantAnalysis.bind(this),
|
||||
"codeQL.monitorRehydratedVariantAnalysis":
|
||||
this.monitorVariantAnalysis.bind(this),
|
||||
"codeQL.openVariantAnalysisLogs": this.openVariantAnalysisLogs.bind(this),
|
||||
"codeQL.openVariantAnalysisView": this.showView.bind(this),
|
||||
"codeQL.runVariantAnalysis":
|
||||
@@ -244,7 +251,7 @@ export class VariantAnalysisManager
|
||||
processedVariantAnalysis.id,
|
||||
);
|
||||
void this.app.commands.execute(
|
||||
"codeQL.monitorVariantAnalysis",
|
||||
"codeQL.monitorNewVariantAnalysis",
|
||||
processedVariantAnalysis,
|
||||
);
|
||||
}
|
||||
@@ -257,15 +264,11 @@ export class VariantAnalysisManager
|
||||
} else {
|
||||
await this.setVariantAnalysis(variantAnalysis);
|
||||
|
||||
try {
|
||||
const repoStates = await readJson(
|
||||
this.getRepoStatesStoragePath(variantAnalysis.id),
|
||||
);
|
||||
this.repoStates.set(variantAnalysis.id, repoStates);
|
||||
} catch (e) {
|
||||
// Ignore this error, we simply might not have downloaded anything yet
|
||||
this.repoStates.set(variantAnalysis.id, {});
|
||||
}
|
||||
const repoStatesFromDisk = await readRepoStates(
|
||||
this.getRepoStatesStoragePath(variantAnalysis.id),
|
||||
);
|
||||
|
||||
this.repoStates.set(variantAnalysis.id, repoStatesFromDisk || {});
|
||||
|
||||
if (
|
||||
!(await isVariantAnalysisComplete(
|
||||
@@ -274,7 +277,7 @@ export class VariantAnalysisManager
|
||||
))
|
||||
) {
|
||||
void this.app.commands.execute(
|
||||
"codeQL.monitorVariantAnalysis",
|
||||
"codeQL.monitorRehydratedVariantAnalysis",
|
||||
variantAnalysis,
|
||||
);
|
||||
}
|
||||
@@ -591,10 +594,13 @@ export class VariantAnalysisManager
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
|
||||
await outputJson(
|
||||
this.getRepoStatesStoragePath(variantAnalysis.id),
|
||||
this.repoStates.get(variantAnalysis.id),
|
||||
);
|
||||
const repoStates = this.repoStates.get(variantAnalysis.id);
|
||||
if (repoStates) {
|
||||
await writeRepoStates(
|
||||
this.getRepoStatesStoragePath(variantAnalysis.id),
|
||||
repoStates,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async enqueueDownload(
|
||||
@@ -696,7 +702,7 @@ export class VariantAnalysisManager
|
||||
private getRepoStatesStoragePath(variantAnalysisId: number): string {
|
||||
return join(
|
||||
this.getVariantAnalysisStorageLocation(variantAnalysisId),
|
||||
VariantAnalysisManager.REPO_STATES_FILENAME,
|
||||
REPO_STATES_FILENAME,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { appendFile, pathExists, mkdir, outputJson, readJson } from "fs-extra";
|
||||
import { appendFile, pathExists } from "fs-extra";
|
||||
import fetch from "node-fetch";
|
||||
import { EOL } from "os";
|
||||
import { join } from "path";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { DisposableObject, DisposeHandler } from "../pure/disposable-object";
|
||||
import { EventEmitter } from "vscode";
|
||||
import { unzipFile } from "../pure/zip";
|
||||
import { readRepoTask, writeRepoTask } from "./store/repo-task-store";
|
||||
|
||||
type CacheKey = `${number}/${string}`;
|
||||
|
||||
@@ -37,7 +38,6 @@ export type LoadResultsOptions = {
|
||||
};
|
||||
|
||||
export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
private static readonly REPO_TASK_FILENAME = "repo_task.json";
|
||||
private static readonly RESULTS_DIRECTORY = "results";
|
||||
|
||||
private readonly cachedResults: Map<
|
||||
@@ -78,14 +78,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
repoTask.repository.fullName,
|
||||
);
|
||||
|
||||
if (!(await pathExists(resultDirectory))) {
|
||||
await mkdir(resultDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
await outputJson(
|
||||
join(resultDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME),
|
||||
repoTask,
|
||||
);
|
||||
await writeRepoTask(resultDirectory, repoTask);
|
||||
|
||||
const zipFilePath = join(resultDirectory, "results.zip");
|
||||
|
||||
@@ -184,8 +177,8 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
repositoryFullName,
|
||||
);
|
||||
|
||||
const repoTask: VariantAnalysisRepositoryTask = await readJson(
|
||||
join(storageDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME),
|
||||
const repoTask: VariantAnalysisRepositoryTask = await readRepoTask(
|
||||
storageDirectory,
|
||||
);
|
||||
|
||||
if (!repoTask.databaseCommitSha || !repoTask.sourceLocationPrefix) {
|
||||
|
||||
@@ -37,6 +37,14 @@ export class VariantAnalysisViewSerializer implements WebviewPanelSerializer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Between the time the webview is deserialized and the time the extension
|
||||
// is fully activated, the user may close the webview. In this case, we
|
||||
// should not attempt to restore the view.
|
||||
let disposed = false;
|
||||
const unregisterOnDidDispose = webviewPanel.onDidDispose(() => {
|
||||
disposed = true;
|
||||
});
|
||||
|
||||
const variantAnalysisState: VariantAnalysisState =
|
||||
state as VariantAnalysisState;
|
||||
|
||||
@@ -46,11 +54,16 @@ export class VariantAnalysisViewSerializer implements WebviewPanelSerializer {
|
||||
variantAnalysisState.variantAnalysisId,
|
||||
);
|
||||
if (existingView) {
|
||||
unregisterOnDidDispose.dispose();
|
||||
await existingView.openView();
|
||||
webviewPanel.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = new VariantAnalysisView(
|
||||
this.ctx,
|
||||
this.app,
|
||||
@@ -58,6 +71,8 @@ export class VariantAnalysisViewSerializer implements WebviewPanelSerializer {
|
||||
manager,
|
||||
);
|
||||
await view.restoreView(webviewPanel);
|
||||
|
||||
unregisterOnDidDispose.dispose();
|
||||
}
|
||||
|
||||
private waitForExtensionFullyLoaded(): Promise<
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import * as React from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ShowProgressMessage,
|
||||
ToDataExtensionsEditorMessage,
|
||||
} from "../../pure/interface-types";
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeDataGrid,
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import styled from "styled-components";
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
import { MethodRow } from "./MethodRow";
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { calculateSupportedPercentage } from "./supported";
|
||||
|
||||
export const DataExtensionsEditorContainer = styled.div`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
type ProgressBarProps = {
|
||||
completion: number;
|
||||
};
|
||||
|
||||
const ProgressBar = styled.div<ProgressBarProps>`
|
||||
height: 10px;
|
||||
width: ${(props) => props.completion * 100}%;
|
||||
|
||||
background-color: var(--vscode-progressBar-background);
|
||||
`;
|
||||
|
||||
export function DataExtensionsEditor(): JSX.Element {
|
||||
const [externalApiUsages, setExternalApiUsages] = useState<
|
||||
ExternalApiUsage[]
|
||||
>([]);
|
||||
const [modeledMethods, setModeledMethods] = useState<
|
||||
Record<string, ModeledMethod>
|
||||
>({});
|
||||
const [progress, setProgress] = useState<Omit<ShowProgressMessage, "t">>({
|
||||
step: 0,
|
||||
maxStep: 0,
|
||||
message: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToDataExtensionsEditorMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setExternalApiUsages":
|
||||
setExternalApiUsages(msg.externalApiUsages);
|
||||
break;
|
||||
case "showProgress":
|
||||
setProgress(msg);
|
||||
break;
|
||||
case "setExistingModeledMethods":
|
||||
setModeledMethods((oldModeledMethods) => {
|
||||
return {
|
||||
...msg.existingModeledMethods,
|
||||
...oldModeledMethods,
|
||||
};
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const supportedPercentage = useMemo(
|
||||
() => calculateSupportedPercentage(externalApiUsages),
|
||||
[externalApiUsages],
|
||||
);
|
||||
|
||||
const unsupportedPercentage = 100 - supportedPercentage;
|
||||
|
||||
const onChange = useCallback(
|
||||
(method: ExternalApiUsage, model: ModeledMethod) => {
|
||||
setModeledMethods((oldModeledMethods) => ({
|
||||
...oldModeledMethods,
|
||||
[method.signature]: model,
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onApplyClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "saveModeledMethods",
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
});
|
||||
}, [externalApiUsages, modeledMethods]);
|
||||
|
||||
return (
|
||||
<DataExtensionsEditorContainer>
|
||||
{progress.maxStep > 0 && (
|
||||
<p>
|
||||
<ProgressBar completion={progress.step / progress.maxStep} />{" "}
|
||||
{progress.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{externalApiUsages.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h3>External API support stats</h3>
|
||||
<ul>
|
||||
<li>Supported: {supportedPercentage.toFixed(2)}%</li>
|
||||
<li>Unsupported: {unsupportedPercentage.toFixed(2)}%</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>External API modelling</h3>
|
||||
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
|
||||
<VSCodeDataGrid>
|
||||
<VSCodeDataGridRow rowType="header">
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
|
||||
Type
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={2}>
|
||||
Method
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
|
||||
Usages
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={4}>
|
||||
Model type
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={5}>
|
||||
Input
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={6}>
|
||||
Output
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={7}>
|
||||
Kind
|
||||
</VSCodeDataGridCell>
|
||||
</VSCodeDataGridRow>
|
||||
{externalApiUsages.map((externalApiUsage) => (
|
||||
<MethodRow
|
||||
key={externalApiUsage.signature}
|
||||
externalApiUsage={externalApiUsage}
|
||||
modeledMethod={modeledMethods[externalApiUsage.signature]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
</VSCodeDataGrid>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DataExtensionsEditorContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
VSCodeDropdown,
|
||||
VSCodeOption,
|
||||
VSCodeTextField,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import * as React from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
import { vscode } from "../vscode-api";
|
||||
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import {
|
||||
ModeledMethod,
|
||||
ModeledMethodType,
|
||||
} from "../../data-extensions-editor/modeled-method";
|
||||
|
||||
const Dropdown = styled(VSCodeDropdown)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const TextField = styled(VSCodeTextField)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type SupportedUnsupportedSpanProps = {
|
||||
supported: boolean;
|
||||
};
|
||||
|
||||
const SupportSpan = styled.span<SupportedUnsupportedSpanProps>`
|
||||
color: ${(props) => (props.supported ? "green" : "red")};
|
||||
`;
|
||||
|
||||
const UsagesButton = styled.button`
|
||||
color: var(--vscode-editor-foreground);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
externalApiUsage: ExternalApiUsage;
|
||||
modeledMethod: ModeledMethod | undefined;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const MethodRow = ({
|
||||
externalApiUsage,
|
||||
modeledMethod,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const argumentsList = useMemo(() => {
|
||||
if (externalApiUsage.methodParameters === "()") {
|
||||
return [];
|
||||
}
|
||||
return externalApiUsage.methodParameters
|
||||
.substring(1, externalApiUsage.methodParameters.length - 1)
|
||||
.split(",");
|
||||
}, [externalApiUsage.methodParameters]);
|
||||
|
||||
const handleTypeInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(externalApiUsage, {
|
||||
// If there are no arguments, we will default to "this", which is Argument[-1]
|
||||
input: argumentsList.length === 0 ? "Argument[-1]" : "Argument[0]",
|
||||
output: "ReturnType",
|
||||
kind: "value",
|
||||
...modeledMethod,
|
||||
type: target.value as ModeledMethodType,
|
||||
});
|
||||
},
|
||||
[onChange, externalApiUsage, modeledMethod, argumentsList],
|
||||
);
|
||||
const handleInputInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
if (!modeledMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(externalApiUsage, {
|
||||
...modeledMethod,
|
||||
input: target.value as ModeledMethod["input"],
|
||||
});
|
||||
},
|
||||
[onChange, externalApiUsage, modeledMethod],
|
||||
);
|
||||
const handleOutputInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
if (!modeledMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(externalApiUsage, {
|
||||
...modeledMethod,
|
||||
output: target.value as ModeledMethod["output"],
|
||||
});
|
||||
},
|
||||
[onChange, externalApiUsage, modeledMethod],
|
||||
);
|
||||
const handleKindInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
if (!modeledMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(externalApiUsage, {
|
||||
...modeledMethod,
|
||||
kind: target.value as ModeledMethod["kind"],
|
||||
});
|
||||
},
|
||||
[onChange, externalApiUsage, modeledMethod],
|
||||
);
|
||||
|
||||
const jumpToUsage = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "jumpToUsage",
|
||||
location: externalApiUsage.usages[0].url,
|
||||
});
|
||||
}, [externalApiUsage]);
|
||||
|
||||
return (
|
||||
<VSCodeDataGridRow>
|
||||
<VSCodeDataGridCell gridColumn={1}>
|
||||
<SupportSpan supported={externalApiUsage.supported}>
|
||||
{externalApiUsage.packageName}.{externalApiUsage.typeName}
|
||||
</SupportSpan>
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={2}>
|
||||
<SupportSpan supported={externalApiUsage.supported}>
|
||||
{externalApiUsage.methodName}
|
||||
{externalApiUsage.methodParameters}
|
||||
</SupportSpan>
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={3}>
|
||||
<UsagesButton onClick={jumpToUsage}>
|
||||
{externalApiUsage.usages.length}
|
||||
</UsagesButton>
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={4}>
|
||||
{(!externalApiUsage.supported ||
|
||||
(modeledMethod && modeledMethod?.type !== "none")) && (
|
||||
<Dropdown
|
||||
value={modeledMethod?.type ?? "none"}
|
||||
onInput={handleTypeInput}
|
||||
>
|
||||
<VSCodeOption value="none">Unmodelled</VSCodeOption>
|
||||
<VSCodeOption value="source">Source</VSCodeOption>
|
||||
<VSCodeOption value="sink">Sink</VSCodeOption>
|
||||
<VSCodeOption value="summary">Flow summary</VSCodeOption>
|
||||
<VSCodeOption value="neutral">Neutral</VSCodeOption>
|
||||
</Dropdown>
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={5}>
|
||||
{modeledMethod?.type &&
|
||||
["sink", "summary"].includes(modeledMethod?.type) && (
|
||||
<Dropdown value={modeledMethod?.input} onInput={handleInputInput}>
|
||||
<VSCodeOption value="Argument[-1]">
|
||||
Argument[-1]: this
|
||||
</VSCodeOption>
|
||||
{argumentsList.map((argument, index) => (
|
||||
<VSCodeOption key={argument} value={`Argument[${index}]`}>
|
||||
Argument[{index}]: {argument}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</Dropdown>
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={6}>
|
||||
{modeledMethod?.type &&
|
||||
["source", "summary"].includes(modeledMethod?.type) && (
|
||||
<Dropdown value={modeledMethod?.output} onInput={handleOutputInput}>
|
||||
<VSCodeOption value="ReturnValue">ReturnValue</VSCodeOption>
|
||||
<VSCodeOption value="Argument[-1]">
|
||||
Argument[-1]: this
|
||||
</VSCodeOption>
|
||||
{argumentsList.map((argument, index) => (
|
||||
<VSCodeOption key={argument} value={`Argument[${index}]`}>
|
||||
Argument[{index}]: {argument}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</Dropdown>
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={7}>
|
||||
{modeledMethod?.type &&
|
||||
["source", "sink", "summary"].includes(modeledMethod?.type) && (
|
||||
<TextField value={modeledMethod?.kind} onInput={handleKindInput} />
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
</VSCodeDataGridRow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { calculateSupportedPercentage } from "../supported";
|
||||
|
||||
describe("calculateSupportedPercentage", () => {
|
||||
it("when there are no external API usages", () => {
|
||||
expect(calculateSupportedPercentage([])).toBe(0);
|
||||
});
|
||||
|
||||
it("when there are is 1 supported external API usage", () => {
|
||||
expect(
|
||||
calculateSupportedPercentage([
|
||||
{
|
||||
supported: true,
|
||||
},
|
||||
]),
|
||||
).toBe(100);
|
||||
});
|
||||
|
||||
it("when there are is 1 unsupported external API usage", () => {
|
||||
expect(
|
||||
calculateSupportedPercentage([
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
]),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("when there are multiple supporte and unsupported external API usage", () => {
|
||||
expect(
|
||||
calculateSupportedPercentage([
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
{
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
{
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
]),
|
||||
).toBeCloseTo(33.33);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from "react";
|
||||
import { WebviewDefinition } from "../webview-definition";
|
||||
import { DataExtensionsEditor } from "./DataExtensionsEditor";
|
||||
|
||||
const definition: WebviewDefinition = {
|
||||
component: <DataExtensionsEditor />,
|
||||
};
|
||||
|
||||
export default definition;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
|
||||
export function calculateSupportedPercentage(
|
||||
externalApiUsages: Array<Pick<ExternalApiUsage, "supported">>,
|
||||
): number {
|
||||
if (externalApiUsages.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const supportedExternalApiUsages = externalApiUsages.filter(
|
||||
(m) => m.supported,
|
||||
);
|
||||
|
||||
const supportedRatio =
|
||||
supportedExternalApiUsages.length / externalApiUsages.length;
|
||||
return supportedRatio * 100;
|
||||
}
|
||||
@@ -83,8 +83,8 @@ const config: Config = {
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<rootDir>/test/__mocks__/fileMock.ts",
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.ts",
|
||||
"<rootDir>/../../test/__mocks__/fileMock.ts",
|
||||
"\\.(css|less)$": "<rootDir>/../../test/__mocks__/styleMock.ts",
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
@@ -186,7 +186,7 @@ const config: Config = {
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
transformIgnorePatterns: [
|
||||
// These use ES modules, so need to be transformed
|
||||
"node_modules/(?!(?:@vscode/webview-ui-toolkit|@microsoft/.+|exenv-es6)/.*)",
|
||||
"node_modules/(?!(?:@vscode/webview-ui-toolkit|@microsoft/.+|exenv-es6|d3|d3-(.*)|internmap|delaunator|robust-predicates)/.*)",
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
|
||||
@@ -15,6 +15,11 @@ Object.defineProperty(window, "matchMedia", {
|
||||
})),
|
||||
});
|
||||
|
||||
// Used by Primer React
|
||||
(window as any).CSS = {
|
||||
supports: jest.fn().mockResolvedValue(false),
|
||||
};
|
||||
|
||||
// Store this on the window so we can mock it
|
||||
(window as any).vsCodeApi = {
|
||||
postMessage: jest.fn(),
|
||||
|
||||
120
extensions/ql-vscode/src/view/results/__tests__/results.spec.tsx
Normal file
120
extensions/ql-vscode/src/view/results/__tests__/results.spec.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as React from "react";
|
||||
import { render as reactRender, screen } from "@testing-library/react";
|
||||
import { ResultsApp } from "../results";
|
||||
import {
|
||||
Interpretation,
|
||||
IntoResultsViewMsg,
|
||||
SortDirection,
|
||||
} from "../../../pure/interface-types";
|
||||
import * as fs from "fs-extra";
|
||||
import { resolve } from "path";
|
||||
import { ColumnKindCode } from "../../../pure/bqrs-cli-types";
|
||||
|
||||
const exampleSarif = fs.readJSONSync(
|
||||
resolve(
|
||||
__dirname,
|
||||
"../../../../test/vscode-tests/no-workspace/data/sarif/validSarif.sarif",
|
||||
),
|
||||
);
|
||||
|
||||
describe(ResultsApp.name, () => {
|
||||
const render = () => reactRender(<ResultsApp />);
|
||||
const postMessage = async (msg: IntoResultsViewMsg) => {
|
||||
// window.postMessage doesn't set the origin correctly, see
|
||||
// https://github.com/jsdom/jsdom/issues/2745
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
source: window,
|
||||
origin: window.location.origin,
|
||||
data: msg,
|
||||
}),
|
||||
);
|
||||
|
||||
// The event is dispatched asynchronously, so we need to wait for it to be handled.
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
};
|
||||
|
||||
it("renders results", async () => {
|
||||
render();
|
||||
|
||||
const interpretation: Interpretation = {
|
||||
sourceLocationPrefix: "/a/b/c",
|
||||
numTruncatedResults: 0,
|
||||
numTotalResults: 1,
|
||||
data: {
|
||||
t: "SarifInterpretationData",
|
||||
sortState: undefined,
|
||||
...exampleSarif,
|
||||
},
|
||||
};
|
||||
const message: IntoResultsViewMsg = {
|
||||
t: "setState",
|
||||
resultsPath: "/a/b/c/results",
|
||||
origResultsPaths: {
|
||||
resultsPath: "/a/b/c/results.bqrs",
|
||||
interpretedResultsPath: "/a/b/c/interpreted-results.sarif",
|
||||
},
|
||||
sortedResultsMap: {
|
||||
"1": {
|
||||
resultsPath: "/a/b/c/results.bqrs",
|
||||
sortState: {
|
||||
columnIndex: 1,
|
||||
sortDirection: SortDirection.asc,
|
||||
},
|
||||
},
|
||||
},
|
||||
interpretation,
|
||||
database: {
|
||||
name: "test-db",
|
||||
databaseUri: "test-db-uri",
|
||||
},
|
||||
metadata: undefined, // TODO
|
||||
queryName: "test-query",
|
||||
queryPath: "/a/b/c/query.ql",
|
||||
shouldKeepOldResultsWhileRendering: false,
|
||||
parsedResultSets: {
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
numPages: 1,
|
||||
numInterpretedPages: 1,
|
||||
resultSetNames: ["#select"],
|
||||
resultSet: {
|
||||
t: "InterpretedResultSet",
|
||||
schema: {
|
||||
name: "#select",
|
||||
rows: 1,
|
||||
columns: [
|
||||
{
|
||||
name: "Path",
|
||||
kind: ColumnKindCode.STRING,
|
||||
},
|
||||
],
|
||||
},
|
||||
name: "#select",
|
||||
interpretation,
|
||||
},
|
||||
},
|
||||
};
|
||||
await postMessage(message);
|
||||
|
||||
expect(
|
||||
screen.getByText("'x' is assigned a value but never used."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await postMessage({
|
||||
...message,
|
||||
t: "showInterpretedPage",
|
||||
pageNumber: 1,
|
||||
numPages: 1,
|
||||
pageSize: 1,
|
||||
resultSetNames: ["#select"],
|
||||
queryName: "test-query",
|
||||
queryPath: "/a/b/c/query.ql",
|
||||
interpretation,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText("'x' is assigned a value but never used."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import { assertNever, getErrorMessage } from "../../pure/helpers-pure";
|
||||
import {
|
||||
DatabaseInfo,
|
||||
Interpretation,
|
||||
@@ -104,7 +104,6 @@ export class ResultsApp extends React.Component<
|
||||
queryPath: msg.queryPath,
|
||||
});
|
||||
|
||||
void this.loadResults();
|
||||
break;
|
||||
case "showInterpretedPage": {
|
||||
const tableName =
|
||||
@@ -141,7 +140,6 @@ export class ResultsApp extends React.Component<
|
||||
queryName: msg.queryName,
|
||||
queryPath: msg.queryPath,
|
||||
});
|
||||
void this.loadResults();
|
||||
break;
|
||||
}
|
||||
case "resultsUpdating":
|
||||
@@ -164,76 +162,45 @@ export class ResultsApp extends React.Component<
|
||||
|
||||
private updateStateWithNewResultsInfo(resultsInfo: ResultsInfo): void {
|
||||
this.setState((prevState) => {
|
||||
const stateWithDisplayedResults = (
|
||||
displayedResults: ResultsState,
|
||||
): ResultsViewState => ({
|
||||
displayedResults,
|
||||
isExpectingResultsUpdate: prevState.isExpectingResultsUpdate,
|
||||
nextResultsInfo: resultsInfo,
|
||||
});
|
||||
|
||||
if (!prevState.isExpectingResultsUpdate && resultsInfo === null) {
|
||||
// No results to display
|
||||
return stateWithDisplayedResults({
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: "No results to display",
|
||||
});
|
||||
}
|
||||
if (!resultsInfo || !resultsInfo.shouldKeepOldResultsWhileRendering) {
|
||||
if (resultsInfo === null && prevState.isExpectingResultsUpdate) {
|
||||
// Display loading message
|
||||
return stateWithDisplayedResults({
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: "Loading results…",
|
||||
});
|
||||
}
|
||||
return stateWithDisplayedResults(prevState.displayedResults);
|
||||
});
|
||||
}
|
||||
|
||||
private getResultSets(resultsInfo: ResultsInfo): readonly ResultSet[] {
|
||||
const parsedResultSets = resultsInfo.parsedResultSets;
|
||||
const resultSet = parsedResultSets.resultSet;
|
||||
if (!resultSet.t) {
|
||||
throw new Error(
|
||||
'Missing result set type. Should be either "InterpretedResultSet" or "RawResultSet".',
|
||||
);
|
||||
}
|
||||
return [resultSet];
|
||||
}
|
||||
|
||||
private async loadResults(): Promise<void> {
|
||||
const resultsInfo = this.state.nextResultsInfo;
|
||||
if (resultsInfo === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let results: Results | null = null;
|
||||
let statusText = "";
|
||||
try {
|
||||
const resultSets = this.getResultSets(resultsInfo);
|
||||
results = {
|
||||
resultSets,
|
||||
database: resultsInfo.database,
|
||||
sortStates: this.getSortStates(resultsInfo),
|
||||
};
|
||||
} catch (e) {
|
||||
let errorMessage: string;
|
||||
if (e instanceof Error) {
|
||||
errorMessage = e.message;
|
||||
} else {
|
||||
errorMessage = "Unknown error";
|
||||
return {
|
||||
displayedResults: {
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: "Loading results…",
|
||||
},
|
||||
isExpectingResultsUpdate: prevState.isExpectingResultsUpdate,
|
||||
nextResultsInfo: resultsInfo,
|
||||
};
|
||||
} else if (resultsInfo === null) {
|
||||
// No results to display
|
||||
return {
|
||||
displayedResults: {
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: "No results to display",
|
||||
},
|
||||
isExpectingResultsUpdate: prevState.isExpectingResultsUpdate,
|
||||
nextResultsInfo: resultsInfo,
|
||||
};
|
||||
}
|
||||
|
||||
statusText = `Error loading results: ${errorMessage}`;
|
||||
}
|
||||
let results: Results | null = null;
|
||||
let statusText = "";
|
||||
try {
|
||||
const resultSets = this.getResultSets(resultsInfo);
|
||||
results = {
|
||||
resultSets,
|
||||
database: resultsInfo.database,
|
||||
sortStates: this.getSortStates(resultsInfo),
|
||||
};
|
||||
} catch (e) {
|
||||
const errorMessage = getErrorMessage(e);
|
||||
|
||||
this.setState((prevState) => {
|
||||
// Only set state if this results info is still current.
|
||||
if (resultsInfo !== prevState.nextResultsInfo) {
|
||||
return null;
|
||||
statusText = `Error loading results: ${errorMessage}`;
|
||||
}
|
||||
|
||||
return {
|
||||
displayedResults: {
|
||||
resultsInfo,
|
||||
@@ -246,6 +213,22 @@ export class ResultsApp extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
private getResultSets(resultsInfo: ResultsInfo): readonly ResultSet[] {
|
||||
const parsedResultSets = resultsInfo.parsedResultSets;
|
||||
const resultSet = parsedResultSets.resultSet;
|
||||
if (
|
||||
resultSet.t !== "InterpretedResultSet" &&
|
||||
resultSet.t !== "RawResultSet"
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid result set type. Should be either "InterpretedResultSet" or "RawResultSet", but got "${
|
||||
(resultSet as { t: string }).t
|
||||
}".`,
|
||||
);
|
||||
}
|
||||
return [resultSet];
|
||||
}
|
||||
|
||||
private getSortStates(
|
||||
resultsInfo: ResultsInfo,
|
||||
): Map<string, RawResultsSortState> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
FromCompareViewMessage,
|
||||
FromDataExtensionsEditorMessage,
|
||||
FromResultsViewMsg,
|
||||
FromVariantAnalysisMessage,
|
||||
VariantAnalysisState,
|
||||
@@ -13,7 +14,8 @@ export interface VsCodeApi {
|
||||
msg:
|
||||
| FromResultsViewMsg
|
||||
| FromCompareViewMessage
|
||||
| FromVariantAnalysisMessage,
|
||||
| FromVariantAnalysisMessage
|
||||
| FromDataExtensionsEditorMessage,
|
||||
): void;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { render as ReactDOM_render } from "react-dom";
|
||||
import * as React from "react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { vscode } from "./vscode-api";
|
||||
|
||||
import { WebviewDefinition } from "./webview-definition";
|
||||
@@ -28,11 +30,13 @@ const render = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const view: WebviewDefinition = require(`./${viewName}/index.tsx`).default;
|
||||
|
||||
ReactDOM_render(
|
||||
view.component,
|
||||
document.getElementById("root"),
|
||||
// Post a message to the extension when fully loaded.
|
||||
() => vscode.postMessage({ t: "viewLoaded", viewName }),
|
||||
const root = createRoot(element);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<div ref={() => vscode.postMessage({ t: "viewLoaded", viewName })}>
|
||||
{view.component}
|
||||
</div>
|
||||
</StrictMode>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[
|
||||
"v2.12.5",
|
||||
"v2.12.6",
|
||||
"v2.11.6",
|
||||
"v2.7.6",
|
||||
"v2.8.5",
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import { decodeBqrsToExternalApiUsages } from "../../../src/data-extensions-editor/bqrs";
|
||||
import { DecodedBqrsChunk } from "../../../src/pure/bqrs-cli-types";
|
||||
|
||||
describe("decodeBqrsToExternalApiUsages", () => {
|
||||
const chunk: DecodedBqrsChunk = {
|
||||
columns: [
|
||||
{ name: "apiName", kind: "String" },
|
||||
{ name: "supported", kind: "Boolean" },
|
||||
{ name: "usage", kind: "Entity" },
|
||||
],
|
||||
tuples: [
|
||||
[
|
||||
"java.io.PrintStream#println(String)",
|
||||
true,
|
||||
{
|
||||
label: "println(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 29,
|
||||
startColumn: 9,
|
||||
endLine: 29,
|
||||
endColumn: 49,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||
false,
|
||||
{
|
||||
label: "run(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/Sql2oExampleApplication.java",
|
||||
startLine: 9,
|
||||
startColumn: 9,
|
||||
endLine: 9,
|
||||
endColumn: 66,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Connection#createQuery(String)",
|
||||
true,
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 56,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Connection#createQuery(String)",
|
||||
true,
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 39,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Query#executeScalar(Class)",
|
||||
true,
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 85,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Query#executeScalar(Class)",
|
||||
true,
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 68,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Sql2o#open()",
|
||||
true,
|
||||
{
|
||||
label: "open(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 14,
|
||||
startColumn: 24,
|
||||
endLine: 14,
|
||||
endColumn: 35,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Sql2o#open()",
|
||||
true,
|
||||
{
|
||||
label: "open(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 25,
|
||||
startColumn: 24,
|
||||
endLine: 25,
|
||||
endColumn: 35,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
true,
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 10,
|
||||
startColumn: 33,
|
||||
endLine: 10,
|
||||
endColumn: 88,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Sql2o#Sql2o(String)",
|
||||
true,
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 23,
|
||||
startColumn: 23,
|
||||
endLine: 23,
|
||||
endColumn: 36,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
it("extracts api usages", () => {
|
||||
// Even though there are a number of usages with the same number of usages, the order returned should be stable:
|
||||
// - Iterating over a map (as done by .values()) is guaranteed to be in insertion order
|
||||
// - Sorting the array of usages is guaranteed to be a stable sort
|
||||
expect(decodeBqrsToExternalApiUsages(chunk)).toEqual([
|
||||
{
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 56,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 39,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 85,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 68,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "open",
|
||||
methodParameters: "()",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "open(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 14,
|
||||
startColumn: 24,
|
||||
endLine: 14,
|
||||
endColumn: 35,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "open(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 25,
|
||||
startColumn: 24,
|
||||
endLine: 25,
|
||||
endColumn: 35,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "java.io.PrintStream#println(String)",
|
||||
packageName: "java.io",
|
||||
typeName: "PrintStream",
|
||||
methodName: "println",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "println(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 29,
|
||||
startColumn: 9,
|
||||
endLine: 29,
|
||||
endColumn: 49,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature:
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||
packageName: "org.springframework.boot",
|
||||
typeName: "SpringApplication",
|
||||
methodName: "run",
|
||||
methodParameters: "(Class,String[])",
|
||||
supported: false,
|
||||
usages: [
|
||||
{
|
||||
label: "run(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/Sql2oExampleApplication.java",
|
||||
startLine: 9,
|
||||
startColumn: 9,
|
||||
endLine: 9,
|
||||
endColumn: 66,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 10,
|
||||
startColumn: 33,
|
||||
endLine: 10,
|
||||
endColumn: 88,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 23,
|
||||
startColumn: 23,
|
||||
endLine: 23,
|
||||
endColumn: 36,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
createDataExtensionYaml,
|
||||
loadDataExtensionYaml,
|
||||
} from "../../../src/data-extensions-editor/yaml";
|
||||
|
||||
describe("createDataExtensionYaml", () => {
|
||||
it("creates the correct YAML file", () => {
|
||||
const yaml = createDataExtensionYaml(
|
||||
[
|
||||
{
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 56,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 39,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 85,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 68,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
"org.sql2o.Connection#createQuery(String)": {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "sql",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sourceModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","manual"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: summaryModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadDataExtensionYaml", () => {
|
||||
it("loads the YAML file", () => {
|
||||
const data = loadDataExtensionYaml({
|
||||
extensions: [
|
||||
{
|
||||
addsTo: { pack: "codeql/java-all", extensible: "sourceModel" },
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
addsTo: { pack: "codeql/java-all", extensible: "sinkModel" },
|
||||
data: [
|
||||
[
|
||||
"org.sql2o",
|
||||
"Connection",
|
||||
true,
|
||||
"createQuery",
|
||||
"(String)",
|
||||
"",
|
||||
"Argument[0]",
|
||||
"sql",
|
||||
"manual",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
addsTo: { pack: "codeql/java-all", extensible: "summaryModel" },
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
addsTo: { pack: "codeql/java-all", extensible: "neutralModel" },
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(data).toEqual({
|
||||
"org.sql2o.Connection#createQuery(String)": {
|
||||
input: "Argument[0]",
|
||||
kind: "sql",
|
||||
output: "",
|
||||
type: "sink",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined if given a string", () => {
|
||||
const data = loadDataExtensionYaml(`extensions:
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","manual"]
|
||||
`);
|
||||
|
||||
expect(data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../../../../.markdownlint.json",
|
||||
"MD010": false, // Hard tabs are from the original snippet in the SARIF file
|
||||
"MD024": false, // Duplicate headings
|
||||
"MD033": false, // Inline HTML for <details> and <summary>
|
||||
"MD051": false // Link fragments are used in Gist to link to other files
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
cp.execSync(cmd); // BAD
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
2. [javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js#L4-L4)
|
||||
<pre><code class="javascript"> path = require("path");
|
||||
function cleanupTemp() {
|
||||
@@ -30,7 +30,7 @@
|
||||
cp.execSync(cmd); // BAD
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
3. [javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js#L4-L4)
|
||||
<pre><code class="javascript"> path = require("path");
|
||||
function cleanupTemp() {
|
||||
@@ -38,7 +38,7 @@
|
||||
cp.execSync(cmd); // BAD
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
4. [javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js#L4-L4)
|
||||
<pre><code class="javascript"> path = require("path");
|
||||
function cleanupTemp() {
|
||||
@@ -46,14 +46,13 @@
|
||||
cp.execSync(cmd); // BAD
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
5. [javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/src/Security/CWE-078/examples/shell-command-injection-from-environment.js#L5-L5)
|
||||
<pre><code class="javascript">function cleanupTemp() {
|
||||
let cmd = "rm -rf " + path.join(__dirname, "temp");
|
||||
cp.execSync(<strong>cmd</strong>); // BAD
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
@@ -79,26 +78,25 @@
|
||||
<pre><code class="javascript">(function() {
|
||||
cp.execFileSync('rm', ['-rf', path.join(__dirname, "temp")]); // GOOD
|
||||
cp.execSync('rm -rf ' + path.join(<strong>__dirname</strong>, "temp")); // BAD
|
||||
|
||||
|
||||
execa.shell('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
</code></pre>
|
||||
|
||||
|
||||
2. [javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js#L6-L6)
|
||||
<pre><code class="javascript">(function() {
|
||||
cp.execFileSync('rm', ['-rf', path.join(__dirname, "temp")]); // GOOD
|
||||
cp.execSync('rm -rf ' + <strong>path.join(__dirname, "temp")</strong>); // BAD
|
||||
|
||||
|
||||
execa.shell('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
</code></pre>
|
||||
|
||||
|
||||
3. [javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js#L6-L6)
|
||||
<pre><code class="javascript">(function() {
|
||||
cp.execFileSync('rm', ['-rf', path.join(__dirname, "temp")]); // GOOD
|
||||
cp.execSync(<strong>'rm -rf ' + path.join(__dirname, "temp")</strong>); // BAD
|
||||
|
||||
|
||||
execa.shell('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
</code></pre>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
@@ -122,28 +120,27 @@
|
||||
|
||||
1. [javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js#L8-L8)
|
||||
<pre><code class="javascript"> cp.execSync('rm -rf ' + path.join(__dirname, "temp")); // BAD
|
||||
|
||||
|
||||
execa.shell('rm -rf ' + path.join(<strong>__dirname</strong>, "temp")); // NOT OK
|
||||
execa.shellSync('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
|
||||
|
||||
</code></pre>
|
||||
|
||||
|
||||
2. [javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js#L8-L8)
|
||||
<pre><code class="javascript"> cp.execSync('rm -rf ' + path.join(__dirname, "temp")); // BAD
|
||||
|
||||
|
||||
execa.shell('rm -rf ' + <strong>path.join(__dirname, "temp")</strong>); // NOT OK
|
||||
execa.shellSync('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
|
||||
|
||||
</code></pre>
|
||||
|
||||
|
||||
3. [javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js#L8-L8)
|
||||
<pre><code class="javascript"> cp.execSync('rm -rf ' + path.join(__dirname, "temp")); // BAD
|
||||
|
||||
|
||||
execa.shell(<strong>'rm -rf ' + path.join(__dirname, "temp")</strong>); // NOT OK
|
||||
execa.shellSync('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
|
||||
|
||||
</code></pre>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
@@ -169,26 +166,25 @@
|
||||
<pre><code class="javascript">
|
||||
execa.shell('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
execa.shellSync('rm -rf ' + path.join(<strong>__dirname</strong>, "temp")); // NOT OK
|
||||
|
||||
|
||||
const safe = "\"" + path.join(__dirname, "temp") + "\"";
|
||||
</code></pre>
|
||||
|
||||
|
||||
2. [javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js#L9-L9)
|
||||
<pre><code class="javascript">
|
||||
execa.shell('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
execa.shellSync('rm -rf ' + <strong>path.join(__dirname, "temp")</strong>); // NOT OK
|
||||
|
||||
|
||||
const safe = "\"" + path.join(__dirname, "temp") + "\"";
|
||||
</code></pre>
|
||||
|
||||
|
||||
3. [javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js](https://github.com/github/codeql/blob/48015e5a2e6202131f2d1062cc066dc33ed69a9b/javascript/ql/test/query-tests/Security/CWE-078/tst_shell-command-injection-from-environment.js#L9-L9)
|
||||
<pre><code class="javascript">
|
||||
execa.shell('rm -rf ' + path.join(__dirname, "temp")); // NOT OK
|
||||
execa.shellSync(<strong>'rm -rf ' + path.join(__dirname, "temp")</strong>); // NOT OK
|
||||
|
||||
|
||||
const safe = "\"" + path.join(__dirname, "temp") + "\"";
|
||||
</code></pre>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -20,18 +20,18 @@
|
||||
<pre><code class="javascript">
|
||||
const meteorLocalFolder = '.meteor';
|
||||
const meteorPath = <strong>path.resolve(rootPath, meteorLocalFolder)</strong>;
|
||||
|
||||
|
||||
module.exports = {
|
||||
</code></pre>
|
||||
|
||||
|
||||
2. [npm-packages/meteor-installer/config.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/config.js#L39-L39)
|
||||
<pre><code class="javascript">
|
||||
const meteorLocalFolder = '.meteor';
|
||||
const <strong>meteorPath = path.resolve(rootPath, meteorLocalFolder)</strong>;
|
||||
|
||||
|
||||
module.exports = {
|
||||
</code></pre>
|
||||
|
||||
|
||||
3. [npm-packages/meteor-installer/config.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/config.js#L44-L44)
|
||||
<pre><code class="javascript"> METEOR_LATEST_VERSION,
|
||||
extractPath: rootPath,
|
||||
@@ -39,7 +39,7 @@
|
||||
release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,
|
||||
rootPath,
|
||||
</code></pre>
|
||||
|
||||
|
||||
4. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L12-L12)
|
||||
<pre><code class="javascript">const os = require('os');
|
||||
const {
|
||||
@@ -47,7 +47,7 @@
|
||||
release,
|
||||
startedPath,
|
||||
</code></pre>
|
||||
|
||||
|
||||
5. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L11-L23)
|
||||
<pre><code class="javascript">const tmp = require('tmp');
|
||||
const os = require('os');
|
||||
@@ -67,7 +67,7 @@
|
||||
const { uninstall } = require('./uninstall');
|
||||
const {
|
||||
</code></pre>
|
||||
|
||||
|
||||
6. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L259-L259)
|
||||
<pre><code class="javascript"> if (isWindows()) {
|
||||
//set for the current session and beyond
|
||||
@@ -75,7 +75,7 @@
|
||||
return;
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
7. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L259-L259)
|
||||
<pre><code class="javascript"> if (isWindows()) {
|
||||
//set for the current session and beyond
|
||||
@@ -83,7 +83,7 @@
|
||||
return;
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
8. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L259-L259)
|
||||
<pre><code class="javascript"> if (isWindows()) {
|
||||
//set for the current session and beyond
|
||||
@@ -91,7 +91,7 @@
|
||||
return;
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
9. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L259-L259)
|
||||
<pre><code class="javascript"> if (isWindows()) {
|
||||
//set for the current session and beyond
|
||||
@@ -99,7 +99,7 @@
|
||||
return;
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
10. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L259-L259)
|
||||
<pre><code class="javascript"> if (isWindows()) {
|
||||
//set for the current session and beyond
|
||||
@@ -107,7 +107,7 @@
|
||||
return;
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
11. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L259-L259)
|
||||
<pre><code class="javascript"> if (isWindows()) {
|
||||
//set for the current session and beyond
|
||||
@@ -115,7 +115,6 @@
|
||||
return;
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
@@ -126,10 +125,10 @@
|
||||
<pre><code class="javascript">
|
||||
const meteorLocalFolder = '.meteor';
|
||||
const meteorPath = <strong>path.resolve(rootPath, meteorLocalFolder)</strong>;
|
||||
|
||||
|
||||
module.exports = {
|
||||
</code></pre>
|
||||
|
||||
|
||||
2. [npm-packages/meteor-installer/install.js](https://github.com/meteor/meteor/blob/73b538fe201cbfe89dd0c709689023f9b3eab1ec/npm-packages/meteor-installer/install.js#L259-L259)
|
||||
<pre><code class="javascript"> if (isWindows()) {
|
||||
//set for the current session and beyond
|
||||
@@ -137,7 +136,6 @@
|
||||
return;
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -41,4 +41,4 @@ select t,
|
||||
| Repository | Results |
|
||||
| --- | --- |
|
||||
| github/codeql | [1 result(s)](#file-result-1-github-codeql-md) |
|
||||
| meteor/meteor | [5 result(s)](#file-result-2-meteor-meteor-md) |
|
||||
| meteor/meteor | [5 result(s)](#file-result-2-meteor-meteor-md) |
|
||||
|
||||
@@ -38,4 +38,4 @@ select c, c.getNumLines()
|
||||
| Repository | Results |
|
||||
| --- | --- |
|
||||
| github/codeql | [22 result(s)](#file-result-1-github-codeql-md) |
|
||||
| meteor/meteor | [2 result(s)](#file-result-2-meteor-meteor-md) |
|
||||
| meteor/meteor | [2 result(s)](#file-result-2-meteor-meteor-md) |
|
||||
|
||||
@@ -23,4 +23,4 @@
|
||||
| [`functio ... true\n}`](https://github.com/github/codeql/blob/cbdd4927cee593b715d8469240ce1d31edaaef9b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/constant.js#L1-L4) | `4` |
|
||||
| [`functio ... n -1;\n}`](https://github.com/github/codeql/blob/cbdd4927cee593b715d8469240ce1d31edaaef9b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/example.js#L1-L12) | `12` |
|
||||
| [`functio ... turn; }`](https://github.com/github/codeql/blob/cbdd4927cee593b715d8469240ce1d31edaaef9b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/tst.js#L8-L8) | `1` |
|
||||
| [`\| functio ... i+1); \|}`](https://github.com/github/codeql/blob/cbdd4927cee593b715d8469240ce1d31edaaef9b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/tst.js#L9-L9) | `1` |
|
||||
| [`\| functio ... i+1); \|}`](https://github.com/github/codeql/blob/cbdd4927cee593b715d8469240ce1d31edaaef9b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/tst.js#L9-L9) | `1` |
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
| c | |
|
||||
| --- | --- |
|
||||
| [`functio ... rn H\|0}`](https://github.com/meteor/meteor/blob/53f3c4442d3542d3d2a012a854472a0d1bef9d12/packages/logic-solver/minisat.js#L7-L7) | `1` |
|
||||
| [`functio ... ext;\n\t}`](https://github.com/meteor/meteor/blob/53f3c4442d3542d3d2a012a854472a0d1bef9d12/packages/sha/sha256.js#L94-L124) | `31` |
|
||||
| [`functio ... ext;\n\t}`](https://github.com/meteor/meteor/blob/53f3c4442d3542d3d2a012a854472a0d1bef9d12/packages/sha/sha256.js#L94-L124) | `31` |
|
||||
|
||||
@@ -45,6 +45,10 @@ import { App } from "../../../../src/common/app";
|
||||
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
|
||||
import { DbConfigStore } from "../../../../src/databases/config/db-config-store";
|
||||
import { mockedObject } from "../../utils/mocking.helpers";
|
||||
import {
|
||||
REPO_STATES_FILENAME,
|
||||
writeRepoStates,
|
||||
} from "../../../../src/variant-analysis/store/repo-states-store";
|
||||
|
||||
// up to 3 minutes per test
|
||||
jest.setTimeout(3 * 60 * 1000);
|
||||
@@ -119,8 +123,12 @@ describe("Variant Analysis Manager", () => {
|
||||
});
|
||||
|
||||
it("should read in the repo states if it exists", async () => {
|
||||
await fs.writeJson(
|
||||
join(storagePath, variantAnalysis.id.toString(), "repo_states.json"),
|
||||
await writeRepoStates(
|
||||
join(
|
||||
storagePath,
|
||||
variantAnalysis.id.toString(),
|
||||
REPO_STATES_FILENAME,
|
||||
),
|
||||
{
|
||||
[scannedRepos[0].repository.id]: {
|
||||
repositoryId: scannedRepos[0].repository.id,
|
||||
@@ -177,7 +185,7 @@ describe("Variant Analysis Manager", () => {
|
||||
repoStatesPath = join(
|
||||
storagePath,
|
||||
variantAnalysis.id.toString(),
|
||||
"repo_states.json",
|
||||
REPO_STATES_FILENAME,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -516,7 +524,7 @@ describe("Variant Analysis Manager", () => {
|
||||
variantAnalysis,
|
||||
);
|
||||
expect(executeCommandSpy).toHaveBeenCalledWith(
|
||||
"codeQL.monitorVariantAnalysis",
|
||||
"codeQL.monitorRehydratedVariantAnalysis",
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("Variant Analysis Manager", () => {
|
||||
);
|
||||
|
||||
expect(executeCommandSpy).toBeCalledWith(
|
||||
"codeQL.monitorVariantAnalysis",
|
||||
"codeQL.monitorNewVariantAnalysis",
|
||||
expect.objectContaining({
|
||||
id: mockApiResponse.id,
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
@@ -149,7 +149,7 @@ describe("Variant Analysis Manager", () => {
|
||||
);
|
||||
|
||||
expect(executeCommandSpy).toBeCalledWith(
|
||||
"codeQL.monitorVariantAnalysis",
|
||||
"codeQL.monitorNewVariantAnalysis",
|
||||
expect.objectContaining({
|
||||
id: mockApiResponse.id,
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
@@ -170,7 +170,7 @@ describe("Variant Analysis Manager", () => {
|
||||
);
|
||||
|
||||
expect(executeCommandSpy).toBeCalledWith(
|
||||
"codeQL.monitorVariantAnalysis",
|
||||
"codeQL.monitorNewVariantAnalysis",
|
||||
expect.objectContaining({
|
||||
id: mockApiResponse.id,
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
@@ -293,7 +293,7 @@ describe("Variant Analysis Manager", () => {
|
||||
|
||||
expect(mockSubmitVariantAnalysis).toBeCalledTimes(1);
|
||||
expect(executeCommandSpy).toBeCalledWith(
|
||||
"codeQL.monitorVariantAnalysis",
|
||||
"codeQL.monitorNewVariantAnalysis",
|
||||
expect.objectContaining({
|
||||
query: expect.objectContaining({ filePath: fileUri.fsPath }),
|
||||
}),
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class JestRunnerInstalledExtensions extends VSCodeTestRunner {
|
||||
?.config as RunnerOptions) ?? {};
|
||||
|
||||
const { version, platform } = options;
|
||||
const versionKey = `${version}-${platform}`;
|
||||
const versionKey = `${version}-${platform}` as const;
|
||||
|
||||
if (installedOnVsCodeVersions.has(versionKey)) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user