├── .eslintrc
├── .github
├── dependabot.yml
└── workflows
│ ├── test.yml
│ └── update-vscode.yml
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── codecov.yml
├── package-lock.json
├── package.json
├── screenshot.png
├── snippets
└── specs.json
├── src
├── checks.ts
├── config.ts
├── crateMetadata.ts
├── dependencies
│ ├── PrustiLocation.ts
│ ├── index.ts
│ ├── prustiTools.ts
│ └── rustup.ts
├── diagnostics.ts
├── extension.ts
├── javaHome.ts
├── server.ts
├── state.ts
├── test
│ ├── extension.test.ts
│ ├── index.ts
│ ├── runTest.ts
│ └── scenarios
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── default
│ │ └── settings.json
│ │ ├── latestVersion
│ │ └── settings.json
│ │ ├── shared
│ │ ├── crates
│ │ │ ├── git_contracts
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ │ ├── main.rs
│ │ │ │ │ └── main.rs.json
│ │ │ ├── latest_contracts
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ │ ├── main.rs
│ │ │ │ │ └── main.rs.json
│ │ │ ├── no_contracts
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ │ ├── main.rs
│ │ │ │ │ └── main.rs.json
│ │ │ └── workspace
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── first
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ │ └── lib.rs
│ │ │ │ └── second
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ ├── main.rs
│ │ │ │ └── main.rs.json
│ │ └── programs
│ │ │ ├── assert_false.rs
│ │ │ ├── assert_false.rs.json
│ │ │ ├── assert_true.rs
│ │ │ ├── assert_true.rs.json
│ │ │ ├── empty.rs
│ │ │ ├── empty.rs.json
│ │ │ ├── failing_post.rs
│ │ │ ├── failing_post.rs.json
│ │ │ ├── lib_assert_true.rs
│ │ │ ├── lib_assert_true.rs.json
│ │ │ ├── notes.rs
│ │ │ └── notes.rs.json
│ │ ├── taggedVersion
│ │ ├── crates
│ │ │ └── contracts_0.1
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ ├── main.rs
│ │ │ │ └── main.rs.json
│ │ └── settings.json
│ │ └── vscode-version
├── toolbox
│ ├── serverManager.ts
│ └── stateMachine.ts
└── util.ts
├── tsconfig.json
├── typings
└── nyc
│ └── index.d.ts
└── webpack.config.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "node": true
5 | },
6 | "parser": "@typescript-eslint/parser",
7 | "parserOptions": {
8 | "sourceType": "module",
9 | "project": "tsconfig.json"
10 | },
11 | "plugins": [
12 | "eslint-plugin-import",
13 | "eslint-plugin-jsdoc",
14 | "eslint-plugin-prefer-arrow",
15 | "eslint-plugin-react",
16 | "@typescript-eslint"
17 | ],
18 | "extends": [
19 | "eslint:recommended",
20 | "plugin:@typescript-eslint/recommended",
21 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
22 | ],
23 | "rules": {
24 | "no-implicit-globals": ["error", {"lexicalBindings": true}],
25 | "import/no-cycle": "error",
26 | "@typescript-eslint/restrict-template-expressions": "off",
27 | "@typescript-eslint/explicit-module-boundary-types": "error",
28 | "@typescript-eslint/no-non-null-assertion": "off"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: npm
5 | directory: "/"
6 | schedule:
7 | interval: monthly
8 | day: monday
9 | ignore:
10 | - dependency-name: "@types/vscode"
11 | groups:
12 | all:
13 | patterns:
14 | - "*"
15 | # Do not update to Chai v5 because it cannot be used from vscode
16 | exclude-patterns:
17 | - "chai"
18 | - "@types/chai"
19 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test and publish
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | push:
7 | branches: "master"
8 |
9 | jobs:
10 | # Test the extension on multiple OSs.
11 | test:
12 | strategy:
13 | matrix:
14 | os: [macos-latest, ubuntu-latest, windows-latest]
15 | fail-fast: false
16 | runs-on: ${{ matrix.os }}
17 |
18 | steps:
19 | - name: Check out the repo
20 | uses: actions/checkout@v3
21 |
22 | - name: Set up Node
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: '18'
26 |
27 | - name: Set up Java
28 | uses: actions/setup-java@v3
29 | with:
30 | java-version: '17'
31 | distribution: 'zulu'
32 |
33 | - name: Install NPM dependencies
34 | run: npm install
35 |
36 | - name: Run linter
37 | run: npm run lint
38 |
39 | - name: Package the extension
40 | run: npm run package
41 |
42 | - name: Run tests (headless)
43 | uses: coactions/setup-xvfb@v1
44 | id: runTests
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.VIPER_ADMIN_TOKEN }}
47 | with:
48 | run: npm test --full-trace
49 |
50 | - name: Collect coverage
51 | if: ${{ steps.runTests.outcome == 'success' }}
52 | run: npx nyc report --reporter=lcov
53 |
54 | - name: Upload coverage to Codecov
55 | uses: codecov/codecov-action@v4
56 | if: ${{ steps.runTests.outcome == 'success' }}
57 | env:
58 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
59 | with:
60 | file: ./coverage/lcov.info
61 |
62 | # Publish the extension when we are on master and the version specified in
63 | # package.json is not the latest published version of the extension.
64 | publish:
65 | if: github.event_name == 'push' && github.ref == 'refs/heads/master'
66 | needs: test
67 | runs-on: ubuntu-latest
68 | steps:
69 | - name: Check out the repo
70 | uses: actions/checkout@v3
71 |
72 | - name: Install NPM dependencies
73 | run: npm install
74 |
75 | - name: Obtain version information
76 | run: |
77 | LAST_PUBLISHED_VERSION="$(
78 | npx vsce show viper-admin.prusti-assistant --json \
79 | | jq '.versions[0].version' --raw-output
80 | )"
81 | CURRENT_VERSION="$(
82 | cat package.json | jq '.version' --raw-output
83 | )"
84 | echo "LAST_PUBLISHED_VERSION=$LAST_PUBLISHED_VERSION" >> $GITHUB_ENV
85 | echo "CURRENT_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV
86 |
87 | - name: Package the extension
88 | run: npm run package
89 |
90 | - name: Publish the extension to Visual Studio Marketplace
91 | uses: HaaLeo/publish-vscode-extension@v0
92 | if: env.CURRENT_VERSION != env.LAST_PUBLISHED_VERSION
93 | with:
94 | pat: ${{ secrets.VSCE_TOKEN }}
95 | registryUrl: https://marketplace.visualstudio.com
96 | extensionFile: prusti-assistant-${{ env.CURRENT_VERSION }}.vsix
97 | packagePath: ''
98 |
99 | - name: Publish the extension to Open VSX Registry
100 | uses: HaaLeo/publish-vscode-extension@v0
101 | if: env.CURRENT_VERSION != env.LAST_PUBLISHED_VERSION
102 | with:
103 | pat: ${{ secrets.OPEN_VSX_TOKEN }}
104 | registryUrl: https://open-vsx.org
105 | extensionFile: prusti-assistant-${{ env.CURRENT_VERSION }}.vsix
106 | packagePath: ''
107 |
108 | - name: Create a release for the published version
109 | uses: softprops/action-gh-release@v1
110 | if: env.CURRENT_VERSION != env.LAST_PUBLISHED_VERSION
111 | env:
112 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
113 | with:
114 | name: Release v${{ env.CURRENT_VERSION }}
115 | tag_name: v${{ env.CURRENT_VERSION }}
116 | files: prusti-assistant-${{ env.CURRENT_VERSION }}.vsix
117 |
--------------------------------------------------------------------------------
/.github/workflows/update-vscode.yml:
--------------------------------------------------------------------------------
1 | name: Update VS Code
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | # At 07:00 UTC on day-of-month 1. Use https://crontab.guru/ to edit this.
7 | - cron: '0 7 1 * *'
8 |
9 | jobs:
10 | # Update the version of rustc, open a PR and assign a developer.
11 | update:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Check out the repo
15 | uses: actions/checkout@v3
16 |
17 | - name: Update VS Code version used by the tests
18 | run: |
19 | TEST_VSCODE_VERSION="$(cat src/test/scenarios/vscode-version)"
20 | echo "The current version of VS Code used in tests is $TEST_VSCODE_VERSION"
21 | LATEST_VSCODE_VERSION="$(
22 | curl -s https://api.github.com/repos/microsoft/vscode/releases/latest | jq -r '.tag_name'
23 | )"
24 | echo "The latest version of VS Code is $LATEST_VSCODE_VERSION"
25 | echo "$LATEST_VSCODE_VERSION" > src/test/scenarios/vscode-version
26 | echo "TEST_VSCODE_VERSION=$TEST_VSCODE_VERSION" >> $GITHUB_ENV
27 | echo "LATEST_VSCODE_VERSION=$LATEST_VSCODE_VERSION" >> $GITHUB_ENV
28 |
29 | - name: Open a pull request
30 | uses: peter-evans/create-pull-request@v3
31 | if: env.TEST_VSCODE_VERSION != env.LATEST_VSCODE_VERSION
32 | with:
33 | # Use viper-admin's token to workaround a restriction of GitHub.
34 | # See: https://github.com/peter-evans/create-pull-request/issues/48
35 | token: ${{ secrets.VIPER_ADMIN_TOKEN }}
36 | commit-message: Update VS Code to ${{ env.LATEST_VSCODE_VERSION }}
37 | title: Update VS Code to ${{ env.LATEST_VSCODE_VERSION }}
38 | branch: auto-update-vscode
39 | delete-branch: true
40 | body: |
41 | * Update VS Code version used in tests to `${{ env.LATEST_VSCODE_VERSION }}`.
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode-test/
2 | .idea/
3 | .nyc_output/
4 | out/
5 | node_modules/
6 | coverage/
7 | *.vsix
8 | *.cpuprofile
9 | *.heapsnapshot
10 |
11 | # Prusti temporary files
12 | log/
13 | nll-facts/
14 | report.csv
15 | target/
16 |
17 | # JVM core dump
18 | hs_err_pid*.log
19 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "runtimeExecutable": "${execPath}",
13 | "args": [
14 | "--disable-extensions",
15 | "--extensionDevelopmentPath=${workspaceFolder}"
16 | ],
17 | "outFiles": [
18 | "${workspaceFolder}/out/**/*.js"
19 | ],
20 | "preLaunchTask": "npm: compile",
21 | "env": {
22 | "PATH": "${env:PATH}"
23 | },
24 | },
25 | {
26 | "name": "Extension Tests",
27 | "type": "extensionHost",
28 | "request": "launch",
29 | "runtimeExecutable": "${execPath}",
30 | "args": [
31 | "--disable-extensions",
32 | "--extensionDevelopmentPath=${workspaceFolder}",
33 | "--extensionTestsPath=${workspaceFolder}/out/test",
34 | ],
35 | "outFiles": [
36 | "${workspaceFolder}/out/test/**/*.js"
37 | ],
38 | "preLaunchTask": "npm: test-compile",
39 | "env": {
40 | "PATH": "${env:PATH}"
41 | },
42 | },
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | },
19 | {
20 | "type": "npm",
21 | "script": "lint",
22 | "problemMatcher": [],
23 | "label": "npm: lint",
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | .vscode-test
3 | .github
4 | .nyc_output
5 | src/
6 | typings/
7 | node_modules/
8 | .eslintrc
9 | .gitignore
10 | codecov.yml
11 | tsconfig.json
12 | webpack.config.js
13 |
14 | # Prusti temporary files
15 | log
16 | nll-facts/
17 | report.csv
18 | *.log
19 | target/
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to the "prusti-assistant" extension will be documented in this file.
4 |
5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
6 |
7 | ## [Unreleased]
8 |
9 | - Initial release
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Joseph Cumbo
4 | Copyright (c) 2019-2020 ETH Zurich
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Prusti Assistant
2 | ================
3 |
4 | [](https://marketplace.visualstudio.com/items?itemName=viper-admin.prusti-assistant)
5 | [](https://open-vsx.org/extension/viper-admin/prusti-assistant)
6 | [](https://github.com/viperproject/prusti-assistant/actions?query=workflow%3A"Test+and+publish"+branch%3Amaster)
7 | [](https://codecov.io/gh/viperproject/prusti-assistant)
8 | [](https://sonarcloud.io/dashboard?id=viperproject_prusti-assistant)
9 |
10 | This Visual Studio Code extension provides interactive IDE features for verifying Rust programs with the [Prusti verifier](https://github.com/viperproject/prusti-dev).
11 |
12 | For advanced use cases, consider switching to the [command-line version of Prusti](https://github.com/viperproject/prusti-dev).
13 |
14 | ## Screenshot
15 |
16 | An example of how verification errors are reported by the extension:
17 |
18 | 
19 |
20 | ## Requirements
21 |
22 | In order to use this extension, please install the following components:
23 |
24 | * Java JDK version 11 or later, 64 bit. For example, [OpenJDK](https://jdk.java.net/).
25 | * [Rustup version 1.23.0 or later](https://rustup.rs/). On Windows, this in turn requires the [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
26 |
27 | If anything fails, check the "Troubleshooting" section below. Note that macOS running on M1 is currently not supported by this extension.
28 |
29 | ## First Usage
30 |
31 | 1. Install the requirements (listed above) and restart the IDE.
32 | * This will ensure that programs like `rustup` are in the program path used by the IDE.
33 | 2. Install the ["Prusti Assistant"](https://marketplace.visualstudio.com/items?itemName=viper-admin.prusti-assistant) extension.
34 | 3. Open a Rust file to activate the extension.
35 | * At its first activation, this extension will automatically download Prusti and install the required Rust toolchain.
36 | 4. Click on the "Prusti" button in the status bar.
37 | * Alternativelly, you can run the `Prusti: verify the current crate or file` command.
38 | * If the program is in a folder with a `Cargo.toml` file, Prusti will verify the crate in which the file is contained.
39 | * If no `Cargo.toml` file can be found in a parent folder of the workspace, Prusti will verify the file as a standalone Rust program. No Cargo dependencies are allowed in this mode.
40 | 5. Follow the progress from the status bar.
41 | * You should see a "Verifying crate [folder name]" or "Verifying file [name]" message while Prusti is running.
42 | 6. The result of the verification is reported in the status bar and in the "Problems" tab.
43 | * You can open the "Problems" tab by clicking on Prusti's status bar.
44 | * Be aware that the "Problems" tab is shared by all extensions. If you are not sure which extension generated which error, try disabling other extensions. (Related VS Code issue: [#51103](https://github.com/microsoft/vscode/issues/51103).)
45 |
46 | To update Prusti, run the command `Prusti: update verifier` in the command palette.
47 |
48 | If anything fails, check the "Troubleshooting" section below.
49 |
50 | ## Features
51 |
52 | ### Commands
53 |
54 | This extension provides the following commands:
55 |
56 | * `Prusti: verify the current crate or file` to verify a Rust program. Clicking the "Prusti" button in the status bar runs this command.
57 | * `Prusti: update verifier` to update Prusti.
58 | * `Prusti: show version` to show the version of Prusti.
59 | * `Prusti: restart Prusti server` to restart the Prusti server used by the extension.
60 | * `Prusti: clear diagnostics` to clear all diagnostics generated by the extension.
61 |
62 | ### Configuration
63 |
64 | The main configuration options used by this extension are the following:
65 |
66 | * `prusti-assistant.verifyOnSave`: Specifies if programs should be verified on save.
67 | * `prusti-assistant.verifyOnOpen`: Specifies if programs should be verified when opened.
68 | * `prusti-assistant.checkForUpdates`: Specifies if Prusti should check for updates at startup.
69 | * `prusti-assistant.javaHome`: Specifies the path of the Java home folder. Leave empty to auto-detect it.
70 | * `prusti-assistant.prustiVersion`: Allows to choose between the latest published Prusti version (the default), a fixed release specified as a GitHub tag (see the [list of releases](https://github.com/viperproject/prusti-dev/releases)), or a local build of Prusti.
71 |
72 | ### Inline Code Diagnostics
73 |
74 | This extension automatically provides inline diagnostics for Prusti errors.
75 |
76 | ### Snippets
77 |
78 | Basic code-completion snippets are provided for Prusti annotations.
79 |
80 | ## Verification cache
81 |
82 | By default, Prusti transparently caches verification requests. To clear the cache, it's enough to restart the Prusti server with the commands described above.
83 |
84 | ## Prusti verification flags
85 |
86 | The verification flags of Prusti can be configured in a `Prusti.toml` file. Its location should be the following:
87 |
88 | * When verifying a standalone Rust file, `Prusti.toml` should be placed in the same folder of the verified file.
89 | * When verifying a Rust crate, `Prusti.toml` should be placed in the innermost folder that (1) contains a `Cargo.toml` file and (2) transitively contains the current active file in the IDE.
90 |
91 | In addition, due to a technical limitation the Prusti server reads its configuration flags from the `Prusti.toml` placed in the root of the IDE workspace (i.e. the navigation panel on the left). When modifying this file, it is necessary to manually restart the server. The server uses only a few of Prusti's flags, mainly to configure the verification cache and to decide whether to dump Viper files for debugging.
92 |
93 | To check whether Prusti picked up the `Prusti.toml` that you wrote, try writing `make_prusti_crash=true` in it (or any other nonexistent configuration flag). When doing so, the IDE should report that Prusti crashed.
94 |
95 | The list of the supported Prusti flags is available [here](https://viperproject.github.io/prusti-dev/dev-guide/config/flags.html), in Prusti's developer guide. Note that by setting these flags in the wrong way it is possible to get incorrect verification results. Many flags only exist of debugging reasons and to provide workarounds in selected cases.
96 |
97 | ## Troubleshooting
98 |
99 | If Prusti fails to run, you can inspect Prusti's log from VS Code (View -> Output -> Prusti Assistant) and see if one of the following solutions applies to you.
100 |
101 | | Problem | Solution |
102 | |---------|----------|
103 | | On Windows, Visual Studio is installed but the `rustup` installer still complains that the Microsoft C++ build tools are missing. | When asked which workloads to install in Visual Studio make sure "C++ build tools" is selected and that the Windows 10 SDK and the English language pack components are included. If the problem persists, check [this Microsoft guide](https://docs.microsoft.com/en-us/windows/dev-environment/rust/setup) and [this Rust guide](https://doc.rust-lang.org/book/ch01-01-installation.html#installing-rustup-on-windows). Then, restart the IDE. |
104 | | The JVM is installed, but the extension cannot auto-detect it. | Open the settings of the IDE, search for "Prusti-assistant: Java Home" and manually set the path of the Java home folder. Alternatively, make sure that the `JAVA_HOME` environment variable is set in your OS. Then, restart the IDE. |
105 | | Prusti crashes mentioning "Unexpected output of Z3" in the log. | Prusti is using an incompatible Z3 version. Make sure that the `Z3_EXE` environment variable is unset in your OS and in the settings of the extension. Then, restart the IDE. |
106 | | `error[E0514]: found crate 'cfg_if' compiled by an incompatible version of rustc` | There is a conflict between Prusti and a previous Cargo compilation. Run `cargo clean` or manually delete the `target` folder. Then, rerun Prusti. |
107 | | `error: the 'cargo' binary, normally provided by the 'cargo' component, is not applicable to the 'nightly-2021-09-20-x86_64-unknown-linux-gnu' toolchain`
or
`error[E0463]: can't find crate for std`
or
`error[E0463]: can't find crate for core` | The Rust toolchain installed by Rustup is probably corrupted (see issue [rustup/#2417](https://github.com/rust-lang/rustup/issues/2417)). [Uninstall](https://stackoverflow.com/questions/42322879/how-to-remove-rust-compiler-toolchains-with-rustup) the nightly toolchain mentioned in the error (or all installed nightly toolchains). Then, rerun Prusti. |
108 | | `error: no override and no default toolchain set` | Rustup has probably been installed without the `default` toolchain. [Install it](https://stackoverflow.com/a/46864309/2491528), then rerun Prusti. |
109 | | `libssl.so.1.1: cannot open shared object file: No such file or directory` on Ubuntu 22.04 | Ubuntu 22.04 deprecated `libssl1.1` and moved to `libssl3`. Consider [this solution](https://stackoverflow.com/a/72366805/2491528) as a temporary workaround to install `libssl1.1`, or compile Prusti from source code to make it use `libssl3`. |
110 | | On macOS running on an M1 chip, the extension doesn't work and the log contains messages such as `incompatible architecture (have (arm64), need (x86_64))`. | We currently don't release precompiled arm64 binaries for macOS. Until we do so, the only option is to [compile Prusti from source code](https://github.com/viperproject/prusti-dev). |
111 |
112 | Thanks to @Pointerbender, @michaelpaper, @fcoury, @Gadiguibou, @djc for their help in reporting, debugging and solving many of these issues!
113 |
114 | In case you still experience difficulties or encounter bugs while using Prusti Assistant, please [open an issue](https://github.com/viperproject/prusti-assistant/issues) or contact us in the [Zulip chat](https://prusti.zulipchat.com/).
115 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | # Documentation: https://docs.codecov.io/docs/codecov-yaml
2 |
3 | codecov:
4 | # Avoid "Missing base report" errors
5 | # https://docs.codecov.io/docs/comparing-commits
6 | allow_coverage_offsets: true
7 |
8 | comment: false
9 |
10 | coverage:
11 | precision: 1
12 | round: down
13 | range: 0..100
14 |
15 | coverage:
16 | status:
17 | project:
18 | default:
19 | target: 30
20 | threshold: 0.10
21 | patch: off
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prusti-assistant",
3 | "displayName": "Prusti Assistant",
4 | "description": "Verify Rust programs with the Prusti verifier.",
5 | "version": "0.12.7",
6 | "publisher": "viper-admin",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/viperproject/prusti-assistant.git"
10 | },
11 | "license": "MIT",
12 | "bugs": {
13 | "url": "https://github.com/viperproject/prusti-assistant/issues"
14 | },
15 | "homepage": "https://github.com/viperproject/prusti-assistant",
16 | "main": "./out/extension",
17 | "engines": {
18 | "vscode": "^1.60.0",
19 | "node": "*"
20 | },
21 | "categories": [
22 | "Programming Languages"
23 | ],
24 | "keywords": [
25 | "rust",
26 | "verification",
27 | "prusti",
28 | "viper"
29 | ],
30 | "activationEvents": [
31 | "onLanguage:rust"
32 | ],
33 | "contributes": {
34 | "snippets": [
35 | {
36 | "language": "rust",
37 | "path": "./snippets/specs.json"
38 | }
39 | ],
40 | "commands": [
41 | {
42 | "command": "prusti-assistant.update",
43 | "title": "update verifier",
44 | "category": "Prusti"
45 | },
46 | {
47 | "command": "prusti-assistant.show-version",
48 | "title": "show version",
49 | "category": "Prusti"
50 | },
51 | {
52 | "command": "prusti-assistant.verify",
53 | "title": "verify the current crate or file",
54 | "category": "Prusti"
55 | },
56 | {
57 | "command": "prusti-assistant.restart-server",
58 | "title": "restart Prusti server",
59 | "category": "Prusti"
60 | },
61 | {
62 | "command": "prusti-assistant.clear-diagnostics",
63 | "title": "clear diagnostics",
64 | "category": "Prusti"
65 | }
66 | ],
67 | "configuration": {
68 | "title": "Prusti Assistant",
69 | "properties": {
70 | "prusti-assistant.prustiVersion": {
71 | "type": "string",
72 | "default": "Latest",
73 | "enum": [
74 | "Latest",
75 | "Tag",
76 | "Local"
77 | ],
78 | "enumDescriptions": [
79 | "The latest release of Prusti.",
80 | "A fixed version of Prusti, specified via prustiTag.",
81 | "A local build of Prusti, specified via localPrustiPath."
82 | ],
83 | "description": "Specifies from which version of Prusti should be used."
84 | },
85 | "prusti-assistant.checkForUpdates": {
86 | "type": "boolean",
87 | "default": true,
88 | "description": "Specifies if Prusti should check for updates at startup."
89 | },
90 | "prusti-assistant.localPrustiPath": {
91 | "type": "string",
92 | "default": "",
93 | "description": "Specifies the path to the local Prusti installation. This setting is only used if prustiVersion is set to Local."
94 | },
95 | "prusti-assistant.prustiTag": {
96 | "type": "string",
97 | "default": "",
98 | "description": "Specifies the GitHub tag of the Prusti release to be used. Visit https://github.com/viperproject/prusti-dev/releases to see all the available tags. This setting is only used if prustiVersion is set to Tag."
99 | },
100 | "prusti-assistant.verifyOnSave": {
101 | "type": "boolean",
102 | "default": false,
103 | "description": "Specifies if programs should be verified on save."
104 | },
105 | "prusti-assistant.verifyOnOpen": {
106 | "type": "boolean",
107 | "default": false,
108 | "description": "Specifies if programs should be verified when opened."
109 | },
110 | "prusti-assistant.reportErrorsOnly": {
111 | "type": "boolean",
112 | "default": false,
113 | "description": "Specifies if only error messages should be reported, hiding compiler's warnings."
114 | },
115 | "prusti-assistant.javaHome": {
116 | "type": "string",
117 | "default": "",
118 | "description": "Specifies the path of the Java home folder (leave empty to auto-detect)."
119 | },
120 | "prusti-assistant.serverAddress": {
121 | "type": "string",
122 | "default": "",
123 | "description": "Specifies the address of a Prusti server to use for verification. If not set, the extension will start up and manage its own server."
124 | },
125 | "prusti-assistant.extraPrustiEnv": {
126 | "type": "object",
127 | "default": {
128 | "RUST_BACKTRACE": "true",
129 | "PRUSTI_LOG": "info"
130 | },
131 | "additionalProperties": {
132 | "type": "string"
133 | },
134 | "description": "Specifies additional environment variables to be passed to all Prusti runs. Remember to restart the Prusti Server after modifying this setting."
135 | },
136 | "prusti-assistant.extraPrustiRustcArgs": {
137 | "type": "array",
138 | "items": {
139 | "type": "string"
140 | },
141 | "default": [
142 | "--edition=2018"
143 | ],
144 | "description": "Specifies additional arguments to be passed to Prusti-Rustc. Used when verifying a Rust file that is not part of a crate."
145 | },
146 | "prusti-assistant.extraCargoPrustiArgs": {
147 | "type": "array",
148 | "items": {
149 | "type": "string"
150 | },
151 | "default": [],
152 | "description": "Specifies additional arguments to be passed to Cargo-Prusti. Used when verifying a crate."
153 | },
154 | "prusti-assistant.extraPrustiServerArgs": {
155 | "type": "array",
156 | "items": {
157 | "type": "string"
158 | },
159 | "default": [],
160 | "description": "Specifies additional arguments to be passed to the Prusti Server. Remember to restart the Prusti Server after modifying this setting."
161 | }
162 | }
163 | }
164 | },
165 | "scripts": {
166 | "webpack-production": "webpack --mode production",
167 | "webpack-development": "webpack --mode development",
168 | "tsc": "tsc",
169 | "vscode:prepublish": "npm-run-all --sequential clean webpack-production",
170 | "compile": "npm-run-all --sequential clean webpack-development",
171 | "lint": "eslint -c .eslintrc --ext .ts ./src",
172 | "test-compile": "npm-run-all --sequential clean tsc",
173 | "pretest": "npm run test-compile",
174 | "test": "node ./out/test/runTest.js",
175 | "report-coverage": "nyc report --reporter=html",
176 | "clean": "rimraf out",
177 | "package": "vsce package --no-dependencies"
178 | },
179 | "devDependencies": {
180 | "@types/chai": "^4.3.11",
181 | "@types/fs-extra": "^11.0.4",
182 | "@types/glob": "^8.1.0",
183 | "@types/mocha": "^10.0.6",
184 | "@types/node": "^20.12.2",
185 | "@types/tmp": "^0.2.6",
186 | "@types/vscode": "^1.60.0",
187 | "@typescript-eslint/eslint-plugin": "^7.4.0",
188 | "@typescript-eslint/parser": "^7.4.0",
189 | "@vscode/test-electron": "^2.3.9",
190 | "chai": "^4.4.1",
191 | "eslint": "^8.57.0",
192 | "eslint-plugin-import": "^2.29.1",
193 | "eslint-plugin-jsdoc": "^48.2.2",
194 | "eslint-plugin-prefer-arrow": "^1.2.3",
195 | "eslint-plugin-react": "^7.34.1",
196 | "glob": "^10.3.12",
197 | "mocha": "^10.4.0",
198 | "npm-run-all": "^4.1.5",
199 | "nyc": "^15.1.0",
200 | "rimraf": "^5.0.5",
201 | "ts-loader": "^9.5.1",
202 | "typescript": "^5.4.3",
203 | "vsce": "^2.15.0",
204 | "webpack": "^5.91.0",
205 | "webpack-cli": "^5.1.4"
206 | },
207 | "dependencies": {
208 | "@viperproject/locate-java-home": "git+https://github.com/viperproject/locate-java-home.git",
209 | "fs-extra": "^11.2.0",
210 | "locate-java-home": "git+https://github.com/viperproject/locate-java-home.git",
211 | "tmp": "^0.2.3",
212 | "tree-kill": "^1.2.2",
213 | "vs-verification-toolbox": "git+https://github.com/viperproject/vs-verification-toolbox.git"
214 | },
215 | "__metadata": {
216 | "id": "03644baf-8510-4e01-9bc8-ef0269607dba",
217 | "publisherDisplayName": "Chair of Programming Methodology - ETH Zurich",
218 | "publisherId": "40c87fab-912c-4304-b2ee-b6c71e280a3c",
219 | "isPreReleaseVersion": false
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/viperproject/prusti-assistant/f6cee5c0cb2f62f0b02752dbd37c960704460246/screenshot.png
--------------------------------------------------------------------------------
/snippets/specs.json:
--------------------------------------------------------------------------------
1 | {
2 | "requires": {
3 | "prefix": "requires",
4 | "body": [
5 | "#[requires(${1:true})]"
6 | ],
7 | "description": "Insert #[requires(...)]"
8 | },
9 | "ensures": {
10 | "prefix": "ensures",
11 | "body": [
12 | "#[ensures(${1:true})]"
13 | ],
14 | "description": "Insert #[ensures(...)]"
15 | },
16 | "body_invariant": {
17 | "prefix": "body_invariant",
18 | "body": [
19 | "body_invariant!(${1:true});"
20 | ],
21 | "description": "Insert body_invariant!(...);"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/checks.ts:
--------------------------------------------------------------------------------
1 | import * as util from "./util";
2 | import * as config from "./config";
3 | import * as path from "path";
4 | import * as fs from "fs-extra";
5 | import { PrustiLocation } from "./dependencies";
6 |
7 | export async function hasPrerequisites(): Promise<[boolean, string]> {
8 | util.log("Checking Java home...");
9 | if (await config.javaHome() === null) {
10 | const msg = "Could not find the Java home. Please install Java 11+ " +
11 | "64bit or set the 'javaHome' setting, then restart the IDE.";
12 | return [false, msg];
13 | }
14 | util.log("Checking Rustup and Cargo...");
15 | try {
16 | await util.spawn("rustup", ["--version"]);
17 | await util.spawn("cargo", ["--version"]);
18 | } catch (err) {
19 | util.log(`Error: ${err}`);
20 | const msg = "Could not run Rustup. Please visit " +
21 | "[https://rustup.rs/](https://rustup.rs/) and install Rustup, " +
22 | "then restart the IDE.";
23 | return [false, msg];
24 | }
25 | util.log("Checking Java...");
26 | try {
27 | const javaPath = path.join(
28 | (await config.javaHome())!.javaExecutable
29 | );
30 | await util.spawn(javaPath, ["-version"]);
31 | } catch (err) {
32 | util.log(`Error: ${err}`);
33 | const msg = "Could not run Java. Please install Java 11+ 64bit " +
34 | "or set the 'javaHome' setting, then restart the IDE.";
35 | return [false, msg];
36 | }
37 | return [true, ""];
38 | }
39 |
40 | export async function checkPrusti(prusti: PrustiLocation): Promise<[boolean, string]> {
41 | util.log("Checking Z3...");
42 | try {
43 | await util.spawn(prusti.z3, ["--version"]);
44 | } catch (err) {
45 | util.log(`Error: ${err}`);
46 | const msg = "Could not run Z3. " +
47 | "Please try updating the verifier, then restart the IDE.";
48 | return [false, msg];
49 | }
50 | util.log("Checking Prusti...");
51 | try {
52 | await util.spawn(prusti.prustiRustc, ["--version"]);
53 | } catch (err) {
54 | util.log("Could not run prusti-rustc");
55 | util.log(`Error: ${err}`);
56 | const msg = "Could not run Prusti. " +
57 | "Please try updating the verifier, then restart the IDE.";
58 | return [false, msg];
59 | }
60 | util.log("Checking Cargo-Prusti...");
61 | try {
62 | await util.spawn(prusti.cargoPrusti, ["--help"]);
63 | } catch (err) {
64 | util.log("Could not run cargo-prusti");
65 | util.log(`Error: ${err}`);
66 | const msg = "Could not run Prusti. " +
67 | "Please try updating the verifier, then restart the IDE.";
68 | return [false, msg];
69 | }
70 | return [true, ""];
71 | }
72 |
73 | // Check if Prusti is older than numDays or is older than the VS Code extension.
74 | export async function isOutdated(prusti: PrustiLocation, numDays = 30): Promise {
75 | // No need to update a fixed Prusti version
76 | if (config.prustiVersion() !== config.PrustiVersion.Latest) {
77 | return false;
78 | }
79 |
80 | // TODO: Lookup on GitHub if there actually is a more recent version to download.
81 | const prustiDownloadDate = (await fs.stat(prusti.rustToolchainFile.path())).ctime.getTime();
82 | const pastNumDays = new Date(new Date().setDate(new Date().getDate() - numDays)).getTime();
83 | const olderThanNumDays = prustiDownloadDate < pastNumDays;
84 | const extensionDownloadDate = (await fs.stat(__filename)).ctime.getTime();
85 | const olderThanExtension = prustiDownloadDate < extensionDownloadDate;
86 | return olderThanNumDays || olderThanExtension;
87 | }
88 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import { Location } from "vs-verification-toolbox";
3 | import * as util from "./util";
4 | import { findJavaHome, JavaHome } from "./javaHome";
5 |
6 | const namespace = "prusti-assistant";
7 |
8 | export function config(): vscode.WorkspaceConfiguration {
9 | return vscode.workspace.getConfiguration(namespace);
10 | }
11 |
12 | export enum PrustiVersion {
13 | Latest = "Latest",
14 | Tag = "Tag",
15 | Local = "Local"
16 | }
17 |
18 | export const prustiVersionKey = "prustiVersion";
19 | export const prustiVersionPath = `${namespace}.${prustiVersionKey}`;
20 |
21 | export function prustiVersion(): PrustiVersion {
22 | const defaultVersion = PrustiVersion.Latest;
23 | const versionName = config().get(prustiVersionKey, defaultVersion as string);
24 | const version = PrustiVersion[
25 | // Convert string to enum. See https://stackoverflow.com/a/17381004/2491528
26 | versionName as keyof typeof PrustiVersion
27 | ];
28 | if (version !== undefined) {
29 | return version;
30 | } else {
31 | util.userError(
32 | `Prusti has no version named ${versionName}; defaulting to ${defaultVersion}. ` +
33 | "This has been probably caused by an update of the extension. " +
34 | "To fix this error, please choose a valid version in the settings."
35 | );
36 | return defaultVersion;
37 | }
38 | }
39 |
40 | const localPrustiPathKey = "localPrustiPath";
41 | export const localPrustiPathPath = `${namespace}.${localPrustiPathKey}`;
42 |
43 | export function localPrustiPath(): string {
44 | return config().get(localPrustiPathKey, "");
45 | }
46 |
47 | const prustiTagKey = "prustiTag";
48 | export const prustiTagPath = `${namespace}.${prustiTagKey}`;
49 |
50 | export function prustiTag(): string {
51 | return config().get(prustiTagKey, "").trim();
52 | }
53 |
54 | export function checkForUpdates(): boolean {
55 | return config().get("checkForUpdates", true);
56 | }
57 |
58 | export function verifyOnSave(): boolean {
59 | return config().get("verifyOnSave", true);
60 | }
61 |
62 | export function verifyOnOpen(): boolean {
63 | return config().get("verifyOnOpen", true);
64 | }
65 |
66 | export function reportErrorsOnly(): boolean {
67 | return config().get("reportErrorsOnly", false);
68 | }
69 |
70 | // Avoid calling `findJavaHome()` each time.
71 | let cachedFindJavaHome: string | null = null;
72 |
73 | export async function javaHome(): Promise {
74 | const configPath = config().get("javaHome", "");
75 | let path;
76 | if (configPath.length > 0) {
77 | path = configPath;
78 | } else {
79 | if (cachedFindJavaHome === null) {
80 | cachedFindJavaHome = await findJavaHome();
81 | }
82 | path = cachedFindJavaHome;
83 | }
84 | if (path === null) { return null; }
85 | return new JavaHome(new Location(path));
86 | }
87 |
88 | const serverAddressKey = "serverAddress";
89 | export const serverAddressPath = `${namespace}.${serverAddressKey}`;
90 |
91 | export function serverAddress(): string {
92 | return config().get(serverAddressKey, "");
93 | }
94 |
95 | export function extraPrustiEnv(): Record {
96 | return config().get("extraPrustiEnv", {});
97 | }
98 |
99 | export function extraPrustiRustcArgs(): string[] {
100 | return config().get("extraPrustiRustcArgs", []);
101 | }
102 |
103 | export function extraCargoPrustiArgs(): string[] {
104 | return config().get("extraCargoPrustiArgs", []);
105 | }
106 |
107 | export function extraPrustiServerArgs(): string[] {
108 | return config().get("extraPrustiServerArgs", []);
109 | }
110 |
--------------------------------------------------------------------------------
/src/crateMetadata.ts:
--------------------------------------------------------------------------------
1 | import * as util from "./util";
2 | import * as config from "./config";
3 | import * as dependencies from "./dependencies";
4 |
5 | export interface CrateMetadata {
6 | target_directory: string;
7 | workspace_root?: string;
8 | }
9 |
10 | export enum CrateMetadataStatus {
11 | Error,
12 | Ok
13 | }
14 |
15 | /**
16 | * Queries for the metadata of a Rust crate using cargo-prusti.
17 | *
18 | * @param prusti The location of Prusti files.
19 | * @param cratePath The path of a Rust crate.
20 | * @param destructors Where to store the destructors of the spawned processes.
21 | * @returns A tuple containing the metadata, the exist status, and the duration of the query.
22 | */
23 | export async function queryCrateMetadata(
24 | prusti: dependencies.PrustiLocation,
25 | cratePath: string,
26 | destructors: Set,
27 | ): Promise<[CrateMetadata, CrateMetadataStatus, util.Duration]> {
28 | const cargoPrustiArgs = ["--no-deps", "--offline", "--format-version=1"].concat(
29 | config.extraCargoPrustiArgs()
30 | );
31 | const cargoPrustiEnv = {
32 | ...process.env, // Needed to run Rustup
33 | ...{
34 | PRUSTI_CARGO_COMMAND: "metadata",
35 | PRUSTI_QUIET: "true",
36 | },
37 | ...config.extraPrustiEnv(),
38 | };
39 | const output = await util.spawn(
40 | prusti.cargoPrusti,
41 | cargoPrustiArgs,
42 | {
43 | options: {
44 | cwd: cratePath,
45 | env: cargoPrustiEnv,
46 | }
47 | },
48 | destructors,
49 | );
50 | let status = CrateMetadataStatus.Error;
51 | if (output.code === 0) {
52 | status = CrateMetadataStatus.Ok;
53 | }
54 | if (/error: internal compiler error/.exec(output.stderr) !== null) {
55 | status = CrateMetadataStatus.Error;
56 | }
57 | if (/^thread '.*' panicked at/.exec(output.stderr) !== null) {
58 | status = CrateMetadataStatus.Error;
59 | }
60 | const metadata = JSON.parse(output.stdout) as CrateMetadata;
61 | return [metadata, status, output.duration];
62 | }
63 |
--------------------------------------------------------------------------------
/src/dependencies/PrustiLocation.ts:
--------------------------------------------------------------------------------
1 | import { Location } from "vs-verification-toolbox";
2 | import * as fs from "fs-extra";
3 |
4 | export class PrustiLocation {
5 | constructor(
6 | private readonly prustiLocation: Location,
7 | private readonly viperToolsLocation: Location,
8 | public readonly rustToolchainFile: Location
9 | ) {
10 | // Set execution flags (ignored on Windows)
11 | fs.chmodSync(this.prustiDriver, 0o775);
12 | fs.chmodSync(this.prustiRustc, 0o775);
13 | fs.chmodSync(this.cargoPrusti, 0o775);
14 | fs.chmodSync(this.z3, 0o775);
15 | fs.chmodSync(this.boogie, 0o775);
16 | fs.chmodSync(this.prustiServerDriver, 0o775);
17 | fs.chmodSync(this.prustiServer, 0o775);
18 | }
19 |
20 | public get prustiDriver(): string {
21 | return this.prustiLocation.executable("prusti-driver");
22 | }
23 |
24 | public get prustiRustc(): string {
25 | return this.prustiLocation.executable("prusti-rustc");
26 | }
27 |
28 | public get cargoPrusti(): string {
29 | return this.prustiLocation.executable("cargo-prusti");
30 | }
31 |
32 | public get prustiServerDriver(): string {
33 | return this.prustiLocation.executable("prusti-server-driver");
34 | }
35 |
36 | public get prustiServer(): string {
37 | return this.prustiLocation.executable("prusti-server");
38 | }
39 |
40 | public get z3(): string {
41 | return this.viperToolsLocation.child("z3").child("bin")
42 | .executable("z3");
43 | }
44 |
45 | public get boogie(): string {
46 | return this.viperToolsLocation.child("boogie")
47 | .child("Binaries").executable("Boogie");
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/dependencies/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./PrustiLocation";
2 | import * as tools from "vs-verification-toolbox";
3 | import * as vscode from "vscode";
4 | import * as config from "../config";
5 | import * as util from "../util";
6 | import * as server from "../server";
7 | import * as rustup from "./rustup";
8 | import { PrustiLocation } from "./PrustiLocation";
9 | import { prustiTools } from "./prustiTools";
10 | import { Location } from "vs-verification-toolbox";
11 |
12 | export let prusti: PrustiLocation | undefined;
13 | export async function installDependencies(context: vscode.ExtensionContext, shouldUpdate: boolean, verificationStatus: vscode.StatusBarItem): Promise {
14 | try {
15 | util.log(`${shouldUpdate ? "Updating" : "Installing"} Prusti dependencies...`);
16 |
17 | // Stop the server before trying to remove its files
18 | await server.stop();
19 |
20 | // TODO: Stop prusti-rustc and cargo-prusti
21 |
22 | const deps = await prustiTools(tools.currentPlatform!, context);
23 | const { result, didReportProgress } = await tools.withProgressInWindow(
24 | `${shouldUpdate ? "Updating" : "Installing"} Prusti`,
25 | listener => deps.install(config.prustiVersion(), shouldUpdate, listener)
26 | );
27 | if (!(result instanceof tools.Success)) {
28 | util.userError(
29 | "Prusti installation has been canceled. Please restart the IDE to retry.",
30 | true, verificationStatus
31 | )
32 | // FIXME: The rest of the extension expects `prusti` to be defined.
33 | return;
34 | }
35 | const location = result.value as tools.Location;
36 | const viperToolsDirectory = await getViperToolsDirectory(location);
37 | const rustToolchainLocation = await getRustToolchainLocation(location);
38 | util.log(`Using Prusti at ${location}`)
39 | prusti = new PrustiLocation(location, viperToolsDirectory, rustToolchainLocation);
40 |
41 | // only notify user about success if we reported anything in between;
42 | // otherwise there was nothing to be done.
43 | if (didReportProgress) {
44 | util.userInfo(
45 | `Prusti ${shouldUpdate ? "updated" : "installed"} successfully.`
46 | );
47 | }
48 |
49 | // Install Rust toolchain
50 | await rustup.ensureRustToolchainInstalled(
51 | context,
52 | rustToolchainLocation
53 | );
54 | } catch (err) {
55 | util.userError(
56 | `Error installing Prusti. Please restart the IDE to retry. Details: ${err}`,
57 | true, verificationStatus
58 | );
59 | throw err;
60 | } finally {
61 | await server.restart(context, verificationStatus);
62 | }
63 | }
64 |
65 | export async function prustiVersion(): Promise {
66 | const output = await util.spawn(prusti!.prustiRustc, ["--version"]);
67 | let version = output.stderr.split("\n")
68 | .filter(line => line.trim().length > 0 && line.indexOf("version") != -1)
69 | .join(". ");
70 | if (version.trim().length === 0) {
71 | version = "";
72 | }
73 | if (version.indexOf("Prusti") === -1) {
74 | version = "Prusti version: " + version;
75 | }
76 | return version;
77 | }
78 |
79 | /**
80 | * Returns the location of the `viper_tools` directory. This function starts the
81 | * search by looking in the Prusti location for a child folder `viper_tools`; if
82 | * not found, it looks upwards until a `viper_tools` directory can be found.
83 | *
84 | * In general, the `viper_tools` directory will be a child of the Prusti
85 | * location; however, when using a development version of Prusti (e.g. where
86 | * Prusti's location would be set as prusti-dev/target/debug), `viper_tools`
87 | * would be in the `prusti-dev` directory.
88 | */
89 | async function getViperToolsDirectory(prustiLocation: Location): Promise {
90 | const location = await searchForChildInEnclosingFolders(prustiLocation, "viper_tools");
91 | if(location) {
92 | return location;
93 | } else {
94 | throw new Error(`Could not find viper_tools directory from ${prustiLocation}.`);
95 | }
96 | }
97 |
98 | /**
99 | * Returns the location of the `rust-toolchain` file. This function starts the
100 | * search by looking in the Prusti location for a child file `rust-toolchain`;
101 | * if not found, it looks upwards until a `rust-toolchain` file can be found.
102 | *
103 | * In general, the `rust-toolchain` file will be a child of the Prusti location;
104 | * however, when using a development version of Prusti (e.g. where Prusti's
105 | * location would be set as prusti-dev/target/debug), `rust-toolchain` would be
106 | * in the `prusti-dev` directory.
107 | */
108 | async function getRustToolchainLocation(prustiLocation: Location): Promise {
109 | const location = await searchForChildInEnclosingFolders(prustiLocation, "rust-toolchain");
110 | if(location) {
111 | return location;
112 | } else {
113 | throw new Error(`Could not find rust-toolchain file from ${prustiLocation}.`);
114 | }
115 | }
116 |
117 | async function searchForChildInEnclosingFolders(initialLocation: Location, childName: string): Promise {
118 | let location = initialLocation;
119 | // eslint-disable-next-line no-constant-condition
120 | while (true) {
121 | const childLocation = location.child(childName);
122 | if(await childLocation.exists()) {
123 | return childLocation;
124 | }
125 | if(location.path() === location.enclosingFolder.path()) {
126 | // We've reached the root folder
127 | return;
128 | }
129 | location = location.enclosingFolder;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/dependencies/prustiTools.ts:
--------------------------------------------------------------------------------
1 | import * as vvt from "vs-verification-toolbox";
2 | import * as path from "path";
3 | import * as vscode from "vscode";
4 | import * as process from "process";
5 | import * as fs from "fs-extra";
6 | import * as config from "../config";
7 |
8 | export async function prustiTools(
9 | platform: vvt.Platform,
10 | context: vscode.ExtensionContext
11 | ): Promise> {
12 | const id = identifier(platform);
13 | const version = config.PrustiVersion;
14 |
15 | // Token used to avoid rate limits while testing
16 | const authorization_token = process.env.GITHUB_TOKEN
17 | if (authorization_token) {
18 | console.info("Using authorization token");
19 | }
20 |
21 | // Get the latest among releases and pre-releases
22 | const getLatestReleaseUrl = (): Promise => {
23 | return vvt.GitHubReleaseAsset.getLatestAssetUrl(
24 | "viperproject", "prusti-dev", `prusti-release-${id}.zip`, true, authorization_token,
25 | );
26 | }
27 |
28 | const getTaggedReleaseUrl = (): Promise => {
29 | const tag = config.prustiTag();
30 | return vvt.GitHubReleaseAsset.getTaggedAssetUrl(
31 | "viperproject", "prusti-dev", `prusti-release-${id}.zip`, tag, authorization_token,
32 | );
33 | }
34 |
35 | if (config.prustiVersion() == config.PrustiVersion.Local
36 | && !await fs.pathExists(config.localPrustiPath())) {
37 | throw new Error(
38 | `In the settings the Prusti version is ${config.PrustiVersion.Local}, but the `
39 | + `specified local path '${config.localPrustiPath()}' does not exist.`
40 | );
41 | }
42 |
43 | if (config.prustiVersion() == config.PrustiVersion.Tag && !config.prustiTag()) {
44 | throw new Error(
45 | `In the settings the Prusti version is ${config.PrustiVersion.Tag}, but `
46 | + `no tag has been provided. Please specify it in the prustiTag field.`
47 | );
48 | }
49 |
50 | return new vvt.Dependency(
51 | path.join(context.globalStoragePath, "prustiTools"),
52 | [version.Latest, new vvt.GitHubZipExtractor(getLatestReleaseUrl, "prusti", authorization_token)],
53 | [version.Tag, new vvt.GitHubZipExtractor(getTaggedReleaseUrl, "prusti", authorization_token)],
54 | [version.Local, new vvt.LocalReference(config.localPrustiPath())],
55 | );
56 | }
57 |
58 | function identifier(platform: vvt.Platform): string {
59 | switch (platform) {
60 | case vvt.Platform.Mac:
61 | return "macos";
62 | case vvt.Platform.Windows:
63 | return "windows";
64 | case vvt.Platform.Linux:
65 | return "ubuntu";
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/dependencies/rustup.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import { Location } from "vs-verification-toolbox";
3 | import * as util from "../util";
4 |
5 | export async function ensureRustToolchainInstalled(context: vscode.ExtensionContext, toolchainFile: Location): Promise {
6 | util.log("Checking rust toolchain version and components...");
7 | util.log(`Using rust-toolchain at ${toolchainFile}`);
8 |
9 | if (!await toolchainFile.exists()) {
10 | throw new Error(`The rust-toolchain file at ${toolchainFile} does not exist.`);
11 | }
12 |
13 | // `rustup show` will install the missing toolchain and components
14 | const rustupOutput = await util.spawn(
15 | "rustup",
16 | ["show"],
17 | { options: { cwd: toolchainFile.enclosingFolder.path() }}
18 | );
19 |
20 | if (rustupOutput.code != 0) {
21 | throw new Error(`Rustup terminated with exit code ${rustupOutput.code} and signal ${rustupOutput.signal}`);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/diagnostics.ts:
--------------------------------------------------------------------------------
1 | import * as util from "./util";
2 | import * as config from "./config";
3 | import * as vscode from "vscode";
4 | import * as path from "path";
5 | import * as vvt from "vs-verification-toolbox";
6 | import * as dependencies from "./dependencies";
7 | import { queryCrateMetadata, CrateMetadataStatus } from "./crateMetadata";
8 |
9 | // ========================================================
10 | // JSON Schemas
11 | // ========================================================
12 |
13 | interface CargoMessage {
14 | message: Message;
15 | target: Target;
16 | }
17 |
18 | interface Target {
19 | src_path: string;
20 | }
21 |
22 | interface Message {
23 | children: Message[];
24 | code: Code | null;
25 | level: Level;
26 | message: string;
27 | spans: Span[];
28 | }
29 |
30 | interface Code {
31 | code: string;
32 | explanation: string;
33 | }
34 |
35 | enum Level {
36 | Error = "error",
37 | Help = "help",
38 | Note = "note",
39 | Warning = "warning",
40 | Empty = "",
41 | }
42 |
43 | interface Span {
44 | column_end: number;
45 | column_start: number;
46 | file_name: string;
47 | is_primary: boolean;
48 | label: string | null;
49 | line_end: number;
50 | line_start: number;
51 | expansion: Expansion | null;
52 | }
53 |
54 | interface Expansion {
55 | span: Span;
56 | }
57 |
58 | // ========================================================
59 | // Diagnostic Parsing
60 | // ========================================================
61 |
62 | interface Diagnostic {
63 | file_path: string;
64 | diagnostic: vscode.Diagnostic;
65 | }
66 |
67 | function parseMessageLevel(level: Level): vscode.DiagnosticSeverity {
68 | switch (level) {
69 | case Level.Error: return vscode.DiagnosticSeverity.Error;
70 | case Level.Note: return vscode.DiagnosticSeverity.Information;
71 | case Level.Help: return vscode.DiagnosticSeverity.Hint;
72 | case Level.Warning: return vscode.DiagnosticSeverity.Warning;
73 | case Level.Empty: return vscode.DiagnosticSeverity.Information;
74 | default: return vscode.DiagnosticSeverity.Error;
75 | }
76 | }
77 |
78 | function dummyRange(): vscode.Range {
79 | return new vscode.Range(0, 0, 0, 0);
80 | }
81 |
82 | function parseMultiSpanRange(multiSpan: Span[]): vscode.Range {
83 | let finalRange;
84 | for (const span of multiSpan) {
85 | const range = parseSpanRange(span);
86 | if (finalRange === undefined) {
87 | finalRange = range;
88 | } else {
89 | // Merge
90 | finalRange = finalRange.union(range);
91 | }
92 | }
93 | return finalRange ?? dummyRange();
94 | }
95 |
96 | function parseSpanRange(span: Span): vscode.Range {
97 | return new vscode.Range(
98 | span.line_start - 1,
99 | span.column_start - 1,
100 | span.line_end - 1,
101 | span.column_end - 1,
102 | );
103 | }
104 |
105 | function parseCargoOutput(output: string): CargoMessage[] {
106 | const messages: CargoMessage[] = [];
107 | for (const line of output.split("\n")) {
108 | if (line[0] !== "{") {
109 | continue;
110 | }
111 |
112 | // Parse the message into a diagnostic.
113 | const diag = JSON.parse(line) as CargoMessage;
114 | if (diag.message !== undefined) {
115 | messages.push(diag);
116 | }
117 | }
118 | return messages;
119 | }
120 |
121 | function parseRustcOutput(output: string): Message[] {
122 | const messages: Message[] = [];
123 | for (const line of output.split("\n")) {
124 | if (line[0] !== "{") {
125 | continue;
126 | }
127 |
128 | // Parse the message into a diagnostic.
129 | const diag = JSON.parse(line) as Message;
130 | if (diag.message !== undefined) {
131 | messages.push(diag);
132 | }
133 | }
134 | return messages;
135 | }
136 |
137 | function getCallSiteSpan(span: Span): Span {
138 | while (span.expansion !== null) {
139 | span = span.expansion.span;
140 | }
141 | return span;
142 | }
143 |
144 | /**
145 | * Parses a message into a diagnostic.
146 | *
147 | * @param msgDiag The message to parse.
148 | * @param basePath The base path to resolve the relative paths in the diagnostics.
149 | * @param defaultRange The default range to use if no span is found in the message.
150 | * @returns The parsed diagnostic.
151 | */
152 | function parseCargoMessage(msgDiag: CargoMessage, basePath: string, defaultRange?: vscode.Range): Diagnostic {
153 | const msg = msgDiag.message;
154 | const level = parseMessageLevel(msg.level);
155 |
156 | // Read primary message
157 | let primaryMessage = msg.message;
158 | if (msg.code !== null) {
159 | primaryMessage = `[${msg.code.code}] ${primaryMessage}.`;
160 | }
161 |
162 | // Parse primary spans
163 | const primaryCallSiteSpans = [];
164 | for (const span of msg.spans) {
165 | if (!span.is_primary) {
166 | continue;
167 | }
168 | if (span.label !== null) {
169 | primaryMessage = `${primaryMessage}\n[Note] ${span.label}`;
170 | }
171 | primaryCallSiteSpans.push(getCallSiteSpan(span));
172 | }
173 |
174 | // Convert MultiSpans to Range and Diagnostic
175 | let primaryFilePath = msgDiag.target.src_path;
176 | let primaryRange = defaultRange ?? dummyRange();
177 | if (primaryCallSiteSpans.length > 0) {
178 | primaryRange = parseMultiSpanRange(primaryCallSiteSpans);
179 | primaryFilePath = primaryCallSiteSpans[0].file_name;
180 | if (!path.isAbsolute(primaryFilePath)) {
181 | primaryFilePath = path.join(basePath, primaryFilePath);
182 | }
183 | }
184 | const diagnostic = new vscode.Diagnostic(
185 | primaryRange,
186 | primaryMessage,
187 | level
188 | );
189 |
190 | // Parse all non-primary spans
191 | const relatedInformation = [];
192 | for (const span of msg.spans) {
193 | if (span.is_primary) {
194 | continue;
195 | }
196 |
197 | const message = `[Note] ${span.label ?? ""}`;
198 | const callSiteSpan = getCallSiteSpan(span);
199 | const range = parseSpanRange(callSiteSpan);
200 | const filePath = path.join(basePath, callSiteSpan.file_name);
201 | const fileUri = vscode.Uri.file(filePath);
202 |
203 | relatedInformation.push(
204 | new vscode.DiagnosticRelatedInformation(
205 | new vscode.Location(fileUri, range),
206 | message
207 | )
208 | );
209 | }
210 |
211 | // Recursively parse child messages.
212 | for (const child of msg.children) {
213 | const childMsgDiag = {
214 | target: {
215 | src_path: primaryFilePath
216 | },
217 | message: child
218 | };
219 | const childDiagnostic = parseCargoMessage(childMsgDiag, basePath, primaryRange);
220 | const fileUri = vscode.Uri.file(childDiagnostic.file_path);
221 | relatedInformation.push(
222 | new vscode.DiagnosticRelatedInformation(
223 | new vscode.Location(
224 | fileUri,
225 | childDiagnostic.diagnostic.range
226 | ),
227 | childDiagnostic.diagnostic.message
228 | )
229 | );
230 | }
231 |
232 | // Set related information
233 | diagnostic.relatedInformation = relatedInformation;
234 |
235 | return {
236 | file_path: primaryFilePath,
237 | diagnostic: diagnostic,
238 | };
239 | }
240 |
241 | /**
242 | * Parses a message into diagnostics.
243 | *
244 | * @param msg The message to parse.
245 | * @param filePath The path of the file that was being compiled.
246 | */
247 | function parseRustcMessage(msg: Message, filePath: string, defaultRange?: vscode.Range): Diagnostic {
248 | const level = parseMessageLevel(msg.level);
249 |
250 | // Read primary message
251 | let primaryMessage = msg.message;
252 | if (msg.code !== null) {
253 | primaryMessage = `[${msg.code.code}] ${primaryMessage}.`;
254 | }
255 |
256 | // Parse primary spans
257 | const primaryCallSiteSpans = [];
258 | for (const span of msg.spans) {
259 | if (!span.is_primary) {
260 | continue;
261 | }
262 | if (span.label !== null) {
263 | primaryMessage = `${primaryMessage}\n[Note] ${span.label}`;
264 | }
265 | primaryCallSiteSpans.push(getCallSiteSpan(span));
266 | }
267 |
268 | // Convert MultiSpans to Range and Diagnostic
269 | let primaryFilePath = filePath;
270 | let primaryRange = defaultRange ?? dummyRange();
271 | if (primaryCallSiteSpans.length > 0) {
272 | primaryRange = parseMultiSpanRange(primaryCallSiteSpans);
273 | primaryFilePath = primaryCallSiteSpans[0].file_name;
274 | }
275 | const diagnostic = new vscode.Diagnostic(
276 | primaryRange,
277 | primaryMessage,
278 | level
279 | );
280 |
281 | // Parse all non-primary spans
282 | const relatedInformation = [];
283 | for (const span of msg.spans) {
284 | if (span.is_primary) {
285 | continue;
286 | }
287 |
288 | const message = `[Note] ${span.label ?? "related expression"}`;
289 | const callSiteSpan = getCallSiteSpan(span);
290 | const range = parseSpanRange(callSiteSpan);
291 | const filePath = callSiteSpan.file_name;
292 | const fileUri = vscode.Uri.file(filePath);
293 |
294 | relatedInformation.push(
295 | new vscode.DiagnosticRelatedInformation(
296 | new vscode.Location(fileUri, range),
297 | message
298 | )
299 | );
300 | }
301 |
302 | // Recursively parse child messages.
303 | for (const child of msg.children) {
304 | const childDiagnostic = parseRustcMessage(child, filePath, primaryRange);
305 | const fileUri = vscode.Uri.file(childDiagnostic.file_path);
306 | relatedInformation.push(
307 | new vscode.DiagnosticRelatedInformation(
308 | new vscode.Location(
309 | fileUri,
310 | childDiagnostic.diagnostic.range
311 | ),
312 | childDiagnostic.diagnostic.message
313 | )
314 | );
315 | }
316 |
317 | // Set related information
318 | diagnostic.relatedInformation = relatedInformation;
319 |
320 | return {
321 | file_path: primaryFilePath,
322 | diagnostic
323 | };
324 | }
325 |
326 | /**
327 | * Removes Rust's metadata in the specified project folder. This is a work
328 | * around for `cargo check` not reissuing warning information for libs.
329 | *
330 | * @param targetPath The target path of a rust project.
331 | */
332 | async function removeDiagnosticMetadata(targetPath: string) {
333 | const pattern = new vscode.RelativePattern(path.join(targetPath, "debug"), "*.rmeta");
334 | const files = await vscode.workspace.findFiles(pattern);
335 | const promises = files.map(file => {
336 | return (new vvt.Location(file.fsPath)).remove()
337 | });
338 | await Promise.all(promises)
339 | }
340 |
341 | enum VerificationStatus {
342 | Crash,
343 | Verified,
344 | Errors
345 | }
346 |
347 | /**
348 | * Queries for the diagnostics of a rust crate using cargo-prusti.
349 | *
350 | * @param prusti The location of Prusti files.
351 | * @param cratePath The path of a Rust crate.
352 | * @param destructors Where to store the destructors of the spawned processes.
353 | * @returns A tuple containing the diagnostics, status and duration of the verification.
354 | */
355 | async function queryCrateDiagnostics(
356 | prusti: dependencies.PrustiLocation,
357 | cratePath: string,
358 | serverAddress: string,
359 | destructors: Set,
360 | ): Promise<[Diagnostic[], VerificationStatus, util.Duration]> {
361 | const [metadata, metadataStatus, metadataDuration] = await queryCrateMetadata(prusti, cratePath, destructors);
362 | if (metadataStatus !== CrateMetadataStatus.Ok) {
363 | return [[], VerificationStatus.Crash, metadataDuration];
364 | }
365 |
366 | // FIXME: Workaround for warning generation for libs.
367 | await removeDiagnosticMetadata(metadata.target_directory);
368 |
369 | const cargoPrustiArgs = ["--message-format=json"].concat(
370 | config.extraCargoPrustiArgs()
371 | );
372 | const cargoPrustiEnv = {
373 | ...process.env, // Needed to run Rustup
374 | ...{
375 | PRUSTI_SERVER_ADDRESS: serverAddress,
376 | PRUSTI_QUIET: "true",
377 | JAVA_HOME: (await config.javaHome())!.path,
378 | },
379 | ...config.extraPrustiEnv(),
380 | };
381 | const output = await util.spawn(
382 | prusti.cargoPrusti,
383 | cargoPrustiArgs,
384 | {
385 | options: {
386 | cwd: cratePath,
387 | env: cargoPrustiEnv,
388 | }
389 | },
390 | destructors,
391 | );
392 | let status = VerificationStatus.Crash;
393 | if (output.code === 0) {
394 | status = VerificationStatus.Verified;
395 | }
396 | if (output.code === 1) {
397 | status = VerificationStatus.Errors;
398 | }
399 | if (output.code === 101) {
400 | status = VerificationStatus.Errors;
401 | }
402 | if (/error: internal compiler error/.exec(output.stderr) !== null) {
403 | status = VerificationStatus.Crash;
404 | }
405 | if (/^thread '.*' panicked at/.exec(output.stderr) !== null) {
406 | status = VerificationStatus.Crash;
407 | }
408 | const basePath = metadata.workspace_root ?? cratePath;
409 | const diagnostics: Diagnostic[] = [];
410 | for (const messages of parseCargoOutput(output.stdout)) {
411 | diagnostics.push(
412 | parseCargoMessage(messages, basePath)
413 | );
414 | }
415 | return [diagnostics, status, output.duration];
416 | }
417 |
418 | /**
419 | * Queries for the diagnostics of a rust crate using prusti-rustc.
420 | *
421 | * @param prusti The location of Prusti files.
422 | * @param filePath The path of a Rust program.
423 | * @param destructors Where to store the destructors of the spawned processes.
424 | * @returns A tuple containing the diagnostics, status and duration of the verification.
425 | */
426 | async function queryProgramDiagnostics(
427 | prusti: dependencies.PrustiLocation,
428 | filePath: string,
429 | serverAddress: string,
430 | destructors: Set,
431 | ): Promise<[Diagnostic[], VerificationStatus, util.Duration]> {
432 | const prustiRustcArgs = [
433 | "--crate-type=lib",
434 | "--error-format=json",
435 | filePath
436 | ].concat(
437 | config.extraPrustiRustcArgs()
438 | );
439 | const prustiRustcEnv = {
440 | ...process.env, // Needed to run Rustup
441 | ...{
442 | PRUSTI_SERVER_ADDRESS: serverAddress,
443 | PRUSTI_QUIET: "true",
444 | JAVA_HOME: (await config.javaHome())!.path,
445 | },
446 | ...config.extraPrustiEnv(),
447 | };
448 | const output = await util.spawn(
449 | prusti.prustiRustc,
450 | prustiRustcArgs,
451 | {
452 | options: {
453 | cwd: path.dirname(filePath),
454 | env: prustiRustcEnv,
455 | }
456 | },
457 | destructors
458 | );
459 | let status = VerificationStatus.Crash;
460 | if (output.code === 0) {
461 | status = VerificationStatus.Verified;
462 | }
463 | if (output.code === 1) {
464 | status = VerificationStatus.Errors;
465 | }
466 | if (output.code === 101) {
467 | status = VerificationStatus.Crash;
468 | }
469 | if (/error: internal compiler error/.exec(output.stderr) !== null) {
470 | status = VerificationStatus.Crash;
471 | }
472 | if (/^thread '.*' panicked at/.exec(output.stderr) !== null) {
473 | status = VerificationStatus.Crash;
474 | }
475 | const diagnostics: Diagnostic[] = [];
476 | for (const messages of parseRustcOutput(output.stderr)) {
477 | diagnostics.push(
478 | parseRustcMessage(messages, filePath)
479 | );
480 | }
481 | return [diagnostics, status, output.duration];
482 | }
483 |
484 | // ========================================================
485 | // Diagnostic Management
486 | // ========================================================
487 |
488 | export class VerificationDiagnostics {
489 | private diagnostics: Map;
490 |
491 | constructor() {
492 | this.diagnostics = new Map();
493 | }
494 |
495 | public hasErrors(): boolean {
496 | let count = 0;
497 | this.diagnostics.forEach((documentDiagnostics: vscode.Diagnostic[]) => {
498 | documentDiagnostics.forEach((diagnostic: vscode.Diagnostic) => {
499 | if (diagnostic.severity === vscode.DiagnosticSeverity.Error) {
500 | count += 1;
501 | }
502 | });
503 | });
504 | return count > 0;
505 | }
506 |
507 | public hasWarnings(): boolean {
508 | let count = 0;
509 | this.diagnostics.forEach((documentDiagnostics: vscode.Diagnostic[]) => {
510 | documentDiagnostics.forEach((diagnostic: vscode.Diagnostic) => {
511 | if (diagnostic.severity === vscode.DiagnosticSeverity.Warning) {
512 | count += 1;
513 | }
514 | });
515 | });
516 | return count > 0;
517 | }
518 |
519 | public isEmpty(): boolean {
520 | return this.diagnostics.size === 0;
521 | }
522 |
523 | public countsBySeverity(): Map {
524 | const counts = new Map();
525 | this.diagnostics.forEach((diags) => {
526 | diags.forEach(diag => {
527 | const count = counts.get(diag.severity);
528 | counts.set(diag.severity, (count === undefined ? 0 : count) + 1);
529 | });
530 | });
531 | return counts;
532 | }
533 |
534 | public addAll(diagnostics: Diagnostic[]): void {
535 | for (const diag of diagnostics) {
536 | this.add(diag);
537 | }
538 | }
539 |
540 | public add(diagnostic: Diagnostic): void {
541 | if (this.reportDiagnostic(diagnostic)) {
542 | const set = this.diagnostics.get(diagnostic.file_path);
543 | if (set !== undefined) {
544 | set.push(diagnostic.diagnostic);
545 | } else {
546 | this.diagnostics.set(diagnostic.file_path, [diagnostic.diagnostic]);
547 | }
548 | } else {
549 | util.log(`Ignored diagnostic message: '${diagnostic.diagnostic.message}'`);
550 | }
551 | }
552 |
553 | public renderIn(target: vscode.DiagnosticCollection): void {
554 | target.clear();
555 | for (const [filePath, fileDiagnostics] of this.diagnostics.entries()) {
556 | const uri = vscode.Uri.file(filePath);
557 | util.log(`Rendering ${fileDiagnostics.length} diagnostics at ${uri}`);
558 | target.set(uri, fileDiagnostics);
559 | }
560 | }
561 |
562 | /// Returns false if the diagnostic should be ignored
563 | private reportDiagnostic(diagnostic: Diagnostic): boolean {
564 | const message = diagnostic.diagnostic.message;
565 | if (config.reportErrorsOnly()) {
566 | if (diagnostic.diagnostic.severity !== vscode.DiagnosticSeverity.Error
567 | && message.indexOf("Prusti") === -1) {
568 | return false;
569 | }
570 | }
571 | if (/^aborting due to (\d+ |)previous error(s|)/.exec(message) !== null) {
572 | return false;
573 | }
574 | if (/^\d+ warning(s|) emitted/.exec(message) !== null) {
575 | return false;
576 | }
577 | return true;
578 | }
579 | }
580 |
581 | export enum VerificationTarget {
582 | StandaloneFile = "file",
583 | Crate = "crate"
584 | }
585 |
586 | export class DiagnosticsManager {
587 | private target: vscode.DiagnosticCollection;
588 | private procDestructors: Set = new Set();
589 | private verificationStatus: vscode.StatusBarItem;
590 | private runCount = 0;
591 |
592 | public constructor(target: vscode.DiagnosticCollection, verificationStatus: vscode.StatusBarItem) {
593 | this.target = target;
594 | this.verificationStatus = verificationStatus;
595 | }
596 |
597 | public dispose(): void {
598 | util.log("Dispose DiagnosticsManager");
599 | this.killAll();
600 | }
601 |
602 | public inProgress(): number {
603 | return this.procDestructors.size
604 | }
605 |
606 | public killAll(): void {
607 | util.log(`Killing ${this.procDestructors.size} processes.`);
608 | this.procDestructors.forEach((kill) => kill());
609 | }
610 |
611 | public clearDiagnostics(uri?: vscode.Uri): void {
612 | if (uri) {
613 | util.log(`Clear diagnostics on ${uri}`);
614 | this.target.delete(uri);
615 | } else {
616 | util.log("Clear all diagnostics");
617 | this.target.clear();
618 | }
619 | this.verificationStatus.text = ""
620 | this.verificationStatus.tooltip = undefined;
621 | this.verificationStatus.command = undefined;
622 | }
623 |
624 | public async verify(prusti: dependencies.PrustiLocation, serverAddress: string, targetPath: string, target: VerificationTarget): Promise {
625 | // Prepare verification
626 | this.runCount += 1;
627 | const currentRun = this.runCount;
628 | util.log(`Preparing verification run #${currentRun}.`);
629 | this.killAll();
630 |
631 | // Run verification
632 | const escapedFileName = path.basename(targetPath).replace("$", "\\$");
633 | this.verificationStatus.text = `$(sync~spin) Verifying ${target} '${escapedFileName}'...`;
634 | this.verificationStatus.tooltip = "Status of the Prusti verification. Click to stop Prusti.";
635 | this.verificationStatus.command = "prusti-assistant.killAll";
636 |
637 | const verificationDiagnostics = new VerificationDiagnostics();
638 | let durationSecMsg: string | null = null;
639 | const crashErrorMsg = "Prusti encountered an unexpected error. " +
640 | "If the issue persists, please open a [bug report](https://github.com/viperproject/prusti-dev/issues/new) or contact us on the [Zulip chat](https://prusti.zulipchat.com/). " +
641 | "See [the logs](command:prusti-assistant.openLogs) for more details.";
642 | let crashed = false;
643 | try {
644 | let diagnostics: Diagnostic[], status: VerificationStatus, duration: util.Duration;
645 | if (target === VerificationTarget.Crate) {
646 | [diagnostics, status, duration] = await queryCrateDiagnostics(prusti, targetPath, serverAddress, this.procDestructors);
647 | } else {
648 | [diagnostics, status, duration] = await queryProgramDiagnostics(prusti, targetPath, serverAddress, this.procDestructors);
649 | }
650 |
651 | verificationDiagnostics.addAll(diagnostics);
652 | durationSecMsg = (duration[0] + duration[1] / 1e9).toFixed(1);
653 | if (status === VerificationStatus.Crash) {
654 | crashed = true;
655 | util.log("Prusti encountered an unexpected error.");
656 | util.userError(crashErrorMsg);
657 | }
658 | if (status === VerificationStatus.Errors && !verificationDiagnostics.hasErrors()) {
659 | crashed = true;
660 | util.log("The verification failed, but there are no errors to report.");
661 | util.userError(crashErrorMsg);
662 | }
663 | } catch (err) {
664 | util.log(`Error while running Prusti: ${err}`);
665 | crashed = true;
666 | util.userError(crashErrorMsg);
667 | }
668 |
669 | if (currentRun != this.runCount) {
670 | util.log(`Discarding the result of the verification run #${currentRun}, because the latest is #${this.runCount}.`);
671 | } else {
672 | // Render diagnostics
673 | verificationDiagnostics.renderIn(this.target);
674 | if (crashed) {
675 | this.verificationStatus.text = `$(error) Verification of ${target} '${escapedFileName}' failed with an unexpected error`;
676 | this.verificationStatus.command = "prusti-assistant.openLogs";
677 | } else if (verificationDiagnostics.hasErrors()) {
678 | const counts = verificationDiagnostics.countsBySeverity();
679 | const errors = counts.get(vscode.DiagnosticSeverity.Error);
680 | const noun = errors === 1 ? "error" : "errors";
681 | this.verificationStatus.text = `$(error) Verification of ${target} '${escapedFileName}' failed with ${errors} ${noun} (${durationSecMsg} s)`;
682 | this.verificationStatus.command = "workbench.action.problems.focus";
683 | } else if (verificationDiagnostics.hasWarnings()) {
684 | const counts = verificationDiagnostics.countsBySeverity();
685 | const warnings = counts.get(vscode.DiagnosticSeverity.Warning);
686 | const noun = warnings === 1 ? "warning" : "warnings";
687 | this.verificationStatus.text = `$(warning) Verification of ${target} '${escapedFileName}' succeeded with ${warnings} ${noun} (${durationSecMsg} s)`;
688 | this.verificationStatus.command = "workbench.action.problems.focus";
689 | } else {
690 | this.verificationStatus.text = `$(check) Verification of ${target} '${escapedFileName}' succeeded (${durationSecMsg} s)`;
691 | this.verificationStatus.command = undefined;
692 | }
693 | this.verificationStatus.tooltip = "Status of the Prusti verification.";
694 | }
695 | }
696 | }
697 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import * as config from "./config";
3 | import * as util from "./util";
4 | import * as diagnostics from "./diagnostics";
5 | import * as checks from "./checks";
6 | import { prusti, installDependencies, prustiVersion } from "./dependencies";
7 | import * as server from "./server";
8 | import * as state from "./state";
9 |
10 | export async function activate(context: vscode.ExtensionContext): Promise {
11 | util.log("Activate Prusti Assistant");
12 | const showVersionCommand = "prusti-assistant.show-version";
13 | const verifyProgramCommand = "prusti-assistant.verify";
14 | const killAllCommand = "prusti-assistant.killAll";
15 | const openLogsCommand = "prusti-assistant.openLogs";
16 | const openServerLogsCommand = "prusti-assistant.openServerLogs";
17 | const updateCommand = "prusti-assistant.update";
18 | const restartServerCommand = "prusti-assistant.restart-server";
19 | const clearDiagnosticsCommand = "prusti-assistant.clear-diagnostics";
20 |
21 | // Open logs on command
22 | context.subscriptions.push(
23 | vscode.commands.registerCommand(openLogsCommand, () => util.showLogs())
24 | );
25 | context.subscriptions.push(
26 | vscode.commands.registerCommand(openServerLogsCommand, () => server.showLogs())
27 | );
28 |
29 | // Verification status
30 | const verificationStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10);
31 | verificationStatus.tooltip = "Status of the Prusti verification.";
32 | verificationStatus.text = "$(sync~spin) Activating Prusti...";
33 | verificationStatus.show();
34 | context.subscriptions.push(verificationStatus);
35 |
36 | // Prerequisites checks
37 | util.log("Checking Prusti prerequisites...");
38 | verificationStatus.text = "$(sync~spin) Checking Prusti prerequisites...";
39 | const [hasPrerequisites, errorMessage] = await checks.hasPrerequisites();
40 | if (!hasPrerequisites) {
41 | verificationStatus.tooltip = "Prusti Assistant's prerequisites are not satisfied.";
42 | util.userError(errorMessage);
43 | util.log("Stopping plugin. Reload the IDE to retry.");
44 | return;
45 | } else {
46 | util.log("Prerequisites are satisfied.");
47 | }
48 |
49 | // Catch server crashes
50 | server.registerCrashHandler(context, verificationStatus);
51 |
52 | // Download dependencies and start the server
53 | util.log("Checking Prusti dependencies...");
54 | verificationStatus.text = "$(sync~spin) Checking Prusti dependencies...";
55 | await installDependencies(context, false, verificationStatus);
56 |
57 | // Check Prusti
58 | util.log("Checking Prusti...");
59 | const [isPrustiOk, prustiErrorMessage] = await checks.checkPrusti(prusti!);
60 | if (!isPrustiOk) {
61 | verificationStatus.tooltip = "Prusti's installation seems broken.";
62 | util.userError(prustiErrorMessage, true, verificationStatus);
63 | util.log("Stopping plugin. Reload the IDE to retry.");
64 | return;
65 | } else {
66 | util.log("Prusti checks completed.");
67 | }
68 |
69 | // Update dependencies on command
70 | context.subscriptions.push(
71 | vscode.commands.registerCommand(updateCommand, async () => {
72 | await installDependencies(context, true, verificationStatus);
73 | })
74 | );
75 |
76 | // Check for updates
77 | if (config.checkForUpdates()) {
78 | util.log("Checking for updates...");
79 | if (await checks.isOutdated(prusti!)) {
80 | util.log("The Prusti version is outdated.");
81 | util.userInfoPopup(
82 | "Your version of Prusti is outdated.",
83 | "Download Update",
84 | () => {
85 | vscode.commands.executeCommand(updateCommand)
86 | .then(undefined, err => util.log(`Error: ${err}`));
87 | }
88 | );
89 | } else {
90 | util.log("Prusti is up-to-date.");
91 | }
92 | }
93 |
94 | // Show version on command
95 | context.subscriptions.push(
96 | vscode.commands.registerCommand(showVersionCommand, async () => {
97 | util.userInfo(await prustiVersion());
98 | })
99 | );
100 |
101 | // Verify on click
102 | const prustiButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 11);
103 | prustiButton.command = verifyProgramCommand;
104 | prustiButton.text = "$(play) Prusti";
105 | prustiButton.tooltip = new vscode.MarkdownString(
106 | "Run the [Prusti verifier](https://github.com/viperproject/prusti-dev) on the current file.\n\n" +
107 | "---\n\n" +
108 | "$(link) [User guide](https://viperproject.github.io/prusti-dev/user-guide/)\n\n" +
109 | "$(link) [Zulip chat](https://prusti.zulipchat.com/)\n\n" +
110 | `[Show version](command:${showVersionCommand})\n\n` +
111 | `[Update Prusti](command:${updateCommand})\n\n` +
112 | `[Restart server](command:${restartServerCommand})\n\n` +
113 | `[Clear diagnostics](command:${clearDiagnosticsCommand})`,
114 | true,
115 | );
116 | prustiButton.tooltip.isTrusted = true;
117 | prustiButton.show();
118 | context.subscriptions.push(prustiButton);
119 |
120 | // Restart the server on command
121 | context.subscriptions.push(
122 | vscode.commands.registerCommand(restartServerCommand, async () => {
123 | await server.restart(context, verificationStatus);
124 | })
125 | );
126 |
127 | // Update dependencies on config change
128 | context.subscriptions.push(
129 | vscode.workspace.onDidChangeConfiguration(async event => {
130 | const hasChangedVersion = event.affectsConfiguration(config.prustiVersionPath);
131 | const hasChangedLocation = (
132 | config.prustiVersion() === config.PrustiVersion.Local
133 | && event.affectsConfiguration(config.localPrustiPathPath)
134 | );
135 | const hasChangedTag = (
136 | config.prustiVersion() === config.PrustiVersion.Tag
137 | && event.affectsConfiguration(config.prustiTagPath)
138 | );
139 | if (hasChangedVersion || hasChangedLocation || hasChangedTag) {
140 | util.log("Install the dependencies because the configuration has changed...");
141 | const reDownload = config.prustiVersion() === config.PrustiVersion.Tag;
142 | await installDependencies(context, reDownload, verificationStatus);
143 | }
144 | const hasChangedServer = event.affectsConfiguration(config.serverAddressPath);
145 | if (hasChangedServer) {
146 | util.log("Restart the server because the configuration has changed...");
147 | await server.restart(context, verificationStatus);
148 | }
149 | // Let the test suite know that the new configuration has been
150 | // processed
151 | state.notifyConfigUpdate();
152 | })
153 | );
154 |
155 | // Diagnostics manager
156 | const verificationDiagnostics = vscode.languages.createDiagnosticCollection("prusti");
157 | context.subscriptions.push(verificationDiagnostics);
158 | const verificationManager = new diagnostics.DiagnosticsManager(
159 | verificationDiagnostics,
160 | verificationStatus,
161 | );
162 | context.subscriptions.push(verificationManager);
163 |
164 | // Clear all diagnostics on command
165 | context.subscriptions.push(
166 | vscode.commands.registerCommand(clearDiagnosticsCommand, () => verificationManager.clearDiagnostics())
167 | );
168 |
169 | // Kill-all on command
170 | context.subscriptions.push(
171 | vscode.commands.registerCommand(killAllCommand, () => verificationManager.killAll())
172 | );
173 |
174 | // Define verification function
175 | async function verify(document: vscode.TextDocument) {
176 | util.log(`Run verification on ${document.uri.fsPath}...`);
177 | const projects = await util.findProjects();
178 | const cratePath = projects.getParent(document.uri.fsPath);
179 |
180 | if (server.address === undefined) {
181 | // Just warn, as Prusti can run even without a server.
182 | util.userWarn(
183 | "Prusti might run slower than usual because the Prusti server is not running."
184 | );
185 | }
186 |
187 | if (cratePath === undefined) {
188 | if (document.languageId !== "rust") {
189 | util.userWarn(
190 | `The active document is not a Rust program (it is ${document.languageId}) and it is not part of a crate.`
191 | );
192 | return;
193 | }
194 |
195 | await verificationManager.verify(
196 | prusti!,
197 | server.address ?? "",
198 | document.uri.fsPath,
199 | diagnostics.VerificationTarget.StandaloneFile
200 | );
201 | } else {
202 | await verificationManager.verify(
203 | prusti!,
204 | server.address ?? "",
205 | cratePath.path,
206 | diagnostics.VerificationTarget.Crate
207 | );
208 | }
209 | }
210 |
211 | // Verify on command
212 | context.subscriptions.push(
213 | vscode.commands.registerCommand(verifyProgramCommand, async () => {
214 | const activeTextEditor = vscode.window.activeTextEditor;
215 | if (activeTextEditor !== undefined) {
216 | await activeTextEditor.document.save().then(
217 | () => verify(activeTextEditor.document)
218 | );
219 | } else {
220 | util.log("vscode.window.activeTextEditor is not ready yet.");
221 | }
222 | })
223 | );
224 |
225 | // Verify on save
226 | context.subscriptions.push(
227 | vscode.workspace.onDidSaveTextDocument(async (document: vscode.TextDocument) => {
228 | if (document.languageId === "rust" && config.verifyOnSave()) {
229 | await verify(document);
230 | }
231 | })
232 | );
233 |
234 | // Verify on open
235 | context.subscriptions.push(
236 | vscode.workspace.onDidOpenTextDocument(async (document: vscode.TextDocument) => {
237 | if (document.languageId === "rust" && config.verifyOnOpen()) {
238 | await verify(document);
239 | }
240 | })
241 | );
242 |
243 | if (config.verifyOnOpen()) {
244 | // Verify on activation
245 | if (vscode.window.activeTextEditor !== undefined) {
246 | await verify(
247 | vscode.window.activeTextEditor.document
248 | );
249 | } else {
250 | util.log("vscode.window.activeTextEditor is not ready yet.");
251 | }
252 | }
253 |
254 | // Stand ready to deactivate the extension
255 | context.subscriptions.push({
256 | dispose: () => {
257 | util.log("Dispose Prusti Assistant");
258 | deactivate().catch(
259 | err => util.log(`Failed to deactivate the extension: ${err}`)
260 | );
261 | }
262 | });
263 | process.on("SIGTERM", () => {
264 | util.log("Received SIGTERM");
265 | deactivate().catch(
266 | err => util.log(`Failed to deactivate the extension: ${err}`)
267 | );
268 | });
269 |
270 | state.notifyExtensionActivation();
271 | }
272 |
273 | export async function deactivate(): Promise {
274 | util.log("Deactivate Prusti Assistant");
275 | await server.stop();
276 | }
277 |
--------------------------------------------------------------------------------
/src/javaHome.ts:
--------------------------------------------------------------------------------
1 | import { Location } from "vs-verification-toolbox";
2 | import * as locatejavaHome from 'locate-java-home';
3 | import * as util from './util';
4 |
5 | export async function findJavaHome(): Promise {
6 | util.log("Searching for Java home...");
7 | let javaHome: string | null = null;
8 | try {
9 | javaHome = await new Promise((resolve, reject) => {
10 | const options = {
11 | version: ">=11"
12 | };
13 | locatejavaHome.default(options, (err, javaHomes) => {
14 | if (err) {
15 | util.log(`Error: ${err}`);
16 | reject(err);
17 | } else {
18 | if (!Array.isArray(javaHomes) || javaHomes.length === 0) {
19 | util.log(
20 | `Could not find a Java home with version ${options.version}. ` +
21 | "See the requirements in the description of the extension."
22 | );
23 | resolve(null);
24 | } else {
25 | const firstJavaHome = javaHomes[0];
26 | util.log(`Using Java home ${JSON.stringify(firstJavaHome, null, 2)}`);
27 | resolve(firstJavaHome.path);
28 | }
29 | }
30 | });
31 | });
32 | } catch (err) {
33 | util.log(`Error while searching for Java home: ${err}`);
34 | }
35 | return javaHome;
36 | }
37 |
38 | export class JavaHome {
39 | constructor(
40 | private readonly location: Location
41 | ) { }
42 |
43 | public get path(): string {
44 | return this.location.basePath;
45 | }
46 |
47 | public get javaExecutable(): string {
48 | return this.location.child("bin").executable("java");
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import * as util from "./util";
3 | import { prusti } from "./dependencies";
4 | import * as config from "./config";
5 | import { ServerManager } from "./toolbox/serverManager";
6 |
7 | const serverChannel = vscode.window.createOutputChannel("Prusti Assistant Server");
8 | const server = new ServerManager(
9 | "Prusti server",
10 | util.log
11 | );
12 |
13 | export function showLogs(): void {
14 | serverChannel.show();
15 | }
16 |
17 | server.waitForUnrecoverable().then(() => {
18 | util.log(`Prusti server is unrecorevable.`);
19 | address = undefined;
20 | util.userError(
21 | "Prusti server stopped working. Please restart the IDE.",
22 | true
23 | );
24 | }).catch(
25 | err => util.log(`Error: ${err}`)
26 | );
27 |
28 | /**
29 | * The address of the server.
30 | */
31 | export function registerCrashHandler(context: vscode.ExtensionContext, verificationStatus: vscode.StatusBarItem): void {
32 | server.waitForCrashed().then(() => {
33 | util.log("Prusti server crashed.");
34 | address = undefined;
35 | // Ask the user to restart the server
36 | util.userErrorPopup(
37 | "Prusti server stopped working. " +
38 | "If the issue persists, please open a [bug report](https://github.com/viperproject/prusti-dev/issues/new) or contact us on the [Zulip chat](https://prusti.zulipchat.com/). " +
39 | "See [the server logs](command:prusti-assistant.openServerLogs) for more details.",
40 | "Restart Server",
41 | () => {
42 | restart(context, verificationStatus).then(
43 | () => registerCrashHandler(context, verificationStatus)
44 | ).catch(
45 | err => util.log(`Error: ${err}`)
46 | );
47 | },
48 | verificationStatus
49 | );
50 | }).catch(
51 | err => util.log(`Error: ${err}`)
52 | );
53 | }
54 |
55 | /**
56 | * The address of the server.
57 | */
58 | export let address: string | undefined;
59 |
60 | /**
61 | * Stop the server.
62 | */
63 | export async function stop(): Promise {
64 | address = undefined;
65 | server.initiateStop();
66 | await server.waitForStopped();
67 | }
68 |
69 | /**
70 | * Wait for the server to become ready, with a timeout.
71 | */
72 | function waitUntilReady(timeout = 10_000): Promise {
73 | return new Promise((resolve, reject) => {
74 | let done = false;
75 | server.waitForReady().then(
76 | () => {
77 | if (!done) {
78 | done = true;
79 | resolve();
80 | }
81 | },
82 | err => {
83 | if (!done) {
84 | done = true;
85 | reject(err);
86 | }
87 | }
88 | );
89 | setTimeout(() => {
90 | if (!done) {
91 | done = true;
92 | reject(
93 | new Error(`Prusti server took more than ${timeout / 1000} seconds to start.`)
94 | );
95 | }
96 | }, timeout);
97 | })
98 | }
99 |
100 | /**
101 | * Start or restart the server.
102 | */
103 | export async function restart(context: vscode.ExtensionContext, verificationStatus: vscode.StatusBarItem): Promise {
104 | await stop();
105 |
106 | const configAddress = config.serverAddress();
107 | if (configAddress !== "") {
108 | util.log(`Using configured Prusti server address: ${configAddress}`);
109 | address = configAddress;
110 | return;
111 | }
112 |
113 | let prustiServerCwd: string | undefined;
114 | if (vscode.workspace.workspaceFolders !== undefined) {
115 | prustiServerCwd = vscode.workspace.workspaceFolders[0].uri.fsPath;
116 | util.log(`Prusti server will be executed in '${prustiServerCwd}'`);
117 | }
118 |
119 | const prustiServerArgs = ["--port=0"].concat(
120 | config.extraPrustiServerArgs()
121 | );
122 | const prustiServerEnv = {
123 | ...process.env, // Needed to run Rustup
124 | ...{
125 | JAVA_HOME: (await config.javaHome())!.path,
126 | },
127 | ...config.extraPrustiEnv(),
128 | };
129 |
130 | server.initiateStart(
131 | prusti!.prustiServer,
132 | prustiServerArgs,
133 | {
134 | cwd: prustiServerCwd,
135 | env: prustiServerEnv,
136 | onStdout: data => {
137 | serverChannel.append(`[stdout] ${data}`);
138 | console.log(`[Prusti Server][stdout] ${data}`);
139 | // Extract the server port from the output
140 | if (address === undefined) {
141 | const port = parseInt(data.toString().split("port: ")[1], 10);
142 | util.log(`Prusti server is listening on port ${port}.`);
143 | address = `localhost:${port}`;
144 | verificationStatus.text = "Prusti server is ready.";
145 | server.setReady();
146 | }
147 | },
148 | onStderr: data => {
149 | serverChannel.append(`[stderr] ${data}`);
150 | console.log(`[Prusti Server][stderr] ${data}`);
151 | }
152 | }
153 | );
154 |
155 | await waitUntilReady();
156 | }
157 |
--------------------------------------------------------------------------------
/src/state.ts:
--------------------------------------------------------------------------------
1 | import * as util from "./util";
2 |
3 | /**
4 | * This module keeps a global state and allows clients to wait for the
5 | * following events:
6 | * - The extension has been fully activated.
7 | * - A change to the settings has been fully processed.
8 | */
9 |
10 | let isExtensionActive = false;
11 |
12 | export type Listener = () => void;
13 |
14 | const waitingForExtensionActivation: Listener[] = [];
15 |
16 | export function waitExtensionActivation(): Promise {
17 | return new Promise(resolve => {
18 | if (isExtensionActive) {
19 | // Resolve immediately
20 | resolve();
21 | } else {
22 | waitingForExtensionActivation.push(resolve);
23 | }
24 | });
25 | }
26 |
27 | export function notifyExtensionActivation(): void {
28 | util.log("The extension is now active.");
29 | isExtensionActive = true;
30 | waitingForExtensionActivation.forEach(listener => listener());
31 | }
32 |
33 | const waitingForConfigUpdate: Listener[] = [];
34 |
35 | export function waitConfigUpdate(): Promise {
36 | return new Promise(resolve => {
37 | waitingForConfigUpdate.push(resolve);
38 | });
39 | }
40 |
41 | export function notifyConfigUpdate(): void {
42 | waitingForConfigUpdate.forEach(listener => listener());
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/extension.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from "assert";
2 | import * as vscode from "vscode";
3 | import * as path from "path";
4 | import * as glob from "glob";
5 | import * as fs from "fs-extra";
6 | import * as os from "os";
7 | import { expect } from "chai";
8 | import * as config from "../config";
9 | import * as state from "../state";
10 | import * as extension from "../extension"
11 |
12 | /**
13 | * Get the path of the workspace.
14 | *
15 | * @returns The path of the workspace.
16 | */
17 | function workspacePath(): string {
18 | assert.ok(vscode.workspace.workspaceFolders?.length);
19 | return vscode.workspace.workspaceFolders[0].uri.fsPath;
20 | }
21 |
22 | /**
23 | * Convert a URI to a relative workspace path with forward slashes.
24 | *
25 | * @param uri The URI to convert.
26 | * @returns The computed relative path.
27 | */
28 | function asRelativeWorkspacePath(target: vscode.Uri): string {
29 | // Resolve symlinks (e.g., in MacOS, `/var` is a symlink to `/private/var`).
30 | // We do this manually becase `vscode.workspace.asRelativePath` does not resolve symlinks.
31 | const normalizedTarget = fs.realpathSync(target.fsPath);
32 | const normalizedWorkspace = fs.realpathSync(workspacePath());
33 | return path.relative(normalizedWorkspace, normalizedTarget).replace(/\\/g, "/");
34 | }
35 |
36 | /**
37 | * Open a file in the IDE
38 | * @param filePath The file to open.
39 | * @returns A promise with the opened document.
40 | */
41 | function openFile(filePath: string): Promise {
42 | return new Promise((resolve, reject) => {
43 | console.log("Open " + filePath);
44 | vscode.workspace.openTextDocument(filePath).then(document => {
45 | vscode.window.showTextDocument(document).then(() => {
46 | resolve(document);
47 | }).then(undefined, reject);
48 | }).then(undefined, reject);
49 | });
50 | }
51 |
52 | /**
53 | * Evaluate one of the filters contained in the `.rs.json` expected diagnostics.
54 | * @param filter The filter dictionary.
55 | * @param name The name of the filter.
56 | * @returns True if the filter is fully satisfied, otherwise false.
57 | */
58 | function evaluateFilter(filter: [string: string], name: string): boolean {
59 | for (const [key, value] of Object.entries(filter)) {
60 | let actualValue: string
61 | if (key == "os") {
62 | actualValue = os.platform();
63 | } else {
64 | actualValue = config.config().get(key, "undefined");
65 | }
66 | if (value != actualValue) {
67 | console.log(
68 | `Filter ${name} requires '${key}' to be '${value}', but the actual value is ` +
69 | `'${actualValue}'.`
70 | );
71 | return false;
72 | }
73 | }
74 | console.log(`Filter ${name} passed.`);
75 | return true;
76 | }
77 |
78 | // JSON-like types used to normalize the diagnostics
79 | type Position = {
80 | line: number,
81 | character: number
82 | }
83 |
84 | type Range = {
85 | start: Position,
86 | end: Position
87 | }
88 |
89 | type RelatedInformation = {
90 | location: {
91 | uri: string,
92 | range: Range,
93 | },
94 | message: string
95 | }
96 |
97 | type Diagnostic = {
98 | // This path is relative to VSCode's workspace
99 | uri: string,
100 | range: Range,
101 | severity: number,
102 | message: string,
103 | relatedInformation?: RelatedInformation[],
104 | }
105 |
106 | function rangeToPlainObject(range: vscode.Range): Range {
107 | return {
108 | start: {
109 | line: range.start.line,
110 | character: range.start.character
111 | },
112 | end: {
113 | line: range.end.line,
114 | character: range.end.character
115 | }
116 | };
117 | }
118 |
119 | /**
120 | * Normalize a diagnostic, converting it to a plain object.
121 | *
122 | * @param uri The URI of the file containing the diagnostic.
123 | * @param diagnostic The diagnostic to convert.
124 | * @returns The normalized diagnostic.
125 | */
126 | function diagnosticToPlainObject(uri: vscode.Uri, diagnostic: vscode.Diagnostic): Diagnostic {
127 | const plainDiagnostic: Diagnostic = {
128 | uri: asRelativeWorkspacePath(uri),
129 | range: rangeToPlainObject(diagnostic.range),
130 | severity: diagnostic.severity,
131 | message: diagnostic.message,
132 | };
133 | if (diagnostic.relatedInformation) {
134 | plainDiagnostic.relatedInformation = diagnostic.relatedInformation.map((relatedInfo) => {
135 | return {
136 | location: {
137 | uri: asRelativeWorkspacePath(relatedInfo.location.uri),
138 | range: rangeToPlainObject(relatedInfo.location.range)
139 | },
140 | message: relatedInfo.message,
141 | };
142 | });
143 | }
144 | return plainDiagnostic;
145 | }
146 |
147 | // Constants used in the tests
148 | const PROJECT_ROOT = path.join(__dirname, "..", "..");
149 | const SCENARIOS_ROOT = path.join(PROJECT_ROOT, "src", "test", "scenarios");
150 | const SCENARIO = process.env.SCENARIO;
151 | assert(SCENARIO, "Cannot run tests because the SCENARIO environment variable is empty.")
152 | const SCENARIO_PATH = path.join(SCENARIOS_ROOT, SCENARIO);
153 | const SHARED_SCENARIO_PATH = path.join(SCENARIOS_ROOT, "shared");
154 | console.log("Scenario path:", SCENARIO_PATH)
155 |
156 | describe("Extension", () => {
157 | before(async () => {
158 | // Prepare the workspace
159 | console.log(`Preparing workspace of scenario '${SCENARIO}'`);
160 | for (const topFolderPath of [SCENARIO_PATH, SHARED_SCENARIO_PATH]) {
161 | for (const scenarioFolder of ["crates", "programs"]) {
162 | const srcPath = path.join(topFolderPath, scenarioFolder);
163 | const dstPath = path.join(workspacePath(), scenarioFolder);
164 | if (!await fs.pathExists(srcPath)) {
165 | continue;
166 | }
167 | const srcFiles = await fs.readdir(srcPath);
168 | for (const srcFileName of srcFiles) {
169 | const srcFile = path.join(srcPath, srcFileName);
170 | const dstFile = path.join(dstPath, srcFileName);
171 | await vscode.workspace.fs.copy(vscode.Uri.file(srcFile), vscode.Uri.file(dstFile));
172 | }
173 | }
174 | }
175 | // Wait until the extension is active
176 | await openFile(path.join(workspacePath(), "programs", "assert_true.rs"));
177 | await state.waitExtensionActivation();
178 | });
179 |
180 | after(async () => {
181 | // HACK: It seems that `deactivate` is not called when using the test
182 | // suite. So, we manually call the deactivate() function.
183 | console.log("Tear down test suite");
184 | await extension.deactivate();
185 | })
186 |
187 | it(`scenario ${SCENARIO} can update Prusti`, async () => {
188 | // Tests are run serially, so nothing will run & break while we're updating
189 | await openFile(path.join(workspacePath(), "programs", "assert_true.rs"));
190 | await vscode.commands.executeCommand("prusti-assistant.update");
191 | });
192 |
193 | // Generate a test for every Rust program with expected diagnostics in the test suite.
194 | const programs: Array = [SCENARIO_PATH, SHARED_SCENARIO_PATH].flatMap(cwd =>
195 | glob.sync("**/*.rs.json", { cwd: cwd }).map(filePath => filePath.replace(/\.json$/, ""))
196 | );
197 | console.log(`Creating tests for ${programs.length} programs: ${programs}`);
198 | assert.ok(programs.length >= 3, `There are not enough programs to test (${programs.length})`);
199 | programs.forEach(program => {
200 | it(`scenario ${SCENARIO} reports expected diagnostics on ${program}`, async () => {
201 | // Verify the program
202 | const programPath = path.join(workspacePath(), program);
203 | await openFile(programPath);
204 | await vscode.commands.executeCommand("prusti-assistant.clear-diagnostics");
205 | await vscode.commands.executeCommand("prusti-assistant.verify");
206 |
207 | // Collect and normalize the diagnostics
208 | const plainDiagnostics = vscode.languages.getDiagnostics().flatMap(pair => {
209 | const [uri, diagnostics] = pair;
210 | return diagnostics.map(diagnostic => diagnosticToPlainObject(uri, diagnostic));
211 | });
212 |
213 | // Load the expected diagnostics. A single JSON file can contain multiple alternatives.
214 | const expectedData = await fs.readFile(programPath + ".json", "utf-8");
215 | type MultiDiagnostics = [
216 | { filter?: [string: string], diagnostics: Diagnostic[] }
217 | ];
218 | const expected = JSON.parse(expectedData) as Diagnostic[] | MultiDiagnostics;
219 | let expectedMultiDiagnostics: MultiDiagnostics;
220 | if (!expected.length || !("diagnostics" in expected[0])) {
221 | expectedMultiDiagnostics = [
222 | { "diagnostics": expected as Diagnostic[] }
223 | ];
224 | } else {
225 | expectedMultiDiagnostics = expected as MultiDiagnostics;
226 | }
227 |
228 | // Select the expected diagnostics to be used for the current environment
229 | let expectedDiagnostics = expectedMultiDiagnostics.find((alternative, index) => {
230 | if (!alternative.filter) {
231 | console.log(
232 | `Find expected diagnostics: using default ` +
233 | `alternative ${index}.`
234 | );
235 | return true;
236 | }
237 | return evaluateFilter(alternative.filter, index.toString());
238 | });
239 | if (!expectedDiagnostics) {
240 | console.log(
241 | "Find expected diagnostics: found no matching alternative."
242 | );
243 | expectedDiagnostics = {
244 | "diagnostics": [] as unknown as Diagnostic[]
245 | };
246 | }
247 |
248 | // Compare the actual with the expected diagnostics
249 | expect(plainDiagnostics).to.deep.equal(expectedDiagnostics.diagnostics);
250 | });
251 | });
252 | });
253 |
--------------------------------------------------------------------------------
/src/test/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import * as Mocha from "mocha";
3 | import * as glob from "glob";
4 |
5 | export async function run(): Promise {
6 | const NYC = await import("nyc");
7 | const nyc = new NYC({
8 | cwd: path.join(__dirname, "..", ".."),
9 | instrument: true,
10 | extension: [
11 | ".ts",
12 | ".tsx"
13 | ],
14 | exclude: [
15 | "**/*.d.ts",
16 | "**/.vscode-test/**"
17 | ],
18 | all: true,
19 | hookRequire: true,
20 | hookRunInContext: true,
21 | hookRunInThisContext: true,
22 | });
23 | await nyc.createTempDirectory();
24 | await nyc.wrap();
25 |
26 | // Create the mocha test
27 | const mocha = new Mocha({
28 | ui: "bdd",
29 | // Installing Rustup and Prusti might take some minutes
30 | timeout: 600_000, // ms
31 | color: true,
32 | });
33 |
34 | const testsRoot = path.resolve(__dirname, "..");
35 | const files: Array = glob.sync("**/*.test.js", { cwd: testsRoot });
36 |
37 | // Add files to the test suite
38 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
39 |
40 | const failures: number = await new Promise(resolve => mocha.run(resolve));
41 | await nyc.writeCoverageFile()
42 |
43 | if (failures > 0) {
44 | throw new Error(`${failures} tests failed.`)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/runTest.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import * as fs from "fs";
3 | import * as glob from "glob";
4 | import * as tmp from "tmp";
5 |
6 | import { runTests } from "@vscode/test-electron";
7 | import { assert } from "console";
8 |
9 | const PROJECT_ROOT = path.join(__dirname, "..", "..");
10 | const SCENARIOS_ROOT = path.join(PROJECT_ROOT, "src", "test", "scenarios");
11 |
12 | async function main() {
13 | // The folder containing the Extension Manifest package.json
14 | // Passed to `--extensionDevelopmentPath`
15 | const extensionDevelopmentPath = path.resolve(__dirname, "../../");
16 |
17 | // The path to the extension test runner script
18 | // Passed to --extensionTestsPath
19 | const extensionTestsPath = path.resolve(__dirname, "./index");
20 |
21 | // Download VS Code, unzip it and run the integration test
22 | console.info("Reading VS Code version...");
23 | const vscodeVersion = fs.readFileSync(path.join(SCENARIOS_ROOT, "vscode-version")).toString().trim();
24 | console.info(`Tests will use VS Code version '${vscodeVersion}'`);
25 | console.info("Reading list of settings...");
26 | const scenarios: Array = glob.sync("*/settings.json", { cwd: SCENARIOS_ROOT })
27 | .map(filePath => path.basename(path.dirname(filePath)));
28 | assert(scenarios.length > 0, "There are no scenarios to test");
29 |
30 | let firstIteration = true;
31 | for (const scenario of scenarios) {
32 | if (!firstIteration) {
33 | // Workaround for a weird "exit code 55" error that happens on
34 | // Mac OS when starting a new vscode instance immediately after
35 | // closing an old one.
36 | await new Promise(resolve => setTimeout(resolve, 5000));
37 | }
38 | console.info("");
39 | console.info(`========== Testing scenario '${scenario}' ==========`);
40 | console.info(`::group::${scenario}`); // For GitHub's output
41 | console.info("");
42 | const tmpWorkspace = tmp.dirSync({ unsafeCleanup: true });
43 | try {
44 | // Prepare the workspace with the settings
45 | console.info(`Using temporary workspace '${tmpWorkspace.name}'`);
46 | const settingsPath = path.join(SCENARIOS_ROOT, scenario, "settings.json");
47 | const workspaceVSCodePath = path.join(tmpWorkspace.name, ".vscode");
48 | const workspaceSettingsPath = path.join(workspaceVSCodePath, "settings.json");
49 | fs.mkdirSync(workspaceVSCodePath);
50 | fs.copyFileSync(settingsPath, workspaceSettingsPath);
51 |
52 | // Run the tests in the workspace
53 | await runTests({
54 | version: vscodeVersion,
55 | extensionDevelopmentPath,
56 | extensionTestsPath,
57 | extensionTestsEnv: { SCENARIO: scenario, ...process.env },
58 | // Disable any other extension
59 | launchArgs: ["--disable-extensions", tmpWorkspace.name],
60 | });
61 | } finally {
62 | // Delete folder even in case of errors
63 | tmpWorkspace.removeCallback();
64 | // Reming which scenario was being tested
65 | console.info(`End of testing scenario '${scenario}'.`);
66 | console.info("::endgroup::"); // For GitHub's output
67 | }
68 | firstIteration = false;
69 | }
70 | }
71 |
72 | main().catch(err => {
73 | console.error(`Failed to run tests. Error: ${err}`);
74 | process.exit(1);
75 | });
76 |
--------------------------------------------------------------------------------
/src/test/scenarios/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 |
--------------------------------------------------------------------------------
/src/test/scenarios/README.md:
--------------------------------------------------------------------------------
1 | # Test cases
2 |
3 |
4 | ## Structure
5 |
6 | The structure of this folder is:
7 |
8 | * `vscode-version` — The vscode version to use to run the tests.
9 | * `/settings.json` — The settings of a scenario.
10 | * `/programs/.rs` — A standalone program test case of the scenario.
11 | * `/programs/.rs.json` — The expected diagnostics of a test case.
12 | * `/crates//...` — A crate test case of the scenario.
13 | * Each `.rs` file is expected to have a corresponding `.rs.json` file containing the expected diagnostics.
14 |
15 | The special "shared" scenario does not contain `settings.json` and is implicitly inherited by all other scenarios.
16 |
17 | ## Expected diagnostics
18 |
19 | The content of the `.rs.json` expected diagnostics can be either:
20 |
21 | * A list of expected diagnostics, like `[{"message": "[Prusti: verification error] ...", "range": { ... }, ...}, ...]`
22 | * A list of alternatives, each as a dictionary with an `"filter"` and a `"diagnostics"` key. The the first alternative whose filter is satisfied is used as expected diagnostics.
23 | * `"filter"` is a dictionary with a "os" key. This key is optional.
24 | * `"diagnostics"` is a list of diagnostics. This key is mandatory.
25 |
--------------------------------------------------------------------------------
/src/test/scenarios/default/settings.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/test/scenarios/latestVersion/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prusti-assistant.prustiVersion": "Latest"
3 | }
4 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/git_contracts/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "contracs"
3 | version = "0.1.0"
4 | edition = "2018"
5 |
6 | [dependencies]
7 | prusti-contracts = { git = "https://github.com/viperproject/prusti-dev.git" }
8 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/git_contracts/src/main.rs:
--------------------------------------------------------------------------------
1 | use prusti_contracts::*;
2 |
3 | #[requires(x > 123)]
4 | fn test(x: i32, unused: usize) {
5 | assert!(x > 123);
6 | }
7 |
8 | fn main() {
9 | test(1, 0);
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/git_contracts/src/main.rs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "[unused_variables] unused variable: `unused`.",
4 | "range": {
5 | "end": {
6 | "character": 22,
7 | "line": 3
8 | },
9 | "start": {
10 | "character": 16,
11 | "line": 3
12 | }
13 | },
14 | "relatedInformation": [
15 | {
16 | "location": {
17 | "range": {
18 | "end": {
19 | "character": 22,
20 | "line": 3
21 | },
22 | "start": {
23 | "character": 16,
24 | "line": 3
25 | }
26 | },
27 | "uri": "crates/git_contracts/src/main.rs"
28 | },
29 | "message": "`#[warn(unused_variables)]` on by default"
30 | },
31 | {
32 | "location": {
33 | "range": {
34 | "end": {
35 | "character": 22,
36 | "line": 3
37 | },
38 | "start": {
39 | "character": 16,
40 | "line": 3
41 | }
42 | },
43 | "uri": "crates/git_contracts/src/main.rs"
44 | },
45 | "message": "if this is intentional, prefix it with an underscore"
46 | }
47 | ],
48 | "severity": 1,
49 | "uri": "crates/git_contracts/src/main.rs"
50 | },
51 | {
52 | "message": "[Prusti: verification error] precondition might not hold.",
53 | "range": {
54 | "end": {
55 | "character": 14,
56 | "line": 8
57 | },
58 | "start": {
59 | "character": 4,
60 | "line": 8
61 | }
62 | },
63 | "relatedInformation": [
64 | {
65 | "location": {
66 | "range": {
67 | "end": {
68 | "character": 18,
69 | "line": 2
70 | },
71 | "start": {
72 | "character": 11,
73 | "line": 2
74 | }
75 | },
76 | "uri": "crates/git_contracts/src/main.rs"
77 | },
78 | "message": "the failing assertion is here"
79 | }
80 | ],
81 | "severity": 0,
82 | "uri": "crates/git_contracts/src/main.rs"
83 | }
84 | ]
85 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/latest_contracts/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "contracs"
3 | version = "0.1.0"
4 | edition = "2018"
5 |
6 | [dependencies]
7 | prusti-contracts = "*"
8 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/latest_contracts/src/main.rs:
--------------------------------------------------------------------------------
1 | use prusti_contracts::*;
2 |
3 | #[requires(x > 123)]
4 | fn test(x: i32, unused: usize) {
5 | assert!(x > 123);
6 | }
7 |
8 | fn main() {
9 | test(1, 0);
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/latest_contracts/src/main.rs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "[unused_variables] unused variable: `unused`.",
4 | "range": {
5 | "end": {
6 | "character": 22,
7 | "line": 3
8 | },
9 | "start": {
10 | "character": 16,
11 | "line": 3
12 | }
13 | },
14 | "relatedInformation": [
15 | {
16 | "location": {
17 | "range": {
18 | "end": {
19 | "character": 22,
20 | "line": 3
21 | },
22 | "start": {
23 | "character": 16,
24 | "line": 3
25 | }
26 | },
27 | "uri": "crates/latest_contracts/src/main.rs"
28 | },
29 | "message": "`#[warn(unused_variables)]` on by default"
30 | },
31 | {
32 | "location": {
33 | "range": {
34 | "end": {
35 | "character": 22,
36 | "line": 3
37 | },
38 | "start": {
39 | "character": 16,
40 | "line": 3
41 | }
42 | },
43 | "uri": "crates/latest_contracts/src/main.rs"
44 | },
45 | "message": "if this is intentional, prefix it with an underscore"
46 | }
47 | ],
48 | "severity": 1,
49 | "uri": "crates/latest_contracts/src/main.rs"
50 | },
51 | {
52 | "message": "[Prusti: verification error] precondition might not hold.",
53 | "range": {
54 | "end": {
55 | "character": 14,
56 | "line": 8
57 | },
58 | "start": {
59 | "character": 4,
60 | "line": 8
61 | }
62 | },
63 | "relatedInformation": [
64 | {
65 | "location": {
66 | "range": {
67 | "end": {
68 | "character": 18,
69 | "line": 2
70 | },
71 | "start": {
72 | "character": 11,
73 | "line": 2
74 | }
75 | },
76 | "uri": "crates/latest_contracts/src/main.rs"
77 | },
78 | "message": "the failing assertion is here"
79 | }
80 | ],
81 | "severity": 0,
82 | "uri": "crates/latest_contracts/src/main.rs"
83 | }
84 | ]
85 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/no_contracts/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "simple"
3 | version = "0.1.0"
4 | edition = "2018"
5 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/no_contracts/src/main.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | assert!(true);
3 | }
4 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/no_contracts/src/main.rs.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/workspace/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "first",
4 | "second",
5 | ]
6 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/workspace/first/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "first"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | prusti-contracts = "*"
8 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/workspace/first/src/lib.rs:
--------------------------------------------------------------------------------
1 | use prusti_contracts::*;
2 |
3 | #[requires(dividend != 0)]
4 | pub fn divide_by(divisor: usize, dividend: usize) -> usize {
5 | divisor / dividend
6 | }
7 |
8 | pub fn generate_error() {
9 | assert!(false)
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/workspace/second/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "second"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | prusti-contracts = "*"
8 | first = { path = "../first" }
9 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/workspace/second/src/main.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | let _ = first::divide_by(10, 2);
3 | }
4 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/crates/workspace/second/src/main.rs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "[Prusti: verification error] the asserted expression might not hold",
4 | "range": {
5 | "end": {
6 | "character": 18,
7 | "line": 8
8 | },
9 | "start": {
10 | "character": 4,
11 | "line": 8
12 | }
13 | },
14 | "relatedInformation": [],
15 | "severity": 0,
16 | "uri": "crates/workspace/first/src/lib.rs"
17 | }
18 | ]
19 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/assert_false.rs:
--------------------------------------------------------------------------------
1 | #[allow(dead_code)]
2 | fn main() {
3 | assert!(false);
4 | }
5 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/assert_false.rs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "[Prusti: verification error] the asserted expression might not hold",
4 | "range": {
5 | "end": {
6 | "character": 18,
7 | "line": 2
8 | },
9 | "start": {
10 | "character": 4,
11 | "line": 2
12 | }
13 | },
14 | "relatedInformation": [],
15 | "severity": 0,
16 | "uri": "programs/assert_false.rs"
17 | }
18 | ]
19 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/assert_true.rs:
--------------------------------------------------------------------------------
1 | #[allow(dead_code)]
2 | fn main() {
3 | assert!(true);
4 | }
5 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/assert_true.rs.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/empty.rs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/viperproject/prusti-assistant/f6cee5c0cb2f62f0b02752dbd37c960704460246/src/test/scenarios/shared/programs/empty.rs
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/empty.rs.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/failing_post.rs:
--------------------------------------------------------------------------------
1 | use prusti_contracts::*;
2 |
3 | #[pure]
4 | fn something_true() -> bool {
5 | true
6 | }
7 |
8 | #[allow(dead_code)]
9 | #[allow(unused_variables)]
10 | #[ensures(something_true() && false)]
11 | fn client(a: u32) {}
12 |
13 | #[allow(dead_code)]
14 | fn main() {}
15 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/failing_post.rs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "filter": {
4 | "prustiVersion": "Tag",
5 | "prustiTag": "v-2023-08-22-1715"
6 | },
7 | "diagnostics": [
8 | {
9 | "message": "[Prusti: verification error] postcondition might not hold.",
10 | "range": {
11 | "end": {
12 | "character": 35,
13 | "line": 9
14 | },
15 | "start": {
16 | "character": 10,
17 | "line": 9
18 | }
19 | },
20 | "relatedInformation": [
21 | {
22 | "location": {
23 | "range": {
24 | "end": {
25 | "character": 20,
26 | "line": 10
27 | },
28 | "start": {
29 | "character": 0,
30 | "line": 10
31 | }
32 | },
33 | "uri": "programs/failing_post.rs"
34 | },
35 | "message": "the error originates here"
36 | }
37 | ],
38 | "severity": 0,
39 | "uri": "programs/failing_post.rs"
40 | }
41 | ]
42 | },
43 | {
44 | "diagnostics": [
45 | {
46 | "message": "[Prusti: verification error] postcondition might not hold.",
47 | "range": {
48 | "end": {
49 | "character": 35,
50 | "line": 9
51 | },
52 | "start": {
53 | "character": 30,
54 | "line": 9
55 | }
56 | },
57 | "relatedInformation": [
58 | {
59 | "location": {
60 | "range": {
61 | "end": {
62 | "character": 20,
63 | "line": 10
64 | },
65 | "start": {
66 | "character": 0,
67 | "line": 10
68 | }
69 | },
70 | "uri": "programs/failing_post.rs"
71 | },
72 | "message": "the error originates here"
73 | }
74 | ],
75 | "severity": 0,
76 | "uri": "programs/failing_post.rs"
77 | }
78 | ]
79 | }
80 | ]
81 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/lib_assert_true.rs:
--------------------------------------------------------------------------------
1 | #[allow(dead_code)]
2 | fn lib() {
3 | assert!(true);
4 | }
5 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/lib_assert_true.rs.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/notes.rs:
--------------------------------------------------------------------------------
1 | #[allow(dead_code)]
2 | fn test(unused: usize) {}
3 |
--------------------------------------------------------------------------------
/src/test/scenarios/shared/programs/notes.rs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "[unused_variables] unused variable: `unused`.",
4 | "range": {
5 | "end": {
6 | "character": 14,
7 | "line": 1
8 | },
9 | "start": {
10 | "character": 8,
11 | "line": 1
12 | }
13 | },
14 | "relatedInformation": [
15 | {
16 | "location": {
17 | "range": {
18 | "end": {
19 | "character": 14,
20 | "line": 1
21 | },
22 | "start": {
23 | "character": 8,
24 | "line": 1
25 | }
26 | },
27 | "uri": "programs/notes.rs"
28 | },
29 | "message": "`#[warn(unused_variables)]` on by default"
30 | },
31 | {
32 | "location": {
33 | "range": {
34 | "end": {
35 | "character": 14,
36 | "line": 1
37 | },
38 | "start": {
39 | "character": 8,
40 | "line": 1
41 | }
42 | },
43 | "uri": "programs/notes.rs"
44 | },
45 | "message": "if this is intentional, prefix it with an underscore"
46 | }
47 | ],
48 | "severity": 1,
49 | "uri": "programs/notes.rs"
50 | }
51 | ]
52 |
--------------------------------------------------------------------------------
/src/test/scenarios/taggedVersion/crates/contracts_0.1/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "contracs"
3 | version = "0.1.0"
4 | edition = "2018"
5 |
6 | [dependencies]
7 | prusti-contracts = "0.1"
8 |
--------------------------------------------------------------------------------
/src/test/scenarios/taggedVersion/crates/contracts_0.1/src/main.rs:
--------------------------------------------------------------------------------
1 | use prusti_contracts::*;
2 |
3 | #[requires(x > 123)]
4 | fn test(x: i32, unused: usize) {
5 | assert!(x > 123);
6 | }
7 |
8 | fn main() {
9 | test(1, 0);
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/scenarios/taggedVersion/crates/contracts_0.1/src/main.rs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "[unused_variables] unused variable: `unused`.",
4 | "range": {
5 | "end": {
6 | "character": 22,
7 | "line": 3
8 | },
9 | "start": {
10 | "character": 16,
11 | "line": 3
12 | }
13 | },
14 | "relatedInformation": [
15 | {
16 | "location": {
17 | "range": {
18 | "end": {
19 | "character": 22,
20 | "line": 3
21 | },
22 | "start": {
23 | "character": 16,
24 | "line": 3
25 | }
26 | },
27 | "uri": "crates/contracts_0.1/src/main.rs"
28 | },
29 | "message": "`#[warn(unused_variables)]` on by default"
30 | },
31 | {
32 | "location": {
33 | "range": {
34 | "end": {
35 | "character": 22,
36 | "line": 3
37 | },
38 | "start": {
39 | "character": 16,
40 | "line": 3
41 | }
42 | },
43 | "uri": "crates/contracts_0.1/src/main.rs"
44 | },
45 | "message": "if this is intentional, prefix it with an underscore"
46 | }
47 | ],
48 | "severity": 1,
49 | "uri": "crates/contracts_0.1/src/main.rs"
50 | },
51 | {
52 | "message": "[Prusti: verification error] precondition might not hold.",
53 | "range": {
54 | "end": {
55 | "character": 14,
56 | "line": 8
57 | },
58 | "start": {
59 | "character": 4,
60 | "line": 8
61 | }
62 | },
63 | "relatedInformation": [
64 | {
65 | "location": {
66 | "range": {
67 | "end": {
68 | "character": 18,
69 | "line": 2
70 | },
71 | "start": {
72 | "character": 11,
73 | "line": 2
74 | }
75 | },
76 | "uri": "crates/contracts_0.1/src/main.rs"
77 | },
78 | "message": "the failing assertion is here"
79 | }
80 | ],
81 | "severity": 0,
82 | "uri": "crates/contracts_0.1/src/main.rs"
83 | }
84 | ]
85 |
--------------------------------------------------------------------------------
/src/test/scenarios/taggedVersion/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prusti-assistant.prustiVersion": "Tag",
3 | "prusti-assistant.prustiTag": "v-2023-08-22-1715"
4 | }
5 |
--------------------------------------------------------------------------------
/src/test/scenarios/vscode-version:
--------------------------------------------------------------------------------
1 | 1.87.2
2 |
--------------------------------------------------------------------------------
/src/toolbox/serverManager.ts:
--------------------------------------------------------------------------------
1 | import * as childProcess from "child_process";
2 | import * as treeKill from "tree-kill";
3 | import { StateMachine } from "./stateMachine";
4 |
5 | /**
6 | * The state of the process of a server.
7 | */
8 | enum State {
9 | /** A process that is running and has not yet been marked as ready. */
10 | Running = "Running",
11 | /** A running process that has been marked as ready. */
12 | Ready = "Ready",
13 | /** A process that never started, or that has been explicitly stopped. */
14 | Stopped = "Stopped",
15 | /** A process that terminated without being explicitly stopped. */
16 | Crashed = "Crashed",
17 | /** A process that failed to get killed. */
18 | Unrecoverable = "Unrecoverable",
19 | }
20 |
21 | export interface StartOptions {
22 | readonly cwd?: string;
23 | readonly env?: NodeJS.ProcessEnv;
24 | readonly onStdout?: (data: string) => void;
25 | readonly onStderr?: (data: string) => void;
26 | }
27 |
28 | export class ServerError extends Error {
29 | constructor(public name: string, m: string) {
30 | super(m);
31 |
32 | // Set the prototype explicitly.
33 | Object.setPrototypeOf(this, ServerError.prototype);
34 | }
35 | }
36 |
37 | export class ServerManager {
38 | private readonly name: string;
39 | private readonly state: StateMachine;
40 | private readonly log: (data: string) => void;
41 | private readonly procExitCallback: (code: unknown) => void;
42 | private proc?: childProcess.ChildProcessWithoutNullStreams;
43 |
44 | /**
45 | * Construct a new server manager.
46 | *
47 | * @param log: the function to be called to report log messages.
48 | */
49 | public constructor(name: string, log?: (data: string) => void) {
50 | this.name = name;
51 | this.log = (data) => (log ?? console.log)(`[${this.name}] ${data}`);
52 | this.state = new StateMachine(`${name} state`, State.Stopped);
53 | this.procExitCallback = (code: unknown) => {
54 | this.log(`Server process unexpected terminated with exit code ${code}`);
55 | this.proc = undefined;
56 | this.setState(State.Crashed);
57 | };
58 | }
59 |
60 | /**
61 | * Return whether the current server state is `state`.
62 | */
63 | private isState(state: State): boolean {
64 | const currentState = State[this.state.getState() as keyof typeof State];
65 | return currentState === state;
66 | }
67 |
68 | /**
69 | * Set a new state of the server.
70 | */
71 | private setState(newState: State): void {
72 | this.log(`Mark server as "${newState}".`);
73 |
74 | // Check an internal invariant.
75 | switch (newState) {
76 | case State.Ready:
77 | case State.Running:
78 | case State.Unrecoverable:
79 | if (this.proc === undefined) {
80 | throw new ServerError(
81 | this.name,
82 | `State will become ${newState}, but proc is undefined.`
83 | );
84 | }
85 | break;
86 | case State.Stopped:
87 | case State.Crashed:
88 | if (this.proc !== undefined) {
89 | throw new ServerError(
90 | this.name,
91 | `State will become ${newState}, but proc is defined.`
92 | );
93 | }
94 | }
95 |
96 | // Check that we are not leaving Unrecoverable.
97 | if (
98 | this.state.getState() === State.Unrecoverable
99 | && newState !== State.Unrecoverable
100 | ) {
101 | throw new ServerError(
102 | this.name,
103 | `State cannot change from ${this.state.getState()} ` +
104 | `to ${newState}.`
105 | );
106 | }
107 |
108 | this.state.setState(State[newState]);
109 | }
110 |
111 | /**
112 | * Start the server process, stopping any previously running process.
113 | *
114 | * After this call the server is *not* guaranteed to be `Running`. Use
115 | * `waitForRunning` for that.
116 | */
117 | public initiateStart(
118 | command: string,
119 | args?: readonly string[] | undefined,
120 | options?: StartOptions | undefined
121 | ): void {
122 | if (this.isState(State.Running) || this.isState(State.Ready)) {
123 | this.initiateStop();
124 | }
125 |
126 | this.waitForStopped().then(() => {
127 | // Start the process
128 | this.log(`Start "${command} ${args?.join(" ") ?? ""}"`);
129 | const proc = childProcess.spawn(
130 | command,
131 | args,
132 | { cwd: options?.cwd, env: options?.env }
133 | );
134 |
135 | if (options?.onStdout) {
136 | const onStdout = options.onStdout;
137 | proc.stdout.on("data", onStdout);
138 | }
139 | if (options?.onStderr) {
140 | const onStderr = options.onStderr;
141 | proc.stderr.on("data", onStderr);
142 | }
143 |
144 | proc.on("error", (err) => {
145 | this.log(`Server process error: ${err}`);
146 | });
147 |
148 | proc.on("exit", this.procExitCallback);
149 |
150 | this.proc = proc;
151 | this.setState(State.Running);
152 | }, err => {
153 | this.log(`Error while waiting for the server to stop: ${err}`);
154 | });
155 | }
156 |
157 | /**
158 | * Stop the server process.
159 | *
160 | * After this call the server is *not* guaranteed to be `Stopped`. Use
161 | * `waitForStopped` for that.
162 | */
163 | public initiateStop(): void {
164 | if (this.isState(State.Running) || this.isState(State.Ready)) {
165 | this.log(`Kill server process ${this.proc?.pid}.`);
166 | const proc = this.proc as childProcess.ChildProcessWithoutNullStreams;
167 | proc.removeListener("exit", this.procExitCallback);
168 | if (proc.pid === undefined) {
169 | this.log("The process id is undefined.");
170 | return;
171 | }
172 | treeKill(proc.pid, "SIGKILL", (err) => {
173 | if (err) {
174 | this.log(`Failed to kill process tree of ${proc.pid}: ${err}`);
175 | const succeeded = proc.kill("SIGKILL");
176 | if (!succeeded) {
177 | this.log(`Failed to kill process ${proc.pid}.`);
178 | }
179 | this.log("This is an unrecorevable error.");
180 | this.setState(State.Unrecoverable);
181 | } else {
182 | // Success
183 | this.proc = undefined;
184 | this.setState(State.Stopped);
185 | }
186 | });
187 | } else {
188 | this.proc = undefined;
189 | this.setState(State.Stopped);
190 | }
191 | }
192 |
193 | /**
194 | * Mark the server as `Ready`, if it is not `Stopped` or `Crashed`.
195 | *
196 | * After this call the server will be `Ready`, unless a `waitForReady`
197 | * promise modified the state in the meantime.
198 | */
199 | public setReady(): void {
200 | if (this.isState(State.Stopped) || this.isState(State.Crashed)) {
201 | return;
202 | }
203 | this.setState(State.Ready);
204 | }
205 |
206 | /**
207 | * Return a promise that will resolve when the server becomes `Running`.
208 | * Only one promise - the last one - is allowed to modify the server state.
209 | */
210 | public waitForRunning(): Promise {
211 | return this.state.waitForState(State[State.Running]);
212 | }
213 |
214 | /**
215 | * Return a promise that will resolve when the server becomes `Ready`.
216 | * Only one promise - the last one - is allowed to modify the server state.
217 | */
218 | public waitForReady(): Promise {
219 | return this.state.waitForState(State[State.Ready]);
220 | }
221 |
222 | /**
223 | * Return a promise that will resolve when the server becomes `Stopped`.
224 | * Only one promise - the last one - is allowed to modify the server state.
225 | */
226 | public waitForStopped(): Promise {
227 | return this.state.waitForState(State[State.Stopped]);
228 | }
229 |
230 | /**
231 | * Return a promise that will resolve when the server becomes `Crashed`.
232 | * Only one promise - the last one - is allowed to modify the server state.
233 | */
234 | public waitForCrashed(): Promise {
235 | return this.state.waitForState(State[State.Crashed]);
236 | }
237 |
238 | /**
239 | * Return a promise that will resolve when the server becomes
240 | * `Unrecoverable`.
241 | */
242 | public waitForUnrecoverable(): Promise {
243 | return this.state.waitForState(State[State.Unrecoverable]);
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/src/toolbox/stateMachine.ts:
--------------------------------------------------------------------------------
1 |
2 | export class StateMachineError extends Error {
3 | constructor(public name: string, m: string) {
4 | super(m);
5 |
6 | // Set the prototype explicitly.
7 | Object.setPrototypeOf(this, StateMachineError.prototype);
8 | }
9 | }
10 |
11 | type ResolveReject = { resolve: () => void, reject: (err: Error) => void };
12 |
13 | export class StateMachine {
14 | private readonly name: string;
15 | private currentState: State;
16 | private waitingForState: Map = new Map();
17 |
18 | /**
19 | * Construct a new state machine.
20 | */
21 | public constructor(
22 | name: string,
23 | initialState: State,
24 | ) {
25 | this.name = name;
26 | this.currentState = initialState;
27 | }
28 |
29 | /**
30 | * Return the current state.
31 | */
32 | public getState(): State {
33 | return this.currentState;
34 | }
35 |
36 | /*
37 | * Get a value of `waitingForState`, inserting an empty array if the key doesn't exist.
38 | */
39 | private getWaitingForState(state: State): ResolveReject[] {
40 | let callbacks = this.waitingForState.get(state);
41 | if (callbacks === undefined) {
42 | callbacks = [];
43 | this.waitingForState.set(state, callbacks);
44 | }
45 | return callbacks;
46 | }
47 |
48 | /**
49 | * Set a new state.
50 | */
51 | public setState(newState: State): void {
52 | this.currentState = newState;
53 |
54 | const callbacks: ResolveReject[] = this.getWaitingForState(newState);
55 |
56 | let badCallback = undefined;
57 | while (callbacks.length) {
58 | const { resolve, reject } = callbacks.shift() as ResolveReject;
59 |
60 | if (badCallback === undefined) {
61 | resolve();
62 | } else {
63 | reject(new StateMachineError(
64 | this.name,
65 | `After the state become "${newState}" the promise ` +
66 | `resolution of (1) modified the state to ` +
67 | `"${this.currentState}" before the promise resolution ` +
68 | `of (2) - waiting for the state to become "${newState}" ` +
69 | `- could run.\n(1): ${badCallback}\n(2): ${resolve}`
70 | ));
71 | }
72 |
73 | if (this.currentState !== newState) {
74 | badCallback = resolve;
75 | }
76 | }
77 | }
78 |
79 | /**
80 | * Return a promise that will resolve when the state becomes `targetState`.
81 | * Only one promise - the last one - is allowed to modify the state.
82 | * If a promise modifies the state any further promise will be rejected.
83 | */
84 | public waitForState(targetState: State): Promise {
85 | return new Promise((resolve, reject) => {
86 | if (this.currentState === targetState) {
87 | resolve();
88 | } else {
89 | this.getWaitingForState(targetState).push({ resolve, reject });
90 | }
91 | });
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | import * as childProcess from "child_process";
2 | import * as vscode from "vscode";
3 | import * as fs from "fs";
4 | import * as path from "path";
5 | import * as treeKill from "tree-kill";
6 |
7 | export function userInfo(message: string, statusBar?: vscode.StatusBarItem): void {
8 | log(message);
9 | if (statusBar) {
10 | statusBar.text = message;
11 | }
12 | vscode.window.showInformationMessage(message).then(
13 | undefined,
14 | err => log(`Error: ${err}`)
15 | );
16 | }
17 |
18 | export function userWarn(message: string, statusBar?: vscode.StatusBarItem): void {
19 | log(message);
20 | if (statusBar) {
21 | statusBar.text = message;
22 | }
23 | vscode.window.showWarningMessage(message).then(
24 | undefined,
25 | err => log(`Error: ${err}`)
26 | );
27 | }
28 |
29 | export function userError(message: string, restart = false, statusBar?: vscode.StatusBarItem): void {
30 | log(message);
31 | if (statusBar) {
32 | statusBar.text = message;
33 | }
34 | if (restart) {
35 | userErrorPopup(
36 | message,
37 | "Restart Now",
38 | () => {
39 | vscode.commands.executeCommand("workbench.action.reloadWindow")
40 | .then(undefined, err => log(`Error: ${err}`));
41 | },
42 | statusBar
43 | );
44 | } else {
45 | vscode.window.showErrorMessage(message)
46 | .then(undefined, err => log(`Error: ${err}`));
47 | }
48 | }
49 |
50 | export function userErrorPopup(message: string, actionLabel: string, action: () => void, statusBar?: vscode.StatusBarItem): void {
51 | log(message);
52 | if (statusBar) {
53 | statusBar.text = message;
54 | }
55 | vscode.window.showErrorMessage(message, actionLabel)
56 | .then(selection => {
57 | if (selection === actionLabel) {
58 | action();
59 | }
60 | })
61 | .then(undefined, err => log(`Error: ${err}`));
62 | }
63 |
64 | export function userInfoPopup(message: string, actionLabel: string, action: () => void, statusBar?: vscode.StatusBarItem): void {
65 | log(message);
66 | if (statusBar) {
67 | statusBar.text = message;
68 | }
69 | vscode.window.showInformationMessage(message, actionLabel)
70 | .then(selection => {
71 | if (selection === actionLabel) {
72 | action();
73 | }
74 | })
75 | .then(undefined, err => log(`Error: ${err}`));
76 | }
77 |
78 | const logChannel = vscode.window.createOutputChannel("Prusti Assistant");
79 | export function log(message: string): void {
80 | console.log(message);
81 | logChannel.appendLine(message);
82 | }
83 | export function showLogs(): void {
84 | logChannel.show();
85 | }
86 |
87 | export type Duration = [seconds: number, nanoseconds: number];
88 | export type KillFunction = () => void;
89 |
90 | export interface Output {
91 | stdout: string;
92 | stderr: string;
93 | code: number | null;
94 | signal: string | null;
95 | duration: Duration;
96 | }
97 |
98 | export function spawn(
99 | cmd: string,
100 | args?: string[] | undefined,
101 | { options, onStdout, onStderr }: {
102 | options?: childProcess.SpawnOptionsWithoutStdio;
103 | onStdout?: ((data: string) => void);
104 | onStderr?: ((data: string) => void);
105 | } = {},
106 | destructors?: Set,
107 | ): Promise