Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4546616871 | ||
|
|
b201c45e2d | ||
|
|
3364af5305 | ||
|
|
d6af6e7c4c | ||
|
|
f9f454509b | ||
|
|
9f0453a80a | ||
|
|
af2a592e3d | ||
|
|
a4fcbd093c | ||
|
|
f02c007bdb | ||
|
|
66874823aa | ||
|
|
1a59467707 | ||
|
|
78383e376c | ||
|
|
32cf05cb4b | ||
|
|
1ec47a4eef | ||
|
|
4d2f84d599 | ||
|
|
a858b39f26 | ||
|
|
5d71ce41a8 | ||
|
|
76354c4518 | ||
|
|
4e310ce652 | ||
|
|
68bfa00707 | ||
|
|
51ef674cc5 | ||
|
|
d2612e2dca | ||
|
|
1db3dda533 | ||
|
|
9c3b0b24c9 | ||
|
|
df06c75070 | ||
|
|
b3c44ea317 | ||
|
|
d6673c4ede | ||
|
|
d68e270e90 | ||
|
|
97b9c43ae1 | ||
|
|
7e00e9cd2f | ||
|
|
459d606751 | ||
|
|
a069ae45fb | ||
|
|
a6225c5edf | ||
|
|
64d4439815 | ||
|
|
b11a9e2c88 | ||
|
|
75ab23bbd8 | ||
|
|
5bab2421ea | ||
|
|
d5edbca4e4 | ||
|
|
1ee349d2ef | ||
|
|
79f89c9e4b | ||
|
|
2fbdefe4f0 | ||
|
|
01403aeee7 | ||
|
|
9f7f34a87c | ||
|
|
987b92ab1e | ||
|
|
6fd2579c9c | ||
|
|
c9e1a64d3a | ||
|
|
03f330da52 | ||
|
|
4f6d72a9ce | ||
|
|
b5d3a612d8 | ||
|
|
48209e7732 | ||
|
|
9934449ae9 | ||
|
|
fdc4d97f93 | ||
|
|
b1ca9418b4 | ||
|
|
7d3a350a28 | ||
|
|
ca4c511227 | ||
|
|
d4df484acb | ||
|
|
3e1d924110 | ||
|
|
cc4666a614 | ||
|
|
51906bbcee | ||
|
|
fcfa6979e2 | ||
|
|
f0783ed274 | ||
|
|
fe45e00fb3 | ||
|
|
4a317d33a9 | ||
|
|
ffc7135c1f | ||
|
|
c805b48f18 | ||
|
|
e1b95c2f7c | ||
|
|
481d2f5404 | ||
|
|
15fa23acb4 | ||
|
|
a625d9aabe | ||
|
|
cd70b19bb3 | ||
|
|
7a58084df9 | ||
|
|
916d16126b | ||
|
|
a5eb915267 | ||
|
|
a5440ef482 | ||
|
|
fc86be7687 | ||
|
|
f0909a9d67 | ||
|
|
40b5b5ba7e | ||
|
|
fa85bcfad6 | ||
|
|
dd1b054f26 | ||
|
|
b96cd6c7e1 | ||
|
|
b466d2aa36 | ||
|
|
7f6c1ad7f7 | ||
|
|
10f4b47019 | ||
|
|
b31a769fdd | ||
|
|
c4e2f11372 | ||
|
|
9494d32144 | ||
|
|
8b4f2d2009 | ||
|
|
12555d90c1 | ||
|
|
d46e03b4cc | ||
|
|
f0e2285122 | ||
|
|
f4d0d23170 | ||
|
|
513fb65560 | ||
|
|
7d353ced9b | ||
|
|
be3506d987 | ||
|
|
3fa6304050 | ||
|
|
bb40e5bcad | ||
|
|
e2a8ae318b | ||
|
|
79867e2f9d | ||
|
|
1bc13d70ce | ||
|
|
0bd359997d | ||
|
|
28a7d1cf34 |
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -5,8 +5,4 @@
|
||||
|
||||
Replace this with a description of the changes your pull request makes.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] [CHANGELOG.md](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md) has been updated to incorporate all user visible changes made by this pull request.
|
||||
- [ ] Issues have been created for any UI or other user-facing changes made by this pull request.
|
||||
- [ ] _[Maintainers only]_ If this pull request makes user-facing changes that require documentation changes, open a corresponding docs pull request in the [github/codeql](https://github.com/github/codeql/tree/main/docs/codeql/codeql-for-visual-studio-code) repo and add the `ready-for-doc-review` label there.
|
||||
Remember to update the [changelog](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md) if there have been user-facing changes!
|
||||
|
||||
4
.github/workflows/e2e-tests.yml
vendored
4
.github/workflows/e2e-tests.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Start containers
|
||||
working-directory: extensions/ql-vscode/test/e2e
|
||||
run: docker-compose -f "docker-compose.yml" up -d --build
|
||||
run: docker compose -f "docker-compose.yml" up -d --build
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
working-directory: extensions/ql-vscode
|
||||
@@ -43,4 +43,4 @@ jobs:
|
||||
- name: Stop containers
|
||||
working-directory: extensions/ql-vscode/test/e2e
|
||||
if: always()
|
||||
run: docker-compose -f "docker-compose.yml" down -v
|
||||
run: docker compose -f "docker-compose.yml" down -v
|
||||
|
||||
13
.github/workflows/main.yml
vendored
13
.github/workflows/main.yml
vendored
@@ -144,6 +144,19 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
# Enable 8.3 filename creation. This is not required to run the extension but it is required for the unit tests to pass.
|
||||
# This feature is currently enabled by default in Windows 11 for the C: drive and therefore we must maintain support for it.
|
||||
# This setting needs to be enabled before files are created, i.e. before we checkout the repository.
|
||||
- name: Enable 8.3 filenames
|
||||
shell: pwsh
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
run: |
|
||||
$shortNameEnableProcess = Start-Process -FilePath fsutil.exe -ArgumentList ('8dot3name', 'set', '0') -Wait -PassThru
|
||||
$shortNameEnableExitCode = $shortNameEnableProcess.ExitCode
|
||||
if ($shortNameEnableExitCode -ne 0) {
|
||||
exit $shortNameEnableExitCode
|
||||
}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -28,6 +28,7 @@ Here are a few things you can do that will increase the likelihood of your pull
|
||||
- [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).
|
||||
- Update the [changelog](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md) if you are making user-facing changes.
|
||||
|
||||
## Setting up a local build
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ Run one of the above MRVAs, but cancel it from within VS Code:
|
||||
import semmle.python.frameworks.data.internal.ApiGraphModelsExtensions
|
||||
|
||||
from string path, string kind
|
||||
where sinkModel("vscode-codeql", path, kind)
|
||||
where sinkModel("vscode-codeql", path, kind, _)
|
||||
select path, kind
|
||||
```
|
||||
|
||||
@@ -210,17 +210,7 @@ Run one of the above MRVAs, but cancel it from within VS Code:
|
||||
4. Open the ".model.yml" file corresponding to the library that was changed.
|
||||
- Check that the file contains entries for the methods that were modeled.
|
||||
|
||||
#### Test Case 3: Model with AI
|
||||
|
||||
Note that this test requires the feature flag: `codeQL.model.llmGeneration`
|
||||
|
||||
A package that the AI normally gives models for is `javax.servlet-api` from the `jhy/jsoup` repository.
|
||||
|
||||
1. Click "Model with AI".
|
||||
- Check that rows change to "Thinking".
|
||||
- Check that results come back and rows get filled out.
|
||||
|
||||
#### Test Case 4: Model as dependency
|
||||
#### Test Case 3: Model as dependency
|
||||
|
||||
Note that this test requires the feature flag: `codeQL.model.flowGeneration`
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
v18.18.2
|
||||
v20.14.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { addons } from "@storybook/manager-api";
|
||||
import { Addon_TypesEnum } from "@storybook/types";
|
||||
import { Addon_TypesEnum } from "storybook/internal/types";
|
||||
import { ThemeSelector } from "./ThemeSelector";
|
||||
|
||||
const ADDON_ID = "vscode-theme-addon";
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.14.0 - 7 August 2024
|
||||
|
||||
- Add Python support to the CodeQL Model Editor. [#3676](https://github.com/github/vscode-codeql/pull/3676)
|
||||
- Update variant analysis view to display the length of the shortest path for path queries. [#3671](https://github.com/github/vscode-codeql/pull/3671)
|
||||
- Remove support for CodeQL CLI versions older than 2.15.5. [#3681](https://github.com/github/vscode-codeql/pull/3681)
|
||||
|
||||
## 1.13.1 - 29 May 2024
|
||||
|
||||
- Fix a bug when re-importing test databases that erroneously showed old source code. [#3616](https://github.com/github/vscode-codeql/pull/3616)
|
||||
|
||||
@@ -111,9 +111,9 @@ If you wish to navigate the query results from your keyboard, you can bind short
|
||||
We recommend reading the [full documentation for the extension](https://docs.github.com/code-security/codeql-for-vs-code/) on the GitHub documentation website. You may also find the following resources useful:
|
||||
|
||||
- [Create a database for a different codebase](https://codeql.github.com/docs/codeql-cli/creating-codeql-databases/).
|
||||
- [Try out variant analysis](https://help.semmle.com/QL/learn-ql/ql-training.html).
|
||||
- [Try out variant analysis](https://docs.github.com/code-security/codeql-for-vs-code/getting-started-with-codeql-for-vs-code/running-codeql-queries-at-scale-with-multi-repository-variant-analysis).
|
||||
- [Learn more about CodeQL](https://codeql.github.com/docs/).
|
||||
- [Read how security researchers use CodeQL to find CVEs](https://securitylab.github.com/research).
|
||||
- [Read how security researchers use CodeQL to find CVEs](https://github.blog/tag/github-security-lab/).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -77,5 +77,8 @@ export function copyWasmFiles() {
|
||||
// to configure the path to the WASM file. So, source-map will always load the file from `__dirname/mappings.wasm`.
|
||||
// In version 0.8.0, it may be possible to do this properly by calling SourceMapConsumer.initialize by
|
||||
// using the "browser" field in source-map's package.json to load the WASM file from a given file path.
|
||||
return src("node_modules/source-map/lib/mappings.wasm").pipe(dest("out"));
|
||||
return src("node_modules/source-map/lib/mappings.wasm", {
|
||||
// WASM is a binary format, so don't try to re-encode it as text.
|
||||
encoding: false,
|
||||
}).pipe(dest("out"));
|
||||
}
|
||||
|
||||
6810
extensions/ql-vscode/package-lock.json
generated
6810
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.13.1",
|
||||
"version": "1.14.0",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.82.0",
|
||||
"node": "^18.18.2",
|
||||
"node": "^20.14.0",
|
||||
"npm": ">=7.20.6"
|
||||
},
|
||||
"categories": [
|
||||
@@ -1790,8 +1790,7 @@
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.trimCache",
|
||||
"when": "codeql.supportsTrimCache"
|
||||
"command": "codeQL.trimCache"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
@@ -1972,7 +1971,7 @@
|
||||
"@octokit/plugin-retry": "^6.0.1",
|
||||
"@octokit/plugin-throttling": "^8.0.0",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@vscode/codicons": "^0.0.35",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"@vscode/debugadapter": "^1.59.0",
|
||||
"@vscode/debugprotocol": "^1.65.0",
|
||||
"@vscode/webview-ui-toolkit": "^1.0.1",
|
||||
@@ -1998,36 +1997,36 @@
|
||||
"tmp-promise": "^3.0.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
"vscode-extension-telemetry": "^0.1.6",
|
||||
"vscode-jsonrpc": "^8.0.2",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageclient": "^8.0.2",
|
||||
"yauzl": "^2.10.0",
|
||||
"zip-a-folder": "^3.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
|
||||
"@babel/core": "^7.24.6",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.24.7",
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@babel/preset-typescript": "^7.21.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@github/markdownlint-github": "^0.6.2",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@storybook/addon-a11y": "^8.1.3",
|
||||
"@storybook/addon-actions": "^8.1.3",
|
||||
"@storybook/addon-essentials": "^8.1.3",
|
||||
"@storybook/addon-interactions": "^8.1.3",
|
||||
"@storybook/addon-links": "^8.1.3",
|
||||
"@storybook/addon-a11y": "^8.2.7",
|
||||
"@storybook/addon-actions": "^8.2.7",
|
||||
"@storybook/addon-essentials": "^8.2.7",
|
||||
"@storybook/addon-interactions": "^8.2.7",
|
||||
"@storybook/addon-links": "^8.2.7",
|
||||
"@storybook/blocks": "^8.0.2",
|
||||
"@storybook/components": "^8.0.2",
|
||||
"@storybook/csf": "^0.1.7",
|
||||
"@storybook/icons": "^1.2.9",
|
||||
"@storybook/manager-api": "^8.1.3",
|
||||
"@storybook/react": "^8.1.3",
|
||||
"@storybook/react-vite": "^8.1.3",
|
||||
"@storybook/theming": "^8.1.3",
|
||||
"@testing-library/dom": "^10.1.0",
|
||||
"@testing-library/jest-dom": "^6.4.5",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@storybook/components": "^8.2.7",
|
||||
"@storybook/csf": "^0.1.11",
|
||||
"@storybook/icons": "^1.2.10",
|
||||
"@storybook/manager-api": "^8.2.7",
|
||||
"@storybook/react": "^8.2.7",
|
||||
"@storybook/react-vite": "^8.2.7",
|
||||
"@storybook/theming": "^8.2.4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/child-process-promise": "^2.2.1",
|
||||
"@types/d3": "^7.4.0",
|
||||
@@ -2039,7 +2038,7 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/js-yaml": "^4.0.6",
|
||||
"@types/nanoid": "^3.0.0",
|
||||
"@types/node": "18.18.*",
|
||||
"@types/node": "20.14.*",
|
||||
"@types/node-fetch": "^2.5.2",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
@@ -2071,10 +2070,10 @@
|
||||
"eslint-plugin-jest-dom": "^5.2.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"glob": "^10.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-esbuild": "^0.12.0",
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
@@ -2088,13 +2087,13 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"storybook": "^8.1.3",
|
||||
"storybook": "^8.2.7",
|
||||
"tar-stream": "^3.1.7",
|
||||
"through2": "^4.0.2",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-jest": "^29.1.4",
|
||||
"ts-json-schema-generator": "^2.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-unused-exports": "^10.0.0",
|
||||
"ts-unused-exports": "^10.1.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^5.2.11",
|
||||
"vite-node": "^1.5.3"
|
||||
|
||||
@@ -12,6 +12,7 @@ interface VersionResult {
|
||||
export interface CliFeatures {
|
||||
featuresInVersionResult?: boolean;
|
||||
mrvaPackCreate?: boolean;
|
||||
generateSummarySymbolMap?: boolean;
|
||||
}
|
||||
|
||||
export interface VersionAndFeatures {
|
||||
|
||||
@@ -1211,10 +1211,15 @@ export class CodeQLCliServer implements Disposable {
|
||||
outputPath: string,
|
||||
endSummaryPath: string,
|
||||
): Promise<string> {
|
||||
const supportsGenerateSummarySymbolMap =
|
||||
await this.cliConstraints.supportsGenerateSummarySymbolMap();
|
||||
const subcommandArgs = [
|
||||
"--format=text",
|
||||
`--end-summary=${endSummaryPath}`,
|
||||
"--sourcemap",
|
||||
...(supportsGenerateSummarySymbolMap
|
||||
? ["--summary-symbol-map", "--minify-output"]
|
||||
: []),
|
||||
inputPath,
|
||||
outputPath,
|
||||
];
|
||||
@@ -1750,14 +1755,6 @@ export class CodeQLCliServer implements Disposable {
|
||||
this._versionChangedListeners.forEach((listener) =>
|
||||
listener(newVersionAndFeatures),
|
||||
);
|
||||
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeql.supportsTrimCache",
|
||||
newVersionAndFeatures.version.compare(
|
||||
CliVersionConstraint.CLI_VERSION_WITH_TRIM_CACHE,
|
||||
) >= 0,
|
||||
);
|
||||
} catch (e) {
|
||||
this._versionChangedListeners.forEach((listener) =>
|
||||
listener(undefined),
|
||||
@@ -1912,13 +1909,7 @@ function shouldDebugCliServer() {
|
||||
export class CliVersionConstraint {
|
||||
// The oldest version of the CLI that we support. This is used to determine
|
||||
// whether to show a warning about the CLI being too old on startup.
|
||||
public static OLDEST_SUPPORTED_CLI_VERSION = new SemVer("2.14.6");
|
||||
|
||||
/**
|
||||
* CLI version where the query server supports the `evaluation/trimCache` method
|
||||
* with `codeql database cleanup --mode=trim` semantics.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_TRIM_CACHE = new SemVer("2.15.1");
|
||||
public static OLDEST_SUPPORTED_CLI_VERSION = new SemVer("v2.15.5");
|
||||
|
||||
public static CLI_VERSION_WITHOUT_MRVA_EXTENSIBLE_PREDICATE_HACK = new SemVer(
|
||||
"2.16.1",
|
||||
@@ -1953,4 +1944,8 @@ export class CliVersionConstraint {
|
||||
async supportsMrvaPackCreate(): Promise<boolean> {
|
||||
return (await this.cli.getFeatures()).mrvaPackCreate === true;
|
||||
}
|
||||
|
||||
async supportsGenerateSummarySymbolMap(): Promise<boolean> {
|
||||
return (await this.cli.getFeatures()).generateSummarySymbolMap === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,21 @@ interface SetStateMsg {
|
||||
parsedResultSets: ParsedResultSets;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
/** Whether to display links to the dataflow models that generated particular nodes in a flow path. */
|
||||
shouldShowProvenance: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_USER_SETTINGS: UserSettings = {
|
||||
shouldShowProvenance: false,
|
||||
};
|
||||
|
||||
/** Message indicating that the user's configuration settings have changed. */
|
||||
interface SetUserSettingsMsg {
|
||||
t: "setUserSettings";
|
||||
userSettings: UserSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message indicating that the results view should display interpreted
|
||||
* results.
|
||||
@@ -191,6 +206,7 @@ interface UntoggleShowProblemsMsg {
|
||||
export type IntoResultsViewMsg =
|
||||
| ResultsUpdatingMsg
|
||||
| SetStateMsg
|
||||
| SetUserSettingsMsg
|
||||
| ShowInterpretedPageMsg
|
||||
| NavigateMsg
|
||||
| UntoggleShowProblemsMsg;
|
||||
@@ -208,13 +224,15 @@ export type FromResultsViewMsg =
|
||||
| OpenFileMsg;
|
||||
|
||||
/**
|
||||
* Message from the results view to open a database source
|
||||
* Message from the results view to open a source
|
||||
* file at the provided location.
|
||||
*/
|
||||
interface ViewSourceFileMsg {
|
||||
t: "viewSourceFile";
|
||||
loc: UrlValueResolvable;
|
||||
databaseUri: string;
|
||||
/** URI of the database whose source archive contains the file, or `undefined` to open a file from
|
||||
* the local disk. The latter case is used for opening links to data extension model files. */
|
||||
databaseUri: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,7 +359,8 @@ interface ChangeCompareMessage {
|
||||
|
||||
export type ToCompareViewMessage =
|
||||
| SetComparisonQueryInfoMessage
|
||||
| SetComparisonsMessage;
|
||||
| SetComparisonsMessage
|
||||
| SetUserSettingsMsg;
|
||||
|
||||
/**
|
||||
* Message to the compare view that sets the metadata of the compared queries.
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import type { Log, Tool } from "sarif";
|
||||
import type { Log } from "sarif";
|
||||
import { createReadStream } from "fs-extra";
|
||||
import { connectTo } from "stream-json/Assembler";
|
||||
import { getErrorMessage } from "./helpers-pure";
|
||||
import { withParser } from "stream-json/filters/Pick";
|
||||
|
||||
const DUMMY_TOOL: Tool = { driver: { name: "" } };
|
||||
import { withParser } from "stream-json/filters/Ignore";
|
||||
|
||||
export async function sarifParser(
|
||||
interpretedResultsPath: string,
|
||||
): Promise<Log> {
|
||||
try {
|
||||
// Parse the SARIF file into token streams, filtering out only the results array.
|
||||
// Parse the SARIF file into token streams, filtering out some of the larger subtrees that we
|
||||
// don't need.
|
||||
const pipeline = createReadStream(interpretedResultsPath).pipe(
|
||||
withParser({ filter: "runs.0.results" }),
|
||||
withParser({
|
||||
// We don't need to run's `artifacts` property, nor the driver's `notifications` property.
|
||||
filter: /^runs\.\d+\.(artifacts|tool\.driver\.notifications)/,
|
||||
}),
|
||||
);
|
||||
|
||||
// Creates JavaScript objects from the token stream
|
||||
@@ -38,15 +40,17 @@ export async function sarifParser(
|
||||
});
|
||||
|
||||
asm.on("done", (asm) => {
|
||||
const log: Log = {
|
||||
version: "2.1.0",
|
||||
runs: [
|
||||
{
|
||||
tool: DUMMY_TOOL,
|
||||
results: asm.current ?? [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const log = asm.current;
|
||||
|
||||
// Do some trivial validation. This isn't a full validation of the SARIF file, but it's at
|
||||
// least enough to ensure that we're not trying to parse complete garbage later.
|
||||
if (log.runs === undefined || log.runs.length < 1) {
|
||||
reject(
|
||||
new Error(
|
||||
"Invalid SARIF file: expecting at least one run with result.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
resolve(log);
|
||||
alreadyDone = true;
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
getResultSetNames,
|
||||
} from "./result-set-names";
|
||||
import { compareInterpretedResults } from "./interpreted-results";
|
||||
import { isCanary } from "../config";
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedLocalQueryInfo;
|
||||
@@ -116,6 +117,13 @@ export class CompareView extends AbstractWebview<
|
||||
panel.reveal(undefined, true);
|
||||
await this.waitForPanelLoaded();
|
||||
|
||||
await this.postMessage({
|
||||
t: "setUserSettings",
|
||||
userSettings: {
|
||||
shouldShowProvenance: isCanary(),
|
||||
},
|
||||
});
|
||||
|
||||
await this.postMessage({
|
||||
t: "setComparisonQueryInfo",
|
||||
stats: {
|
||||
|
||||
@@ -840,7 +840,6 @@ const LLM_GENERATION_DEV_ENDPOINT = new Setting(
|
||||
const MODEL_EVALUATION = new Setting("evaluation", MODEL_SETTING);
|
||||
const MODEL_PACK_LOCATION = new Setting("packLocation", MODEL_SETTING);
|
||||
const MODEL_PACK_NAME = new Setting("packName", MODEL_SETTING);
|
||||
const ENABLE_PYTHON = new Setting("enablePython", MODEL_SETTING);
|
||||
|
||||
export type ModelConfigPackVariables = {
|
||||
database: string;
|
||||
@@ -857,7 +856,6 @@ export interface ModelConfig {
|
||||
variables: ModelConfigPackVariables,
|
||||
): string;
|
||||
getPackName(languageId: string, variables: ModelConfigPackVariables): string;
|
||||
enablePython: boolean;
|
||||
}
|
||||
|
||||
export class ModelConfigListener extends ConfigListener implements ModelConfig {
|
||||
@@ -919,10 +917,6 @@ export class ModelConfigListener extends ConfigListener implements ModelConfig {
|
||||
variables,
|
||||
);
|
||||
}
|
||||
|
||||
public get enablePython(): boolean {
|
||||
return !!ENABLE_PYTHON.getValue<boolean>();
|
||||
}
|
||||
}
|
||||
|
||||
const GITHUB_DATABASE_SETTING = new Setting("githubDatabase", ROOT_SETTING);
|
||||
|
||||
@@ -37,11 +37,12 @@ export const shownLocationLineDecoration =
|
||||
/**
|
||||
* Resolves the specified CodeQL location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
* @param databaseItem Database in which to resolve the file location, or `undefined` to resolve
|
||||
* from the local file system.
|
||||
*/
|
||||
function resolveFivePartLocation(
|
||||
loc: UrlValueLineColumnLocation,
|
||||
databaseItem: DatabaseItem,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
): Location {
|
||||
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
|
||||
// are one-based. Adjust accordingly.
|
||||
@@ -52,7 +53,10 @@ function resolveFivePartLocation(
|
||||
Math.max(1, loc.endColumn),
|
||||
);
|
||||
|
||||
return new Location(databaseItem.resolveSourceFile(loc.uri), range);
|
||||
return new Location(
|
||||
databaseItem?.resolveSourceFile(loc.uri) ?? Uri.parse(loc.uri),
|
||||
range,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,22 +66,26 @@ function resolveFivePartLocation(
|
||||
*/
|
||||
function resolveWholeFileLocation(
|
||||
loc: UrlValueWholeFileLocation,
|
||||
databaseItem: DatabaseItem,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
): Location {
|
||||
// A location corresponding to the start of the file.
|
||||
const range = new Range(0, 0, 0, 0);
|
||||
return new Location(databaseItem.resolveSourceFile(loc.uri), range);
|
||||
return new Location(
|
||||
databaseItem?.resolveSourceFile(loc.uri) ?? Uri.parse(loc.uri),
|
||||
range,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
|
||||
* can be resolved, returns `undefined`.
|
||||
* @param loc CodeQL location to resolve
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
* @param databaseItem Database in which to resolve the file location, or `undefined` to resolve
|
||||
* from the local file system.
|
||||
*/
|
||||
export function tryResolveLocation(
|
||||
loc: UrlValueResolvable | undefined,
|
||||
databaseItem: DatabaseItem,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
): Location | undefined {
|
||||
if (!loc) {
|
||||
return;
|
||||
@@ -95,7 +103,7 @@ export function tryResolveLocation(
|
||||
|
||||
export async function showResolvableLocation(
|
||||
loc: UrlValueResolvable,
|
||||
databaseItem: DatabaseItem,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
try {
|
||||
@@ -151,13 +159,14 @@ export async function showLocation(location?: Location) {
|
||||
}
|
||||
|
||||
export async function jumpToLocation(
|
||||
databaseUri: string,
|
||||
databaseUri: string | undefined,
|
||||
loc: UrlValueResolvable,
|
||||
databaseManager: DatabaseManager,
|
||||
logger: Logger,
|
||||
) {
|
||||
const databaseItem = databaseManager.findDatabaseItem(Uri.parse(databaseUri));
|
||||
if (databaseItem !== undefined) {
|
||||
await showResolvableLocation(loc, databaseItem, logger);
|
||||
}
|
||||
const databaseItem =
|
||||
databaseUri !== undefined
|
||||
? databaseManager.findDatabaseItem(Uri.parse(databaseUri))
|
||||
: undefined;
|
||||
await showResolvableLocation(loc, databaseItem, logger);
|
||||
}
|
||||
|
||||
@@ -717,12 +717,13 @@ async function installOrUpdateThenTryActivate(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const PACK_GLOBS = [
|
||||
const CLEAR_PACK_CACHE_ON_EDIT_GLOBS = [
|
||||
"**/codeql-pack.yml",
|
||||
"**/qlpack.yml",
|
||||
"**/queries.xml",
|
||||
"**/codeql-pack.lock.yml",
|
||||
"**/qlpack.lock.yml",
|
||||
"**/*.dbscheme",
|
||||
".codeqlmanifest.json",
|
||||
"codeql-workspace.yml",
|
||||
];
|
||||
@@ -769,7 +770,7 @@ async function activateWithInstalledDistribution(
|
||||
ctx,
|
||||
);
|
||||
|
||||
for (const glob of PACK_GLOBS) {
|
||||
for (const glob of CLEAR_PACK_CACHE_ON_EDIT_GLOBS) {
|
||||
const fsWatcher = workspace.createFileSystemWatcher(glob);
|
||||
ctx.subscriptions.push(fsWatcher);
|
||||
|
||||
|
||||
@@ -537,6 +537,14 @@ export class ResultsView extends AbstractWebview<
|
||||
resultSetNames,
|
||||
};
|
||||
|
||||
await this.postMessage({
|
||||
t: "setUserSettings",
|
||||
userSettings: {
|
||||
// Only show provenance info in canary mode for now.
|
||||
shouldShowProvenance: isCanary(),
|
||||
},
|
||||
});
|
||||
|
||||
await this.postMessage({
|
||||
t: "setState",
|
||||
interpretation: interpretationPage,
|
||||
|
||||
@@ -33,6 +33,7 @@ export function decodeBqrsToMethods(
|
||||
let libraryVersion: string | undefined;
|
||||
let type: ModeledMethodType;
|
||||
let classification: CallClassification;
|
||||
let endpointKindColumn: string | BqrsEntityValue | undefined;
|
||||
let endpointType: EndpointType | undefined = undefined;
|
||||
|
||||
if (mode === Mode.Application) {
|
||||
@@ -47,6 +48,7 @@ export function decodeBqrsToMethods(
|
||||
libraryVersion,
|
||||
type,
|
||||
classification,
|
||||
endpointKindColumn,
|
||||
] = tuple as ApplicationModeTuple;
|
||||
} else {
|
||||
[
|
||||
@@ -58,6 +60,7 @@ export function decodeBqrsToMethods(
|
||||
supported,
|
||||
library,
|
||||
type,
|
||||
endpointKindColumn,
|
||||
] = tuple as FrameworkModeTuple;
|
||||
|
||||
classification = CallClassification.Unknown;
|
||||
@@ -68,13 +71,18 @@ export function decodeBqrsToMethods(
|
||||
}
|
||||
|
||||
if (definition.endpointTypeForEndpoint) {
|
||||
endpointType = definition.endpointTypeForEndpoint({
|
||||
endpointType,
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters,
|
||||
});
|
||||
endpointType = definition.endpointTypeForEndpoint(
|
||||
{
|
||||
endpointType,
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters,
|
||||
},
|
||||
typeof endpointKindColumn === "object"
|
||||
? endpointKindColumn.label
|
||||
: endpointKindColumn,
|
||||
);
|
||||
}
|
||||
|
||||
if (endpointType === undefined) {
|
||||
|
||||
@@ -174,11 +174,14 @@ export type ModelsAsDataLanguage = {
|
||||
* be determined by heuristics.
|
||||
* @param method The method to get the endpoint type for. The endpoint type can be undefined if the
|
||||
* query does not return an endpoint type.
|
||||
* @param endpointKind An optional column that may be provided by the query to help determine the
|
||||
* endpoint type.
|
||||
*/
|
||||
endpointTypeForEndpoint?: (
|
||||
method: Omit<MethodDefinition, "endpointType"> & {
|
||||
endpointType: EndpointType | undefined;
|
||||
},
|
||||
endpointKind: string | undefined,
|
||||
) => EndpointType | undefined;
|
||||
predicates: ModelsAsDataLanguagePredicates;
|
||||
modelGeneration?: ModelsAsDataLanguageModelGeneration;
|
||||
|
||||
@@ -4,7 +4,26 @@ import { EndpointType } from "../../method";
|
||||
|
||||
const memberTokenRegex = /^Member\[(.+)]$/;
|
||||
|
||||
export function parsePythonAccessPath(path: string): {
|
||||
// In Python, the type can contain both the package name and the type name.
|
||||
export function parsePythonType(type: string) {
|
||||
// The first part is always the package name. All remaining parts are the type
|
||||
// name.
|
||||
|
||||
const parts = type.split(".");
|
||||
const packageName = parts.shift() ?? "";
|
||||
|
||||
return {
|
||||
packageName,
|
||||
typeName: parts.join("."),
|
||||
};
|
||||
}
|
||||
|
||||
// The type name can also be specified in the type, so this will combine
|
||||
// the already parsed type name and the type name from the access path.
|
||||
export function parsePythonAccessPath(
|
||||
path: string,
|
||||
shortTypeName: string,
|
||||
): {
|
||||
typeName: string;
|
||||
methodName: string;
|
||||
endpointType: EndpointType;
|
||||
@@ -13,8 +32,12 @@ export function parsePythonAccessPath(path: string): {
|
||||
const tokens = parseAccessPathTokens(path);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
const typeName = shortTypeName.endsWith("!")
|
||||
? shortTypeName.slice(0, -1)
|
||||
: shortTypeName;
|
||||
|
||||
return {
|
||||
typeName: "",
|
||||
typeName,
|
||||
methodName: "",
|
||||
endpointType: EndpointType.Method,
|
||||
path: "",
|
||||
@@ -23,6 +46,10 @@ export function parsePythonAccessPath(path: string): {
|
||||
|
||||
const typeParts = [];
|
||||
let endpointType = EndpointType.Function;
|
||||
// If a short type name was given and it doesn't end in a `!`, then this refers to a method.
|
||||
if (shortTypeName !== "" && !shortTypeName.endsWith("!")) {
|
||||
endpointType = EndpointType.Method;
|
||||
}
|
||||
|
||||
let remainingTokens: typeof tokens = [];
|
||||
|
||||
@@ -32,6 +59,7 @@ export function parsePythonAccessPath(path: string): {
|
||||
if (memberMatch) {
|
||||
typeParts.push(memberMatch[1]);
|
||||
} else if (token.text === "Instance") {
|
||||
// Alternative way of specifying that this refers to a method.
|
||||
endpointType = EndpointType.Method;
|
||||
} else {
|
||||
remainingTokens = tokens.slice(i);
|
||||
@@ -40,9 +68,22 @@ export function parsePythonAccessPath(path: string): {
|
||||
}
|
||||
|
||||
const methodName = typeParts.pop() ?? "";
|
||||
const typeName = typeParts.join(".");
|
||||
let typeName = typeParts.join(".");
|
||||
const remainingPath = remainingTokens.map((token) => token.text).join(".");
|
||||
|
||||
if (shortTypeName !== "") {
|
||||
if (shortTypeName.endsWith("!")) {
|
||||
// The actual type name is the name without the `!`.
|
||||
shortTypeName = shortTypeName.slice(0, -1);
|
||||
}
|
||||
|
||||
if (typeName !== "") {
|
||||
typeName = `${shortTypeName}.${typeName}`;
|
||||
} else {
|
||||
typeName = shortTypeName;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
methodName,
|
||||
typeName,
|
||||
@@ -51,53 +92,59 @@ export function parsePythonAccessPath(path: string): {
|
||||
};
|
||||
}
|
||||
|
||||
export function parsePythonTypeAndPath(
|
||||
type: string,
|
||||
path: string,
|
||||
): {
|
||||
packageName: string;
|
||||
typeName: string;
|
||||
methodName: string;
|
||||
endpointType: EndpointType;
|
||||
path: string;
|
||||
} {
|
||||
const { packageName, typeName: shortTypeName } = parsePythonType(type);
|
||||
const {
|
||||
typeName,
|
||||
methodName,
|
||||
endpointType,
|
||||
path: remainingPath,
|
||||
} = parsePythonAccessPath(path, shortTypeName);
|
||||
|
||||
return {
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
endpointType,
|
||||
path: remainingPath,
|
||||
};
|
||||
}
|
||||
|
||||
export function pythonMethodSignature(typeName: string, methodName: string) {
|
||||
return `${typeName}#${methodName}`;
|
||||
}
|
||||
|
||||
function pythonTypePath(typeName: string) {
|
||||
if (typeName === "") {
|
||||
export function pythonType(
|
||||
packageName: string,
|
||||
typeName: string,
|
||||
endpointType: EndpointType,
|
||||
) {
|
||||
if (typeName !== "" && packageName !== "") {
|
||||
return `${packageName}.${typeName}${endpointType === EndpointType.Function ? "!" : ""}`;
|
||||
}
|
||||
|
||||
return `${packageName}${typeName}`;
|
||||
}
|
||||
|
||||
export function pythonMethodPath(methodName: string) {
|
||||
if (methodName === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return typeName
|
||||
.split(".")
|
||||
.map((part) => `Member[${part}]`)
|
||||
.join(".");
|
||||
return `Member[${methodName}]`;
|
||||
}
|
||||
|
||||
export function pythonMethodPath(
|
||||
typeName: string,
|
||||
methodName: string,
|
||||
endpointType: EndpointType,
|
||||
) {
|
||||
if (methodName === "") {
|
||||
return pythonTypePath(typeName);
|
||||
}
|
||||
|
||||
const typePath = pythonTypePath(typeName);
|
||||
|
||||
let result = typePath;
|
||||
if (typePath !== "" && endpointType === EndpointType.Method) {
|
||||
result += ".Instance";
|
||||
}
|
||||
|
||||
if (result !== "") {
|
||||
result += ".";
|
||||
}
|
||||
|
||||
result += `Member[${methodName}]`;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function pythonPath(
|
||||
typeName: string,
|
||||
methodName: string,
|
||||
endpointType: EndpointType,
|
||||
path: string,
|
||||
) {
|
||||
const methodPath = pythonMethodPath(typeName, methodName, endpointType);
|
||||
export function pythonPath(methodName: string, path: string) {
|
||||
const methodPath = pythonMethodPath(methodName);
|
||||
if (methodPath === "") {
|
||||
return path;
|
||||
}
|
||||
@@ -111,7 +158,24 @@ export function pythonPath(
|
||||
|
||||
export function pythonEndpointType(
|
||||
method: Omit<MethodDefinition, "endpointType">,
|
||||
endpointKind: string | undefined,
|
||||
): EndpointType {
|
||||
switch (endpointKind) {
|
||||
case "Function":
|
||||
return EndpointType.Function;
|
||||
case "InstanceMethod":
|
||||
return EndpointType.Method;
|
||||
case "ClassMethod":
|
||||
return EndpointType.ClassMethod;
|
||||
case "StaticMethod":
|
||||
return EndpointType.StaticMethod;
|
||||
case "InitMethod":
|
||||
return EndpointType.Constructor;
|
||||
case "Class":
|
||||
return EndpointType.Class;
|
||||
}
|
||||
|
||||
// Legacy behavior for when the kind column is missing.
|
||||
if (
|
||||
method.methodParameters.startsWith("(self,") ||
|
||||
method.methodParameters === "(self)"
|
||||
@@ -120,3 +184,12 @@ export function pythonEndpointType(
|
||||
}
|
||||
return EndpointType.Function;
|
||||
}
|
||||
|
||||
export function hasPythonSelfArgument(endpointType: EndpointType): boolean {
|
||||
// Instance methods and class methods both use `Argument[self]` for the first parameter. The first
|
||||
// parameter after self is called `Argument[0]`.
|
||||
return (
|
||||
endpointType === EndpointType.Method ||
|
||||
endpointType === EndpointType.ClassMethod
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,44 +4,48 @@ import { Mode } from "../../shared/mode";
|
||||
import type { MethodArgument } from "../../method";
|
||||
import { EndpointType, getArgumentsList } from "../../method";
|
||||
import {
|
||||
parsePythonAccessPath,
|
||||
hasPythonSelfArgument,
|
||||
parsePythonTypeAndPath,
|
||||
pythonEndpointType,
|
||||
pythonMethodPath,
|
||||
pythonMethodSignature,
|
||||
pythonPath,
|
||||
pythonType,
|
||||
} from "./access-paths";
|
||||
|
||||
export const python: ModelsAsDataLanguage = {
|
||||
availableModes: [Mode.Framework],
|
||||
createMethodSignature: ({ typeName, methodName }) =>
|
||||
`${typeName}#${methodName}`,
|
||||
endpointTypeForEndpoint: (method) => pythonEndpointType(method),
|
||||
endpointTypeForEndpoint: (method, endpointKind) =>
|
||||
pythonEndpointType(method, endpointKind),
|
||||
predicates: {
|
||||
source: {
|
||||
extensiblePredicate: sharedExtensiblePredicates.source,
|
||||
supportedKinds: sharedKinds.source,
|
||||
supportedEndpointTypes: [EndpointType.Method, EndpointType.Function],
|
||||
supportedEndpointTypes: [
|
||||
EndpointType.Method,
|
||||
EndpointType.Function,
|
||||
EndpointType.Constructor,
|
||||
EndpointType.ClassMethod,
|
||||
EndpointType.StaticMethod,
|
||||
],
|
||||
// extensible predicate sourceModel(
|
||||
// string type, string path, string kind
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.packageName,
|
||||
pythonPath(
|
||||
method.typeName,
|
||||
method.methodName,
|
||||
method.endpointType,
|
||||
method.output,
|
||||
),
|
||||
pythonType(method.packageName, method.typeName, method.endpointType),
|
||||
pythonPath(method.methodName, method.output),
|
||||
method.kind,
|
||||
],
|
||||
readModeledMethod: (row) => {
|
||||
const packageName = row[0] as string;
|
||||
const {
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
endpointType,
|
||||
path: output,
|
||||
} = parsePythonAccessPath(row[1] as string);
|
||||
} = parsePythonTypeAndPath(row[0] as string, row[1] as string);
|
||||
return {
|
||||
type: "source",
|
||||
output,
|
||||
@@ -59,30 +63,31 @@ export const python: ModelsAsDataLanguage = {
|
||||
sink: {
|
||||
extensiblePredicate: sharedExtensiblePredicates.sink,
|
||||
supportedKinds: sharedKinds.sink,
|
||||
supportedEndpointTypes: [EndpointType.Method, EndpointType.Function],
|
||||
supportedEndpointTypes: [
|
||||
EndpointType.Method,
|
||||
EndpointType.Function,
|
||||
EndpointType.Constructor,
|
||||
EndpointType.ClassMethod,
|
||||
EndpointType.StaticMethod,
|
||||
],
|
||||
// extensible predicate sinkModel(
|
||||
// string type, string path, string kind
|
||||
// );
|
||||
generateMethodDefinition: (method) => {
|
||||
return [
|
||||
method.packageName,
|
||||
pythonPath(
|
||||
method.typeName,
|
||||
method.methodName,
|
||||
method.endpointType,
|
||||
method.input,
|
||||
),
|
||||
pythonType(method.packageName, method.typeName, method.endpointType),
|
||||
pythonPath(method.methodName, method.input),
|
||||
method.kind,
|
||||
];
|
||||
},
|
||||
readModeledMethod: (row) => {
|
||||
const packageName = row[0] as string;
|
||||
const {
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
endpointType,
|
||||
path: input,
|
||||
} = parsePythonAccessPath(row[1] as string);
|
||||
} = parsePythonTypeAndPath(row[0] as string, row[1] as string);
|
||||
return {
|
||||
type: "sink",
|
||||
input,
|
||||
@@ -100,25 +105,26 @@ export const python: ModelsAsDataLanguage = {
|
||||
summary: {
|
||||
extensiblePredicate: sharedExtensiblePredicates.summary,
|
||||
supportedKinds: sharedKinds.summary,
|
||||
supportedEndpointTypes: [EndpointType.Method, EndpointType.Function],
|
||||
supportedEndpointTypes: [
|
||||
EndpointType.Method,
|
||||
EndpointType.Function,
|
||||
EndpointType.Constructor,
|
||||
EndpointType.ClassMethod,
|
||||
EndpointType.StaticMethod,
|
||||
],
|
||||
// extensible predicate summaryModel(
|
||||
// string type, string path, string input, string output, string kind
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.packageName,
|
||||
pythonMethodPath(
|
||||
method.typeName,
|
||||
method.methodName,
|
||||
method.endpointType,
|
||||
),
|
||||
pythonType(method.packageName, method.typeName, method.endpointType),
|
||||
pythonMethodPath(method.methodName),
|
||||
method.input,
|
||||
method.output,
|
||||
method.kind,
|
||||
],
|
||||
readModeledMethod: (row) => {
|
||||
const packageName = row[0] as string;
|
||||
const { typeName, methodName, endpointType, path } =
|
||||
parsePythonAccessPath(row[1] as string);
|
||||
const { packageName, typeName, methodName, endpointType, path } =
|
||||
parsePythonTypeAndPath(row[0] as string, row[1] as string);
|
||||
if (path !== "") {
|
||||
throw new Error("Summary path must be a method");
|
||||
}
|
||||
@@ -144,18 +150,13 @@ export const python: ModelsAsDataLanguage = {
|
||||
// string type, string path, string kind
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.packageName,
|
||||
pythonMethodPath(
|
||||
method.typeName,
|
||||
method.methodName,
|
||||
method.endpointType,
|
||||
),
|
||||
pythonType(method.packageName, method.typeName, method.endpointType),
|
||||
pythonMethodPath(method.methodName),
|
||||
method.kind,
|
||||
],
|
||||
readModeledMethod: (row) => {
|
||||
const packageName = row[0] as string;
|
||||
const { typeName, methodName, endpointType, path } =
|
||||
parsePythonAccessPath(row[1] as string);
|
||||
const { packageName, typeName, methodName, endpointType, path } =
|
||||
parsePythonTypeAndPath(row[0] as string, row[1] as string);
|
||||
if (path !== "") {
|
||||
throw new Error("Neutral path must be a method");
|
||||
}
|
||||
@@ -172,25 +173,46 @@ export const python: ModelsAsDataLanguage = {
|
||||
};
|
||||
},
|
||||
},
|
||||
type: {
|
||||
extensiblePredicate: "typeModel",
|
||||
// extensible predicate typeModel(string type1, string type2, string path);
|
||||
generateMethodDefinition: (method) => [
|
||||
method.relatedTypeName,
|
||||
pythonType(method.packageName, method.typeName, method.endpointType),
|
||||
pythonPath(method.methodName, method.path),
|
||||
],
|
||||
readModeledMethod: (row) => {
|
||||
const { packageName, typeName, methodName, endpointType, path } =
|
||||
parsePythonTypeAndPath(row[1] as string, row[2] as string);
|
||||
|
||||
return {
|
||||
type: "type",
|
||||
relatedTypeName: row[0] as string,
|
||||
path,
|
||||
signature: pythonMethodSignature(typeName, methodName),
|
||||
endpointType,
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
getArgumentOptions: (method) => {
|
||||
// Argument and Parameter are equivalent in Python, but we'll use Argument in the model editor
|
||||
const argumentsList = getArgumentsList(method.methodParameters).map(
|
||||
(argument, index): MethodArgument => {
|
||||
if (
|
||||
method.endpointType === EndpointType.Method &&
|
||||
argument === "self" &&
|
||||
index === 0
|
||||
) {
|
||||
if (hasPythonSelfArgument(method.endpointType) && index === 0) {
|
||||
return {
|
||||
path: "Argument[self]",
|
||||
label: "Argument[self]: self",
|
||||
label: `Argument[self]: ${argument}`,
|
||||
};
|
||||
}
|
||||
|
||||
// If this is a method, self does not count as an argument index, so we
|
||||
// If this endpoint has a self argument, self does not count as an argument index so we
|
||||
// should start at 0 for the second argument
|
||||
if (method.endpointType === EndpointType.Method) {
|
||||
if (hasPythonSelfArgument(method.endpointType)) {
|
||||
index -= 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ export enum EndpointType {
|
||||
Method = "method",
|
||||
Constructor = "constructor",
|
||||
Function = "function",
|
||||
StaticMethod = "staticMethod",
|
||||
ClassMethod = "classMethod",
|
||||
}
|
||||
|
||||
export interface MethodDefinition {
|
||||
|
||||
@@ -17,6 +17,7 @@ export type Query = {
|
||||
* - libraryVersion: the version of the library that contains the external API. This is a string and can be empty if the version cannot be determined.
|
||||
* - type: the modeled kind of the method, either "sink", "source", "summary", or "neutral"
|
||||
* - classification: the classification of the use of the method, either "source", "test", "generated", or "unknown"
|
||||
* - kind: the kind of the endpoint, language-specific, e.g. "method" or "function"
|
||||
*/
|
||||
applicationModeQuery: string;
|
||||
/**
|
||||
@@ -32,6 +33,7 @@ export type Query = {
|
||||
* - supported: whether this method is modeled. This should be a string representation of a boolean to satify the result pattern for a problem query.
|
||||
* - libraryName: the name of the file or library that contains the method. This is a string and usually the basename of a file.
|
||||
* - type: the modeled kind of the method, either "sink", "source", "summary", or "neutral"
|
||||
* - kind: the kind of the endpoint, language-specific, e.g. "method" or "function"
|
||||
*/
|
||||
frameworkModeQuery: string;
|
||||
dependencies?: {
|
||||
@@ -50,6 +52,7 @@ export type ApplicationModeTuple = [
|
||||
string,
|
||||
ModeledMethodType,
|
||||
CallClassification,
|
||||
string | BqrsEntityValue | undefined,
|
||||
];
|
||||
|
||||
export type FrameworkModeTuple = [
|
||||
@@ -61,4 +64,5 @@ export type FrameworkModeTuple = [
|
||||
boolean,
|
||||
string,
|
||||
ModeledMethodType,
|
||||
string | BqrsEntityValue | undefined,
|
||||
];
|
||||
|
||||
@@ -9,20 +9,16 @@ export const SUPPORTED_LANGUAGES: QueryLanguage[] = [
|
||||
QueryLanguage.Java,
|
||||
QueryLanguage.CSharp,
|
||||
QueryLanguage.Ruby,
|
||||
QueryLanguage.Python,
|
||||
];
|
||||
|
||||
export function isSupportedLanguage(
|
||||
language: QueryLanguage,
|
||||
modelConfig: ModelConfig,
|
||||
_modelConfig: ModelConfig,
|
||||
) {
|
||||
if (SUPPORTED_LANGUAGES.includes(language)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (language === QueryLanguage.Python) {
|
||||
// Python is only enabled when the config setting is set
|
||||
return modelConfig.enablePython;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -544,9 +544,16 @@ export async function generateEvalLogSummaries(
|
||||
await cliServer.generateJsonLogSummary(log, jsonSummary);
|
||||
|
||||
if (humanReadableSummary !== undefined) {
|
||||
progress(progressUpdate(3, 3, "Generating summary symbols file"));
|
||||
summarySymbols = outputDir.evalLogSummarySymbolsPath;
|
||||
await generateSummarySymbolsFile(humanReadableSummary, summarySymbols);
|
||||
if (
|
||||
!(await cliServer.cliConstraints.supportsGenerateSummarySymbolMap())
|
||||
) {
|
||||
// We're using an old CLI that cannot generate the summary symbols file while generating the
|
||||
// human-readable log summary. As a fallback, create it by parsing the human-readable
|
||||
// summary.
|
||||
progress(progressUpdate(3, 3, "Generating summary symbols file"));
|
||||
await generateSummarySymbolsFile(humanReadableSummary, summarySymbols);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,18 @@ const ShowPathsLink = styled(VSCodeLink)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Label = styled.span`
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
function getShortestPathLength(codeFlows: CodeFlow[]): number {
|
||||
const allPathLengths = codeFlows
|
||||
.map((codeFlow) => codeFlow.threadFlows.length)
|
||||
.flat();
|
||||
return Math.min(...allPathLengths);
|
||||
}
|
||||
|
||||
export type CodePathsProps = {
|
||||
codeFlows: CodeFlow[];
|
||||
ruleDescription: string;
|
||||
@@ -40,6 +52,7 @@ export const CodePaths = ({
|
||||
return (
|
||||
<>
|
||||
<ShowPathsLink onClick={onShowPathsClick}>Show paths</ShowPathsLink>
|
||||
<Label>(Shortest: {getShortestPathLength(codeFlows)})</Label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,12 @@ describe(CodePaths.name, () => {
|
||||
expect(screen.getByText("Show paths")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders shortest path for code flows", () => {
|
||||
render();
|
||||
|
||||
expect(screen.getByText("(Shortest: 1)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("posts extension message when 'show paths' link clicked", async () => {
|
||||
render();
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import type {
|
||||
ToCompareViewMessage,
|
||||
SetComparisonsMessage,
|
||||
SetComparisonQueryInfoMessage,
|
||||
UserSettings,
|
||||
} from "../../common/interface-types";
|
||||
import { DEFAULT_USER_SETTINGS } from "../../common/interface-types";
|
||||
import CompareSelector from "./CompareSelector";
|
||||
import { vscode } from "../vscode-api";
|
||||
import CompareTable from "./CompareTable";
|
||||
@@ -31,6 +33,9 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
|
||||
const [comparison, setComparison] = useState<SetComparisonsMessage | null>(
|
||||
null,
|
||||
);
|
||||
const [userSettings, setUserSettings] = useState<UserSettings>(
|
||||
DEFAULT_USER_SETTINGS,
|
||||
);
|
||||
|
||||
const message = comparison?.message || "Empty comparison";
|
||||
const hasRows =
|
||||
@@ -48,6 +53,9 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
|
||||
case "setComparisons":
|
||||
setComparison(msg);
|
||||
break;
|
||||
case "setUserSettings":
|
||||
setUserSettings(msg.userSettings);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -85,6 +93,7 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
|
||||
<CompareTable
|
||||
queryInfo={queryInfo}
|
||||
comparison={comparison}
|
||||
userSettings={userSettings}
|
||||
></CompareTable>
|
||||
) : (
|
||||
<Message>{message}</Message>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
SetComparisonQueryInfoMessage,
|
||||
SetComparisonsMessage,
|
||||
UserSettings,
|
||||
} from "../../common/interface-types";
|
||||
import { className } from "../results/result-table-utils";
|
||||
import { vscode } from "../vscode-api";
|
||||
@@ -12,6 +13,7 @@ import { InterpretedCompareResultTable } from "./InterpretedCompareResultTable";
|
||||
interface Props {
|
||||
queryInfo: SetComparisonQueryInfoMessage;
|
||||
comparison: SetComparisonsMessage;
|
||||
userSettings: UserSettings;
|
||||
}
|
||||
|
||||
const OpenButton = styled(TextButton)`
|
||||
@@ -29,7 +31,11 @@ const Table = styled.table`
|
||||
}
|
||||
`;
|
||||
|
||||
export default function CompareTable({ queryInfo, comparison }: Props) {
|
||||
export default function CompareTable({
|
||||
queryInfo,
|
||||
comparison,
|
||||
userSettings,
|
||||
}: Props) {
|
||||
const result = comparison.result!;
|
||||
|
||||
async function openQuery(kind: "from" | "to") {
|
||||
@@ -78,6 +84,7 @@ export default function CompareTable({ queryInfo, comparison }: Props) {
|
||||
{result.kind === "interpreted" && (
|
||||
<InterpretedCompareResultTable
|
||||
results={result.from}
|
||||
userSettings={userSettings}
|
||||
databaseUri={queryInfo.databaseUri}
|
||||
sourceLocationPrefix={result.sourceLocationPrefix}
|
||||
/>
|
||||
@@ -96,6 +103,7 @@ export default function CompareTable({ queryInfo, comparison }: Props) {
|
||||
{result.kind === "interpreted" && (
|
||||
<InterpretedCompareResultTable
|
||||
results={result.to}
|
||||
userSettings={userSettings}
|
||||
databaseUri={queryInfo.databaseUri}
|
||||
sourceLocationPrefix={result.sourceLocationPrefix}
|
||||
/>
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import type { Result } from "sarif";
|
||||
import type { Result, Run } from "sarif";
|
||||
import { AlertTable } from "../results/AlertTable";
|
||||
import type { UserSettings } from "../../common/interface-types";
|
||||
|
||||
type Props = {
|
||||
results: Result[];
|
||||
databaseUri: string;
|
||||
sourceLocationPrefix: string;
|
||||
run?: Run;
|
||||
userSettings: UserSettings;
|
||||
};
|
||||
|
||||
export const InterpretedCompareResultTable = ({
|
||||
results,
|
||||
databaseUri,
|
||||
sourceLocationPrefix,
|
||||
userSettings,
|
||||
}: Props) => {
|
||||
return (
|
||||
<AlertTable
|
||||
results={results}
|
||||
userSettings={userSettings}
|
||||
databaseUri={databaseUri}
|
||||
sourceLocationPrefix={sourceLocationPrefix}
|
||||
header={
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={2}></th>
|
||||
<th className={`vscode-codeql__alert-message-cell`} colSpan={3}>
|
||||
<th className={`vscode-codeql__alert-message-cell`} colSpan={4}>
|
||||
Message
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Location, Result } from "sarif";
|
||||
import type { Location, Result, Run } from "sarif";
|
||||
import type {
|
||||
PathNode,
|
||||
Result as ResultKeysResult,
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
import { getPath, getPathNode, getResult, keyToString } from "./result-keys";
|
||||
import { className, jumpToLocation } from "./result-table-utils";
|
||||
import { onNavigation } from "./navigation";
|
||||
import type { NavigateMsg } from "../../common/interface-types";
|
||||
import type { NavigateMsg, UserSettings } from "../../common/interface-types";
|
||||
import { NavigationDirection } from "../../common/interface-types";
|
||||
import { isNoLocation, parseSarifLocation } from "../../common/sarif-utils";
|
||||
import { sendTelemetry } from "../common/telemetry";
|
||||
@@ -21,6 +21,8 @@ type Props = {
|
||||
results: Result[];
|
||||
databaseUri: string;
|
||||
sourceLocationPrefix: string;
|
||||
run?: Run;
|
||||
userSettings: UserSettings;
|
||||
numTruncatedResults?: number;
|
||||
|
||||
header: ReactNode;
|
||||
@@ -31,6 +33,8 @@ export function AlertTable({
|
||||
results,
|
||||
databaseUri,
|
||||
sourceLocationPrefix,
|
||||
run,
|
||||
userSettings,
|
||||
numTruncatedResults,
|
||||
header,
|
||||
noResults,
|
||||
@@ -202,6 +206,8 @@ export function AlertTable({
|
||||
selectedItem={selectedItem}
|
||||
selectedItemRef={selectedItemRef}
|
||||
databaseUri={databaseUri}
|
||||
run={run}
|
||||
userSettings={userSettings}
|
||||
sourceLocationPrefix={sourceLocationPrefix}
|
||||
updateSelectionCallback={updateSelectionCallback}
|
||||
toggleExpanded={toggle}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function AlertTableHeader({
|
||||
<th colSpan={2}></th>
|
||||
<th
|
||||
className={`${sortClass()} vscode-codeql__alert-message-cell`}
|
||||
colSpan={3}
|
||||
colSpan={4}
|
||||
onClick={toggleSortStateForColumn}
|
||||
>
|
||||
Message
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ThreadFlowLocation } from "sarif";
|
||||
import type { ThreadFlowLocation, Run } from "sarif";
|
||||
import type {
|
||||
PathNode,
|
||||
Result as ResultKeysResult,
|
||||
@@ -9,6 +9,8 @@ import { SarifLocation } from "./locations/SarifLocation";
|
||||
import { selectableZebraStripe } from "./result-table-utils";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { VerticalRule } from "../common/VerticalRule";
|
||||
import type { UserSettings } from "../../common/interface-types";
|
||||
import { TaxaLocations } from "./locations/TaxaLocations";
|
||||
|
||||
interface Props {
|
||||
step: ThreadFlowLocation;
|
||||
@@ -19,6 +21,8 @@ interface Props {
|
||||
selectedItemRef: React.RefObject<HTMLTableRowElement>;
|
||||
databaseUri: string;
|
||||
sourceLocationPrefix: string;
|
||||
run?: Run;
|
||||
userSettings: UserSettings;
|
||||
updateSelectionCallback: (
|
||||
resultKey: PathNode | ResultKeysResult | undefined,
|
||||
) => void;
|
||||
@@ -34,6 +38,8 @@ export function AlertTablePathNodeRow(props: Props) {
|
||||
selectedItemRef,
|
||||
databaseUri,
|
||||
sourceLocationPrefix,
|
||||
run,
|
||||
userSettings,
|
||||
updateSelectionCallback,
|
||||
} = props;
|
||||
|
||||
@@ -86,6 +92,23 @@ export function AlertTablePathNodeRow(props: Props) {
|
||||
"[no location]"
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
{...selectableZebraStripe(
|
||||
isSelected,
|
||||
zebraIndex,
|
||||
"vscode-codeql__taxa-cell",
|
||||
)}
|
||||
>
|
||||
{userSettings.shouldShowProvenance ? (
|
||||
<div className="vscode-codeql__taxa-cell-contents">
|
||||
<TaxaLocations
|
||||
taxa={step.taxa}
|
||||
run={run}
|
||||
onClick={handleSarifLocationClicked}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td
|
||||
{...selectableZebraStripe(
|
||||
isSelected,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ThreadFlow } from "sarif";
|
||||
import type { Run, ThreadFlow } from "sarif";
|
||||
import type {
|
||||
PathNode,
|
||||
Result as ResultKeysResult,
|
||||
@@ -10,6 +10,7 @@ import { AlertTablePathNodeRow } from "./AlertTablePathNodeRow";
|
||||
import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCell";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { VerticalRule } from "../common/VerticalRule";
|
||||
import type { UserSettings } from "../../common/interface-types";
|
||||
|
||||
interface Props {
|
||||
path: ThreadFlow;
|
||||
@@ -20,6 +21,8 @@ interface Props {
|
||||
selectedItemRef: React.RefObject<HTMLTableRowElement>;
|
||||
databaseUri: string;
|
||||
sourceLocationPrefix: string;
|
||||
run?: Run;
|
||||
userSettings: UserSettings;
|
||||
updateSelectionCallback: (
|
||||
resultKey: PathNode | ResultKeysResult | undefined,
|
||||
) => void;
|
||||
@@ -61,7 +64,7 @@ export function AlertTablePathRow(props: Props) {
|
||||
expanded={currentPathExpanded}
|
||||
onClick={handleDropdownClick}
|
||||
/>
|
||||
<td className="vscode-codeql__text-center" colSpan={3}>
|
||||
<td className="vscode-codeql__text-center" colSpan={4}>
|
||||
Path
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Result } from "sarif";
|
||||
import type { Result, Run } from "sarif";
|
||||
import type {
|
||||
PathNode,
|
||||
Result as ResultKeysResult,
|
||||
@@ -12,6 +12,7 @@ import { useCallback, useMemo } from "react";
|
||||
import { SarifLocation } from "./locations/SarifLocation";
|
||||
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
|
||||
import { AlertTablePathRow } from "./AlertTablePathRow";
|
||||
import type { UserSettings } from "../../common/interface-types";
|
||||
|
||||
interface Props {
|
||||
result: Result;
|
||||
@@ -21,6 +22,8 @@ interface Props {
|
||||
selectedItemRef: React.RefObject<HTMLTableRowElement>;
|
||||
databaseUri: string;
|
||||
sourceLocationPrefix: string;
|
||||
run?: Run;
|
||||
userSettings: UserSettings;
|
||||
updateSelectionCallback: (
|
||||
resultKey: PathNode | ResultKeysResult | undefined,
|
||||
) => void;
|
||||
@@ -90,7 +93,7 @@ export function AlertTableResultRow(props: Props) {
|
||||
{result.codeFlows === undefined ? (
|
||||
<>
|
||||
<td className="vscode-codeql__icon-cell">{info}</td>
|
||||
<td colSpan={3}>{msg}</td>
|
||||
<td colSpan={4}>{msg}</td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -99,7 +102,7 @@ export function AlertTableResultRow(props: Props) {
|
||||
onClick={handleDropdownClick}
|
||||
/>
|
||||
<td className="vscode-codeql__icon-cell">{listUnordered}</td>
|
||||
<td colSpan={2}>{msg}</td>
|
||||
<td colSpan={3}>{msg}</td>
|
||||
</>
|
||||
)}
|
||||
<td className="vscode-codeql__location-cell">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AlertTableNoResults } from "./AlertTableNoResults";
|
||||
import { AlertTableHeader } from "./AlertTableHeader";
|
||||
|
||||
export function ResultTable(props: ResultTableProps) {
|
||||
const { resultSet } = props;
|
||||
const { resultSet, userSettings } = props;
|
||||
switch (resultSet.t) {
|
||||
case "RawResultSet":
|
||||
return <RawTable {...props} resultSet={resultSet.resultSet} />;
|
||||
@@ -21,6 +21,8 @@ export function ResultTable(props: ResultTableProps) {
|
||||
sourceLocationPrefix={
|
||||
resultSet.interpretation.sourceLocationPrefix
|
||||
}
|
||||
run={data.runs[0]}
|
||||
userSettings={userSettings}
|
||||
numTruncatedResults={resultSet.interpretation.numTruncatedResults}
|
||||
header={<AlertTableHeader sortState={data.sortState} />}
|
||||
noResults={
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ResultSet,
|
||||
ParsedResultSets,
|
||||
IntoResultsViewMsg,
|
||||
UserSettings,
|
||||
} from "../../common/interface-types";
|
||||
import {
|
||||
ALERTS_TABLE_NAME,
|
||||
@@ -33,6 +34,7 @@ interface ResultTablesProps {
|
||||
rawResultSets: readonly ResultSet[];
|
||||
interpretation: Interpretation | undefined;
|
||||
database: DatabaseInfo;
|
||||
userSettings: UserSettings;
|
||||
metadata?: QueryMetadata;
|
||||
resultsPath: string;
|
||||
origResultsPaths: ResultsPaths;
|
||||
@@ -94,6 +96,7 @@ export function ResultTables(props: ResultTablesProps) {
|
||||
interpretation,
|
||||
database,
|
||||
resultsPath,
|
||||
userSettings,
|
||||
metadata,
|
||||
origResultsPaths,
|
||||
isLoadingNewResults,
|
||||
@@ -242,6 +245,7 @@ export function ResultTables(props: ResultTablesProps) {
|
||||
<ResultTable
|
||||
key={resultSetName}
|
||||
resultSet={resultSet}
|
||||
userSettings={userSettings}
|
||||
databaseUri={database.databaseUri}
|
||||
resultsPath={resultsPath}
|
||||
sortState={sortStates.get(resultSetName)}
|
||||
|
||||
@@ -9,9 +9,11 @@ import type {
|
||||
ResultsPaths,
|
||||
ParsedResultSets,
|
||||
ResultSet,
|
||||
UserSettings,
|
||||
} from "../../common/interface-types";
|
||||
import {
|
||||
ALERTS_TABLE_NAME,
|
||||
DEFAULT_USER_SETTINGS,
|
||||
GRAPH_TABLE_NAME,
|
||||
} from "../../common/interface-types";
|
||||
import { ResultTables } from "./ResultTables";
|
||||
@@ -77,6 +79,10 @@ export function ResultsApp() {
|
||||
isExpectingResultsUpdate: true,
|
||||
});
|
||||
|
||||
const [userSettings, setUserSettings] = useState<UserSettings>(
|
||||
DEFAULT_USER_SETTINGS,
|
||||
);
|
||||
|
||||
const updateStateWithNewResultsInfo = useCallback(
|
||||
(resultsInfo: ResultsInfo): void => {
|
||||
let results: Results | null = null;
|
||||
@@ -110,6 +116,10 @@ export function ResultsApp() {
|
||||
const handleMessage = useCallback(
|
||||
(msg: IntoResultsViewMsg): void => {
|
||||
switch (msg.t) {
|
||||
case "setUserSettings":
|
||||
setUserSettings(msg.userSettings);
|
||||
break;
|
||||
|
||||
case "setState":
|
||||
updateStateWithNewResultsInfo({
|
||||
resultsPath: msg.resultsPath,
|
||||
@@ -217,6 +227,7 @@ export function ResultsApp() {
|
||||
? displayedResults.resultsInfo.interpretation
|
||||
: undefined
|
||||
}
|
||||
userSettings={userSettings}
|
||||
database={displayedResults.results.database}
|
||||
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
|
||||
resultsPath={displayedResults.resultsInfo.resultsPath}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { UrlValueResolvable } from "../../../common/raw-result-types";
|
||||
interface Props {
|
||||
loc: UrlValueResolvable;
|
||||
label: string;
|
||||
databaseUri: string;
|
||||
databaseUri: string | undefined;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,17 +26,13 @@ export function Location({
|
||||
const displayLabel = useMemo(() => convertNonPrintableChars(label), [label]);
|
||||
|
||||
if (loc === undefined) {
|
||||
return <NonClickableLocation msg={displayLabel} />;
|
||||
return <NonClickableLocation msg={displayLabel} locationHint={title} />;
|
||||
}
|
||||
|
||||
if (loc.type === "string") {
|
||||
return <a href={loc.value}>{loc.value}</a>;
|
||||
}
|
||||
|
||||
if (databaseUri === undefined) {
|
||||
return <NonClickableLocation msg={displayLabel} locationHint={title} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClickableLocation
|
||||
loc={loc}
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
text?: string;
|
||||
loc?: SarifLogLocation;
|
||||
sourceLocationPrefix: string;
|
||||
databaseUri: string;
|
||||
databaseUri: string | undefined;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import type {
|
||||
Location as SarifLogLocation,
|
||||
ArtifactLocation,
|
||||
PhysicalLocation,
|
||||
ReportingDescriptorReference,
|
||||
Run,
|
||||
ToolComponent,
|
||||
} from "sarif";
|
||||
import { SarifLocation } from "./SarifLocation";
|
||||
|
||||
/** The definition of a taxon for a data extension model row. */
|
||||
interface ModelTaxon {
|
||||
kind: "model";
|
||||
location: SarifLogLocation;
|
||||
}
|
||||
|
||||
/** A taxon for a built-in model, such as `AdditionalFlowStep`. */
|
||||
interface BuiltInTaxon {
|
||||
kind: "string";
|
||||
text: string;
|
||||
}
|
||||
|
||||
type TaxonDefinition = ModelTaxon | BuiltInTaxon;
|
||||
|
||||
/** Resolve an `ArtifactLocation` that might contain a relative reference instead of an absolute
|
||||
* URI.
|
||||
*/
|
||||
function resolveArtifactLocation(
|
||||
location: ArtifactLocation,
|
||||
baseUri: URL,
|
||||
): ArtifactLocation {
|
||||
if (location.uri === undefined) {
|
||||
// No URI at all. Just return the original location.
|
||||
return location;
|
||||
}
|
||||
return {
|
||||
...location,
|
||||
uri: new URL(location.uri, baseUri).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Get the URI of the pack's local root directory, if available. */
|
||||
function getLocalPackUri(extension: ToolComponent): URL | undefined {
|
||||
if (extension.locations === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const localPackLocation = extension.locations.find(
|
||||
(loc) =>
|
||||
loc.properties !== undefined &&
|
||||
loc.properties.tags !== undefined &&
|
||||
loc.properties.tags.includes("CodeQL/LocalPackRoot"),
|
||||
);
|
||||
if (localPackLocation === undefined || localPackLocation.uri === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new URL(localPackLocation.uri);
|
||||
}
|
||||
|
||||
/** Resolve a `ReportingDescriptorReference` to the built-in taxon it refers to, or `undefined` if
|
||||
* it is not a built-in taxon.
|
||||
*/
|
||||
function resolveBuiltInTaxon(
|
||||
taxonRef: ReportingDescriptorReference,
|
||||
): BuiltInTaxon | undefined {
|
||||
if (
|
||||
taxonRef.id !== undefined &&
|
||||
taxonRef.index === undefined &&
|
||||
taxonRef.toolComponent === undefined
|
||||
) {
|
||||
return {
|
||||
kind: "string",
|
||||
text: taxonRef.id,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a `ReportingDescriptorReference` to the MaD taxon definition it refers to, or
|
||||
* `undefined` if it does not refer to a MaD model.
|
||||
*/
|
||||
function resolveModelTaxon(
|
||||
taxonRef: ReportingDescriptorReference,
|
||||
run: Run,
|
||||
): ModelTaxon | undefined {
|
||||
const extensions = run.tool.extensions;
|
||||
if (extensions === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const extensionIndex = taxonRef.toolComponent?.index;
|
||||
if (
|
||||
extensionIndex === undefined ||
|
||||
extensionIndex < 0 ||
|
||||
extensionIndex >= extensions.length
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const extension = extensions[extensionIndex];
|
||||
if (extension.taxa === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const localPackUri = getLocalPackUri(extension);
|
||||
if (localPackUri === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const taxonIndex = taxonRef.index;
|
||||
if (
|
||||
taxonIndex === undefined ||
|
||||
taxonIndex < 0 ||
|
||||
taxonIndex >= extension.taxa.length
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const taxonDef = extension.taxa[taxonIndex];
|
||||
if (taxonDef.properties === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const location: PhysicalLocation =
|
||||
taxonDef.properties["CodeQL/DataExtensionLocation"];
|
||||
if (location === undefined || location.artifactLocation === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "model",
|
||||
location: {
|
||||
physicalLocation: {
|
||||
...location,
|
||||
artifactLocation: resolveArtifactLocation(
|
||||
location.artifactLocation,
|
||||
localPackUri,
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve a `ReportingDescriptorReference` to the taxon definition it refers to. */
|
||||
function resolveTaxonDefinition(
|
||||
run: Run,
|
||||
taxonRef: ReportingDescriptorReference,
|
||||
): TaxonDefinition | undefined {
|
||||
return resolveModelTaxon(taxonRef, run) ?? resolveBuiltInTaxon(taxonRef);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
taxa: ReportingDescriptorReference[] | undefined;
|
||||
run: Run | undefined;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
/** Generate the React elements for each taxon. */
|
||||
export function TaxaLocations({
|
||||
taxa,
|
||||
run,
|
||||
onClick,
|
||||
}: Props): React.JSX.Element[] {
|
||||
if (taxa === undefined || taxa.length === 0 || run === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return taxa.flatMap((taxonRef, index) => {
|
||||
if (taxonRef.properties === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const role = taxonRef.properties["CodeQL/DataflowRole"];
|
||||
if (typeof role !== "string") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const taxonDef = resolveTaxonDefinition(run, taxonRef);
|
||||
if (taxonDef === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{`(${role}) `}
|
||||
<SarifLocation
|
||||
loc={taxonDef.kind === "model" ? taxonDef.location : undefined}
|
||||
databaseUri={undefined}
|
||||
text={taxonDef.kind === "string" ? taxonDef.text : undefined}
|
||||
sourceLocationPrefix=""
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
QueryMetadata,
|
||||
RawResultsSortState,
|
||||
ResultSet,
|
||||
UserSettings,
|
||||
} from "../../common/interface-types";
|
||||
import { SortDirection } from "../../common/interface-types";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
@@ -11,6 +12,7 @@ import type { UrlValueResolvable } from "../../common/raw-result-types";
|
||||
export interface ResultTableProps {
|
||||
resultSet: ResultSet;
|
||||
databaseUri: string;
|
||||
userSettings: UserSettings;
|
||||
metadata?: QueryMetadata;
|
||||
resultsPath: string | undefined;
|
||||
sortState?: RawResultsSortState;
|
||||
@@ -41,7 +43,7 @@ export const selectedRowClassName = "vscode-codeql__result-table-row--selected";
|
||||
|
||||
export function jumpToLocation(
|
||||
loc: UrlValueResolvable,
|
||||
databaseUri: string,
|
||||
databaseUri: string | undefined,
|
||||
): void {
|
||||
vscode.postMessage({
|
||||
t: "viewSourceFile",
|
||||
|
||||
@@ -144,3 +144,12 @@ td.vscode-codeql__path-index-cell {
|
||||
.vscode-codeql__location-cell {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.vscode-codeql__taxa-cell {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.vscode-codeql__taxa-cell-contents {
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
"v2.17.3",
|
||||
"v2.18.1",
|
||||
"v2.17.6",
|
||||
"v2.16.6",
|
||||
"v2.15.5",
|
||||
"v2.14.6",
|
||||
"nightly"
|
||||
]
|
||||
|
||||
@@ -1,16 +1,59 @@
|
||||
import {
|
||||
parsePythonAccessPath,
|
||||
parsePythonType,
|
||||
pythonEndpointType,
|
||||
pythonPath,
|
||||
} from "../../../../../src/model-editor/languages/python/access-paths";
|
||||
import { EndpointType } from "../../../../../src/model-editor/method";
|
||||
|
||||
describe("parsePythonType", () => {
|
||||
it("parses a type with a package", () => {
|
||||
expect(parsePythonType("requests.utils")).toEqual({
|
||||
packageName: "requests",
|
||||
typeName: "utils",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a nested type with a package", () => {
|
||||
expect(parsePythonType("requests.adapters.HTTPAdapter")).toEqual({
|
||||
packageName: "requests",
|
||||
typeName: "adapters.HTTPAdapter",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a package without a type", () => {
|
||||
expect(parsePythonType("requests")).toEqual({
|
||||
packageName: "requests",
|
||||
typeName: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses an empty string", () => {
|
||||
expect(parsePythonType("")).toEqual({
|
||||
packageName: "",
|
||||
typeName: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const testCases: Array<{
|
||||
path: string;
|
||||
shortTypeName: string;
|
||||
method: ReturnType<typeof parsePythonAccessPath>;
|
||||
}> = [
|
||||
{
|
||||
path: "Member[CommonTokens].Member[Class].Instance.Member[foo]",
|
||||
shortTypeName: "",
|
||||
method: {
|
||||
typeName: "CommonTokens.Class",
|
||||
methodName: "foo",
|
||||
endpointType: EndpointType.Method,
|
||||
path: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "Member[foo]",
|
||||
shortTypeName: "CommonTokens.Class",
|
||||
method: {
|
||||
typeName: "CommonTokens.Class",
|
||||
methodName: "foo",
|
||||
@@ -20,6 +63,17 @@ const testCases: Array<{
|
||||
},
|
||||
{
|
||||
path: "Member[CommonTokens].Member[Class].Instance.Member[foo].Parameter[self]",
|
||||
shortTypeName: "",
|
||||
method: {
|
||||
typeName: "CommonTokens.Class",
|
||||
methodName: "foo",
|
||||
endpointType: EndpointType.Method,
|
||||
path: "Parameter[self]",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "Member[foo].Parameter[self]",
|
||||
shortTypeName: "CommonTokens.Class",
|
||||
method: {
|
||||
typeName: "CommonTokens.Class",
|
||||
methodName: "foo",
|
||||
@@ -29,6 +83,7 @@ const testCases: Array<{
|
||||
},
|
||||
{
|
||||
path: "Member[getSource].ReturnValue",
|
||||
shortTypeName: "",
|
||||
method: {
|
||||
typeName: "",
|
||||
methodName: "getSource",
|
||||
@@ -38,6 +93,17 @@ const testCases: Array<{
|
||||
},
|
||||
{
|
||||
path: "Member[CommonTokens].Member[makePromise].ReturnValue.Awaited",
|
||||
shortTypeName: "",
|
||||
method: {
|
||||
typeName: "CommonTokens",
|
||||
methodName: "makePromise",
|
||||
endpointType: EndpointType.Function,
|
||||
path: "ReturnValue.Awaited",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "Member[makePromise].ReturnValue.Awaited",
|
||||
shortTypeName: "CommonTokens!",
|
||||
method: {
|
||||
typeName: "CommonTokens",
|
||||
methodName: "makePromise",
|
||||
@@ -47,6 +113,17 @@ const testCases: Array<{
|
||||
},
|
||||
{
|
||||
path: "Member[ArgPos].Member[anyParam].Argument[any]",
|
||||
shortTypeName: "",
|
||||
method: {
|
||||
typeName: "ArgPos",
|
||||
methodName: "anyParam",
|
||||
endpointType: EndpointType.Function,
|
||||
path: "Argument[any]",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "Member[anyParam].Argument[any]",
|
||||
shortTypeName: "ArgPos!",
|
||||
method: {
|
||||
typeName: "ArgPos",
|
||||
methodName: "anyParam",
|
||||
@@ -56,6 +133,17 @@ const testCases: Array<{
|
||||
},
|
||||
{
|
||||
path: "Member[ArgPos].Instance.Member[self_thing].Argument[self]",
|
||||
shortTypeName: "",
|
||||
method: {
|
||||
typeName: "ArgPos",
|
||||
methodName: "self_thing",
|
||||
endpointType: EndpointType.Method,
|
||||
path: "Argument[self]",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "Member[self_thing].Argument[self]",
|
||||
shortTypeName: "ArgPos",
|
||||
method: {
|
||||
typeName: "ArgPos",
|
||||
methodName: "self_thing",
|
||||
@@ -66,44 +154,138 @@ const testCases: Array<{
|
||||
];
|
||||
|
||||
describe("parsePythonAccessPath", () => {
|
||||
it.each(testCases)("parses $path", ({ path, method }) => {
|
||||
expect(parsePythonAccessPath(path)).toEqual(method);
|
||||
});
|
||||
it.each(testCases)(
|
||||
"parses $path with $shortTypeName",
|
||||
({ path, shortTypeName, method }) => {
|
||||
expect(parsePythonAccessPath(path, shortTypeName)).toEqual(method);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("pythonPath", () => {
|
||||
it.each(testCases)("constructs $path", ({ path, method }) => {
|
||||
expect(
|
||||
pythonPath(
|
||||
method.typeName,
|
||||
method.methodName,
|
||||
method.endpointType,
|
||||
method.path,
|
||||
),
|
||||
).toEqual(path);
|
||||
it("returns empty for an empty method name", () => {
|
||||
expect(pythonPath("", "ReturnValue")).toEqual("ReturnValue");
|
||||
});
|
||||
|
||||
it("returns empty for an empty path", () => {
|
||||
expect(pythonPath("foo", "")).toEqual("Member[foo]");
|
||||
});
|
||||
|
||||
it("returns correctly for a full method name and path", () => {
|
||||
expect(pythonPath("foo", "ReturnValue")).toEqual("Member[foo].ReturnValue");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pythonEndpointType", () => {
|
||||
it("returns method for a method", () => {
|
||||
expect(
|
||||
pythonEndpointType({
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "foo",
|
||||
methodParameters: "(self,a)",
|
||||
}),
|
||||
pythonEndpointType(
|
||||
{
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "foo",
|
||||
methodParameters: "(self,a)",
|
||||
},
|
||||
"InstanceMethod",
|
||||
),
|
||||
).toEqual(EndpointType.Method);
|
||||
});
|
||||
|
||||
it("returns class method for a class method", () => {
|
||||
expect(
|
||||
pythonEndpointType(
|
||||
{
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "foo",
|
||||
methodParameters: "(cls,a)",
|
||||
},
|
||||
"ClassMethod",
|
||||
),
|
||||
).toEqual(EndpointType.ClassMethod);
|
||||
});
|
||||
|
||||
it("returns static method for a static method", () => {
|
||||
expect(
|
||||
pythonEndpointType(
|
||||
{
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "foo",
|
||||
methodParameters: "(a)",
|
||||
},
|
||||
"StaticMethod",
|
||||
),
|
||||
).toEqual(EndpointType.StaticMethod);
|
||||
});
|
||||
|
||||
it("returns function for a function", () => {
|
||||
expect(
|
||||
pythonEndpointType({
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "foo",
|
||||
methodParameters: "(a)",
|
||||
}),
|
||||
pythonEndpointType(
|
||||
{
|
||||
packageName: "testlib",
|
||||
typeName: "",
|
||||
methodName: "foo",
|
||||
methodParameters: "(a)",
|
||||
},
|
||||
"Function",
|
||||
),
|
||||
).toEqual(EndpointType.Function);
|
||||
});
|
||||
|
||||
it("returns constructor for an init method", () => {
|
||||
expect(
|
||||
pythonEndpointType(
|
||||
{
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "foo",
|
||||
methodParameters: "(a)",
|
||||
},
|
||||
"InitMethod",
|
||||
),
|
||||
).toEqual(EndpointType.Constructor);
|
||||
});
|
||||
|
||||
it("returns class for a class", () => {
|
||||
expect(
|
||||
pythonEndpointType(
|
||||
{
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "",
|
||||
methodParameters: "",
|
||||
},
|
||||
"Class",
|
||||
),
|
||||
).toEqual(EndpointType.Class);
|
||||
});
|
||||
|
||||
it("returns method for a method without endpoint kind", () => {
|
||||
expect(
|
||||
pythonEndpointType(
|
||||
{
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "foo",
|
||||
methodParameters: "(self,a)",
|
||||
},
|
||||
undefined,
|
||||
),
|
||||
).toEqual(EndpointType.Method);
|
||||
});
|
||||
|
||||
it("returns function for a function without endpoint kind", () => {
|
||||
expect(
|
||||
pythonEndpointType(
|
||||
{
|
||||
packageName: "testlib",
|
||||
typeName: "CommonTokens",
|
||||
methodName: "foo",
|
||||
methodParameters: "(a)",
|
||||
},
|
||||
undefined,
|
||||
),
|
||||
).toEqual(EndpointType.Function);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user