diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c1844208..7ab13b9f 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -32,7 +32,7 @@ matchManagers: [ 'custom.regex', ], - matchPackageNames: [ + matchDepNames: [ 'STABLE', ], extractVersion: '^(?\\d+\\.\\d+)', // Drop the patch version diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 35b3da84..2da233df 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -47,7 +47,7 @@ jobs: - bans licenses sources steps: - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check ${{ matrix.checks }} rust-version: stable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89809f55..aa6d8bbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: permissions: contents: none name: CI - needs: [test, msrv, lockfile, docs, rustfmt, clippy] + needs: [test, msrv, lockfile, docs, rustfmt, clippy, minimal-versions] runs-on: ubuntu-latest if: "always()" steps: @@ -34,7 +34,7 @@ jobs: name: Test strategy: matrix: - os: ["ubuntu-latest", "windows-latest", "macos-14"] + os: ["ubuntu-latest", "windows-latest", "macos-latest"] rust: ["stable"] continue-on-error: ${{ matrix.rust != 'stable' }} runs-on: ${{ matrix.os }} @@ -64,7 +64,25 @@ jobs: - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@cargo-hack - name: Default features - run: cargo hack check --feature-powerset --locked --rust-version --ignore-private --workspace --all-targets + run: cargo hack check --feature-powerset --locked --rust-version --ignore-private --workspace --lib --bins + minimal-versions: + name: Minimal versions + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install stable Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Install nightly Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + - name: Downgrade dependencies to minimal versions + run: cargo +nightly generate-lockfile -Z minimal-versions + - name: Compile with minimal versions + run: cargo +stable check --workspace --all-features --locked lockfile: runs-on: ubuntu-latest steps: @@ -86,7 +104,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: "1.80" # STABLE + toolchain: "1.86" # STABLE - uses: Swatinem/rust-cache@v2 - name: Check documentation env: @@ -101,7 +119,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: "1.80" # STABLE + toolchain: "1.86" # STABLE components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Check formatting @@ -117,7 +135,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: "1.80" # STABLE + toolchain: "1.86" # STABLE components: clippy - uses: Swatinem/rust-cache@v2 - name: Install SARIF tools @@ -126,7 +144,7 @@ jobs: run: cargo install sarif-fmt --locked - name: Check run: > - cargo clippy --workspace --all-features --all-targets --message-format=json -- -D warnings --allow deprecated + cargo clippy --workspace --all-features --all-targets --message-format=json | clippy-sarif | tee clippy-results.sarif | sarif-fmt @@ -138,3 +156,22 @@ jobs: wait-for-processing: true - name: Report status run: cargo clippy --workspace --all-features --all-targets -- -D warnings --allow deprecated + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@v2 + - name: Install cargo-tarpaulin + run: cargo install cargo-tarpaulin + - name: Gather coverage + run: cargo tarpaulin --output-dir coverage --out lcov --timeout 120 + - name: Publish to Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 3d04055c..491030a1 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -24,4 +24,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.x' - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/rust-next.yml b/.github/workflows/rust-next.yml index ab499633..e98386c4 100644 --- a/.github/workflows/rust-next.yml +++ b/.github/workflows/rust-next.yml @@ -21,7 +21,7 @@ jobs: name: Test strategy: matrix: - os: ["ubuntu-latest", "windows-latest", "macos-latest", "macos-14"] + os: ["ubuntu-latest", "windows-latest", "macos-latest"] rust: ["stable", "beta"] include: - os: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7d2d22..ed078cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +## [0.11.5] - 2024-12-09 + +### Added + +- `rustc`'s multiline annotation special case [#133](https://github.com/rust-lang/annotate-snippets-rs/pull/133) + - This special case happens when: + - The start of a multiline annotation is at the start of the line disregarding any leading whitespace + - No other multiline annotations overlap it +- `simd` feature for faster folding [#146](https://github.com/rust-lang/annotate-snippets-rs/pull/146) + +### Changed + +- Multiline annotations with matching spans get merged [#133](https://github.com/rust-lang/annotate-snippets-rs/pull/133) +- Multiple annotations on one line are no longer rendered on separate lines [#133](https://github.com/rust-lang/annotate-snippets-rs/pull/133) + +### Fixed + +- Overlapping multiline annotations are now correctly rendered [#133](https://github.com/rust-lang/annotate-snippets-rs/pull/133) +- Origin position is now correctly calculated when an annotation starts at the beginning of the line [#154](https://github.com/rust-lang/annotate-snippets-rs/pull/154) + ## [0.11.4] - 2024-06-15 ### Fixes @@ -144,7 +164,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Update the syntax to Rust 2018 idioms. (#4) -[Unreleased]: https://github.com/rust-lang/annotate-snippets-rs/compare/0.11.4...HEAD +[Unreleased]: https://github.com/rust-lang/annotate-snippets-rs/compare/0.11.5...HEAD +[0.11.5]: https://github.com/rust-lang/annotate-snippets-rs/compare/0.11.4...0.11.5 [0.11.4]: https://github.com/rust-lang/annotate-snippets-rs/compare/0.11.3...0.11.4 [0.11.3]: https://github.com/rust-lang/annotate-snippets-rs/compare/0.11.2...0.11.3 [0.11.2]: https://github.com/rust-lang/annotate-snippets-rs/compare/0.11.1...0.11.2 diff --git a/Cargo.lock b/Cargo.lock index a686b028..532bb57a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,62 +2,29 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "annotate-snippets" -version = "0.11.4" +version = "0.11.5" dependencies = [ - "anstream 0.6.15", + "annotate-snippets", + "anstream", "anstyle", - "criterion", - "difference", - "glob", - "serde", + "divan", + "memchr", "snapbox", - "toml", - "tryfn", - "unicode-width", -] - -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon 1.0.2", - "colorchoice", - "is-terminal", - "utf8parse", + "unicode-width 0.2.0", ] [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", - "anstyle-wincon 3.0.3", + "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", @@ -102,60 +69,28 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbf0bf947d663010f0b4132f28ca08da9151f3b9035fa7578a38de521c1d1aa" dependencies = [ - "anstream 0.6.15", + "anstream", "anstyle", "anstyle-lossy", "html-escape", - "unicode-width", + "unicode-width 0.1.13", ] [[package]] name = "anstyle-wincon" -version = "1.0.2" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] -name = "anstyle-wincon" -version = "3.0.3" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" -dependencies = [ - "anstyle", - "windows-sys 0.52.0", -] - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "bstr" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cfg-if" @@ -163,33 +98,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "clap" version = "4.3.24" @@ -197,8 +105,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", - "clap_derive", - "once_cell", ] [[package]] @@ -207,22 +113,9 @@ version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ - "anstream 0.3.2", "anstyle", "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", + "terminal_size", ] [[package]] @@ -238,98 +131,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" +name = "condtype" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools", -] +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" [[package]] -name = "crossbeam-deque" -version = "0.8.5" +name = "divan" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", + "cfg-if", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.18" +name = "divan-macros" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" dependencies = [ - "crossbeam-utils", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "difference" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" - -[[package]] -name = "either" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" - -[[package]] -name = "escape8259" -version = "0.5.2" +name = "errno" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4911e3666fcd7826997b4745c8224295a6f3072f1418c3067b97a67557ee" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "rustversion", + "libc", + "windows-sys 0.52.0", ] [[package]] name = "escargot" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eb5f6eeda986377996e9ed570cbc20cc16d30440696f82f129c863e4e3e83" +checksum = "05a3ac187a16b5382fef8c69fd1bad123c67b7cf3932240a2d43dcdd32cded88" dependencies = [ "log", "once_cell", @@ -337,40 +183,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "globset" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "half" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" -dependencies = [ - "crunchy", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "hermit-abi" version = "0.3.9" @@ -387,20 +199,14 @@ dependencies = [ ] [[package]] -name = "ignore" -version = "0.4.20" +name = "io-lifetimes" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", + "hermit-abi", + "libc", + "windows-sys 0.48.0", ] [[package]] @@ -423,36 +229,12 @@ dependencies = [ "is-terminal", ] -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.155" @@ -460,16 +242,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] -name = "libtest-mimic" -version = "0.7.3" +name = "linux-raw-sys" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58" -dependencies = [ - "clap", - "escape8259", - "termcolor", - "threadpool", -] +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "log" @@ -479,9 +255,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "normalize-line-endings" @@ -489,37 +265,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "oorandom" -version = "11.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" - [[package]] name = "os_pipe" version = "1.2.0" @@ -530,34 +281,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "plotters" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" - -[[package]] -name = "plotters-svg" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" -dependencies = [ - "plotters-backend", -] - [[package]] name = "proc-macro2" version = "1.0.85" @@ -577,89 +300,45 @@ dependencies = [ ] [[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "regex" -version = "1.10.4" +name = "regex-lite" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] -name = "regex-automata" -version = "0.4.6" +name = "rustix" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", ] -[[package]] -name = "regex-syntax" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" - -[[package]] -name = "rustversion" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "serde" -version = "1.0.204" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -685,11 +364,11 @@ checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "snapbox" -version = "0.6.16" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027c936207f85d10d015e21faf5c676c7e08c453ed371adf55c0874c443ca77a" +checksum = "96dcfc4581e3355d70ac2ee14cfdf81dce3d85c85f1ed9e2c1d3013f53b3436b" dependencies = [ - "anstream 0.6.15", + "anstream", "anstyle", "anstyle-svg", "escargot", @@ -700,7 +379,7 @@ dependencies = [ "similar", "snapbox-macros", "wait-timeout", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -709,20 +388,14 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af" dependencies = [ - "anstream 0.6.15", + "anstream", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "syn" -version = "2.0.66" +version = "2.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" dependencies = [ "proc-macro2", "quote", @@ -730,61 +403,13 @@ dependencies = [ ] [[package]] -name = "termcolor" -version = "1.4.1" +name = "terminal_size" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ - "winapi-util", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "tryfn" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe242ee9e646acec9ab73a5c540e8543ed1b107f0ce42be831e0775d423c396" -dependencies = [ - "ignore", - "libtest-mimic", - "snapbox", + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -799,6 +424,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8-width" version = "0.1.7" @@ -820,89 +451,6 @@ dependencies = [ "libc", ] -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "web-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi-util" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -918,7 +466,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -938,18 +495,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -960,9 +517,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -972,9 +529,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -984,15 +541,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1002,9 +559,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1014,9 +571,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1026,9 +583,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1038,6 +595,6 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 62ab3871..84345673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,55 +1,23 @@ -[package] -name = "annotate-snippets" -version = "0.11.4" -edition = "2021" -rust-version = "1.65" # MSRV -authors = ["Zibi Braniecki "] -description = "Library for building code annotations" -license = "Apache-2.0/MIT" -repository = "https://github.com/rust-lang/annotate-snippets-rs" -readme = "README.md" -keywords = ["code", "analysis", "ascii", "errors", "debug"] +[workspace] +resolver = "2" -[package.metadata.release] -tag-name = "{{version}}" -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, - {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/rust-lang/annotate-snippets-rs/compare/{{tag_name}}...HEAD", exactly=1}, +[workspace.package] +repository = "https://github.com/rust-lang/annotate-snippets-rs" +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.66.0" # MSRV +include = [ + "build.rs", + "src/**/*", + "Cargo.toml", + "Cargo.lock", + "LICENSE*", + "README.md", + "benches/**/*", + "examples/**/*" ] -[badges] -maintenance = { status = "actively-developed" } - -[dependencies] -anstyle = "1.0.4" -unicode-width = "0.1.11" - -[dev-dependencies] -anstream = "0.6.13" -criterion = "0.5.1" -difference = "2.0.0" -glob = "0.3.1" -serde = { version = "1.0.199", features = ["derive"] } -snapbox = { version = "0.6.0", features = ["diff", "term-svg", "cmd", "examples"] } -toml = "0.5.11" -tryfn = "0.2.1" - -[[bench]] -name = "simple" -harness = false - -[[test]] -name = "fixtures" -harness = false - -[features] -default = [] -testing-colors = [] - -[lints.rust] +[workspace.lints.rust] rust_2018_idioms = { level = "warn", priority = -1 } unreachable_pub = "warn" unsafe_op_in_unsafe_fn = "warn" @@ -57,7 +25,7 @@ unused_lifetimes = "warn" unused_macro_rules = "warn" unused_qualifications = "warn" -[lints.clippy] +[workspace.lints.clippy] bool_assert_comparison = "allow" branches_sharing_code = "allow" checked_conversions = "warn" @@ -102,6 +70,7 @@ rc_mutex = "warn" redundant_feature_names = "warn" ref_option_ref = "warn" rest_pat_in_fully_bound_structs = "warn" +result_large_err = "allow" same_functions_in_if_condition = "warn" self_named_module_files = "warn" semicolon_if_nothing_returned = "warn" @@ -112,6 +81,59 @@ string_lit_as_bytes = "warn" string_to_string = "warn" todo = "warn" trait_duplication_in_bounds = "warn" +uninlined_format_args = "warn" verbose_file_reads = "warn" wildcard_imports = "warn" zero_sized_map_values = "warn" + +[package] +name = "annotate-snippets" +version = "0.11.5" +description = "Library for building code annotations" +categories = [] +keywords = ["code", "analysis", "ascii", "errors", "debug"] +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +include.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] + +[package.metadata.release] +tag-name = "{{version}}" +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, + {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, + {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, + {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/rust-lang/annotate-snippets-rs/compare/{{tag_name}}...HEAD", exactly=1}, +] + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] +anstyle = "1.0.4" +memchr = { version = "2.7.4", optional = true } +unicode-width = "0.2.0" + +[dev-dependencies] +annotate-snippets = { path = ".", features = ["testing-colors"] } +anstream = "0.6.13" +divan = "0.1.14" +snapbox = { version = "0.6.0", features = ["diff", "term-svg", "cmd", "examples"] } + +[[bench]] +name = "bench" +harness = false + +[features] +default = [] +simd = ["memchr"] +testing-colors = [] + +[lints] +workspace = true diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 261eeb9e..8f71f43f 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,3 +199,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + diff --git a/LICENSE-MIT b/LICENSE-MIT index 5655fa31..a2d01088 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,11 +1,11 @@ -Copyright 2017 Mozilla +Copyright (c) Individual contributors -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. diff --git a/benches/bench.rs b/benches/bench.rs new file mode 100644 index 00000000..c3799fbd --- /dev/null +++ b/benches/bench.rs @@ -0,0 +1,93 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +#[divan::bench] +fn simple() -> String { + let source = r#") -> Option { + for ann in annotations { + match (ann.range.0, ann.range.1) { + (None, None) => continue, + (Some(start), Some(end)) if start > end_index => continue, + (Some(start), Some(end)) if start >= start_index => { + let label = if let Some(ref label) = ann.label { + format!(" {}", label) + } else { + String::from("") + }; + + return Some(format!( + "{}{}{}", + " ".repeat(start - start_index), + "^".repeat(end - start), + label + )); + } + _ => continue, + } + }"#; + let message = Level::ERROR.header("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(source) + .line_start(51) + .origin("src/format.rs") + .annotation( + AnnotationKind::Context + .span(5..19) + .label("expected `Option` because of return type"), + ) + .annotation( + AnnotationKind::Primary + .span(26..724) + .label("expected enum `std::option::Option`"), + ), + ), + ); + + let renderer = Renderer::plain(); + let rendered = renderer.render(message); + rendered +} + +#[divan::bench(args=[0, 1, 10, 100, 1_000, 10_000, 100_000])] +fn fold(bencher: divan::Bencher<'_, '_>, context: usize) { + bencher + .with_inputs(|| { + let line = "012345678901234567890123456789"; + let mut input = String::new(); + for _ in 1..=context { + input.push_str(line); + input.push('\n'); + } + let span_start = input.len() + line.len(); + let span = span_start..span_start; + + input.push_str(line); + input.push('\n'); + for _ in 1..=context { + input.push_str(line); + input.push('\n'); + } + (input, span) + }) + .bench_values(|(input, span)| { + let message = Level::ERROR.header("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(&input) + .fold(true) + .origin("src/format.rs") + .annotation( + AnnotationKind::Context + .span(span) + .label("expected `Option` because of return type"), + ), + ), + ); + + let renderer = Renderer::plain(); + let rendered = renderer.render(message); + rendered + }); +} + +fn main() { + divan::main(); +} diff --git a/benches/simple.rs b/benches/simple.rs deleted file mode 100644 index 723793ea..00000000 --- a/benches/simple.rs +++ /dev/null @@ -1,58 +0,0 @@ -#![allow(clippy::unit_arg)] -#[macro_use] -extern crate criterion; - -use criterion::{black_box, Criterion}; - -use annotate_snippets::{Level, Renderer, Snippet}; - -fn create_snippet(renderer: Renderer) { - let source = r#") -> Option { - for ann in annotations { - match (ann.range.0, ann.range.1) { - (None, None) => continue, - (Some(start), Some(end)) if start > end_index => continue, - (Some(start), Some(end)) if start >= start_index => { - let label = if let Some(ref label) = ann.label { - format!(" {}", label) - } else { - String::from("") - }; - - return Some(format!( - "{}{}{}", - " ".repeat(start - start_index), - "^".repeat(end - start), - label - )); - } - _ => continue, - } - }"#; - let message = Level::Error.title("mismatched types").id("E0308").snippet( - Snippet::source(source) - .line_start(51) - .origin("src/format.rs") - .annotation( - Level::Warning - .span(5..19) - .label("expected `Option` because of return type"), - ) - .annotation( - Level::Error - .span(26..724) - .label("expected enum `std::option::Option`"), - ), - ); - - let _result = renderer.render(message).to_string(); -} - -pub fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("format", |b| { - b.iter(|| black_box(create_snippet(Renderer::plain()))); - }); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/examples/custom_error.rs b/examples/custom_error.rs new file mode 100644 index 00000000..b9e27b31 --- /dev/null +++ b/examples/custom_error.rs @@ -0,0 +1,37 @@ +use annotate_snippets::renderer::OutputTheme; +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +fn main() { + let source = r#"//@ compile-flags: -Ztreat-err-as-bug +//@ failure-status: 101 +//@ error-pattern: aborting due to `-Z treat-err-as-bug=1` +//@ error-pattern: [eval_static_initializer] evaluating initializer of static `C` +//@ normalize-stderr: "note: .*\n\n" -> "" +//@ normalize-stderr: "thread 'rustc' panicked.*:\n.*\n" -> "" +//@ rustc-env:RUST_BACKTRACE=0 + +#![crate_type = "rlib"] + +pub static C: u32 = 0 - 1; +//~^ ERROR could not evaluate static initializer +"#; + let message = Level::ERROR + .text(Some("error: internal compiler error")) + .header("could not evaluate static initializer") + .id("E0080") + .group( + Group::new().element( + Snippet::source(source) + .origin("$DIR/err.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(386..391) + .label("attempt to compute `0_u32 - 1_u32`, which would overflow"), + ), + ), + ); + + let renderer = Renderer::styled().theme(OutputTheme::Unicode); + anstream::println!("{}", renderer.render(message)); +} diff --git a/examples/custom_error.svg b/examples/custom_error.svg new file mode 100644 index 00000000..af3611a9 --- /dev/null +++ b/examples/custom_error.svg @@ -0,0 +1,36 @@ + + + + + + + error: internal compiler error[E0080]: could not evaluate static initializer + + ╭▸ $DIR/err.rs:11:21 + + + + 11 pub static C: u32 = 0 - 1; + + ╰╴ ━━━━━ attempt to compute `0_u32 - 1_u32`, which would overflow + + + + + + diff --git a/examples/custom_level.rs b/examples/custom_level.rs new file mode 100644 index 00000000..b2af361a --- /dev/null +++ b/examples/custom_level.rs @@ -0,0 +1,71 @@ +use annotate_snippets::renderer::OutputTheme; +use annotate_snippets::{AnnotationKind, Group, Level, Patch, Renderer, Snippet}; + +fn main() { + let source = r#"// Regression test for issue #114529 +// Tests that we do not ICE during const eval for a +// break-with-value in contexts where it is illegal + +#[allow(while_true)] +fn main() { + [(); { + while true { + break 9; //~ ERROR `break` with value from a `while` loop + }; + 51 + }]; + + [(); { + while let Some(v) = Some(9) { + break v; //~ ERROR `break` with value from a `while` loop + }; + 51 + }]; + + while true { + break (|| { //~ ERROR `break` with value from a `while` loop + let local = 9; + }); + } +} +"#; + let message = Level::ERROR + .header("`break` with value from a `while` loop") + .id("E0571") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/issue-114529-illegal-break-with-value.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(483..581) + .label("can only break with a value inside `loop` or breakable block"), + ) + .annotation( + AnnotationKind::Context + .span(462..472) + .label("you can't `break` with a value in a `while` loop"), + ), + ), + ) + .group( + Group::new() + .element( + Level::HELP + .text(Some("suggestion")) + .title("use `break` on its own without a value inside this `while` loop"), + ) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/issue-114529-illegal-break-with-value.rs") + .fold(true) + .patch(Patch::new(483..581, "break")), + ), + ); + + let renderer = Renderer::styled().theme(OutputTheme::Unicode); + anstream::println!("{}", renderer.render(message)); +} diff --git a/examples/custom_level.svg b/examples/custom_level.svg new file mode 100644 index 00000000..eebff280 --- /dev/null +++ b/examples/custom_level.svg @@ -0,0 +1,62 @@ + + + + + + + error[E0571]: `break` with value from a `while` loop + + ╭▸ $DIR/issue-114529-illegal-break-with-value.rs:22:9 + + + + 21 while true { + + ────────── you can't `break` with a value in a `while` loop + + 22 break (|| { //~ ERROR `break` with value from a `while` loop + + 23 let local = 9; + + 24 }); + + ┗━━━━━━━━━━┛ can only break with a value inside `loop` or breakable block + + ╰╴ + + suggestion: use `break` on its own without a value inside this `while` loop + + ╭╴ + + 22 - break (|| { //~ ERROR `break` with value from a `while` loop + + 23 - let local = 9; + + 24 - }); + + 22 + break; + + ╰╴ + + + + + + diff --git a/examples/expected_type.rs b/examples/expected_type.rs index 0184deeb..02abdecf 100644 --- a/examples/expected_type.rs +++ b/examples/expected_type.rs @@ -1,22 +1,27 @@ -use annotate_snippets::{Level, Renderer, Snippet}; +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; fn main() { let source = r#" annotations: vec![SourceAnnotation { label: "expected struct `annotate_snippets::snippet::Slice`, found reference" , range: <22, 25>,"#; - let message = Level::Error.title("expected type, found `22`").snippet( - Snippet::source(source) - .line_start(26) - .origin("examples/footer.rs") - .fold(true) - .annotation( - Level::Error - .span(193..195) - .label("expected struct `annotate_snippets::snippet::Slice`, found reference"), - ) - .annotation(Level::Info.span(34..50).label("while parsing this struct")), - ); + let message = + Level::ERROR.header("expected type, found `22`").group( + Group::new().element( + Snippet::source(source) + .line_start(26) + .origin("examples/footer.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(193..195).label( + "expected struct `annotate_snippets::snippet::Slice`, found reference", + )) + .annotation( + AnnotationKind::Context + .span(34..50) + .label("while parsing this struct"), + ), + ), + ); let renderer = Renderer::styled(); anstream::println!("{}", renderer.render(message)); diff --git a/examples/expected_type.svg b/examples/expected_type.svg index ed19ef38..7c1b073d 100644 --- a/examples/expected_type.svg +++ b/examples/expected_type.svg @@ -1,4 +1,4 @@ - +
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn text(self, text: Option<&'a str>) -> Level<'a> { + Level { + name: Some(text), + level: self.level, + } + } +} + +impl<'a> Level<'a> { + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn header(self, header: &'a str) -> Message<'a> { + Message { + id: None, + groups: vec![Group::new().element(Element::Title(Title { + level: self, + title: header, + primary: true, + }))], + } + } + + ///
+ /// + /// Text passed to this function is allowed to be pre-styled, as such all + /// text is considered "trusted input" and has no normalizations applied to + /// it. [`normalize_untrusted_str`](crate::normalize_untrusted_str) can be + /// used to normalize untrusted text before it is passed to this function. + /// + ///
+ pub fn title(self, title: &'a str) -> Title<'a> { + Title { + level: self, + title, + primary: false, + } + } + + pub(crate) fn as_str(&self) -> &'a str { + match (self.name, self.level) { + (Some(Some(name)), _) => name, + (Some(None), _) => "", + (None, LevelInner::Error) => ERROR_TXT, + (None, LevelInner::Warning) => WARNING_TXT, + (None, LevelInner::Info) => INFO_TXT, + (None, LevelInner::Note) => NOTE_TXT, + (None, LevelInner::Help) => HELP_TXT, + } + } + + pub(crate) fn style(&self, stylesheet: &Stylesheet) -> Style { + self.level.style(stylesheet) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum LevelInner { + Error, + Warning, + Info, + Note, + Help, +} + +impl LevelInner { + pub(crate) fn style(self, stylesheet: &Stylesheet) -> Style { + match self { + LevelInner::Error => stylesheet.error, + LevelInner::Warning => stylesheet.warning, + LevelInner::Info => stylesheet.info, + LevelInner::Note => stylesheet.note, + LevelInner::Help => stylesheet.help, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index ed9b3f84..76836d0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,9 +42,19 @@ #![warn(clippy::print_stdout)] #![warn(missing_debug_implementations)] +pub mod level; pub mod renderer; mod snippet; +/// Normalize the string to avoid any unicode control characters. +/// This is important for untrusted input, as it can contain +/// invalid unicode sequences. +pub fn normalize_untrusted_str(s: &str) -> String { + renderer::normalize_whitespace(s) +} + +#[doc(inline)] +pub use level::Level; #[doc(inline)] pub use renderer::Renderer; pub use snippet::*; diff --git a/src/renderer/display_list.rs b/src/renderer/display_list.rs deleted file mode 100644 index eea49718..00000000 --- a/src/renderer/display_list.rs +++ /dev/null @@ -1,1724 +0,0 @@ -//! `display_list` module stores the output model for the snippet. -//! -//! `DisplayList` is a central structure in the crate, which contains -//! the structured list of lines to be displayed. -//! -//! It is made of two types of lines: `Source` and `Raw`. All `Source` lines -//! are structured using four columns: -//! -//! ```text -//! /------------ (1) Line number column. -//! | /--------- (2) Line number column delimiter. -//! | | /------- (3) Inline marks column. -//! | | | /--- (4) Content column with the source and annotations for slices. -//! | | | | -//! ============================================================================= -//! error[E0308]: mismatched types -//! --> src/format.rs:51:5 -//! | -//! 151 | / fn test() -> String { -//! 152 | | return "test"; -//! 153 | | } -//! | |___^ error: expected `String`, for `&str`. -//! | -//! ``` -//! -//! The first two lines of the example above are `Raw` lines, while the rest -//! are `Source` lines. -//! -//! `DisplayList` does not store column alignment information, and those are -//! only calculated by the implementation of `std::fmt::Display` using information such as -//! styling. -//! -//! The above snippet has been built out of the following structure: -use crate::snippet; -use std::cmp::{max, min, Reverse}; -use std::collections::HashMap; -use std::fmt::Display; -use std::ops::Range; -use std::{cmp, fmt}; - -use crate::renderer::styled_buffer::StyledBuffer; -use crate::renderer::{stylesheet::Stylesheet, Margin, Style, DEFAULT_TERM_WIDTH}; - -const ANONYMIZED_LINE_NUM: &str = "LL"; -const ERROR_TXT: &str = "error"; -const HELP_TXT: &str = "help"; -const INFO_TXT: &str = "info"; -const NOTE_TXT: &str = "note"; -const WARNING_TXT: &str = "warning"; - -/// List of lines to be displayed. -pub(crate) struct DisplayList<'a> { - pub(crate) body: Vec>, - pub(crate) stylesheet: &'a Stylesheet, - pub(crate) anonymized_line_numbers: bool, -} - -impl<'a> PartialEq for DisplayList<'a> { - fn eq(&self, other: &Self) -> bool { - self.body == other.body && self.anonymized_line_numbers == other.anonymized_line_numbers - } -} - -impl<'a> fmt::Debug for DisplayList<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DisplayList") - .field("body", &self.body) - .field("anonymized_line_numbers", &self.anonymized_line_numbers) - .finish() - } -} - -impl<'a> Display for DisplayList<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let lineno_width = self.body.iter().fold(0, |max, set| { - set.display_lines.iter().fold(max, |max, line| match line { - DisplayLine::Source { lineno, .. } => cmp::max(lineno.unwrap_or(0), max), - _ => max, - }) - }); - let lineno_width = if lineno_width == 0 { - lineno_width - } else if self.anonymized_line_numbers { - ANONYMIZED_LINE_NUM.len() - } else { - ((lineno_width as f64).log10().floor() as usize) + 1 - }; - - let multiline_depth = self.body.iter().fold(0, |max, set| { - set.display_lines.iter().fold(max, |max2, line| match line { - DisplayLine::Source { annotations, .. } => cmp::max( - annotations.iter().fold(max2, |max3, line| { - cmp::max( - match line.annotation_part { - DisplayAnnotationPart::Standalone => 0, - DisplayAnnotationPart::LabelContinuation => 0, - DisplayAnnotationPart::MultilineStart(depth) => depth + 1, - DisplayAnnotationPart::MultilineEnd(depth) => depth + 1, - }, - max3, - ) - }), - max, - ), - _ => max2, - }) - }); - let mut buffer = StyledBuffer::new(); - for set in self.body.iter() { - self.format_set(set, lineno_width, multiline_depth, &mut buffer)?; - } - write!(f, "{}", buffer.render(self.stylesheet)?) - } -} - -impl<'a> DisplayList<'a> { - pub(crate) fn new( - message: snippet::Message<'a>, - stylesheet: &'a Stylesheet, - anonymized_line_numbers: bool, - term_width: usize, - ) -> DisplayList<'a> { - let body = format_message(message, term_width, anonymized_line_numbers, true); - - Self { - body, - stylesheet, - anonymized_line_numbers, - } - } - - fn format_set( - &self, - set: &DisplaySet<'_>, - lineno_width: usize, - multiline_depth: usize, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - for line in &set.display_lines { - set.format_line( - line, - lineno_width, - multiline_depth, - self.stylesheet, - self.anonymized_line_numbers, - buffer, - )?; - } - Ok(()) - } -} - -#[derive(Debug, PartialEq)] -pub(crate) struct DisplaySet<'a> { - pub(crate) display_lines: Vec>, - pub(crate) margin: Margin, -} - -impl<'a> DisplaySet<'a> { - fn format_label( - &self, - line_offset: usize, - label: &[DisplayTextFragment<'_>], - stylesheet: &Stylesheet, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - for fragment in label { - let style = match fragment.style { - DisplayTextStyle::Regular => stylesheet.none(), - DisplayTextStyle::Emphasis => stylesheet.emphasis(), - }; - buffer.append(line_offset, fragment.content, *style); - } - Ok(()) - } - fn format_annotation( - &self, - line_offset: usize, - annotation: &Annotation<'_>, - continuation: bool, - stylesheet: &Stylesheet, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - let color = get_annotation_style(&annotation.annotation_type, stylesheet); - let formatted_len = if let Some(id) = &annotation.id { - 2 + id.len() + annotation_type_len(&annotation.annotation_type) - } else { - annotation_type_len(&annotation.annotation_type) - }; - - if continuation { - for _ in 0..formatted_len + 2 { - buffer.append(line_offset, " ", Style::new()); - } - return self.format_label(line_offset, &annotation.label, stylesheet, buffer); - } - if formatted_len == 0 { - self.format_label(line_offset, &annotation.label, stylesheet, buffer) - } else { - let id = match &annotation.id { - Some(id) => format!("[{}]", id), - None => String::new(), - }; - buffer.append( - line_offset, - &format!("{}{}", annotation_type_str(&annotation.annotation_type), id), - *color, - ); - - if !is_annotation_empty(annotation) { - buffer.append(line_offset, ": ", stylesheet.none); - self.format_label(line_offset, &annotation.label, stylesheet, buffer)?; - } - Ok(()) - } - } - - #[inline] - fn format_raw_line( - &self, - line_offset: usize, - line: &DisplayRawLine<'_>, - lineno_width: usize, - stylesheet: &Stylesheet, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - match line { - DisplayRawLine::Origin { - path, - pos, - header_type, - } => { - let header_sigil = match header_type { - DisplayHeaderType::Initial => "-->", - DisplayHeaderType::Continuation => ":::", - }; - let lineno_color = stylesheet.line_no(); - buffer.puts(line_offset, lineno_width, header_sigil, *lineno_color); - buffer.puts(line_offset, lineno_width + 4, path, stylesheet.none); - if let Some((col, row)) = pos { - buffer.append(line_offset, ":", stylesheet.none); - buffer.append(line_offset, col.to_string().as_str(), stylesheet.none); - buffer.append(line_offset, ":", stylesheet.none); - buffer.append(line_offset, row.to_string().as_str(), stylesheet.none); - } - Ok(()) - } - DisplayRawLine::Annotation { - annotation, - source_aligned, - continuation, - } => { - if *source_aligned { - if *continuation { - for _ in 0..lineno_width + 3 { - buffer.append(line_offset, " ", stylesheet.none); - } - } else { - let lineno_color = stylesheet.line_no(); - for _ in 0..lineno_width + 1 { - buffer.append(line_offset, " ", stylesheet.none); - } - buffer.append(line_offset, "=", *lineno_color); - buffer.append(line_offset, " ", *lineno_color); - } - } - self.format_annotation(line_offset, annotation, *continuation, stylesheet, buffer) - } - } - } - - // Adapted from https://github.com/rust-lang/rust/blob/d371d17496f2ce3a56da76aa083f4ef157572c20/compiler/rustc_errors/src/emitter.rs#L706-L1211 - #[inline] - fn format_line( - &self, - dl: &DisplayLine<'_>, - lineno_width: usize, - multiline_depth: usize, - stylesheet: &Stylesheet, - anonymized_line_numbers: bool, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - let line_offset = buffer.num_lines(); - match dl { - DisplayLine::Source { - lineno, - inline_marks, - line, - annotations, - } => { - let lineno_color = stylesheet.line_no(); - if anonymized_line_numbers && lineno.is_some() { - let num = format!("{:>width$} |", ANONYMIZED_LINE_NUM, width = lineno_width); - buffer.puts(line_offset, 0, &num, *lineno_color); - } else { - match lineno { - Some(n) => { - let num = format!("{:>width$} |", n, width = lineno_width); - buffer.puts(line_offset, 0, &num, *lineno_color); - } - None => { - buffer.putc(line_offset, lineno_width + 1, '|', *lineno_color); - } - }; - } - if let DisplaySourceLine::Content { text, .. } = line { - // The width of the line number, a space, pipe, and a space - // `123 | ` is `lineno_width + 3`. - let width_offset = lineno_width + 3; - let code_offset = if multiline_depth == 0 { - width_offset - } else { - width_offset + multiline_depth + 1 - }; - - // Add any inline marks to the code line - if !inline_marks.is_empty() || 0 < multiline_depth { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - - let text = normalize_whitespace(text); - let line_len = text.as_bytes().len(); - let left = self.margin.left(line_len); - let right = self.margin.right(line_len); - - // On long lines, we strip the source line, accounting for unicode. - let mut taken = 0; - let code: String = text - .chars() - .skip(left) - .take_while(|ch| { - // Make sure that the trimming on the right will fall within the terminal width. - // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` - // is. For now, just accept that sometimes the code line will be longer than - // desired. - let next = unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1); - if taken + next > right - left { - return false; - } - taken += next; - true - }) - .collect(); - buffer.puts(line_offset, code_offset, &code, Style::new()); - if self.margin.was_cut_left() { - // We have stripped some code/whitespace from the beginning, make it clear. - buffer.puts(line_offset, code_offset, "...", *lineno_color); - } - if self.margin.was_cut_right(line_len) { - buffer.puts(line_offset, code_offset + taken - 3, "...", *lineno_color); - } - - let left: usize = text - .chars() - .take(left) - .map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1)) - .sum(); - - let mut annotations = annotations.clone(); - annotations.sort_by_key(|a| Reverse(a.range.0)); - - let mut annotations_positions = vec![]; - let mut line_len: usize = 0; - let mut p = 0; - for (i, annotation) in annotations.iter().enumerate() { - for (j, next) in annotations.iter().enumerate() { - // This label overlaps with another one and both take space ( - // they have text and are not multiline lines). - if overlaps(next, annotation, 0) - && annotation.has_label() - && j > i - && p == 0 - // We're currently on the first line, move the label one line down - { - // If we're overlapping with an un-labelled annotation with the same span - // we can just merge them in the output - if next.range.0 == annotation.range.0 - && next.range.1 == annotation.range.1 - && !next.has_label() - { - continue; - } - - // This annotation needs a new line in the output. - p += 1; - break; - } - } - annotations_positions.push((p, annotation)); - for (j, next) in annotations.iter().enumerate() { - if j > i { - let l = next - .annotation - .label - .iter() - .map(|label| label.content) - .collect::>() - .join("") - .len() - + 2; - // Do not allow two labels to be in the same line if they - // overlap including padding, to avoid situations like: - // - // fn foo(x: u32) { - // -------^------ - // | | - // fn_spanx_span - // - // Both labels must have some text, otherwise they are not - // overlapping. Do not add a new line if this annotation or - // the next are vertical line placeholders. If either this - // or the next annotation is multiline start/end, move it - // to a new line so as not to overlap the horizontal lines. - if (overlaps(next, annotation, l) - && annotation.has_label() - && next.has_label()) - || (annotation.takes_space() && next.has_label()) - || (annotation.has_label() && next.takes_space()) - || (annotation.takes_space() && next.takes_space()) - || (overlaps(next, annotation, l) - && next.range.1 <= annotation.range.1 - && next.has_label() - && p == 0) - // Avoid #42595. - { - // This annotation needs a new line in the output. - p += 1; - break; - } - } - } - line_len = max(line_len, p); - } - - if line_len != 0 { - line_len += 1; - } - - if annotations_positions.iter().all(|(_, ann)| { - matches!( - ann.annotation_part, - DisplayAnnotationPart::MultilineStart(_) - ) - }) { - if let Some(max_pos) = - annotations_positions.iter().map(|(pos, _)| *pos).max() - { - // Special case the following, so that we minimize overlapping multiline spans. - // - // 3 │ X0 Y0 Z0 - // │ ┏━━━━━┛ │ │ < We are writing these lines - // │ ┃┌───────┘ │ < by reverting the "depth" of - // │ ┃│┌─────────┘ < their multilne spans. - // 4 │ ┃││ X1 Y1 Z1 - // 5 │ ┃││ X2 Y2 Z2 - // │ ┃│└────╿──│──┘ `Z` label - // │ ┃└─────│──┤ - // │ ┗━━━━━━┥ `Y` is a good letter too - // ╰╴ `X` is a good letter - for (pos, _) in &mut annotations_positions { - *pos = max_pos - *pos; - } - // We know then that we don't need an additional line for the span label, saving us - // one line of vertical space. - line_len = line_len.saturating_sub(1); - } - } - - // This is a special case where we have a multiline - // annotation that is at the start of the line disregarding - // any leading whitespace, and no other multiline - // annotations overlap it. In this case, we want to draw - // - // 2 | fn foo() { - // | _^ - // 3 | | - // 4 | | } - // | |_^ test - // - // we simplify the output to: - // - // 2 | / fn foo() { - // 3 | | - // 4 | | } - // | |_^ test - if multiline_depth == 1 - && annotations_positions.len() == 1 - && annotations_positions - .first() - .map_or(false, |(_, annotation)| { - matches!( - annotation.annotation_part, - DisplayAnnotationPart::MultilineStart(_) - ) && text - .chars() - .take(annotation.range.0) - .all(|c| c.is_whitespace()) - }) - { - let (_, ann) = annotations_positions.remove(0); - let style = get_annotation_style(&ann.annotation_type, stylesheet); - buffer.putc(line_offset, 3 + lineno_width, '/', *style); - } - - // Draw the column separator for any extra lines that were - // created - // - // After this we will have: - // - // 2 | fn foo() { - // | - // | - // | - // 3 | - // 4 | } - // | - if !annotations_positions.is_empty() { - for pos in 0..=line_len { - buffer.putc( - line_offset + pos + 1, - lineno_width + 1, - '|', - stylesheet.line_no, - ); - } - } - - // Write the horizontal lines for multiline annotations - // (only the first and last lines need this). - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | - // | - // 3 | - // 4 | } - // | _ - for &(pos, annotation) in &annotations_positions { - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - let pos = pos + 1; - match annotation.annotation_part { - DisplayAnnotationPart::MultilineStart(depth) - | DisplayAnnotationPart::MultilineEnd(depth) => { - for col in width_offset + depth - ..(code_offset + annotation.range.0).saturating_sub(left) - { - buffer.putc(line_offset + pos, col + 1, '_', *style); - } - } - _ => {} - } - } - - // Write the vertical lines for labels that are on a different line as the underline. - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | | | - // | | - // 3 | | - // 4 | | } - // | |_ - for &(pos, annotation) in &annotations_positions { - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - let pos = pos + 1; - if pos > 1 && (annotation.has_label() || annotation.takes_space()) { - for p in line_offset + 2..=line_offset + pos { - buffer.putc( - p, - (code_offset + annotation.range.0).saturating_sub(left), - '|', - *style, - ); - } - } - match annotation.annotation_part { - DisplayAnnotationPart::MultilineStart(depth) => { - for p in line_offset + pos + 1..line_offset + line_len + 2 { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - DisplayAnnotationPart::MultilineEnd(depth) => { - for p in line_offset..=line_offset + pos { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - _ => {} - } - } - - // Add in any inline marks for any extra lines that have - // been created. Output should look like above. - for inline_mark in inline_marks { - let DisplayMarkType::AnnotationThrough(depth) = inline_mark.mark_type; - let style = get_annotation_style(&inline_mark.annotation_type, stylesheet); - if annotations_positions.is_empty() { - buffer.putc(line_offset, width_offset + depth, '|', *style); - } else { - for p in line_offset..=line_offset + line_len + 1 { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - } - - // Write the labels on the annotations that actually have a label. - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | | - // | something about `foo` - // 3 | - // 4 | } - // | _ test - for &(pos, annotation) in &annotations_positions { - if !is_annotation_empty(&annotation.annotation) { - let style = - get_annotation_style(&annotation.annotation_type, stylesheet); - let mut formatted_len = if let Some(id) = &annotation.annotation.id { - 2 + id.len() - + annotation_type_len(&annotation.annotation.annotation_type) - } else { - annotation_type_len(&annotation.annotation.annotation_type) - }; - let (pos, col) = if pos == 0 { - (pos + 1, (annotation.range.1 + 1).saturating_sub(left)) - } else { - (pos + 2, annotation.range.0.saturating_sub(left)) - }; - if annotation.annotation_part - == DisplayAnnotationPart::LabelContinuation - { - formatted_len = 0; - } else if formatted_len != 0 { - formatted_len += 2; - let id = match &annotation.annotation.id { - Some(id) => format!("[{}]", id), - None => String::new(), - }; - buffer.puts( - line_offset + pos, - col + code_offset, - &format!( - "{}{}: ", - annotation_type_str(&annotation.annotation_type), - id - ), - *style, - ); - } else { - formatted_len = 0; - } - let mut before = 0; - for fragment in &annotation.annotation.label { - let inner_col = before + formatted_len + col + code_offset; - buffer.puts(line_offset + pos, inner_col, fragment.content, *style); - before += fragment.content.len(); - } - } - } - - // Sort from biggest span to smallest span so that smaller spans are - // represented in the output: - // - // x | fn foo() - // | ^^^---^^ - // | | | - // | | something about `foo` - // | something about `fn foo()` - annotations_positions.sort_by_key(|(_, ann)| { - // Decreasing order. When annotations share the same length, prefer `Primary`. - Reverse(ann.len()) - }); - - // Write the underlines. - // - // After this we will have: - // - // 2 | fn foo() { - // | ____-_____^ - // | | - // | something about `foo` - // 3 | - // 4 | } - // | _^ test - for &(_, annotation) in &annotations_positions { - let mark = match annotation.annotation_type { - DisplayAnnotationType::Error => '^', - DisplayAnnotationType::Warning => '-', - DisplayAnnotationType::Info => '-', - DisplayAnnotationType::Note => '-', - DisplayAnnotationType::Help => '-', - DisplayAnnotationType::None => ' ', - }; - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - for p in annotation.range.0..annotation.range.1 { - buffer.putc( - line_offset + 1, - (code_offset + p).saturating_sub(left), - mark, - *style, - ); - } - } - } else if !inline_marks.is_empty() { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - Ok(()) - } - DisplayLine::Fold { inline_marks } => { - buffer.puts(line_offset, 0, "...", *stylesheet.line_no()); - if !inline_marks.is_empty() || 0 < multiline_depth { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - Ok(()) - } - DisplayLine::Raw(line) => { - self.format_raw_line(line_offset, line, lineno_width, stylesheet, buffer) - } - } - } -} - -/// Inline annotation which can be used in either Raw or Source line. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct Annotation<'a> { - pub(crate) annotation_type: DisplayAnnotationType, - pub(crate) id: Option<&'a str>, - pub(crate) label: Vec>, -} - -/// A single line used in `DisplayList`. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplayLine<'a> { - /// A line with `lineno` portion of the slice. - Source { - lineno: Option, - inline_marks: Vec, - line: DisplaySourceLine<'a>, - annotations: Vec>, - }, - - /// A line indicating a folded part of the slice. - Fold { inline_marks: Vec }, - - /// A line which is displayed outside of slices. - Raw(DisplayRawLine<'a>), -} - -/// A source line. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplaySourceLine<'a> { - /// A line with the content of the Snippet. - Content { - text: &'a str, - range: (usize, usize), // meta information for annotation placement. - end_line: EndLine, - }, - /// An empty source line. - Empty, -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct DisplaySourceAnnotation<'a> { - pub(crate) annotation: Annotation<'a>, - pub(crate) range: (usize, usize), - pub(crate) annotation_type: DisplayAnnotationType, - pub(crate) annotation_part: DisplayAnnotationPart, -} - -impl<'a> DisplaySourceAnnotation<'a> { - fn has_label(&self) -> bool { - !self - .annotation - .label - .iter() - .all(|label| label.content.is_empty()) - } - - // Length of this annotation as displayed in the stderr output - fn len(&self) -> usize { - // Account for usize underflows - if self.range.1 > self.range.0 { - self.range.1 - self.range.0 - } else { - self.range.0 - self.range.1 - } - } - - fn takes_space(&self) -> bool { - // Multiline annotations always have to keep vertical space. - matches!( - self.annotation_part, - DisplayAnnotationPart::MultilineStart(_) | DisplayAnnotationPart::MultilineEnd(_) - ) - } -} - -/// Raw line - a line which does not have the `lineno` part and is not considered -/// a part of the snippet. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplayRawLine<'a> { - /// A line which provides information about the location of the given - /// slice in the project structure. - Origin { - path: &'a str, - pos: Option<(usize, usize)>, - header_type: DisplayHeaderType, - }, - - /// An annotation line which is not part of any snippet. - Annotation { - annotation: Annotation<'a>, - - /// If set to `true`, the annotation will be aligned to the - /// lineno delimiter of the snippet. - source_aligned: bool, - /// If set to `true`, only the label of the `Annotation` will be - /// displayed. It allows for a multiline annotation to be aligned - /// without displaying the meta information (`type` and `id`) to be - /// displayed on each line. - continuation: bool, - }, -} - -/// An inline text fragment which any label is composed of. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct DisplayTextFragment<'a> { - pub(crate) content: &'a str, - pub(crate) style: DisplayTextStyle, -} - -/// A style for the `DisplayTextFragment` which can be visually formatted. -/// -/// This information may be used to emphasis parts of the label. -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) enum DisplayTextStyle { - Regular, - Emphasis, -} - -/// An indicator of what part of the annotation a given `Annotation` is. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayAnnotationPart { - /// A standalone, single-line annotation. - Standalone, - /// A continuation of a multi-line label of an annotation. - LabelContinuation, - /// A line starting a multiline annotation. - MultilineStart(usize), - /// A line ending a multiline annotation. - MultilineEnd(usize), -} - -/// A visual mark used in `inline_marks` field of the `DisplaySourceLine`. -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct DisplayMark { - pub(crate) mark_type: DisplayMarkType, - pub(crate) annotation_type: DisplayAnnotationType, -} - -/// A type of the `DisplayMark`. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayMarkType { - /// A mark indicating a multiline annotation going through the current line. - AnnotationThrough(usize), -} - -/// A type of the `Annotation` which may impact the sigils, style or text displayed. -/// -/// There are several ways to uses this information when formatting the `DisplayList`: -/// -/// * An annotation may display the name of the type like `error` or `info`. -/// * An underline for `Error` may be `^^^` while for `Warning` it could be `---`. -/// * `ColorStylesheet` may use different colors for different annotations. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayAnnotationType { - None, - Error, - Warning, - Info, - Note, - Help, -} - -impl From for DisplayAnnotationType { - fn from(at: snippet::Level) -> Self { - match at { - snippet::Level::Error => DisplayAnnotationType::Error, - snippet::Level::Warning => DisplayAnnotationType::Warning, - snippet::Level::Info => DisplayAnnotationType::Info, - snippet::Level::Note => DisplayAnnotationType::Note, - snippet::Level::Help => DisplayAnnotationType::Help, - } - } -} - -/// Information whether the header is the initial one or a consequitive one -/// for multi-slice cases. -// TODO: private -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayHeaderType { - /// Initial header is the first header in the snippet. - Initial, - - /// Continuation marks all headers of following slices in the snippet. - Continuation, -} - -struct CursorLines<'a>(&'a str); - -impl<'a> CursorLines<'a> { - fn new(src: &str) -> CursorLines<'_> { - CursorLines(src) - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) enum EndLine { - Eof = 0, - Crlf = 1, - Lf = 2, -} - -impl<'a> Iterator for CursorLines<'a> { - type Item = (&'a str, EndLine); - - fn next(&mut self) -> Option { - if self.0.is_empty() { - None - } else { - self.0 - .find('\n') - .map(|x| { - let ret = if 0 < x { - if self.0.as_bytes()[x - 1] == b'\r' { - (&self.0[..x - 1], EndLine::Lf) - } else { - (&self.0[..x], EndLine::Crlf) - } - } else { - ("", EndLine::Crlf) - }; - self.0 = &self.0[x + 1..]; - ret - }) - .or_else(|| { - let ret = Some((self.0, EndLine::Eof)); - self.0 = ""; - ret - }) - } - } -} - -fn format_message( - message: snippet::Message<'_>, - term_width: usize, - anonymized_line_numbers: bool, - primary: bool, -) -> Vec> { - let snippet::Message { - level, - id, - title, - footer, - snippets, - } = message; - - let mut sets = vec![]; - let body = if !snippets.is_empty() || primary { - vec![format_title(level, id, title)] - } else { - format_footer(level, id, title) - }; - - for (idx, snippet) in snippets.into_iter().enumerate() { - let snippet = fold_prefix_suffix(snippet); - sets.push(format_snippet( - snippet, - idx == 0, - !footer.is_empty(), - term_width, - anonymized_line_numbers, - )); - } - - if let Some(first) = sets.first_mut() { - for line in body { - first.display_lines.insert(0, line); - } - } else { - sets.push(DisplaySet { - display_lines: body, - margin: Margin::new(0, 0, 0, 0, DEFAULT_TERM_WIDTH, 0), - }); - } - - for annotation in footer { - sets.extend(format_message( - annotation, - term_width, - anonymized_line_numbers, - false, - )); - } - - sets -} - -fn format_title<'a>(level: crate::Level, id: Option<&'a str>, label: &'a str) -> DisplayLine<'a> { - DisplayLine::Raw(DisplayRawLine::Annotation { - annotation: Annotation { - annotation_type: DisplayAnnotationType::from(level), - id, - label: format_label(Some(label), Some(DisplayTextStyle::Emphasis)), - }, - source_aligned: false, - continuation: false, - }) -} - -fn format_footer<'a>( - level: crate::Level, - id: Option<&'a str>, - label: &'a str, -) -> Vec> { - let mut result = vec![]; - for (i, line) in label.lines().enumerate() { - result.push(DisplayLine::Raw(DisplayRawLine::Annotation { - annotation: Annotation { - annotation_type: DisplayAnnotationType::from(level), - id, - label: format_label(Some(line), None), - }, - source_aligned: true, - continuation: i != 0, - })); - } - result -} - -fn format_label( - label: Option<&str>, - style: Option, -) -> Vec> { - let mut result = vec![]; - if let Some(label) = label { - let element_style = style.unwrap_or(DisplayTextStyle::Regular); - result.push(DisplayTextFragment { - content: label, - style: element_style, - }); - } - result -} - -fn format_snippet( - snippet: snippet::Snippet<'_>, - is_first: bool, - has_footer: bool, - term_width: usize, - anonymized_line_numbers: bool, -) -> DisplaySet<'_> { - let main_range = snippet.annotations.first().map(|x| x.range.start); - let origin = snippet.origin; - let need_empty_header = origin.is_some() || is_first; - let mut body = format_body( - snippet, - need_empty_header, - has_footer, - term_width, - anonymized_line_numbers, - ); - let header = format_header(origin, main_range, &body.display_lines, is_first); - - if let Some(header) = header { - body.display_lines.insert(0, header); - } - - body -} - -#[inline] -// TODO: option_zip -fn zip_opt(a: Option, b: Option) -> Option<(A, B)> { - a.and_then(|a| b.map(|b| (a, b))) -} - -fn format_header<'a>( - origin: Option<&'a str>, - main_range: Option, - body: &[DisplayLine<'_>], - is_first: bool, -) -> Option> { - let display_header = if is_first { - DisplayHeaderType::Initial - } else { - DisplayHeaderType::Continuation - }; - - if let Some((main_range, path)) = zip_opt(main_range, origin) { - let mut col = 1; - let mut line_offset = 1; - - for item in body { - if let DisplayLine::Source { - line: - DisplaySourceLine::Content { - text, - range, - end_line, - }, - lineno, - .. - } = item - { - if main_range >= range.0 && main_range <= range.1 + *end_line as usize { - let char_column = text[0..(main_range - range.0).min(text.len())] - .chars() - .count(); - col = char_column + 1; - line_offset = lineno.unwrap_or(1); - break; - } - } - } - - return Some(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: Some((line_offset, col)), - header_type: display_header, - })); - } - - if let Some(path) = origin { - return Some(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: None, - header_type: display_header, - })); - } - - None -} - -fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_> { - if !snippet.fold { - return snippet; - } - - let ann_start = snippet - .annotations - .iter() - .map(|ann| ann.range.start) - .min() - .unwrap_or(0); - if let Some(before_new_start) = snippet.source[0..ann_start].rfind('\n') { - let new_start = before_new_start + 1; - - let line_offset = snippet.source[..new_start].lines().count(); - snippet.line_start += line_offset; - - snippet.source = &snippet.source[new_start..]; - - for ann in &mut snippet.annotations { - let range_start = ann.range.start - new_start; - let range_end = ann.range.end - new_start; - ann.range = range_start..range_end; - } - } - - let ann_end = snippet - .annotations - .iter() - .map(|ann| ann.range.end) - .max() - .unwrap_or(snippet.source.len()); - if let Some(end_offset) = snippet.source[ann_end..].find('\n') { - let new_end = ann_end + end_offset; - snippet.source = &snippet.source[..new_end]; - } - - snippet -} - -fn fold_body(body: Vec>) -> Vec> { - const INNER_CONTEXT: usize = 1; - const INNER_UNFOLD_SIZE: usize = INNER_CONTEXT * 2 + 1; - - let mut lines = vec![]; - let mut unhighlighed_lines = vec![]; - for line in body { - match &line { - DisplayLine::Source { annotations, .. } => { - if annotations.is_empty() { - unhighlighed_lines.push(line); - } else { - if lines.is_empty() { - // Ignore leading unhighlighed lines - unhighlighed_lines.clear(); - } - match unhighlighed_lines.len() { - 0 => {} - n if n <= INNER_UNFOLD_SIZE => { - // Rather than render `...`, don't fold - lines.append(&mut unhighlighed_lines); - } - _ => { - lines.extend(unhighlighed_lines.drain(..INNER_CONTEXT)); - let inline_marks = lines - .last() - .and_then(|line| { - if let DisplayLine::Source { - ref inline_marks, .. - } = line - { - let inline_marks = inline_marks.clone(); - Some(inline_marks) - } else { - None - } - }) - .unwrap_or_default(); - lines.push(DisplayLine::Fold { - inline_marks: inline_marks.clone(), - }); - unhighlighed_lines - .drain(..unhighlighed_lines.len().saturating_sub(INNER_CONTEXT)); - lines.append(&mut unhighlighed_lines); - } - } - lines.push(line); - } - } - _ => { - unhighlighed_lines.push(line); - } - } - } - - lines -} - -fn format_body( - snippet: snippet::Snippet<'_>, - need_empty_header: bool, - has_footer: bool, - term_width: usize, - anonymized_line_numbers: bool, -) -> DisplaySet<'_> { - let source_len = snippet.source.len(); - if let Some(bigger) = snippet.annotations.iter().find_map(|x| { - // Allow highlighting one past the last character in the source. - if source_len + 1 < x.range.end { - Some(&x.range) - } else { - None - } - }) { - panic!( - "SourceAnnotation range `{:?}` is beyond the end of buffer `{}`", - bigger, source_len - ) - } - - let mut body = vec![]; - let mut current_line = snippet.line_start; - let mut current_index = 0; - - let mut whitespace_margin = usize::MAX; - let mut span_left_margin = usize::MAX; - let mut span_right_margin = 0; - let mut label_right_margin = 0; - let mut max_line_len = 0; - - let mut depth_map: HashMap = HashMap::new(); - let mut current_depth = 0; - let mut annotations = snippet.annotations; - let ranges = annotations - .iter() - .map(|a| a.range.clone()) - .collect::>(); - // We want to merge multiline annotations that have the same range into one - // multiline annotation to save space. This is done by making any duplicate - // multiline annotations into a single-line annotation pointing at the end - // of the range. - // - // 3 | X0 Y0 Z0 - // | _____^ - // | | ____| - // | || ___| - // | ||| - // 4 | ||| X1 Y1 Z1 - // 5 | ||| X2 Y2 Z2 - // | ||| ^ - // | |||____| - // | ||____`X` is a good letter - // | |____`Y` is a good letter too - // | `Z` label - // Should be - // error: foo - // --> test.rs:3:3 - // | - // 3 | / X0 Y0 Z0 - // 4 | | X1 Y1 Z1 - // 5 | | X2 Y2 Z2 - // | | ^ - // | |____| - // | `X` is a good letter - // | `Y` is a good letter too - // | `Z` label - // | - ranges.iter().enumerate().for_each(|(r_idx, range)| { - annotations - .iter_mut() - .enumerate() - .skip(r_idx + 1) - .for_each(|(ann_idx, ann)| { - // Skip if the annotation's index matches the range index - if ann_idx != r_idx - // We only want to merge multiline annotations - && snippet.source[ann.range.clone()].lines().count() > 1 - // We only want to merge annotations that have the same range - && ann.range.start == range.start - && ann.range.end == range.end - { - ann.range.start = ann.range.end.saturating_sub(1); - } - }); - }); - annotations.sort_by_key(|a| a.range.start); - let mut annotations = annotations.into_iter().enumerate().collect::>(); - - for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() { - let line_length: usize = line.len(); - let line_range = (current_index, current_index + line_length); - let end_line_size = end_line as usize; - body.push(DisplayLine::Source { - lineno: Some(current_line), - inline_marks: vec![], - line: DisplaySourceLine::Content { - text: line, - range: line_range, - end_line, - }, - annotations: vec![], - }); - - let leading_whitespace = line - .chars() - .take_while(|c| c.is_whitespace()) - .map(|c| { - match c { - // Tabs are displayed as 4 spaces - '\t' => 4, - _ => 1, - } - }) - .sum(); - if line.chars().any(|c| !c.is_whitespace()) { - whitespace_margin = min(whitespace_margin, leading_whitespace); - } - max_line_len = max(max_line_len, line_length); - - let line_start_index = line_range.0; - let line_end_index = line_range.1; - current_line += 1; - current_index += line_length + end_line_size; - - // It would be nice to use filter_drain here once it's stable. - annotations.retain(|(key, annotation)| { - let body_idx = idx; - let annotation_type = match annotation.level { - snippet::Level::Error => DisplayAnnotationType::None, - snippet::Level::Warning => DisplayAnnotationType::None, - _ => DisplayAnnotationType::from(annotation.level), - }; - let label_right = annotation.label.map_or(0, |label| label.len() + 1); - match annotation.range { - // This handles if the annotation is on the next line. We add - // the `end_line_size` to account for annotating the line end. - Range { start, .. } if start > line_end_index + end_line_size => true, - // This handles the case where an annotation is contained - // within the current line including any line-end characters. - Range { start, end } - if start >= line_start_index - // We add at least one to `line_end_index` to allow - // highlighting the end of a file - && end <= line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let annotation_start_col = line - [0..(start - line_start_index).min(line_length)] - .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum::(); - let mut annotation_end_col = line - [0..(end - line_start_index).min(line_length)] - .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum::(); - if annotation_start_col == annotation_end_col { - // At least highlight something - annotation_end_col += 1; - } - - span_left_margin = min(span_left_margin, annotation_start_col); - span_right_margin = max(span_right_margin, annotation_end_col); - label_right_margin = - max(label_right_margin, annotation_end_col + label_right); - - let range = (annotation_start_col, annotation_end_col); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: format_label(annotation.label, None), - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::Standalone, - }); - } - false - } - // This handles the case where a multiline annotation starts - // somewhere on the current line, including any line-end chars - Range { start, end } - if start >= line_start_index - // The annotation can start on a line ending - && start <= line_end_index + end_line_size.saturating_sub(1) - && end > line_end_index => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let annotation_start_col = line - [0..(start - line_start_index).min(line_length)] - .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum::(); - let annotation_end_col = annotation_start_col + 1; - - span_left_margin = min(span_left_margin, annotation_start_col); - span_right_margin = max(span_right_margin, annotation_end_col); - label_right_margin = - max(label_right_margin, annotation_end_col + label_right); - - let range = (annotation_start_col, annotation_end_col); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: vec![], - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineStart(current_depth), - }); - depth_map.insert(*key, current_depth); - current_depth += 1; - } - true - } - // This handles the case where a multiline annotation starts - // somewhere before this line and ends after it as well - Range { start, end } - if start < line_start_index && end > line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut inline_marks, - .. - } = body[body_idx] - { - let depth = depth_map.get(key).cloned().unwrap_or_default(); - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationThrough(depth), - annotation_type: DisplayAnnotationType::from(annotation.level), - }); - } - true - } - // This handles the case where a multiline annotation ends - // somewhere on the current line, including any line-end chars - Range { start, end } - if start < line_start_index - && end >= line_start_index - // We add at least one to `line_end_index` to allow - // highlighting the end of a file - && end <= line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let end_mark = line[0..(end - line_start_index).min(line_length)] - .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum::() - .saturating_sub(1); - // If the annotation ends on a line-end character, we - // need to annotate one past the end of the line - let (end_mark, end_plus_one) = if end > line_end_index - // Special case for highlighting the end of a file - || (end == line_end_index + 1 && end_line_size == 0) - { - (end_mark + 1, end_mark + 2) - } else { - (end_mark, end_mark + 1) - }; - - span_left_margin = min(span_left_margin, end_mark); - span_right_margin = max(span_right_margin, end_plus_one); - label_right_margin = max(label_right_margin, end_plus_one + label_right); - - let range = (end_mark, end_plus_one); - let depth = depth_map.remove(key).unwrap_or(0); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: format_label(annotation.label, None), - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineEnd(depth), - }); - } - false - } - _ => true, - } - }); - // Reset the depth counter, but only after we've processed all - // annotations for a given line. - let max = depth_map.len(); - if current_depth > max { - current_depth = max; - } - } - - if snippet.fold { - body = fold_body(body); - } - - if need_empty_header { - body.insert( - 0, - DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }, - ); - } - - if has_footer { - body.push(DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }); - } else if let Some(DisplayLine::Source { .. }) = body.last() { - body.push(DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }); - } - let max_line_num_len = if anonymized_line_numbers { - ANONYMIZED_LINE_NUM.len() - } else { - current_line.to_string().len() - }; - - let width_offset = 3 + max_line_num_len; - - if span_left_margin == usize::MAX { - span_left_margin = 0; - } - - let margin = Margin::new( - whitespace_margin, - span_left_margin, - span_right_margin, - label_right_margin, - term_width.saturating_sub(width_offset), - max_line_len, - ); - - DisplaySet { - display_lines: body, - margin, - } -} - -#[inline] -fn annotation_type_str(annotation_type: &DisplayAnnotationType) -> &'static str { - match annotation_type { - DisplayAnnotationType::Error => ERROR_TXT, - DisplayAnnotationType::Help => HELP_TXT, - DisplayAnnotationType::Info => INFO_TXT, - DisplayAnnotationType::Note => NOTE_TXT, - DisplayAnnotationType::Warning => WARNING_TXT, - DisplayAnnotationType::None => "", - } -} - -fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize { - match annotation_type { - DisplayAnnotationType::Error => ERROR_TXT.len(), - DisplayAnnotationType::Help => HELP_TXT.len(), - DisplayAnnotationType::Info => INFO_TXT.len(), - DisplayAnnotationType::Note => NOTE_TXT.len(), - DisplayAnnotationType::Warning => WARNING_TXT.len(), - DisplayAnnotationType::None => 0, - } -} - -fn get_annotation_style<'a>( - annotation_type: &DisplayAnnotationType, - stylesheet: &'a Stylesheet, -) -> &'a Style { - match annotation_type { - DisplayAnnotationType::Error => stylesheet.error(), - DisplayAnnotationType::Warning => stylesheet.warning(), - DisplayAnnotationType::Info => stylesheet.info(), - DisplayAnnotationType::Note => stylesheet.note(), - DisplayAnnotationType::Help => stylesheet.help(), - DisplayAnnotationType::None => stylesheet.none(), - } -} - -#[inline] -fn is_annotation_empty(annotation: &Annotation<'_>) -> bool { - annotation - .label - .iter() - .all(|fragment| fragment.content.is_empty()) -} - -// We replace some characters so the CLI output is always consistent and underlines aligned. -const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ - ('\t', " "), // We do our own tab replacement - ('\u{200D}', ""), // Replace ZWJ with nothing for consistent terminal output of grapheme clusters. - ('\u{202A}', ""), // The following unicode text flow control characters are inconsistently - ('\u{202B}', ""), // supported across CLIs and can cause confusion due to the bytes on disk - ('\u{202D}', ""), // not corresponding to the visible source code, so we replace them always. - ('\u{202E}', ""), - ('\u{2066}', ""), - ('\u{2067}', ""), - ('\u{2068}', ""), - ('\u{202C}', ""), - ('\u{2069}', ""), -]; - -fn normalize_whitespace(str: &str) -> String { - let mut s = str.to_owned(); - for (c, replacement) in OUTPUT_REPLACEMENTS { - s = s.replace(*c, replacement); - } - s -} - -fn overlaps( - a1: &DisplaySourceAnnotation<'_>, - a2: &DisplaySourceAnnotation<'_>, - padding: usize, -) -> bool { - (a2.range.0..a2.range.1).contains(&a1.range.0) - || (a1.range.0..a1.range.1 + padding).contains(&a2.range.0) -} - -fn format_inline_marks( - line: usize, - inline_marks: &[DisplayMark], - lineno_width: usize, - stylesheet: &Stylesheet, - buf: &mut StyledBuffer, -) -> fmt::Result { - for mark in inline_marks.iter() { - let annotation_style = get_annotation_style(&mark.annotation_type, stylesheet); - match mark.mark_type { - DisplayMarkType::AnnotationThrough(depth) => { - buf.putc(line, 3 + lineno_width + depth, '|', *annotation_style); - } - }; - } - Ok(()) -} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index b9edcc6c..be2abab4 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -1,27 +1,61 @@ +// Most of this file is adapted from https://github.com/rust-lang/rust/blob/160905b6253f42967ed4aef4b98002944c7df24c/compiler/rustc_errors/src/emitter.rs + //! The renderer for [`Message`]s //! //! # Example //! ``` -//! use annotate_snippets::{Renderer, Snippet, Level}; -//! let snippet = Level::Error.title("mismatched types") -//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs")) -//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs")); +//! use annotate_snippets::*; +//! use annotate_snippets::Level; +//! +//! let source = r#" +//! use baz::zed::bar; //! -//! let renderer = Renderer::styled(); -//! println!("{}", renderer.render(snippet)); +//! mod baz {} +//! mod zed { +//! pub fn bar() { println!("bar3"); } +//! } +//! fn main() { +//! bar(); +//! } +//! "#; +//! Level::ERROR +//! .header("unresolved import `baz::zed`") +//! .id("E0432") +//! .group( +//! Group::new().element( +//! Snippet::source(source) +//! .origin("temp.rs") +//! .line_start(1) +//! .fold(true) +//! .annotation( +//! AnnotationKind::Primary +//! .span(10..13) +//! .label("could not find `zed` in `baz`"), +//! ) +//! ) +//! ); +//! ``` -mod display_list; mod margin; +pub(crate) mod source_map; mod styled_buffer; pub(crate) mod stylesheet; -use crate::snippet::Message; +use crate::level::{Level, LevelInner}; +use crate::renderer::source_map::{ + AnnotatedLineInfo, LineInfo, Loc, SourceMap, SubstitutionHighlight, +}; +use crate::renderer::styled_buffer::StyledBuffer; +use crate::{Annotation, AnnotationKind, Element, Group, Message, Origin, Patch, Snippet, Title}; pub use anstyle::*; -use display_list::DisplayList; use margin::Margin; -use std::fmt::Display; +use std::borrow::Cow; +use std::cmp::{max, min, Ordering, Reverse}; +use std::collections::{HashMap, VecDeque}; +use std::ops::Range; use stylesheet::Stylesheet; +const ANONYMIZED_LINE_NUM: &str = "LL"; pub const DEFAULT_TERM_WIDTH: usize = 140; /// A renderer for [`Message`]s @@ -29,6 +63,7 @@ pub const DEFAULT_TERM_WIDTH: usize = 140; pub struct Renderer { anonymized_line_numbers: bool, term_width: usize, + theme: OutputTheme, stylesheet: Stylesheet, } @@ -38,6 +73,7 @@ impl Renderer { Self { anonymized_line_numbers: false, term_width: DEFAULT_TERM_WIDTH, + theme: OutputTheme::Ascii, stylesheet: Stylesheet::plain(), } } @@ -65,7 +101,7 @@ impl Renderer { info: BRIGHT_BLUE.effects(Effects::BOLD), note: AnsiColor::BrightGreen.on_default().effects(Effects::BOLD), help: AnsiColor::BrightCyan.on_default().effects(Effects::BOLD), - line_no: BRIGHT_BLUE.effects(Effects::BOLD), + line_num: BRIGHT_BLUE.effects(Effects::BOLD), emphasis: if USE_WINDOWS_COLORS { AnsiColor::BrightWhite.on_default() } else { @@ -73,6 +109,9 @@ impl Renderer { } .effects(Effects::BOLD), none: Style::new(), + context: BRIGHT_BLUE.effects(Effects::BOLD), + addition: AnsiColor::BrightGreen.on_default(), + removal: AnsiColor::BrightRed.on_default(), }, ..Self::plain() } @@ -103,6 +142,11 @@ impl Renderer { self } + pub const fn theme(mut self, output_theme: OutputTheme) -> Self { + self.theme = output_theme; + self + } + /// Set the output style for `error` pub const fn error(mut self, style: Style) -> Self { self.stylesheet.error = style; @@ -134,8 +178,8 @@ impl Renderer { } /// Set the output style for line numbers - pub const fn line_no(mut self, style: Style) -> Self { - self.stylesheet.line_no = style; + pub const fn line_num(mut self, style: Style) -> Self { + self.stylesheet.line_num = style; self } @@ -150,14 +194,2543 @@ impl Renderer { self.stylesheet.none = style; self } +} + +impl Renderer { + pub fn render(&self, mut message: Message<'_>) -> String { + let mut buffer = StyledBuffer::new(); + let max_line_num_len = if self.anonymized_line_numbers { + ANONYMIZED_LINE_NUM.len() + } else { + let n = message.max_line_number(); + num_decimal_digits(n) + }; + let title = message.groups.remove(0).elements.remove(0); + let level = if let Element::Title(title) = &title { + title.level.clone() + } else { + panic!("Expected a title as the first element of the message") + }; + if let Some(first) = message.groups.first_mut() { + first.elements.insert(0, title); + } else { + message.groups.push(Group::new().element(title)); + } + self.render_message(&mut buffer, message, max_line_num_len); + + buffer.render(level, &self.stylesheet).unwrap() + } + + fn render_message( + &self, + buffer: &mut StyledBuffer, + message: Message<'_>, + max_line_num_len: usize, + ) { + let og_primary_origin = message + .groups + .iter() + .find_map(|group| { + group.elements.iter().find_map(|s| match &s { + Element::Cause(cause) => { + if cause.markers.iter().any(|m| m.kind.is_primary()) { + Some(cause.origin) + } else { + None + } + } + Element::Origin(origin) => { + if origin.primary { + Some(Some(origin.origin)) + } else { + None + } + } + _ => None, + }) + }) + .unwrap_or( + message + .groups + .iter() + .find_map(|group| { + group.elements.iter().find_map(|s| match &s { + Element::Cause(cause) => Some(cause.origin), + Element::Origin(origin) => Some(Some(origin.origin)), + _ => None, + }) + }) + .unwrap_or_default(), + ); + let group_len = message.groups.len(); + for (g, group) in message.groups.into_iter().enumerate() { + let primary_origin = group + .elements + .iter() + .find_map(|s| match &s { + Element::Cause(cause) => { + if cause.markers.iter().any(|m| m.kind.is_primary()) { + Some(cause.origin) + } else { + None + } + } + Element::Origin(origin) => { + if origin.primary { + Some(Some(origin.origin)) + } else { + None + } + } + _ => None, + }) + .unwrap_or( + group + .elements + .iter() + .find_map(|s| match &s { + Element::Cause(cause) => Some(cause.origin), + Element::Origin(origin) => Some(Some(origin.origin)), + _ => None, + }) + .unwrap_or_default(), + ); + let mut source_map_annotated_lines = VecDeque::new(); + let mut max_depth = 0; + for e in &group.elements { + if let Element::Cause(cause) = e { + let source_map = SourceMap::new(cause.source, cause.line_start); + let (depth, annotated_lines) = + source_map.annotated_lines(cause.markers.clone(), cause.fold); + max_depth = max(max_depth, depth); + source_map_annotated_lines.push_back((source_map, annotated_lines)); + } + } + let mut message_iter = group.elements.iter().enumerate().peekable(); + let mut last_was_suggestion = false; + while let Some((i, section)) = message_iter.next() { + let peek = message_iter.peek().map(|(_, s)| s).copied(); + match §ion { + Element::Title(title) => { + self.render_title( + buffer, + title, + peek, + max_line_num_len, + if i == 0 { false } else { !title.primary }, + message.id.as_ref().and_then(|id| { + if g == 0 && i == 0 { + Some(id) + } else { + None + } + }), + matches!(peek, Some(Element::Title(_))), + ); + last_was_suggestion = false; + } + Element::Cause(cause) => { + if let Some((source_map, annotated_lines)) = + source_map_annotated_lines.pop_front() + { + self.render_snippet_annotations( + buffer, + max_line_num_len, + cause, + primary_origin, + &source_map, + &annotated_lines, + max_depth, + peek.is_some() || (g == 0 && group_len > 1), + ); + + if g == 0 && group_len > 1 { + if matches!(peek, Some(Element::Title(level)) if level.level.name != Some(None)) + { + self.draw_col_separator_no_space( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + // We want to draw the separator when it is + // requested, or when it is the last element + } else if peek.is_none() { + self.draw_col_separator_end( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + } + } + } + + last_was_suggestion = false; + } + Element::Suggestion(suggestion) => { + let source_map = SourceMap::new(suggestion.source, suggestion.line_start); + self.emit_suggestion_default( + buffer, + suggestion, + max_line_num_len, + &source_map, + primary_origin.or(og_primary_origin), + last_was_suggestion, + ); + last_was_suggestion = true; + } + + Element::Origin(origin) => { + self.render_origin(buffer, max_line_num_len, origin); + last_was_suggestion = false; + } + Element::Padding(_) => { + self.draw_col_separator_no_space( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + } + } + if g == 0 + && (matches!(section, Element::Origin(_)) + || (matches!(section, Element::Title(_)) && i == 0) + || matches!(section, Element::Title(level) if level.level.name == Some(None))) + { + if peek.is_none() && group_len > 1 { + self.draw_col_separator_end( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + } else if matches!(peek, Some(Element::Title(level)) if level.level.name != Some(None)) + { + self.draw_col_separator_no_space( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + } + } + } + } + } + + #[allow(clippy::too_many_arguments)] + fn render_title( + &self, + buffer: &mut StyledBuffer, + title: &Title<'_>, + next_section: Option<&Element<'_>>, + max_line_num_len: usize, + is_secondary: bool, + id: Option<&&str>, + is_cont: bool, + ) { + let line_offset = buffer.num_lines(); + + let (has_primary_spans, has_span_labels) = + next_section.map_or((false, false), |s| match s { + Element::Title(_) | Element::Padding(_) => (false, false), + Element::Cause(cause) => ( + cause.markers.iter().any(|m| m.kind.is_primary()), + cause.markers.iter().any(|m| m.label.is_some()), + ), + Element::Suggestion(_) => (true, false), + Element::Origin(_) => (false, true), + }); + + if !has_primary_spans && !has_span_labels && is_secondary { + // This is a secondary message with no span info + for _ in 0..max_line_num_len { + buffer.prepend(line_offset, " ", ElementStyle::NoStyle); + } + + if title.level.name != Some(None) { + self.draw_note_separator(buffer, line_offset, max_line_num_len + 1, is_cont); + buffer.append( + line_offset, + title.level.as_str(), + ElementStyle::MainHeaderMsg, + ); + buffer.append(line_offset, ": ", ElementStyle::NoStyle); + } + + let printed_lines = + self.msgs_to_buffer(buffer, title.title, max_line_num_len, "note", None); + if is_cont && matches!(self.theme, OutputTheme::Unicode) { + // There's another note after this one, associated to the subwindow above. + // We write additional vertical lines to join them: + // ╭▸ test.rs:3:3 + // │ + // 3 │ code + // │ ━━━━ + // │ + // ├ note: foo + // │ bar + // ╰ note: foo + // bar + for i in line_offset + 1..=printed_lines { + self.draw_col_separator_no_space(buffer, i, max_line_num_len + 1); + } + } + } else { + let mut label_width = 0; + + if title.level.name != Some(None) { + buffer.append( + line_offset, + title.level.as_str(), + ElementStyle::Level(title.level.level), + ); + } + label_width += title.level.as_str().len(); + if let Some(id) = id { + buffer.append(line_offset, "[", ElementStyle::Level(title.level.level)); + buffer.append(line_offset, id, ElementStyle::Level(title.level.level)); + buffer.append(line_offset, "]", ElementStyle::Level(title.level.level)); + label_width += 2 + id.len(); + } + let header_style = if is_secondary { + ElementStyle::HeaderMsg + } else { + ElementStyle::MainHeaderMsg + }; + if title.level.name != Some(None) { + buffer.append(line_offset, ": ", header_style); + label_width += 2; + } + if !title.title.is_empty() { + for (line, text) in normalize_whitespace(title.title).lines().enumerate() { + buffer.append( + line_offset + line, + &format!( + "{}{}", + if line == 0 { + String::new() + } else { + " ".repeat(label_width) + }, + text + ), + header_style, + ); + } + } + } + } + + /// Adds a left margin to every line but the first, given a padding length and the label being + /// displayed, keeping the provided highlighting. + fn msgs_to_buffer( + &self, + buffer: &mut StyledBuffer, + title: &str, + padding: usize, + label: &str, + override_style: Option, + ) -> usize { + // The extra 5 ` ` is padding that's always needed to align to the `note: `: + // + // error: message + // --> file.rs:13:20 + // | + // 13 | + // | ^^^^ + // | + // = note: multiline + // message + // ++^^^----xx + // | | | | + // | | | magic `2` + // | | length of label + // | magic `3` + // `max_line_num_len` + let padding = " ".repeat(padding + label.len() + 5); + + let mut line_number = buffer.num_lines().saturating_sub(1); + + // Provided the following diagnostic message: + // + // let msgs = vec![ + // (" + // ("highlighted multiline\nstring to\nsee how it ", Style::NoStyle), + // ("looks", Style::Highlight), + // ("with\nvery ", Style::NoStyle), + // ("weird", Style::Highlight), + // (" formats\n", Style::NoStyle), + // ("see?", Style::Highlight), + // ]; + // + // the expected output on a note is (* surround the highlighted text) + // + // = note: highlighted multiline + // string to + // see how it *looks* with + // very *weird* formats + // see? + let style = if let Some(override_style) = override_style { + override_style + } else { + ElementStyle::NoStyle + }; + let lines = title.split('\n').collect::>(); + if lines.len() > 1 { + for (i, line) in lines.iter().enumerate() { + if i != 0 { + line_number += 1; + buffer.append(line_number, &padding, ElementStyle::NoStyle); + } + buffer.append(line_number, line, style); + } + } else { + buffer.append(line_number, title, style); + } + line_number + } + + fn render_origin( + &self, + buffer: &mut StyledBuffer, + max_line_num_len: usize, + origin: &Origin<'_>, + ) { + let buffer_msg_line_offset = buffer.num_lines(); + if origin.primary { + buffer.prepend( + buffer_msg_line_offset, + self.file_start(), + ElementStyle::LineNumber, + ); + } else { + // if !origin.standalone { + // // Add spacing line, as shown: + // // --> $DIR/file:54:15 + // // | + // // LL | code + // // | ^^^^ + // // | (<- It prints *this* line) + // // ::: $DIR/other_file.rs:15:5 + // // | + // // LL | code + // // | ---- + // self.draw_col_separator_no_space( + // buffer, + // buffer_msg_line_offset, + // max_line_num_len + 1, + // ); + // + // buffer_msg_line_offset += 1; + // } + // Then, the secondary file indicator + buffer.prepend( + buffer_msg_line_offset, + self.secondary_file_start(), + ElementStyle::LineNumber, + ); + } + + let str = match (&origin.line, &origin.char_column) { + (Some(line), Some(col)) => { + format!("{}:{}:{}", origin.origin, line, col) + } + (Some(line), None) => format!("{}:{}", origin.origin, line), + _ => origin.origin.to_owned(), + }; + buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn); + for _ in 0..max_line_num_len { + buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle); + } + + if let Some(label) = &origin.label { + self.draw_col_separator_no_space( + buffer, + buffer_msg_line_offset + 1, + max_line_num_len + 1, + ); + let title = Level::NOTE.title(label); + self.render_title(buffer, &title, None, max_line_num_len, true, None, false); + } + } + + #[allow(clippy::too_many_arguments)] + fn render_snippet_annotations( + &self, + buffer: &mut StyledBuffer, + max_line_num_len: usize, + snippet: &Snippet<'_, Annotation<'_>>, + primary_origin: Option<&str>, + sm: &SourceMap<'_>, + annotated_lines: &[AnnotatedLineInfo<'_>], + multiline_depth: usize, + is_cont: bool, + ) { + if let Some(origin) = snippet.origin { + let mut origin = Origin::new(origin); + // print out the span location and spacer before we print the annotated source + // to do this, we need to know if this span will be primary + let is_primary = primary_origin == Some(origin.origin); + + if is_primary { + origin.primary = true; + if let Some(primary_line) = annotated_lines + .iter() + .find(|l| l.annotations.iter().any(LineAnnotation::is_primary)) + .or(annotated_lines.iter().find(|l| !l.annotations.is_empty())) + { + origin.line = Some(primary_line.line_index); + if let Some(first_annotation) = primary_line + .annotations + .iter() + .find(|a| a.is_primary()) + .or(primary_line.annotations.first()) + { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + } else { + let buffer_msg_line_offset = buffer.num_lines(); + // Add spacing line, as shown: + // --> $DIR/file:54:15 + // | + // LL | code + // | ^^^^ + // | (<- It prints *this* line) + // ::: $DIR/other_file.rs:15:5 + // | + // LL | code + // | ---- + self.draw_col_separator_no_space( + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + if let Some(first_line) = annotated_lines.first() { + origin.line = Some(first_line.line_index); + if let Some(first_annotation) = first_line.annotations.first() { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + } + self.render_origin(buffer, max_line_num_len, &origin); + } + + // Put in the spacer between the location and annotated source + let buffer_msg_line_offset = buffer.num_lines(); + self.draw_col_separator_no_space(buffer, buffer_msg_line_offset, max_line_num_len + 1); + + // Contains the vertical lines' positions for active multiline annotations + let mut multilines = Vec::new(); + + // Get the left-side margin to remove it + let mut whitespace_margin = usize::MAX; + for line_info in annotated_lines { + // Whitespace can only be removed (aka considered leading) + // if the lexer considers it whitespace. + // non-rustc_lexer::is_whitespace() chars are reported as an + // error (ex. no-break-spaces \u{a0}), and thus can't be considered + // for removal during error reporting. + let leading_whitespace = line_info + .line + .chars() + .take_while(|c| c.is_whitespace()) + .map(|c| { + match c { + // Tabs are displayed as 4 spaces + '\t' => 4, + _ => 1, + } + }) + .sum(); + if line_info.line.chars().any(|c| !c.is_whitespace()) { + whitespace_margin = min(whitespace_margin, leading_whitespace); + } + } + if whitespace_margin == usize::MAX { + whitespace_margin = 0; + } + + // Left-most column any visible span points at. + let mut span_left_margin = usize::MAX; + for line_info in annotated_lines { + for ann in &line_info.annotations { + span_left_margin = min(span_left_margin, ann.start.display); + span_left_margin = min(span_left_margin, ann.end.display); + } + } + if span_left_margin == usize::MAX { + span_left_margin = 0; + } + + // Right-most column any visible span points at. + let mut span_right_margin = 0; + let mut label_right_margin = 0; + let mut max_line_len = 0; + for line_info in annotated_lines { + max_line_len = max(max_line_len, line_info.line.len()); + for ann in &line_info.annotations { + span_right_margin = max(span_right_margin, ann.start.display); + span_right_margin = max(span_right_margin, ann.end.display); + // FIXME: account for labels not in the same line + let label_right = ann.label.as_ref().map_or(0, |l| l.len() + 1); + label_right_margin = max(label_right_margin, ann.end.display + label_right); + } + } + let width_offset = 3 + max_line_num_len; + let code_offset = if multiline_depth == 0 { + width_offset + } else { + width_offset + multiline_depth + 1 + }; + + let column_width = self.term_width.saturating_sub(code_offset); + + let margin = Margin::new( + whitespace_margin, + span_left_margin, + span_right_margin, + label_right_margin, + column_width, + max_line_len, + ); + + // Next, output the annotate source for this file + for annotated_line_idx in 0..annotated_lines.len() { + let previous_buffer_line = buffer.num_lines(); + + let depths = self.render_source_line( + &annotated_lines[annotated_line_idx], + buffer, + width_offset, + code_offset, + max_line_num_len, + margin, + !is_cont && annotated_line_idx + 1 == annotated_lines.len(), + ); + + let mut to_add = HashMap::new(); + + for (depth, style) in depths { + if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) { + multilines.swap_remove(index); + } else { + to_add.insert(depth, style); + } + } + + // Set the multiline annotation vertical lines to the left of + // the code in this line. + for (depth, style) in &multilines { + for line in previous_buffer_line..buffer.num_lines() { + self.draw_multiline_line(buffer, line, width_offset, *depth, *style); + } + } + // check to see if we need to print out or elide lines that come between + // this annotated line and the next one. + if annotated_line_idx < (annotated_lines.len() - 1) { + let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index + - annotated_lines[annotated_line_idx].line_index; + match line_idx_delta.cmp(&2) { + Ordering::Greater => { + let last_buffer_line_num = buffer.num_lines(); + + self.draw_line_separator(buffer, last_buffer_line_num, width_offset); + + // Set the multiline annotation vertical lines on `...` bridging line. + for (depth, style) in &multilines { + self.draw_multiline_line( + buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + ); + } + if let Some(line) = annotated_lines.get(annotated_line_idx) { + for ann in &line.annotations { + if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type + { + // In the case where we have elided the entire start of the + // multispan because those lines were empty, we still need + // to draw the `|`s across the `...`. + self.draw_multiline_line( + buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary() { + ElementStyle::UnderlinePrimary + } else { + ElementStyle::UnderlineSecondary + }, + ); + } + } + } + } + + Ordering::Equal => { + let unannotated_line = sm + .get_line(annotated_lines[annotated_line_idx].line_index + 1) + .unwrap_or(""); + + let last_buffer_line_num = buffer.num_lines(); + + self.draw_line( + buffer, + &normalize_whitespace(unannotated_line), + annotated_lines[annotated_line_idx + 1].line_index - 1, + last_buffer_line_num, + width_offset, + code_offset, + max_line_num_len, + margin, + ); + + for (depth, style) in &multilines { + self.draw_multiline_line( + buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + ); + } + if let Some(line) = annotated_lines.get(annotated_line_idx) { + for ann in &line.annotations { + if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type + { + self.draw_multiline_line( + buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary() { + ElementStyle::UnderlinePrimary + } else { + ElementStyle::UnderlineSecondary + }, + ); + } + } + } + } + Ordering::Less => {} + } + } + + multilines.extend(to_add); + } + } + + #[allow(clippy::too_many_arguments)] + fn render_source_line( + &self, + line_info: &AnnotatedLineInfo<'_>, + buffer: &mut StyledBuffer, + width_offset: usize, + code_offset: usize, + max_line_num_len: usize, + margin: Margin, + close_window: bool, + ) -> Vec<(usize, ElementStyle)> { + // Draw: + // + // LL | ... code ... + // | ^^-^ span label + // | | + // | secondary span label + // + // ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it + // | | | | + // | | | actual code found in your source code and the spans we use to mark it + // | | when there's too much wasted space to the left, trim it + // | vertical divider between the column number and the code + // column number + + if line_info.line_index == 0 { + return Vec::new(); + } + + let source_string = normalize_whitespace(line_info.line); + + let line_offset = buffer.num_lines(); + + // Left trim + let left = margin.left(str_width(&source_string)); + + // FIXME: This looks fishy. See #132860. + // Account for unicode characters of width !=0 that were removed. + let mut taken = 0; + source_string.chars().for_each(|ch| { + let next = char_width(ch); + if taken + next <= left { + taken += next; + } + }); + + let left = taken; + self.draw_line( + buffer, + &source_string, + line_info.line_index, + line_offset, + width_offset, + code_offset, + max_line_num_len, + margin, + ); + + // Special case when there's only one annotation involved, it is the start of a multiline + // span and there's no text at the beginning of the code line. Instead of doing the whole + // graph: + // + // 2 | fn foo() { + // | _^ + // 3 | | + // 4 | | } + // | |_^ test + // + // we simplify the output to: + // + // 2 | / fn foo() { + // 3 | | + // 4 | | } + // | |_^ test + let mut buffer_ops = vec![]; + let mut annotations = vec![]; + let mut short_start = true; + for ann in &line_info.annotations { + if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type { + if source_string + .chars() + .take(ann.start.display) + .all(char::is_whitespace) + { + let uline = self.underline(ann.is_primary()); + let chr = uline.multiline_whole_line; + annotations.push((depth, uline.style)); + buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style)); + } else { + short_start = false; + break; + } + } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type { + } else { + short_start = false; + break; + } + } + if short_start { + for (y, x, c, s) in buffer_ops { + buffer.putc(y, x, c, s); + } + return annotations; + } + + // We want to display like this: + // + // vec.push(vec.pop().unwrap()); + // --- ^^^ - previous borrow ends here + // | | + // | error occurs here + // previous borrow of `vec` occurs here + // + // But there are some weird edge cases to be aware of: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here + // || + // |this makes no sense + // previous borrow of `vec` occurs here + // + // For this reason, we group the lines into "highlight lines" + // and "annotations lines", where the highlight lines have the `^`. + + // Sort the annotations by (start, end col) + // The labels are reversed, sort and then reversed again. + // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where + // the letter signifies the span. Here we are only sorting by the + // span and hence, the order of the elements with the same span will + // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get + // (C1, C2, B1, B2, A1, A2). All the elements with the same span are + // still ordered first to last, but all the elements with different + // spans are ordered by their spans in last to first order. Last to + // first order is important, because the jiggly lines and | are on + // the left, so the rightmost span needs to be rendered first, + // otherwise the lines would end up needing to go over a message. + + let mut annotations = line_info.annotations.clone(); + annotations.sort_by_key(|a| Reverse(a.start.display)); + + // First, figure out where each label will be positioned. + // + // In the case where you have the following annotations: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here [C] + // || + // |this makes no sense [B] + // previous borrow of `vec` occurs here [A] + // + // `annotations_position` will hold [(2, A), (1, B), (0, C)]. + // + // We try, when possible, to stick the rightmost annotation at the end + // of the highlight line: + // + // vec.push(vec.pop().unwrap()); + // --- --- - previous borrow ends here + // + // But sometimes that's not possible because one of the other + // annotations overlaps it. For example, from the test + // `span_overlap_label`, we have the following annotations + // (written on distinct lines for clarity): + // + // fn foo(x: u32) { + // -------------- + // - + // + // In this case, we can't stick the rightmost-most label on + // the highlight line, or we would get: + // + // fn foo(x: u32) { + // -------- x_span + // | + // fn_span + // + // which is totally weird. Instead we want: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // fn_span + // + // which is...less weird, at least. In fact, in general, if + // the rightmost span overlaps with any other span, we should + // use the "hang below" version, so we can at least make it + // clear where the span *starts*. There's an exception for this + // logic, when the labels do not have a message: + // + // fn foo(x: u32) { + // -------------- + // | + // x_span + // + // instead of: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // + // + let mut annotations_position = vec![]; + let mut line_len: usize = 0; + let mut p = 0; + for (i, annotation) in annotations.iter().enumerate() { + for (j, next) in annotations.iter().enumerate() { + if overlaps(next, annotation, 0) // This label overlaps with another one and both + && annotation.has_label() // take space (they have text and are not + && j > i // multiline lines). + && p == 0 + // We're currently on the first line, move the label one line down + { + // If we're overlapping with an un-labelled annotation with the same span + // we can just merge them in the output + if next.start.display == annotation.start.display + && next.end.display == annotation.end.display + && !next.has_label() + { + continue; + } + + // This annotation needs a new line in the output. + p += 1; + break; + } + } + annotations_position.push((p, annotation)); + for (j, next) in annotations.iter().enumerate() { + if j > i { + let l = next.label.as_ref().map_or(0, |label| label.len() + 2); + if (overlaps(next, annotation, l) // Do not allow two labels to be in the same + // line if they overlap including padding, to + // avoid situations like: + // + // fn foo(x: u32) { + // -------^------ + // | | + // fn_spanx_span + // + && annotation.has_label() // Both labels must have some text, otherwise + && next.has_label()) // they are not overlapping. + // Do not add a new line if this annotation + // or the next are vertical line placeholders. + || (annotation.takes_space() // If either this or the next annotation is + && next.has_label()) // multiline start/end, move it to a new line + || (annotation.has_label() // so as not to overlap the horizontal lines. + && next.takes_space()) + || (annotation.takes_space() && next.takes_space()) + || (overlaps(next, annotation, l) + && next.end.display <= annotation.end.display + && next.has_label() + && p == 0) + // Avoid #42595. + { + // This annotation needs a new line in the output. + p += 1; + break; + } + } + } + line_len = max(line_len, p); + } + + if line_len != 0 { + line_len += 1; + } + + // If there are no annotations or the only annotations on this line are + // MultilineLine, then there's only code being shown, stop processing. + if line_info.annotations.iter().all(LineAnnotation::is_line) { + return vec![]; + } + + if annotations_position + .iter() + .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_))) + { + if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() { + // Special case the following, so that we minimize overlapping multiline spans. + // + // 3 │ X0 Y0 Z0 + // │ ┏━━━━━┛ │ │ < We are writing these lines + // │ ┃┌───────┘ │ < by reverting the "depth" of + // │ ┃│┌─────────┘ < their multiline spans. + // 4 │ ┃││ X1 Y1 Z1 + // 5 │ ┃││ X2 Y2 Z2 + // │ ┃│└────╿──│──┘ `Z` label + // │ ┃└─────│──┤ + // │ ┗━━━━━━┥ `Y` is a good letter too + // ╰╴ `X` is a good letter + for (pos, _) in &mut annotations_position { + *pos = max_pos - *pos; + } + // We know then that we don't need an additional line for the span label, saving us + // one line of vertical space. + line_len = line_len.saturating_sub(1); + } + } + + // Write the column separator. + // + // After this we will have: + // + // 2 | fn foo() { + // | + // | + // | + // 3 | + // 4 | } + // | + for pos in 0..=line_len { + self.draw_col_separator_no_space(buffer, line_offset + pos + 1, width_offset - 2); + } + if close_window { + self.draw_col_separator_end(buffer, line_offset + line_len + 1, width_offset - 2); + } + // Write the horizontal lines for multiline annotations + // (only the first and last lines need this). + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | + // | + // 3 | + // 4 | } + // | _ + for &(pos, annotation) in &annotations_position { + let underline = self.underline(annotation.is_primary()); + let pos = pos + 1; + match annotation.annotation_type { + LineAnnotationType::MultilineStart(depth) + | LineAnnotationType::MultilineEnd(depth) => { + self.draw_range( + buffer, + underline.multiline_horizontal, + line_offset + pos, + width_offset + depth, + (code_offset + annotation.start.display).saturating_sub(left), + underline.style, + ); + } + _ if annotation.highlight_source => { + buffer.set_style_range( + line_offset, + (code_offset + annotation.start.display).saturating_sub(left), + (code_offset + annotation.end.display).saturating_sub(left), + underline.style, + annotation.is_primary(), + ); + } + _ => {} + } + } + + // Write the vertical lines for labels that are on a different line as the underline. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | | + // | | + // 3 | | + // 4 | | } + // | |_ + for &(pos, annotation) in &annotations_position { + let underline = self.underline(annotation.is_primary()); + let pos = pos + 1; + + if pos > 1 && (annotation.has_label() || annotation.takes_space()) { + for p in line_offset + 1..=line_offset + pos { + buffer.putc( + p, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineLine(_) => underline.multiline_vertical, + _ => underline.vertical_text_line, + }, + underline.style, + ); + } + if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start.display).saturating_sub(left), + underline.bottom_right, + underline.style, + ); + } + if matches!( + annotation.annotation_type, + LineAnnotationType::MultilineEnd(_) + ) && annotation.has_label() + { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start.display).saturating_sub(left), + underline.multiline_bottom_right_with_text, + underline.style, + ); + } + } + match annotation.annotation_type { + LineAnnotationType::MultilineStart(depth) => { + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.top_left, + underline.style, + ); + for p in line_offset + pos + 1..line_offset + line_len + 2 { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + } + LineAnnotationType::MultilineEnd(depth) => { + for p in line_offset..line_offset + pos { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.bottom_left, + underline.style, + ); + } + _ => (), + } + } + + // Write the labels on the annotations that actually have a label. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _ test + for &(pos, annotation) in &annotations_position { + let style = if annotation.is_primary() { + ElementStyle::LabelPrimary + } else { + ElementStyle::LabelSecondary + }; + let (pos, col) = if pos == 0 { + if annotation.end.display == 0 { + (pos + 1, (annotation.end.display + 2).saturating_sub(left)) + } else { + (pos + 1, (annotation.end.display + 1).saturating_sub(left)) + } + } else { + (pos + 2, annotation.start.display.saturating_sub(left)) + }; + if let Some(label) = annotation.label { + buffer.puts(line_offset + pos, code_offset + col, label, style); + } + } + + // Sort from biggest span to smallest span so that smaller spans are + // represented in the output: + // + // x | fn foo() + // | ^^^---^^ + // | | | + // | | something about `foo` + // | something about `fn foo()` + annotations_position.sort_by_key(|(_, ann)| { + // Decreasing order. When annotations share the same length, prefer `Primary`. + (Reverse(ann.len()), ann.is_primary()) + }); + + // Write the underlines. + // + // After this we will have: + // + // 2 | fn foo() { + // | ____-_____^ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _^ test + for &(pos, annotation) in &annotations_position { + let uline = self.underline(annotation.is_primary()); + for p in annotation.start.display..annotation.end.display { + // The default span label underline. + buffer.putc( + line_offset + 1, + (code_offset + p).saturating_sub(left), + uline.underline, + uline.style, + ); + } + + if pos == 0 + && matches!( + annotation.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on the same line. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineStart(_) => uline.top_right_flat, + LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 + && matches!( + annotation.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on another line, + // so we start going down first. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineStart(_) => uline.multiline_start_down, + LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 && annotation.has_label() { + // The beginning of a span label with an actual label, we'll point down. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + uline.label_start, + uline.style, + ); + } + } + annotations_position + .iter() + .filter_map(|&(_, annotation)| match annotation.annotation_type { + LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => { + let style = if annotation.is_primary() { + ElementStyle::LabelPrimary + } else { + ElementStyle::LabelSecondary + }; + Some((p, style)) + } + _ => None, + }) + .collect::>() + } + + fn emit_suggestion_default( + &self, + buffer: &mut StyledBuffer, + suggestion: &Snippet<'_, Patch<'_>>, + max_line_num_len: usize, + sm: &SourceMap<'_>, + primary_origin: Option<&str>, + is_cont: bool, + ) { + let suggestions = sm.splice_lines(suggestion.markers.clone()); + + let buffer_offset = buffer.num_lines(); + let mut row_num = buffer_offset + usize::from(!is_cont); + for (i, (complete, parts, highlights)) in suggestions.iter().enumerate() { + let has_deletion = parts + .iter() + .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm)); + let is_multiline = complete.lines().count() > 1; + + if i == 0 { + self.draw_col_separator_start(buffer, row_num - 1, max_line_num_len + 1); + } else { + buffer.puts( + row_num - 1, + max_line_num_len + 1, + self.multi_suggestion_separator(), + ElementStyle::LineNumber, + ); + } + if suggestion.origin != primary_origin { + if let Some(origin) = suggestion.origin { + let (loc, _) = sm.span_to_locations(parts[0].span.clone()); + // --> file.rs:line:col + // | + let arrow = self.file_start(); + buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber); + let message = format!("{}:{}:{}", origin, loc.line, loc.char + 1); + if is_cont { + buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn); + } else { + let col = usize::max(max_line_num_len + 1, arrow.len()); + buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn); + } + for _ in 0..max_line_num_len { + buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle); + } + self.draw_col_separator_no_space(buffer, row_num, max_line_num_len + 1); + row_num += 1; + } + } + let show_code_change = if has_deletion && !is_multiline { + DisplaySuggestion::Diff + } else if parts.len() == 1 + && parts.first().map_or(false, |p| { + p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim() + }) + { + // We are adding a line(s) of code before code that was already there. + DisplaySuggestion::Add + } else if (parts.len() != 1 || parts[0].replacement.trim() != complete.trim()) + && !is_multiline + { + DisplaySuggestion::Underline + } else { + DisplaySuggestion::None + }; + + if let DisplaySuggestion::Diff = show_code_change { + row_num += 1; + } + + let file_lines = sm.span_to_lines(parts[0].span.clone()); + let (line_start, line_end) = sm.span_to_locations(parts[0].span.clone()); + let mut lines = complete.lines(); + if lines.clone().next().is_none() { + // Account for a suggestion to completely remove a line(s) with whitespace (#94192). + for line in line_start.line..=line_end.line { + buffer.puts( + row_num - 1 + line - line_start.line, + 0, + &self.maybe_anonymized(line), + ElementStyle::LineNumber, + ); + buffer.puts( + row_num - 1 + line - line_start.line, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + buffer.puts( + row_num - 1 + line - line_start.line, + max_line_num_len + 3, + &normalize_whitespace(sm.get_line(line).unwrap()), + ElementStyle::Removal, + ); + } + row_num += line_end.line - line_start.line; + } + let mut last_pos = 0; + let mut is_item_attribute = false; + let mut unhighlighted_lines = Vec::new(); + for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() { + last_pos = line_pos; + + // Remember lines that are not highlighted to hide them if needed + if highlight_parts.is_empty() { + unhighlighted_lines.push((line_pos, line)); + continue; + } + if highlight_parts.len() == 1 + && line.trim().starts_with("#[") + && line.trim().ends_with(']') + { + is_item_attribute = true; + } + + match unhighlighted_lines.len() { + 0 => (), + // Since we show first line, "..." line and last line, + // There is no reason to hide if there are 3 or less lines + // (because then we just replace a line with ... which is + // not helpful) + n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| { + self.draw_code_line( + buffer, + &mut row_num, + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + }), + // Print first unhighlighted line, "..." and last unhighlighted line, like so: + // + // LL | this line was highlighted + // LL | this line is just for context + // ... + // LL | this line is just for context + // LL | this line was highlighted + _ => { + let last_line = unhighlighted_lines.pop(); + let first_line = unhighlighted_lines.drain(..).next(); + + if let Some((p, l)) = first_line { + self.draw_code_line( + buffer, + &mut row_num, + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + + let placeholder = self.margin(); + let padding = str_width(placeholder); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + placeholder, + ElementStyle::LineNumber, + ); + row_num += 1; + + if let Some((p, l)) = last_line { + self.draw_code_line( + buffer, + &mut row_num, + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + } + } + self.draw_code_line( + buffer, + &mut row_num, + highlight_parts, + line_pos + line_start.line, + line, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } - /// Render a snippet into a `Display`able object - pub fn render<'a>(&'a self, msg: Message<'a>) -> impl Display + 'a { - DisplayList::new( - msg, - &self.stylesheet, - self.anonymized_line_numbers, - self.term_width, + if matches!(show_code_change, DisplaySuggestion::Add) && is_item_attribute { + // The suggestion adds an entire line of code, ending on a newline, so we'll also + // print the *following* line, to provide context of what we're advising people to + // do. Otherwise you would only see contextless code that can be confused for + // already existing code, despite the colors and UI elements. + // We special case `#[derive(_)]\n` and other attribute suggestions, because those + // are the ones where context is most useful. + let file_lines = sm.span_to_lines(parts[0].span.end..parts[0].span.end); + let (lo, _) = sm.span_to_locations(parts[0].span.clone()); + let line_num = lo.line; + if let Some(line) = sm.get_line(line_num) { + let line = normalize_whitespace(line); + self.draw_code_line( + buffer, + &mut row_num, + &[], + line_num + last_pos + 1, + &line, + DisplaySuggestion::None, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + } + // This offset and the ones below need to be signed to account for replacement code + // that is shorter than the original code. + let mut offsets: Vec<(usize, isize)> = Vec::new(); + // Only show an underline in the suggestions if the suggestion is not the + // entirety of the code being shown and the displayed code is not multiline. + if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add = + show_code_change + { + for part in parts { + let (span_start, span_end) = sm.span_to_locations(part.span.clone()); + let span_start_pos = span_start.display; + let span_end_pos = span_end.display; + + // If this addition is _only_ whitespace, then don't trim it, + // or else we're just not rendering anything. + let is_whitespace_addition = part.replacement.trim().is_empty(); + + // Do not underline the leading... + let start = if is_whitespace_addition { + 0 + } else { + part.replacement + .len() + .saturating_sub(part.replacement.trim_start().len()) + }; + // ...or trailing spaces. Account for substitutions containing unicode + // characters. + let sub_len: usize = str_width(if is_whitespace_addition { + part.replacement + } else { + part.replacement.trim() + }); + + let offset: isize = offsets + .iter() + .filter_map(|(start, v)| { + if span_start_pos < *start { + None + } else { + Some(v) + } + }) + .sum(); + let underline_start = (span_start_pos + start) as isize + offset; + let underline_end = (span_start_pos + start + sub_len) as isize + offset; + assert!(underline_start >= 0 && underline_end >= 0); + let padding: usize = max_line_num_len + 3; + for p in underline_start..underline_end { + if matches!(show_code_change, DisplaySuggestion::Underline) + && is_different(sm, part.replacement, part.span.clone()) + { + // If this is a replacement, underline with `~`, if this is an addition + // underline with `+`. + buffer.putc( + row_num, + (padding as isize + p) as usize, + if part.is_addition(sm) { + '+' + } else { + self.diff() + }, + ElementStyle::Addition, + ); + } + } + if let DisplaySuggestion::Diff = show_code_change { + // Colorize removal with red in diff format. + buffer.set_style_range( + row_num - 2, + (padding as isize + span_start_pos as isize) as usize, + (padding as isize + span_end_pos as isize) as usize, + ElementStyle::Removal, + true, + ); + } + + // length of the code after substitution + let full_sub_len = str_width(part.replacement) as isize; + + // length of the code to be substituted + let snippet_len = span_end_pos as isize - span_start_pos as isize; + // For multiple substitutions, use the position *after* the previous + // substitutions have happened, only when further substitutions are + // located strictly after. + offsets.push((span_end_pos, full_sub_len - snippet_len)); + } + row_num += 1; + } + + // if we elided some lines, add an ellipsis + if lines.next().is_some() { + let placeholder = self.margin(); + let padding = str_width(placeholder); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + placeholder, + ElementStyle::LineNumber, + ); + } else { + let row = match show_code_change { + DisplaySuggestion::Diff + | DisplaySuggestion::Add + | DisplaySuggestion::Underline => row_num - 1, + DisplaySuggestion::None => row_num, + }; + self.draw_col_separator_end(buffer, row, max_line_num_len + 1); + row_num = row + 1; + } + } + } + + #[allow(clippy::too_many_arguments)] + fn draw_code_line( + &self, + buffer: &mut StyledBuffer, + row_num: &mut usize, + highlight_parts: &[SubstitutionHighlight], + line_num: usize, + line_to_add: &str, + show_code_change: DisplaySuggestion, + max_line_num_len: usize, + file_lines: &[&LineInfo<'_>], + is_multiline: bool, + ) { + if let DisplaySuggestion::Diff = show_code_change { + // We need to print more than one line if the span we need to remove is multiline. + // For more info: https://github.com/rust-lang/rust/issues/92741 + let lines_to_remove = file_lines.iter().take(file_lines.len() - 1); + for (index, line_to_remove) in lines_to_remove.enumerate() { + buffer.puts( + *row_num - 1, + 0, + &self.maybe_anonymized(line_num + index), + ElementStyle::LineNumber, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + let line = normalize_whitespace(line_to_remove.line); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &line, + ElementStyle::NoStyle, + ); + *row_num += 1; + } + // If the last line is exactly equal to the line we need to add, we can skip both of + // them. This allows us to avoid output like the following: + // 2 - & + // 2 + if true { true } else { false } + // 3 - if true { true } else { false } + // If those lines aren't equal, we print their diff + let last_line = &file_lines.last().unwrap(); + if last_line.line == line_to_add { + *row_num -= 2; + } else { + buffer.puts( + *row_num - 1, + 0, + &self.maybe_anonymized(line_num + file_lines.len() - 1), + ElementStyle::LineNumber, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &normalize_whitespace(last_line.line), + ElementStyle::NoStyle, + ); + if line_to_add.trim().is_empty() { + *row_num -= 1; + } else { + // Check if after the removal, the line is left with only whitespace. If so, we + // will not show an "addition" line, as removing the whole line is what the user + // would really want. + // For example, for the following: + // | + // 2 - .await + // 2 + (note the left over whitespace) + // | + // We really want + // | + // 2 - .await + // | + // *row_num -= 1; + buffer.puts( + *row_num, + 0, + &self.maybe_anonymized(line_num), + ElementStyle::LineNumber, + ); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } + } + } else if is_multiline { + buffer.puts( + *row_num, + 0, + &self.maybe_anonymized(line_num), + ElementStyle::LineNumber, + ); + match &highlight_parts { + [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => { + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + } + [] => { + // FIXME: needed? Doesn't get exercised in any test. + self.draw_col_separator_no_space(buffer, *row_num, max_line_num_len + 1); + } + _ => { + let diff = self.diff(); + buffer.puts( + *row_num, + max_line_num_len + 1, + &format!("{diff} "), + ElementStyle::Addition, + ); + } + } + // LL | line_to_add + // ++^^^ + // | | + // | magic `3` + // `max_line_num_len` + buffer.puts( + *row_num, + max_line_num_len + 3, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } else if let DisplaySuggestion::Add = show_code_change { + buffer.puts( + *row_num, + 0, + &self.maybe_anonymized(line_num), + ElementStyle::LineNumber, + ); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } else { + buffer.puts( + *row_num, + 0, + &self.maybe_anonymized(line_num), + ElementStyle::LineNumber, + ); + self.draw_col_separator(buffer, *row_num, max_line_num_len + 1); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } + + // Colorize addition/replacements with green. + for &SubstitutionHighlight { start, end } in highlight_parts { + // This is a no-op for empty ranges + if start != end { + // Account for tabs when highlighting (#87972). + let tabs: usize = line_to_add + .chars() + .take(start) + .map(|ch| match ch { + '\t' => 3, + _ => 0, + }) + .sum(); + buffer.set_style_range( + *row_num, + max_line_num_len + 3 + start + tabs, + max_line_num_len + 3 + end + tabs, + ElementStyle::Addition, + true, + ); + } + } + *row_num += 1; + } + + #[allow(clippy::too_many_arguments)] + fn draw_line( + &self, + buffer: &mut StyledBuffer, + source_string: &str, + line_index: usize, + line_offset: usize, + width_offset: usize, + code_offset: usize, + max_line_num_len: usize, + margin: Margin, + ) { + // Tabs are assumed to have been replaced by spaces in calling code. + debug_assert!(!source_string.contains('\t')); + let line_len = str_width(source_string); + // Create the source line we will highlight. + let left = margin.left(line_len); + let right = margin.right(line_len); + // FIXME: The following code looks fishy. See #132860. + // On long lines, we strip the source line, accounting for unicode. + let mut taken = 0; + let mut skipped = 0; + let code: String = source_string + .chars() + .skip_while(|ch| { + skipped += char_width(*ch); + skipped <= left + }) + .take_while(|ch| { + // Make sure that the trimming on the right will fall within the terminal width. + taken += char_width(*ch); + taken <= (right - left) + }) + .collect(); + + buffer.puts(line_offset, code_offset, &code, ElementStyle::Quotation); + let placeholder = self.margin(); + let padding = str_width(placeholder); + let (width_taken, bytes_taken) = if margin.was_cut_left() { + // We have stripped some code/whitespace from the beginning, make it clear. + let mut bytes_taken = 0; + let mut width_taken = 0; + for ch in code.chars() { + width_taken += char_width(ch); + bytes_taken += ch.len_utf8(); + + if width_taken >= padding { + break; + } + } + buffer.puts( + line_offset, + code_offset, + &format!("{placeholder:>width_taken$}"), + ElementStyle::LineNumber, + ); + (width_taken, bytes_taken) + } else { + (0, 0) + }; + + buffer.puts( + line_offset, + code_offset + width_taken, + &code[bytes_taken..], + ElementStyle::Quotation, + ); + + if margin.was_cut_right(line_len) { + // We have stripped some code/whitespace from the beginning, make it clear. + let mut char_taken = 0; + let mut width_taken_inner = 0; + for ch in code.chars().rev() { + width_taken_inner += char_width(ch); + char_taken += 1; + + if width_taken_inner >= padding { + break; + } + } + + buffer.puts( + line_offset, + code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken, + placeholder, + ElementStyle::LineNumber, + ); + } + + buffer.puts( + line_offset, + 0, + &format!("{:>max_line_num_len$}", self.maybe_anonymized(line_index)), + ElementStyle::LineNumber, + ); + + self.draw_col_separator_no_space(buffer, line_offset, width_offset - 2); + } + + fn draw_range( + &self, + buffer: &mut StyledBuffer, + symbol: char, + line: usize, + col_from: usize, + col_to: usize, + style: ElementStyle, + ) { + for col in col_from..col_to { + buffer.putc(line, col, symbol, style); + } + } + + fn draw_multiline_line( + &self, + buffer: &mut StyledBuffer, + line: usize, + offset: usize, + depth: usize, + style: ElementStyle, + ) { + let chr = match (style, self.theme) { + (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, OutputTheme::Ascii) => { + '|' + } + (_, OutputTheme::Ascii) => '|', + (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, OutputTheme::Unicode) => { + '┃' + } + (_, OutputTheme::Unicode) => '│', + }; + buffer.putc(line, offset + depth - 1, chr, style); + } + + fn col_separator(&self) -> char { + match self.theme { + OutputTheme::Ascii => '|', + OutputTheme::Unicode => '│', + } + } + + fn multi_suggestion_separator(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "|", + OutputTheme::Unicode => "├╴", + } + } + + fn draw_col_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let chr = self.col_separator(); + buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber); + } + + fn draw_col_separator_no_space(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let chr = self.col_separator(); + self.draw_col_separator_no_space_with_style( + buffer, + chr, + line, + col, + ElementStyle::LineNumber, + ); + } + + fn draw_col_separator_start(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + match self.theme { + OutputTheme::Ascii => { + self.draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + ElementStyle::LineNumber, + ); + } + OutputTheme::Unicode => { + self.draw_col_separator_no_space_with_style( + buffer, + '╭', + line, + col, + ElementStyle::LineNumber, + ); + self.draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + ElementStyle::LineNumber, + ); + } + } + } + + fn draw_col_separator_end(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + match self.theme { + OutputTheme::Ascii => { + self.draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + ElementStyle::LineNumber, + ); + } + OutputTheme::Unicode => { + self.draw_col_separator_no_space_with_style( + buffer, + '╰', + line, + col, + ElementStyle::LineNumber, + ); + self.draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + ElementStyle::LineNumber, + ); + } + } + } + + fn draw_col_separator_no_space_with_style( + &self, + buffer: &mut StyledBuffer, + chr: char, + line: usize, + col: usize, + style: ElementStyle, + ) { + buffer.putc(line, col, chr, style); + } + + fn maybe_anonymized(&self, line_num: usize) -> Cow<'static, str> { + if self.anonymized_line_numbers { + Cow::Borrowed(ANONYMIZED_LINE_NUM) + } else { + Cow::Owned(line_num.to_string()) + } + } + + fn file_start(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "--> ", + OutputTheme::Unicode => " ╭▸ ", + } + } + + fn secondary_file_start(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "::: ", + OutputTheme::Unicode => " ⸬ ", + } + } + + fn draw_note_separator( + &self, + buffer: &mut StyledBuffer, + line: usize, + col: usize, + is_cont: bool, + ) { + let chr = match self.theme { + OutputTheme::Ascii => "= ", + OutputTheme::Unicode if is_cont => "├ ", + OutputTheme::Unicode => "╰ ", + }; + buffer.puts(line, col, chr, ElementStyle::LineNumber); + } + + fn diff(&self) -> char { + match self.theme { + OutputTheme::Ascii => '~', + OutputTheme::Unicode => '±', + } + } + + fn draw_line_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let (column, dots) = match self.theme { + OutputTheme::Ascii => (0, "..."), + OutputTheme::Unicode => (col - 2, "‡"), + }; + buffer.puts(line, column, dots, ElementStyle::LineNumber); + } + + fn margin(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "...", + OutputTheme::Unicode => "…", + } + } + + fn underline(&self, is_primary: bool) -> UnderlineParts { + // X0 Y0 + // label_start > ┯━━━━ < underline + // │ < vertical_text_line + // text + + // multiline_start_down ⤷ X0 Y0 + // top_left > ┌───╿──┘ < top_right_flat + // top_left > ┏│━━━┙ < top_right + // multiline_vertical > ┃│ + // ┃│ X1 Y1 + // ┃│ X2 Y2 + // ┃└────╿──┘ < multiline_end_same_line + // bottom_left > ┗━━━━━┥ < bottom_right_with_text + // multiline_horizontal ^ `X` is a good letter + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┗━━━━┛ < multiline_end_same_line + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┃ ╿ < multiline_end_up + // ┗━━┛ < bottom_right + + match (self.theme, is_primary) { + (OutputTheme::Ascii, true) => UnderlineParts { + style: ElementStyle::UnderlinePrimary, + underline: '^', + label_start: '^', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '^', + bottom_right: '|', + top_left: ' ', + top_right_flat: '^', + bottom_left: '|', + multiline_end_up: '^', + multiline_end_same_line: '^', + multiline_bottom_right_with_text: '|', + }, + (OutputTheme::Ascii, false) => UnderlineParts { + style: ElementStyle::UnderlineSecondary, + underline: '-', + label_start: '-', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '-', + bottom_right: '|', + top_left: ' ', + top_right_flat: '-', + bottom_left: '|', + multiline_end_up: '-', + multiline_end_same_line: '-', + multiline_bottom_right_with_text: '|', + }, + (OutputTheme::Unicode, true) => UnderlineParts { + style: ElementStyle::UnderlinePrimary, + underline: '━', + label_start: '┯', + vertical_text_line: '│', + multiline_vertical: '┃', + multiline_horizontal: '━', + multiline_whole_line: '┏', + multiline_start_down: '╿', + bottom_right: '┙', + top_left: '┏', + top_right_flat: '┛', + bottom_left: '┗', + multiline_end_up: '╿', + multiline_end_same_line: '┛', + multiline_bottom_right_with_text: '┥', + }, + (OutputTheme::Unicode, false) => UnderlineParts { + style: ElementStyle::UnderlineSecondary, + underline: '─', + label_start: '┬', + vertical_text_line: '│', + multiline_vertical: '│', + multiline_horizontal: '─', + multiline_whole_line: '┌', + multiline_start_down: '│', + bottom_right: '┘', + top_left: '┌', + top_right_flat: '┘', + bottom_left: '└', + multiline_end_up: '│', + multiline_end_same_line: '┘', + multiline_bottom_right_with_text: '┤', + }, + } + } +} + +// instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until +// we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which +// is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway. +// This is also why we need the max number of decimal digits within a `usize`. +fn num_decimal_digits(num: usize) -> usize { + #[cfg(target_pointer_width = "64")] + const MAX_DIGITS: usize = 20; + + #[cfg(target_pointer_width = "32")] + const MAX_DIGITS: usize = 10; + + #[cfg(target_pointer_width = "16")] + const MAX_DIGITS: usize = 5; + + let mut lim = 10; + for num_digits in 1..MAX_DIGITS { + if num < lim { + return num_digits; + } + lim = lim.wrapping_mul(10); + } + MAX_DIGITS +} + +pub fn str_width(s: &str) -> usize { + s.chars().map(char_width).sum() +} + +pub fn char_width(ch: char) -> usize { + // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now, + // just accept that sometimes the code line will be longer than desired. + match ch { + '\t' => 4, + // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These + // are control points that we replace before printing with a visible codepoint for the sake + // of being able to point at them with underlines. + '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}' + | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}' + | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}' + | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}' + | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}' + | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}' + | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1, + _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1), + } +} + +fn num_overlap( + a_start: usize, + a_end: usize, + b_start: usize, + b_end: usize, + inclusive: bool, +) -> bool { + let extra = usize::from(inclusive); + (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start) +} + +fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool { + num_overlap( + a1.start.display, + a1.end.display + padding, + a2.start.display, + a2.end.display, + false, + ) +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum LineAnnotationType { + /// Annotation under a single line of code + Singleline, + + // The Multiline type above is replaced with the following three in order + // to reuse the current label drawing code. + // + // Each of these corresponds to one part of the following diagram: + // + // x | foo(1 + bar(x, + // | _________^ < MultilineStart + // x | | y), < MultilineLine + // | |______________^ label < MultilineEnd + // x | z); + /// Annotation marking the first character of a fully shown multiline span + MultilineStart(usize), + /// Annotation marking the last character of a fully shown multiline span + MultilineEnd(usize), + /// Line at the left enclosing the lines of a fully shown multiline span + // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4 + // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in + // `draw_multiline_line`. + MultilineLine(usize), +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct LineAnnotation<'a> { + /// Start column. + /// Note that it is important that this field goes + /// first, so that when we sort, we sort orderings by start + /// column. + pub start: Loc, + + /// End column within the line (exclusive) + pub end: Loc, + + /// level + pub kind: AnnotationKind, + + /// Optional label to display adjacent to the annotation. + pub label: Option<&'a str>, + + /// Is this a single line, multiline or multiline span minimized down to a + /// smaller span. + pub annotation_type: LineAnnotationType, + + /// Whether the source code should be highlighted + pub highlight_source: bool, +} + +impl LineAnnotation<'_> { + pub(crate) fn is_primary(&self) -> bool { + self.kind == AnnotationKind::Primary + } + + /// Whether this annotation is a vertical line placeholder. + pub(crate) fn is_line(&self) -> bool { + matches!(self.annotation_type, LineAnnotationType::MultilineLine(_)) + } + + /// Length of this annotation as displayed in the stderr output + pub(crate) fn len(&self) -> usize { + // Account for usize underflows + if self.end.display > self.start.display { + self.end.display - self.start.display + } else { + self.start.display - self.end.display + } + } + + pub(crate) fn has_label(&self) -> bool { + if let Some(label) = self.label { + // Consider labels with no text as effectively not being there + // to avoid weird output with unnecessary vertical lines, like: + // + // X | fn foo(x: u32) { + // | -------^------ + // | | | + // | | + // | + // + // Note that this would be the complete output users would see. + !label.is_empty() + } else { + false + } + } + + pub(crate) fn takes_space(&self) -> bool { + // Multiline annotations always have to keep vertical space. + matches!( + self.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) ) } } + +#[derive(Clone, Copy, Debug)] +pub(crate) enum DisplaySuggestion { + Underline, + Diff, + None, + Add, +} + +// We replace some characters so the CLI output is always consistent and underlines aligned. +// Keep the following list in sync with `rustc_span::char_width`. +const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ + // In terminals without Unicode support the following will be garbled, but in *all* terminals + // the underlying codepoint will be as well. We could gate this replacement behind a "unicode + // support" gate. + ('\0', "␀"), + ('\u{0001}', "␁"), + ('\u{0002}', "␂"), + ('\u{0003}', "␃"), + ('\u{0004}', "␄"), + ('\u{0005}', "␅"), + ('\u{0006}', "␆"), + ('\u{0007}', "␇"), + ('\u{0008}', "␈"), + ('\t', " "), // We do our own tab replacement + ('\u{000b}', "␋"), + ('\u{000c}', "␌"), + ('\u{000d}', "␍"), + ('\u{000e}', "␎"), + ('\u{000f}', "␏"), + ('\u{0010}', "␐"), + ('\u{0011}', "␑"), + ('\u{0012}', "␒"), + ('\u{0013}', "␓"), + ('\u{0014}', "␔"), + ('\u{0015}', "␕"), + ('\u{0016}', "␖"), + ('\u{0017}', "␗"), + ('\u{0018}', "␘"), + ('\u{0019}', "␙"), + ('\u{001a}', "␚"), + ('\u{001b}', "␛"), + ('\u{001c}', "␜"), + ('\u{001d}', "␝"), + ('\u{001e}', "␞"), + ('\u{001f}', "␟"), + ('\u{007f}', "␡"), + ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters. + ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently + ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk + ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always. + ('\u{202d}', "�"), + ('\u{202e}', "�"), + ('\u{2066}', "�"), + ('\u{2067}', "�"), + ('\u{2068}', "�"), + ('\u{2069}', "�"), +]; + +pub(crate) fn normalize_whitespace(s: &str) -> String { + // Scan the input string for a character in the ordered table above. + // If it's present, replace it with its alternative string (it can be more than 1 char!). + // Otherwise, retain the input char. + s.chars().fold(String::with_capacity(s.len()), |mut s, c| { + match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) { + Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1), + _ => s.push(c), + } + s + }) +} + +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum ElementStyle { + MainHeaderMsg, + HeaderMsg, + LineAndColumn, + LineNumber, + Quotation, + UnderlinePrimary, + UnderlineSecondary, + LabelPrimary, + LabelSecondary, + NoStyle, + Level(LevelInner), + Addition, + Removal, +} + +impl ElementStyle { + fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style { + match self { + ElementStyle::Addition => stylesheet.addition, + ElementStyle::Removal => stylesheet.removal, + ElementStyle::LineAndColumn => stylesheet.none, + ElementStyle::LineNumber => stylesheet.line_num, + ElementStyle::Quotation => stylesheet.none, + ElementStyle::MainHeaderMsg => stylesheet.emphasis, + ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet), + ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context, + ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none, + ElementStyle::Level(lvl) => lvl.style(stylesheet), + } + } +} + +#[derive(Debug, Clone, Copy)] +struct UnderlineParts { + style: ElementStyle, + underline: char, + label_start: char, + vertical_text_line: char, + multiline_vertical: char, + multiline_horizontal: char, + multiline_whole_line: char, + multiline_start_down: char, + bottom_right: char, + top_left: char, + top_right_flat: char, + bottom_left: char, + multiline_end_up: char, + multiline_end_same_line: char, + multiline_bottom_right_with_text: char, +} + +/// Whether the original and suggested code are the same. +pub(crate) fn is_different(sm: &SourceMap<'_>, suggested: &str, range: Range) -> bool { + match sm.span_to_snippet(range) { + Some(s) => s != suggested, + None => true, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputTheme { + Ascii, + Unicode, +} + +#[cfg(test)] +mod test { + use super::OUTPUT_REPLACEMENTS; + use snapbox::IntoData; + + fn format_replacements(replacements: Vec<(char, &str)>) -> String { + replacements + .into_iter() + .map(|r| format!(" {r:?}")) + .collect::>() + .join("\n") + } + + #[test] + /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to + /// work) and must contain no duplicate entries + fn ensure_output_replacements_is_sorted() { + let mut expected = OUTPUT_REPLACEMENTS.to_owned(); + expected.sort_by_key(|r| r.0); + expected.dedup_by_key(|r| r.0); + let expected = format_replacements(expected); + let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned()); + snapbox::assert_data_eq!(actual, expected.into_data().raw()); + } +} diff --git a/src/renderer/source_map.rs b/src/renderer/source_map.rs new file mode 100644 index 00000000..d014bb01 --- /dev/null +++ b/src/renderer/source_map.rs @@ -0,0 +1,662 @@ +use crate::renderer::{char_width, is_different, num_overlap, LineAnnotation, LineAnnotationType}; +use crate::{Annotation, AnnotationKind, Patch}; +use std::cmp::{max, min}; +use std::ops::Range; + +#[derive(Debug)] +pub(crate) struct SourceMap<'a> { + lines: Vec>, + pub(crate) source: &'a str, +} + +impl<'a> SourceMap<'a> { + pub(crate) fn new(source: &'a str, line_start: usize) -> Self { + let mut current_index = 0; + + let mut mapping = vec![]; + for (idx, (line, end_line)) in CursorLines::new(source).enumerate() { + let line_length = line.len(); + let line_range = current_index..current_index + line_length; + let end_line_size = end_line.len(); + + mapping.push(LineInfo { + line, + line_index: line_start + idx, + start_byte: line_range.start, + end_byte: line_range.end + end_line_size, + end_line_size, + }); + + current_index += line_length + end_line_size; + } + Self { + lines: mapping, + source, + } + } + + pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> { + self.lines + .iter() + .find(|l| l.line_index == idx) + .map(|info| info.line) + } + + pub(crate) fn span_to_locations(&self, span: Range) -> (Loc, Loc) { + let start_info = self + .lines + .iter() + .find(|info| span.start >= info.start_byte && span.start < info.end_byte) + .unwrap_or(self.lines.last().unwrap()); + let (mut start_char_pos, start_display_pos) = start_info.line + [0..(span.start - start_info.start_byte).min(start_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + // correct the char pos if we are highlighting the end of a line + if (span.start - start_info.start_byte).saturating_sub(start_info.line.len()) > 0 { + start_char_pos += 1; + } + let start = Loc { + line: start_info.line_index, + char: start_char_pos, + display: start_display_pos, + byte: span.start, + }; + + if span.start == span.end { + return (start, start); + } + + let end_info = self + .lines + .iter() + .find(|info| info.end_byte > span.end.saturating_sub(1)) + .unwrap_or(self.lines.last().unwrap()); + let (mut end_char_pos, end_display_pos) = end_info.line + [0..(span.end - end_info.start_byte).min(end_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + + // correct the char pos if we are highlighting the end of a line + if (span.end - end_info.start_byte).saturating_sub(end_info.line.len()) > 0 { + end_char_pos += 1; + } + let mut end = Loc { + line: end_info.line_index, + char: end_char_pos, + display: end_display_pos, + byte: span.end, + }; + if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size { + end.char += 1; + end.display += 1; + } + + (start, end) + } + + pub(crate) fn span_to_snippet(&self, span: Range) -> Option<&str> { + self.source.get(span) + } + + pub(crate) fn span_to_lines(&self, span: Range) -> Vec<&LineInfo<'a>> { + let mut lines = vec![]; + let start = span.start; + let end = span.end; + for line_info in &self.lines { + if start >= line_info.end_byte { + continue; + } + if end <= line_info.start_byte { + break; + } + lines.push(line_info); + } + lines + } + + pub(crate) fn annotated_lines( + &self, + annotations: Vec>, + fold: bool, + ) -> (usize, Vec>) { + let source_len = self.source.len(); + if let Some(bigger) = annotations.iter().find_map(|x| { + // Allow highlighting one past the last character in the source. + if source_len + 1 < x.span.end { + Some(&x.span) + } else { + None + } + }) { + panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`") + } + + let mut annotated_line_infos = self + .lines + .iter() + .map(|info| AnnotatedLineInfo { + line: info.line, + line_index: info.line_index, + annotations: vec![], + }) + .collect::>(); + let mut multiline_annotations = vec![]; + + for Annotation { + span, + label, + kind, + highlight_source, + } in annotations + { + let (lo, mut hi) = self.span_to_locations(span.clone()); + + // Watch out for "empty spans". If we get a span like 6..6, we + // want to just display a `^` at 6, so convert that to + // 6..7. This is degenerate input, but it's best to degrade + // gracefully -- and the parser likes to supply a span like + // that for EOF, in particular. + + if lo.display == hi.display && lo.line == hi.line { + hi.display += 1; + } + + if lo.line == hi.line { + let line_ann = LineAnnotation { + start: lo, + end: hi, + kind, + label, + annotation_type: LineAnnotationType::Singleline, + highlight_source, + }; + self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann); + } else { + multiline_annotations.push(MultilineAnnotation { + depth: 1, + start: lo, + end: hi, + kind, + label, + overlaps_exactly: false, + highlight_source, + }); + } + } + + let mut primary_spans = vec![]; + + // Find overlapping multiline annotations, put them at different depths + multiline_annotations + .sort_by_key(|ml| (ml.start.line, usize::MAX - ml.end.line, ml.start.byte)); + for ann in multiline_annotations.clone() { + if ann.kind.is_primary() { + primary_spans.push((ann.start, ann.end)); + } + for a in &mut multiline_annotations { + // Move all other multiline annotations overlapping with this one + // one level to the right. + if !ann.same_span(a) + && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true) + { + a.increase_depth(); + } else if ann.same_span(a) && &ann != a { + a.overlaps_exactly = true; + } else { + if primary_spans + .iter() + .any(|(s, e)| a.start == *s && a.end == *e) + { + a.kind = AnnotationKind::Primary; + } + break; + } + } + } + + let mut max_depth = 0; // max overlapping multiline spans + for ann in &multiline_annotations { + max_depth = max(max_depth, ann.depth); + } + // Change order of multispan depth to minimize the number of overlaps in the ASCII art. + for a in &mut multiline_annotations { + a.depth = max_depth - a.depth + 1; + } + for ann in multiline_annotations { + let mut end_ann = ann.as_end(); + if ann.overlaps_exactly { + end_ann.annotation_type = LineAnnotationType::Singleline; + } else { + // avoid output like + // + // | foo( + // | _____^ + // | |_____| + // | || bar, + // | || ); + // | || ^ + // | ||______| + // | |______foo + // | baz + // + // and instead get + // + // | foo( + // | _____^ + // | | bar, + // | | ); + // | | ^ + // | | | + // | |______foo + // | baz + self.add_annotation_to_file( + &mut annotated_line_infos, + ann.start.line, + ann.as_start(), + ); + // 4 is the minimum vertical length of a multiline span when presented: two lines + // of code and two lines of underline. This is not true for the special case where + // the beginning doesn't have an underline, but the current logic seems to be + // working correctly. + let middle = min(ann.start.line + 4, ann.end.line); + // We'll show up to 4 lines past the beginning of the multispan start. + // We will *not* include the tail of lines that are only whitespace, a comment or + // a bare delimiter. + let filter = |s: &str| { + let s = s.trim(); + // Consider comments as empty, but don't consider docstrings to be empty. + !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!"))) + // Consider lines with nothing but whitespace, a single delimiter as empty. + && !["", "{", "}", "(", ")", "[", "]"].contains(&s) + }; + let until = (ann.start.line..middle) + .rev() + .filter_map(|line| self.get_line(line).map(|s| (line + 1, s))) + .find(|(_, s)| filter(s)) + .map_or(ann.start.line, |(line, _)| line); + for line in ann.start.line + 1..until { + // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`). + self.add_annotation_to_file(&mut annotated_line_infos, line, ann.as_line()); + } + let line_end = ann.end.line - 1; + let end_is_empty = self.get_line(line_end).map_or(false, |s| !filter(s)); + if middle < line_end && !end_is_empty { + self.add_annotation_to_file(&mut annotated_line_infos, line_end, ann.as_line()); + } + } + self.add_annotation_to_file(&mut annotated_line_infos, end_ann.end.line, end_ann); + } + + if fold { + annotated_line_infos.retain(|l| !l.annotations.is_empty()); + } + + annotated_line_infos + .iter_mut() + .for_each(|l| l.annotations.sort_by(|a, b| a.start.cmp(&b.start))); + + (max_depth, annotated_line_infos) + } + + fn add_annotation_to_file( + &self, + annotated_line_infos: &mut Vec>, + line_index: usize, + line_ann: LineAnnotation<'a>, + ) { + if let Some(line_info) = annotated_line_infos + .iter_mut() + .find(|line_info| line_info.line_index == line_index) + { + line_info.annotations.push(line_ann); + } else { + let info = self + .lines + .iter() + .find(|l| l.line_index == line_index) + .unwrap(); + annotated_line_infos.push(AnnotatedLineInfo { + line: info.line, + line_index, + annotations: vec![line_ann], + }); + annotated_line_infos.sort_by_key(|l| l.line_index); + } + } + + pub(crate) fn splice_lines<'b>( + &'b self, + mut patches: Vec>, + ) -> Vec<(String, Vec>, Vec>)> { + fn push_trailing( + buf: &mut String, + line_opt: Option<&str>, + lo: &Loc, + hi_opt: Option<&Loc>, + ) -> usize { + let mut line_count = 0; + // Convert CharPos to Usize, as CharPose is character offset + // Extract low index and high index + let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char)); + if let Some(line) = line_opt { + if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) { + // Get high index while account for rare unicode and emoji with char_indices + let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi)); + match hi_opt { + // If high index exist, take string from low to high index + Some(hi) if hi > lo => { + // count how many '\n' exist + line_count = line[lo..hi].matches('\n').count(); + buf.push_str(&line[lo..hi]); + } + Some(_) => (), + // If high index absence, take string from low index till end string.len + None => { + // count how many '\n' exist + line_count = line[lo..].matches('\n').count(); + buf.push_str(&line[lo..]); + } + } + } + // If high index is None + if hi_opt.is_none() { + buf.push('\n'); + } + } + line_count + } + // Assumption: all spans are in the same file, and all spans + // are disjoint. Sort in ascending order. + patches.sort_by_key(|p| p.span.start); + + // Find the bounding span. + let Some(lo) = patches.iter().map(|p| p.span.start).min() else { + return Vec::new(); + }; + let Some(hi) = patches.iter().map(|p| p.span.end).max() else { + return Vec::new(); + }; + + let lines = self.span_to_lines(lo..hi); + + let mut highlights = vec![]; + // To build up the result, we do this for each span: + // - push the line segment trailing the previous span + // (at the beginning a "phantom" span pointing at the start of the line) + // - push lines between the previous and current span (if any) + // - if the previous and current span are not on the same line + // push the line segment leading up to the current span + // - splice in the span substitution + // + // Finally push the trailing line segment of the last span + let (mut prev_hi, _) = self.span_to_locations(lo..hi); + prev_hi.char = 0; + let mut prev_line = lines.first().map(|line| line.line); + let mut buf = String::new(); + + let mut line_highlight = vec![]; + // We need to keep track of the difference between the existing code and the added + // or deleted code in order to point at the correct column *after* substitution. + let mut acc = 0; + for part in &mut patches { + // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the + // suggestion and snippet to look as if we just suggested to add + // `"b"`, which is typically much easier for the user to understand. + part.trim_trivial_replacements(self); + let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone()); + if prev_hi.line == cur_lo.line { + let mut count = push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo)); + while count > 0 { + highlights.push(std::mem::take(&mut line_highlight)); + acc = 0; + count -= 1; + } + } else { + acc = 0; + highlights.push(std::mem::take(&mut line_highlight)); + let mut count = push_trailing(&mut buf, prev_line, &prev_hi, None); + while count > 0 { + highlights.push(std::mem::take(&mut line_highlight)); + count -= 1; + } + // push lines between the previous and current span (if any) + for idx in prev_hi.line + 1..(cur_lo.line) { + if let Some(line) = self.get_line(idx) { + buf.push_str(line.as_ref()); + buf.push('\n'); + highlights.push(std::mem::take(&mut line_highlight)); + } + } + if let Some(cur_line) = self.get_line(cur_lo.line) { + let end = match cur_line.char_indices().nth(cur_lo.char) { + Some((i, _)) => i, + None => cur_line.len(), + }; + buf.push_str(&cur_line[..end]); + } + } + // Add a whole line highlight per line in the snippet. + let len: isize = part + .replacement + .split('\n') + .next() + .unwrap_or(part.replacement) + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + if !is_different(self, part.replacement, part.span.clone()) { + // Account for cases where we are suggesting the same code that's already + // there. This shouldn't happen often, but in some cases for multipart + // suggestions it's much easier to handle it here than in the origin. + } else { + line_highlight.push(SubstitutionHighlight { + start: (cur_lo.char as isize + acc) as usize, + end: (cur_lo.char as isize + acc + len) as usize, + }); + } + buf.push_str(part.replacement); + // Account for the difference between the width of the current code and the + // snippet being suggested, so that the *later* suggestions are correctly + // aligned on the screen. Note that cur_hi and cur_lo can be on different + // lines, so cur_hi.col can be smaller than cur_lo.col + acc += len - (cur_hi.char as isize - cur_lo.char as isize); + prev_hi = cur_hi; + prev_line = self.get_line(prev_hi.line); + for line in part.replacement.split('\n').skip(1) { + acc = 0; + highlights.push(std::mem::take(&mut line_highlight)); + let end: usize = line + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + line_highlight.push(SubstitutionHighlight { start: 0, end }); + } + } + highlights.push(std::mem::take(&mut line_highlight)); + // if the replacement already ends with a newline, don't print the next line + if !buf.ends_with('\n') { + push_trailing(&mut buf, prev_line, &prev_hi, None); + } + // remove trailing newlines + while buf.ends_with('\n') { + buf.pop(); + } + if highlights.iter().all(|parts| parts.is_empty()) { + Vec::new() + } else { + vec![(buf, patches, highlights)] + } + } +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct MultilineAnnotation<'a> { + pub depth: usize, + pub start: Loc, + pub end: Loc, + pub kind: AnnotationKind, + pub label: Option<&'a str>, + pub overlaps_exactly: bool, + pub highlight_source: bool, +} + +impl<'a> MultilineAnnotation<'a> { + pub(crate) fn increase_depth(&mut self) { + self.depth += 1; + } + + /// Compare two `MultilineAnnotation`s considering only the `Span` they cover. + pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool { + self.start == other.start && self.end == other.end + } + + pub(crate) fn as_start(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: self.start, + end: Loc { + line: self.start.line, + char: self.start.char + 1, + display: self.start.display + 1, + byte: self.start.byte + 1, + }, + kind: self.kind, + label: None, + annotation_type: LineAnnotationType::MultilineStart(self.depth), + highlight_source: self.highlight_source, + } + } + + pub(crate) fn as_end(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: Loc { + line: self.end.line, + char: self.end.char.saturating_sub(1), + display: self.end.display.saturating_sub(1), + byte: self.end.byte.saturating_sub(1), + }, + end: self.end, + kind: self.kind, + label: self.label, + annotation_type: LineAnnotationType::MultilineEnd(self.depth), + highlight_source: self.highlight_source, + } + } + + pub(crate) fn as_line(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: Loc::default(), + end: Loc::default(), + kind: self.kind, + label: None, + annotation_type: LineAnnotationType::MultilineLine(self.depth), + highlight_source: self.highlight_source, + } + } +} + +#[derive(Debug)] +pub(crate) struct LineInfo<'a> { + pub(crate) line: &'a str, + pub(crate) line_index: usize, + pub(crate) start_byte: usize, + pub(crate) end_byte: usize, + end_line_size: usize, +} + +#[derive(Debug)] +pub(crate) struct AnnotatedLineInfo<'a> { + pub(crate) line: &'a str, + pub(crate) line_index: usize, + pub(crate) annotations: Vec>, +} + +/// A source code location used for error reporting. +#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct Loc { + /// The (1-based) line number. + pub(crate) line: usize, + /// The (0-based) column offset. + pub(crate) char: usize, + /// The (0-based) column offset when displayed. + pub(crate) display: usize, + /// The (0-based) byte offset. + pub(crate) byte: usize, +} + +struct CursorLines<'a>(&'a str); + +impl CursorLines<'_> { + fn new(src: &str) -> CursorLines<'_> { + CursorLines(src) + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +enum EndLine { + Eof, + Lf, + Crlf, +} + +impl EndLine { + /// The number of characters this line ending occupies in bytes. + pub(crate) fn len(self) -> usize { + match self { + EndLine::Eof => 0, + EndLine::Lf => 1, + EndLine::Crlf => 2, + } + } +} + +impl<'a> Iterator for CursorLines<'a> { + type Item = (&'a str, EndLine); + + fn next(&mut self) -> Option { + if self.0.is_empty() { + None + } else { + self.0 + .find('\n') + .map(|x| { + let ret = if 0 < x { + if self.0.as_bytes()[x - 1] == b'\r' { + (&self.0[..x - 1], EndLine::Crlf) + } else { + (&self.0[..x], EndLine::Lf) + } + } else { + ("", EndLine::Lf) + }; + self.0 = &self.0[x + 1..]; + ret + }) + .or_else(|| { + let ret = Some((self.0, EndLine::Eof)); + self.0 = ""; + ret + }) + } + } +} + +/// Used to translate between `Span`s and byte positions within a single output line in highlighted +/// code of structured suggestions. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SubstitutionHighlight { + pub(crate) start: usize, + pub(crate) end: usize, +} diff --git a/src/renderer/styled_buffer.rs b/src/renderer/styled_buffer.rs index ec834e1b..c9b805a0 100644 --- a/src/renderer/styled_buffer.rs +++ b/src/renderer/styled_buffer.rs @@ -3,7 +3,9 @@ //! [styled_buffer]: https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_errors/src/styled_buffer.rs use crate::renderer::stylesheet::Stylesheet; -use anstyle::Style; +use crate::renderer::ElementStyle; +use crate::Level; + use std::fmt; use std::fmt::Write; @@ -15,13 +17,13 @@ pub(crate) struct StyledBuffer { #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct StyledChar { ch: char, - style: Style, + style: ElementStyle, } impl StyledChar { - pub(crate) const SPACE: Self = StyledChar::new(' ', Style::new()); + pub(crate) const SPACE: Self = StyledChar::new(' ', ElementStyle::NoStyle); - pub(crate) const fn new(ch: char, style: Style) -> StyledChar { + pub(crate) const fn new(ch: char, style: ElementStyle) -> StyledChar { StyledChar { ch, style } } } @@ -37,19 +39,24 @@ impl StyledBuffer { } } - pub(crate) fn render(&self, stylesheet: &Stylesheet) -> Result { + pub(crate) fn render( + &self, + level: Level<'_>, + stylesheet: &Stylesheet, + ) -> Result { let mut str = String::new(); for (i, line) in self.lines.iter().enumerate() { let mut current_style = stylesheet.none; - for ch in line { - if ch.style != current_style { + for StyledChar { ch, style } in line { + let ch_style = style.color_spec(&level, stylesheet); + if ch_style != current_style { if !line.is_empty() { write!(str, "{}", current_style.render_reset())?; } - current_style = ch.style; + current_style = ch_style; write!(str, "{}", current_style.render())?; } - write!(str, "{}", ch.ch)?; + write!(str, "{ch}")?; } write!(str, "{}", current_style.render_reset())?; if i != self.lines.len() - 1 { @@ -62,7 +69,7 @@ impl StyledBuffer { /// Sets `chr` with `style` for given `line`, `col`. /// If `line` does not exist in our buffer, adds empty lines up to the given /// and fills the last line with unstyled whitespace. - pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: Style) { + pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: ElementStyle) { self.ensure_lines(line); if col >= self.lines[line].len() { self.lines[line].resize(col + 1, StyledChar::SPACE); @@ -73,16 +80,17 @@ impl StyledBuffer { /// Sets `string` with `style` for given `line`, starting from `col`. /// If `line` does not exist in our buffer, adds empty lines up to the given /// and fills the last line with unstyled whitespace. - pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: Style) { + pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: ElementStyle) { let mut n = col; for c in string.chars() { self.putc(line, n, c, style); n += 1; } } + /// For given `line` inserts `string` with `style` after old content of that line, /// adding lines if needed - pub(crate) fn append(&mut self, line: usize, string: &str, style: Style) { + pub(crate) fn append(&mut self, line: usize, string: &str, style: ElementStyle) { if line >= self.lines.len() { self.puts(line, 0, string, style); } else { @@ -91,7 +99,58 @@ impl StyledBuffer { } } + /// For given `line` inserts `string` with `style` before old content of that line, + /// adding lines if needed + pub(crate) fn prepend(&mut self, line: usize, string: &str, style: ElementStyle) { + self.ensure_lines(line); + let string_len = string.chars().count(); + + if !self.lines[line].is_empty() { + // Push the old content over to make room for new content + for _ in 0..string_len { + self.lines[line].insert(0, StyledChar::SPACE); + } + } + + self.puts(line, 0, string, style); + } + pub(crate) fn num_lines(&self) -> usize { self.lines.len() } + + /// Set `style` for `line`, `col_start..col_end` range if: + /// 1. That line and column range exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style_range( + &mut self, + line: usize, + col_start: usize, + col_end: usize, + style: ElementStyle, + overwrite: bool, + ) { + for col in col_start..col_end { + self.set_style(line, col, style, overwrite); + } + } + + /// Set `style` for `line`, `col` if: + /// 1. That line and column exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style( + &mut self, + line: usize, + col: usize, + style: ElementStyle, + overwrite: bool, + ) { + if let Some(ref mut line) = self.lines.get_mut(line) { + if let Some(StyledChar { style: s, .. }) = line.get_mut(col) { + if overwrite || matches!(s, ElementStyle::NoStyle | ElementStyle::Quotation) { + *s = style; + } + } + } + } } diff --git a/src/renderer/stylesheet.rs b/src/renderer/stylesheet.rs index ee1ab937..075cad42 100644 --- a/src/renderer/stylesheet.rs +++ b/src/renderer/stylesheet.rs @@ -7,9 +7,12 @@ pub(crate) struct Stylesheet { pub(crate) info: Style, pub(crate) note: Style, pub(crate) help: Style, - pub(crate) line_no: Style, + pub(crate) line_num: Style, pub(crate) emphasis: Style, pub(crate) none: Style, + pub(crate) context: Style, + pub(crate) addition: Style, + pub(crate) removal: Style, } impl Default for Stylesheet { @@ -26,43 +29,12 @@ impl Stylesheet { info: Style::new(), note: Style::new(), help: Style::new(), - line_no: Style::new(), + line_num: Style::new(), emphasis: Style::new(), none: Style::new(), + context: Style::new(), + addition: Style::new(), + removal: Style::new(), } } } - -impl Stylesheet { - pub(crate) fn error(&self) -> &Style { - &self.error - } - - pub(crate) fn warning(&self) -> &Style { - &self.warning - } - - pub(crate) fn info(&self) -> &Style { - &self.info - } - - pub(crate) fn note(&self) -> &Style { - &self.note - } - - pub(crate) fn help(&self) -> &Style { - &self.help - } - - pub(crate) fn line_no(&self) -> &Style { - &self.line_no - } - - pub(crate) fn emphasis(&self) -> &Style { - &self.emphasis - } - - pub(crate) fn none(&self) -> &Style { - &self.none - } -} diff --git a/src/snippet.rs b/src/snippet.rs index 8e9a3a88..03bfe3dd 100644 --- a/src/snippet.rs +++ b/src/snippet.rs @@ -1,157 +1,482 @@ //! Structures used as an input for the library. -//! -//! Example: -//! -//! ``` -//! use annotate_snippets::*; -//! -//! Level::Error.title("mismatched types") -//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs")) -//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs")); -//! ``` +use crate::renderer::source_map::SourceMap; +use crate::Level; use std::ops::Range; -/// Primary structure provided for formatting -/// -/// See [`Level::title`] to create a [`Message`] +pub(crate) const ERROR_TXT: &str = "error"; +pub(crate) const HELP_TXT: &str = "help"; +pub(crate) const INFO_TXT: &str = "info"; +pub(crate) const NOTE_TXT: &str = "note"; +pub(crate) const WARNING_TXT: &str = "warning"; + +/// Top-level user message #[derive(Debug)] pub struct Message<'a> { - pub(crate) level: Level, - pub(crate) id: Option<&'a str>, - pub(crate) title: &'a str, - pub(crate) snippets: Vec>, - pub(crate) footer: Vec>, + pub(crate) id: Option<&'a str>, // for "correctness", could be sloppy and be on Title + pub(crate) groups: Vec>, } impl<'a> Message<'a> { + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
pub fn id(mut self, id: &'a str) -> Self { self.id = Some(id); self } - pub fn snippet(mut self, slice: Snippet<'a>) -> Self { - self.snippets.push(slice); + /// Add an [`Element`] container + pub fn group(mut self, group: Group<'a>) -> Self { + self.groups.push(group); self } - pub fn snippets(mut self, slice: impl IntoIterator>) -> Self { - self.snippets.extend(slice); - self + pub(crate) fn max_line_number(&self) -> usize { + self.groups + .iter() + .map(|v| { + v.elements + .iter() + .map(|s| match s { + Element::Title(_) | Element::Origin(_) | Element::Padding(_) => 0, + Element::Cause(cause) => { + let end = cause + .markers + .iter() + .map(|a| a.span.end) + .max() + .unwrap_or(cause.source.len()) + .min(cause.source.len()); + + cause.line_start + newline_count(&cause.source[..end]) + } + Element::Suggestion(suggestion) => { + let end = suggestion + .markers + .iter() + .map(|a| a.span.end) + .max() + .unwrap_or(suggestion.source.len()) + .min(suggestion.source.len()); + + suggestion.line_start + newline_count(&suggestion.source[..end]) + } + }) + .max() + .unwrap_or(1) + }) + .max() + .unwrap_or(1) + } +} + +/// An [`Element`] container +#[derive(Debug)] +pub struct Group<'a> { + pub(crate) elements: Vec>, +} + +impl Default for Group<'_> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> Group<'a> { + pub fn new() -> Self { + Self { elements: vec![] } } - pub fn footer(mut self, footer: Message<'a>) -> Self { - self.footer.push(footer); + pub fn element(mut self, section: impl Into>) -> Self { + self.elements.push(section.into()); self } - pub fn footers(mut self, footer: impl IntoIterator>) -> Self { - self.footer.extend(footer); + pub fn elements(mut self, sections: impl IntoIterator>>) -> Self { + self.elements.extend(sections.into_iter().map(Into::into)); self } + + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } +} + +/// A section of content within a [`Group`] +#[derive(Debug)] +#[non_exhaustive] +pub enum Element<'a> { + Title(Title<'a>), + Cause(Snippet<'a, Annotation<'a>>), + Suggestion(Snippet<'a, Patch<'a>>), + Origin(Origin<'a>), + Padding(Padding), +} + +impl<'a> From> for Element<'a> { + fn from(value: Title<'a>) -> Self { + Element::Title(value) + } } -/// Structure containing the slice of text to be annotated and -/// basic information about the location of the slice. +impl<'a> From>> for Element<'a> { + fn from(value: Snippet<'a, Annotation<'a>>) -> Self { + Element::Cause(value) + } +} + +impl<'a> From>> for Element<'a> { + fn from(value: Snippet<'a, Patch<'a>>) -> Self { + Element::Suggestion(value) + } +} + +impl<'a> From> for Element<'a> { + fn from(value: Origin<'a>) -> Self { + Element::Origin(value) + } +} + +impl From for Element<'_> { + fn from(value: Padding) -> Self { + Self::Padding(value) + } +} + +/// A whitespace [`Element`] in a [`Group`] +#[derive(Debug)] +pub struct Padding; + +/// A text [`Element`] in a [`Group`] /// -/// One `Snippet` is meant to represent a single, continuous, -/// slice of source code that you want to annotate. +/// See [`Level::title`] to create this. +#[derive(Debug)] +pub struct Title<'a> { + pub(crate) level: Level<'a>, + pub(crate) title: &'a str, + pub(crate) primary: bool, +} + +impl Title<'_> { + pub fn primary(mut self, primary: bool) -> Self { + self.primary = primary; + self + } +} + +/// A source view [`Element`] in a [`Group`] #[derive(Debug)] -pub struct Snippet<'a> { +pub struct Snippet<'a, T> { pub(crate) origin: Option<&'a str>, pub(crate) line_start: usize, - pub(crate) source: &'a str, - pub(crate) annotations: Vec>, - + pub(crate) markers: Vec, pub(crate) fold: bool, } -impl<'a> Snippet<'a> { +impl<'a, T: Clone> Snippet<'a, T> { + /// The source code to be rendered + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
pub fn source(source: &'a str) -> Self { Self { origin: None, line_start: 1, source, - annotations: vec![], + markers: vec![], fold: false, } } + /// When manually [`fold`][Self::fold]ing, + /// the [`source`][Self::source]s line offset from the original start pub fn line_start(mut self, line_start: usize) -> Self { self.line_start = line_start; self } + /// The location of the [`source`][Self::source] (e.g. a path) + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
pub fn origin(mut self, origin: &'a str) -> Self { self.origin = Some(origin); self } - pub fn annotation(mut self, annotation: Annotation<'a>) -> Self { - self.annotations.push(annotation); + /// Hide lines without [`Annotation`]s + pub fn fold(mut self, fold: bool) -> Self { + self.fold = fold; + self + } +} + +impl<'a> Snippet<'a, Annotation<'a>> { + /// Highlight and describe a span of text within the [`source`][Self::source] + pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> { + self.markers.push(annotation); self } + /// Highlight and describe spans of text within the [`source`][Self::source] pub fn annotations(mut self, annotation: impl IntoIterator>) -> Self { - self.annotations.extend(annotation); + self.markers.extend(annotation); self } +} - /// Hide lines without [`Annotation`]s - pub fn fold(mut self, fold: bool) -> Self { - self.fold = fold; +impl<'a> Snippet<'a, Patch<'a>> { + /// Suggest to the user an edit to the [`source`][Self::source] + pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> { + self.markers.push(patch); + self + } + + /// Suggest to the user edits to the [`source`][Self::source] + pub fn patches(mut self, patches: impl IntoIterator>) -> Self { + self.markers.extend(patches); self } } -/// An annotation for a [`Snippet`]. +/// Highlighted and describe a span of text within a [`Snippet`] /// -/// See [`Level::span`] to create a [`Annotation`] -#[derive(Debug)] +/// See [`AnnotationKind`] to create an annotation. +#[derive(Clone, Debug)] pub struct Annotation<'a> { - /// The byte range of the annotation in the `source` string - pub(crate) range: Range, + pub(crate) span: Range, pub(crate) label: Option<&'a str>, - pub(crate) level: Level, + pub(crate) kind: AnnotationKind, + pub(crate) highlight_source: bool, } impl<'a> Annotation<'a> { + /// Describe the reason the span is highlighted + /// + /// This will be styled according to the [`AnnotationKind`] + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
pub fn label(mut self, label: &'a str) -> Self { self.label = Some(label); self } + + /// Style the source according to the [`AnnotationKind`] + pub fn highlight_source(mut self, highlight_source: bool) -> Self { + self.highlight_source = highlight_source; + self + } } -/// Types of annotations. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Level { - /// Error annotations are displayed using red color and "^" character. - Error, - /// Warning annotations are displayed using blue color and "-" character. - Warning, - Info, - Note, - Help, +/// The category of the [`Annotation`] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum AnnotationKind { + /// Color to [`Message`]'s [`Level`] + Primary, + /// "secondary"; fixed color + Context, } -impl Level { - pub fn title(self, title: &str) -> Message<'_> { - Message { - level: self, - id: None, - title, - snippets: vec![], - footer: vec![], +impl AnnotationKind { + pub fn span<'a>(self, span: Range) -> Annotation<'a> { + Annotation { + span, + label: None, + kind: self, + highlight_source: false, } } - /// Create a [`Annotation`] with the given span for a [`Snippet`] - pub fn span<'a>(self, span: Range) -> Annotation<'a> { - Annotation { - range: span, + pub(crate) fn is_primary(&self) -> bool { + matches!(self, AnnotationKind::Primary) + } +} + +/// Suggested edit to the [`Snippet`] +#[derive(Clone, Debug)] +pub struct Patch<'a> { + pub(crate) span: Range, + pub(crate) replacement: &'a str, +} + +impl<'a> Patch<'a> { + /// Splice `replacement` into the [`Snippet`] at the `span` + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn new(span: Range, replacement: &'a str) -> Self { + Self { span, replacement } + } + + pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool { + !self.replacement.is_empty() && !self.replaces_meaningful_content(sm) + } + + pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool { + self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm) + } + + pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool { + !self.replacement.is_empty() && self.replaces_meaningful_content(sm) + } + + /// Whether this is a replacement that overwrites source with a snippet + /// in a way that isn't a superset of the original string. For example, + /// replacing "abc" with "abcde" is not destructive, but replacing it + /// it with "abx" is, since the "c" character is lost. + pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool { + self.is_replacement(sm) + && !sm + .span_to_snippet(self.span.clone()) + // This should use `is_some_and` when our MSRV is >= 1.70 + .map_or(false, |s| { + as_substr(s.trim(), self.replacement.trim()).is_some() + }) + } + + fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool { + sm.span_to_snippet(self.span.clone()) + .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty()) + } + + /// Try to turn a replacement into an addition when the span that is being + /// overwritten matches either the prefix or suffix of the replacement. + pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) { + if self.replacement.is_empty() { + return; + } + let Some(snippet) = sm.span_to_snippet(self.span.clone()) else { + return; + }; + + if let Some((prefix, substr, suffix)) = as_substr(snippet, self.replacement) { + self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix); + self.replacement = substr; + } + } +} + +/// The location of the [`Snippet`] (e.g. a path) +#[derive(Clone, Debug)] +pub struct Origin<'a> { + pub(crate) origin: &'a str, + pub(crate) line: Option, + pub(crate) char_column: Option, + pub(crate) primary: bool, + pub(crate) label: Option<&'a str>, +} + +impl<'a> Origin<'a> { + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn new(origin: &'a str) -> Self { + Self { + origin, + line: None, + char_column: None, + primary: false, label: None, - level: self, } } + + /// Set the default line number to display + /// + /// Otherwise this will be inferred from the primary [`Annotation`] + pub fn line(mut self, line: usize) -> Self { + self.line = Some(line); + self + } + + /// Set the default column to display + /// + /// Otherwise this will be inferred from the primary [`Annotation`] + pub fn char_column(mut self, char_column: usize) -> Self { + self.char_column = Some(char_column); + self + } + + pub fn primary(mut self, primary: bool) -> Self { + self.primary = primary; + self + } + + /// Like [`Annotation::label`], but when there is no source + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn label(mut self, label: &'a str) -> Self { + self.label = Some(label); + self + } +} + +fn newline_count(body: &str) -> usize { + #[cfg(feature = "simd")] + { + memchr::memchr_iter(b'\n', body.as_bytes()) + .count() + .saturating_sub(1) + } + #[cfg(not(feature = "simd"))] + { + body.lines().count().saturating_sub(1) + } +} + +/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect +/// the case where a substring of the suggestion is "sandwiched" in the original, like +/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length +/// of the suffix. +fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> { + let common_prefix = original + .chars() + .zip(suggestion.chars()) + .take_while(|(c1, c2)| c1 == c2) + .map(|(c, _)| c.len_utf8()) + .sum(); + let original = &original[common_prefix..]; + let suggestion = &suggestion[common_prefix..]; + if let Some(stripped) = suggestion.strip_suffix(original) { + let common_suffix = original.len(); + Some((common_prefix, stripped, common_suffix)) + } else { + None + } } diff --git a/tests/color/ann_eof.rs b/tests/color/ann_eof.rs new file mode 100644 index 00000000..00e34b16 --- /dev/null +++ b/tests/color/ann_eof.rs @@ -0,0 +1,18 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let input = Level::ERROR.header("expected `.`, `=`").group( + Group::new().element( + Snippet::source("asdf") + .origin("Cargo.toml") + .line_start(1) + .annotation(AnnotationKind::Primary.span(4..4).label("")), + ), + ); + let expected = file!["ann_eof.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/ann_eof.term.svg b/tests/color/ann_eof.term.svg new file mode 100644 index 00000000..aeb4f8cf --- /dev/null +++ b/tests/color/ann_eof.term.svg @@ -0,0 +1,34 @@ + + + + + + + error: expected `.`, `=` + + --> Cargo.toml:1:5 + + | + + 1 | asdf + + | ^ + + + + diff --git a/tests/color/ann_insertion.rs b/tests/color/ann_insertion.rs new file mode 100644 index 00000000..802a0c78 --- /dev/null +++ b/tests/color/ann_insertion.rs @@ -0,0 +1,18 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let input = Level::ERROR.header("expected `.`, `=`").group( + Group::new().element( + Snippet::source("asf") + .origin("Cargo.toml") + .line_start(1) + .annotation(AnnotationKind::Primary.span(2..2).label("'d' belongs here")), + ), + ); + let expected = file!["ann_insertion.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/ann_insertion.term.svg b/tests/color/ann_insertion.term.svg new file mode 100644 index 00000000..57c90a23 --- /dev/null +++ b/tests/color/ann_insertion.term.svg @@ -0,0 +1,34 @@ + + + + + + + error: expected `.`, `=` + + --> Cargo.toml:1:3 + + | + + 1 | asf + + | ^ 'd' belongs here + + + + diff --git a/tests/color/ann_multiline.rs b/tests/color/ann_multiline.rs new file mode 100644 index 00000000..4b561ed3 --- /dev/null +++ b/tests/color/ann_multiline.rs @@ -0,0 +1,31 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#" if let DisplayLine::Source { + ref mut inline_marks, + } = body[body_idx] +"#; + + let input = Level::ERROR + .header("pattern does not mention fields `lineno`, `content`") + .id("E0027") + .group( + Group::new().element( + Snippet::source(source) + .origin("src/display_list.rs") + .line_start(139) + .fold(false) + .annotation( + AnnotationKind::Primary + .span(31..128) + .label("missing fields `lineno`, `content`"), + ), + ), + ); + let expected = file!["ann_multiline.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/ann_multiline.term.svg b/tests/color/ann_multiline.term.svg new file mode 100644 index 00000000..2ff0364b --- /dev/null +++ b/tests/color/ann_multiline.term.svg @@ -0,0 +1,40 @@ + + + + + + + error[E0027]: pattern does not mention fields `lineno`, `content` + + --> src/display_list.rs:139:32 + + | + + 139 | if let DisplayLine::Source { + + | ________________________________^ + + 140 | | ref mut inline_marks, + + 141 | | } = body[body_idx] + + | |_________________________^ missing fields `lineno`, `content` + + + + diff --git a/tests/color/ann_multiline2.rs b/tests/color/ann_multiline2.rs new file mode 100644 index 00000000..9996fa97 --- /dev/null +++ b/tests/color/ann_multiline2.rs @@ -0,0 +1,31 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#"This is an example +of an edge case of an annotation overflowing +to exactly one character on next line. +"#; + + let input = Level::ERROR + .header("spacing error found") + .id("E####") + .group( + Group::new().element( + Snippet::source(source) + .origin("foo.txt") + .line_start(26) + .fold(false) + .annotation( + AnnotationKind::Primary + .span(11..19) + .label("this should not be on separate lines"), + ), + ), + ); + let expected = file!["ann_multiline2.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/ann_multiline2.term.svg b/tests/color/ann_multiline2.term.svg new file mode 100644 index 00000000..24827f66 --- /dev/null +++ b/tests/color/ann_multiline2.term.svg @@ -0,0 +1,38 @@ + + + + + + + error[E####]: spacing error found + + --> foo.txt:26:12 + + | + + 26 | This is an example + + | ^^^^^^^ this should not be on separate lines + + 27 | of an edge case of an annotation overflowing + + 28 | to exactly one character on next line. + + + + diff --git a/tests/color/ann_removed_nl.rs b/tests/color/ann_removed_nl.rs new file mode 100644 index 00000000..45a64626 --- /dev/null +++ b/tests/color/ann_removed_nl.rs @@ -0,0 +1,18 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let input = Level::ERROR.header("expected `.`, `=`").group( + Group::new().element( + Snippet::source("asdf") + .origin("Cargo.toml") + .line_start(1) + .annotation(AnnotationKind::Primary.span(4..5).label("")), + ), + ); + let expected = file!["ann_removed_nl.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/ann_removed_nl.term.svg b/tests/color/ann_removed_nl.term.svg new file mode 100644 index 00000000..aeb4f8cf --- /dev/null +++ b/tests/color/ann_removed_nl.term.svg @@ -0,0 +1,34 @@ + + + + + + + error: expected `.`, `=` + + --> Cargo.toml:1:5 + + | + + 1 | asdf + + | ^ + + + + diff --git a/tests/color/ensure_emoji_highlight_width.rs b/tests/color/ensure_emoji_highlight_width.rs new file mode 100644 index 00000000..b2397845 --- /dev/null +++ b/tests/color/ensure_emoji_highlight_width.rs @@ -0,0 +1,24 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#""haha this isn't a valid name 🐛" = { package = "libc", version = "0.1" } +"#; + + let input = Level::ERROR.header("invalid character ` ` in package name: `haha this isn't a valid name 🐛`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters)") + .group( + Group::new() + .element( + Snippet::source(source) + .origin("") + .line_start(7) + .annotation(AnnotationKind::Primary.span(0..35).label("")) + ) + ) +; + let expected = file!["ensure_emoji_highlight_width.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/ensure_emoji_highlight_width.term.svg b/tests/color/ensure_emoji_highlight_width.term.svg new file mode 100644 index 00000000..14624fb6 --- /dev/null +++ b/tests/color/ensure_emoji_highlight_width.term.svg @@ -0,0 +1,34 @@ + + + + + + + error: invalid character ` ` in package name: `haha this isn't a valid name 🐛`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) + + --> <file>:7:1 + + | + + 7 | "haha this isn't a valid name 🐛" = { package = "libc", version = "0.1" } + + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + + + diff --git a/tests/color/fold_ann_multiline.rs b/tests/color/fold_ann_multiline.rs new file mode 100644 index 00000000..3995b686 --- /dev/null +++ b/tests/color/fold_ann_multiline.rs @@ -0,0 +1,50 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#") -> Option { + for ann in annotations { + match (ann.range.0, ann.range.1) { + (None, None) => continue, + (Some(start), Some(end)) if start > end_index || end < start_index => continue, + (Some(start), Some(end)) if start >= start_index && end <= end_index => { + let label = if let Some(ref label) = ann.label { + format!(" {}", label) + } else { + String::from("") + }; + + return Some(format!( + "{}{}{}", + " ".repeat(start - start_index), + "^".repeat(end - start), + label + )); + } + _ => continue, + } + } +"#; + + let input = Level::ERROR.header("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(source) + .origin("src/format.rs") + .line_start(51) + .fold(true) + .annotation(AnnotationKind::Context.span(5..19).label( + "expected `std::option::Option` because of return type", + )) + .annotation( + AnnotationKind::Primary + .span(22..766) + .label("expected enum `std::option::Option`, found ()"), + ), + ), + ); + let expected = file!["fold_ann_multiline.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/fold_ann_multiline.term.svg b/tests/color/fold_ann_multiline.term.svg new file mode 100644 index 00000000..80197e5c --- /dev/null +++ b/tests/color/fold_ann_multiline.term.svg @@ -0,0 +1,48 @@ + + + + + + + error[E0308]: mismatched types + + --> src/format.rs:52:1 + + | + + 51 | ) -> Option<String> { + + | -------------- expected `std::option::Option<std::string::String>` because of return type + + 52 | / for ann in annotations { + + 53 | | match (ann.range.0, ann.range.1) { + + 54 | | (None, None) => continue, + + 55 | | (Some(start), Some(end)) if start > end_index || end < start_index => continue, + + ... | + + 72 | | } + + | |_____^ expected enum `std::option::Option`, found () + + + + diff --git a/tests/color/fold_bad_origin_line.rs b/tests/color/fold_bad_origin_line.rs new file mode 100644 index 00000000..1a21a5ef --- /dev/null +++ b/tests/color/fold_bad_origin_line.rs @@ -0,0 +1,24 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#" + +invalid syntax +"#; + + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("path/to/error.rs") + .line_start(1) + .fold(true) + .annotation(AnnotationKind::Context.span(2..16).label("error here")), + ), + ); + let expected = file!["fold_bad_origin_line.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/fold_bad_origin_line.term.svg b/tests/color/fold_bad_origin_line.term.svg new file mode 100644 index 00000000..66083276 --- /dev/null +++ b/tests/color/fold_bad_origin_line.term.svg @@ -0,0 +1,34 @@ + + + + + + + error: + + --> path/to/error.rs:3:1 + + | + + 3 | invalid syntax + + | -------------- error here + + + + diff --git a/tests/color/fold_leading.rs b/tests/color/fold_leading.rs new file mode 100644 index 00000000..93ba4992 --- /dev/null +++ b/tests/color/fold_leading.rs @@ -0,0 +1,35 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#"[workspace] + +[package] +name = "hello" +version = "1.0.0" +license = "MIT" +rust-version = "1.70" +edition = "2021" + +[lints] +workspace = 20 +"#; + + let input = Level::ERROR + .header("invalid type: integer `20`, expected a bool") + .id("E0308") + .group( + Group::new().element( + Snippet::source(source) + .origin("Cargo.toml") + .line_start(1) + .fold(true) + .annotation(AnnotationKind::Primary.span(132..134).label("")), + ), + ); + let expected = file!["fold_leading.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/fold_leading.term.svg b/tests/color/fold_leading.term.svg new file mode 100644 index 00000000..23b31d4a --- /dev/null +++ b/tests/color/fold_leading.term.svg @@ -0,0 +1,34 @@ + + + + + + + error[E0308]: invalid type: integer `20`, expected a bool + + --> Cargo.toml:11:13 + + | + + 11 | workspace = 20 + + | ^^ + + + + diff --git a/tests/color/fold_trailing.rs b/tests/color/fold_trailing.rs new file mode 100644 index 00000000..f86ade78 --- /dev/null +++ b/tests/color/fold_trailing.rs @@ -0,0 +1,34 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#"lints = 20 + +[workspace] + +[package] +name = "hello" +version = "1.0.0" +license = "MIT" +rust-version = "1.70" +edition = "2021" +"#; + + let input = Level::ERROR + .header("invalid type: integer `20`, expected a lints table") + .id("E0308") + .group( + Group::new().element( + Snippet::source(source) + .origin("Cargo.toml") + .line_start(1) + .fold(true) + .annotation(AnnotationKind::Primary.span(8..10).label("")), + ), + ); + let expected = file!["fold_trailing.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/fold_trailing.term.svg b/tests/color/fold_trailing.term.svg new file mode 100644 index 00000000..46071da6 --- /dev/null +++ b/tests/color/fold_trailing.term.svg @@ -0,0 +1,34 @@ + + + + + + + error[E0308]: invalid type: integer `20`, expected a lints table + + --> Cargo.toml:1:9 + + | + + 1 | lints = 20 + + | ^^ + + + + diff --git a/tests/color/issue_9.rs b/tests/color/issue_9.rs new file mode 100644 index 00000000..2accd2f2 --- /dev/null +++ b/tests/color/issue_9.rs @@ -0,0 +1,31 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let input = Level::ERROR.header("expected one of `.`, `;`, `?`, or an operator, found `for`") + .group( + Group::new() + .element( + Snippet::source("let x = vec![1];") + .origin("/code/rust/src/test/ui/annotate-snippet/suggestion.rs") + .line_start(4) + .annotation(AnnotationKind::Context.span(4..5).label("move occurs because `x` has type `std::vec::Vec`, which does not implement the `Copy` trait")) + ) + .element( + Snippet::source("let y = x;") + .line_start(7) + .annotation(AnnotationKind::Context.span(8..9).label("value moved here")) + ) + .element( + Snippet::source("x;") + .line_start(9) + .annotation(AnnotationKind::Primary.span(0..1).label("value used here after move")) + ) + ) +; + let expected = file!["issue_9.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/issue_9.term.svg b/tests/color/issue_9.term.svg new file mode 100644 index 00000000..5ae5da77 --- /dev/null +++ b/tests/color/issue_9.term.svg @@ -0,0 +1,48 @@ + + + + + + + error: expected one of `.`, `;`, `?`, or an operator, found `for` + + | + + ::: /code/rust/src/test/ui/annotate-snippet/suggestion.rs:4:5 + + | + + 4 | let x = vec![1]; + + | - move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait + + | + + 7 | let y = x; + + | - value moved here + + | + + 9 | x; + + | ^ value used here after move + + + + diff --git a/tests/color/main.rs b/tests/color/main.rs new file mode 100644 index 00000000..f954bb7a --- /dev/null +++ b/tests/color/main.rs @@ -0,0 +1,16 @@ +mod ann_eof; +mod ann_insertion; +mod ann_multiline; +mod ann_multiline2; +mod ann_removed_nl; +mod ensure_emoji_highlight_width; +mod fold_ann_multiline; +mod fold_bad_origin_line; +mod fold_leading; +mod fold_trailing; +mod issue_9; +mod multiple_annotations; +mod simple; +mod strip_line; +mod strip_line_char; +mod strip_line_non_ws; diff --git a/tests/color/multiple_annotations.rs b/tests/color/multiple_annotations.rs new file mode 100644 index 00000000..b568b919 --- /dev/null +++ b/tests/color/multiple_annotations.rs @@ -0,0 +1,42 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#"fn add_title_line(result: &mut Vec, main_annotation: Option<&Annotation>) { + if let Some(annotation) = main_annotation { + result.push(format_title_line( + &annotation.annotation_type, + None, + &annotation.label, + )); + } +} +"#; + + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .line_start(96) + .annotation( + AnnotationKind::Primary + .span(100..110) + .label("Variable defined here"), + ) + .annotation( + AnnotationKind::Primary + .span(184..194) + .label("Referenced here"), + ) + .annotation( + AnnotationKind::Primary + .span(243..253) + .label("Referenced again here"), + ), + ), + ); + let expected = file!["multiple_annotations.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/multiple_annotations.term.svg b/tests/color/multiple_annotations.term.svg new file mode 100644 index 00000000..2c5c4a81 --- /dev/null +++ b/tests/color/multiple_annotations.term.svg @@ -0,0 +1,52 @@ + + + + + + + error: + + | + + 96 | fn add_title_line(result: &mut Vec<String>, main_annotation: Option<&Annotation>) { + + 97 | if let Some(annotation) = main_annotation { + + | ^^^^^^^^^^ Variable defined here + + 98 | result.push(format_title_line( + + 99 | &annotation.annotation_type, + + | ^^^^^^^^^^ Referenced here + + 100 | None, + + 101 | &annotation.label, + + | ^^^^^^^^^^ Referenced again here + + 102 | )); + + 103 | } + + 104 | } + + + + diff --git a/tests/color/simple.rs b/tests/color/simple.rs new file mode 100644 index 00000000..35e83d38 --- /dev/null +++ b/tests/color/simple.rs @@ -0,0 +1,34 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#" }) + + for line in &self.body { +"#; + + let input = Level::ERROR + .header("expected one of `.`, `;`, `?`, or an operator, found `for`") + .group( + Group::new().element( + Snippet::source(source) + .origin("src/format_color.rs") + .line_start(169) + .annotation( + AnnotationKind::Primary + .span(20..23) + .label("unexpected token"), + ) + .annotation( + AnnotationKind::Context + .span(10..11) + .label("expected one of `.`, `;`, `?`, or an operator here"), + ), + ), + ); + let expected = file!["simple.term.svg"]; + let renderer = Renderer::styled(); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/simple.term.svg b/tests/color/simple.term.svg new file mode 100644 index 00000000..b849cf46 --- /dev/null +++ b/tests/color/simple.term.svg @@ -0,0 +1,40 @@ + + + + + + + error: expected one of `.`, `;`, `?`, or an operator, found `for` + + --> src/format_color.rs:171:9 + + | + + 169 | }) + + | - expected one of `.`, `;`, `?`, or an operator here + + 170 | + + 171 | for line in &self.body { + + | ^^^ unexpected token + + + + diff --git a/tests/color/strip_line.rs b/tests/color/strip_line.rs new file mode 100644 index 00000000..fd1ba588 --- /dev/null +++ b/tests/color/strip_line.rs @@ -0,0 +1,24 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#" let _: () = 42;"#; + + let input = Level::ERROR.header("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(source) + .origin("$DIR/whitespace-trimming.rs") + .line_start(4) + .annotation( + AnnotationKind::Primary + .span(192..194) + .label("expected (), found integer"), + ), + ), + ); + let expected = file!["strip_line.term.svg"]; + let renderer = Renderer::styled().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/strip_line.term.svg b/tests/color/strip_line.term.svg new file mode 100644 index 00000000..14fbf9dc --- /dev/null +++ b/tests/color/strip_line.term.svg @@ -0,0 +1,34 @@ + + + + + + + error[E0308]: mismatched types + + --> $DIR/whitespace-trimming.rs:4:193 + + | + + LL | ... let _: () = 42; + + | ^^ expected (), found integer + + + + diff --git a/tests/color/strip_line_char.rs b/tests/color/strip_line_char.rs new file mode 100644 index 00000000..df609e2f --- /dev/null +++ b/tests/color/strip_line_char.rs @@ -0,0 +1,24 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#" let _: () = 42ñ"#; + + let input = Level::ERROR.header("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(source) + .origin("$DIR/whitespace-trimming.rs") + .line_start(4) + .annotation( + AnnotationKind::Primary + .span(192..194) + .label("expected (), found integer"), + ), + ), + ); + let expected = file!["strip_line_char.term.svg"]; + let renderer = Renderer::styled().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/strip_line_char.term.svg b/tests/color/strip_line_char.term.svg new file mode 100644 index 00000000..b37d4a9a --- /dev/null +++ b/tests/color/strip_line_char.term.svg @@ -0,0 +1,34 @@ + + + + + + + error[E0308]: mismatched types + + --> $DIR/whitespace-trimming.rs:4:193 + + | + + LL | ... let _: () = 42ñ + + | ^^ expected (), found integer + + + + diff --git a/tests/color/strip_line_non_ws.rs b/tests/color/strip_line_non_ws.rs new file mode 100644 index 00000000..f82d369b --- /dev/null +++ b/tests/color/strip_line_non_ws.rs @@ -0,0 +1,30 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, file}; + +#[test] +fn case() { + let source = r#" let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = 42; let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); +"#; + + let input = Level::ERROR.header("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(source) + .origin("$DIR/non-whitespace-trimming.rs") + .line_start(4) + .annotation( + AnnotationKind::Primary + .span(237..239) + .label("expected `()`, found integer"), + ) + .annotation( + AnnotationKind::Primary + .span(232..234) + .label("expected due to this"), + ), + ), + ); + let expected = file!["strip_line_non_ws.term.svg"]; + let renderer = Renderer::styled().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} diff --git a/tests/color/strip_line_non_ws.term.svg b/tests/color/strip_line_non_ws.term.svg new file mode 100644 index 00000000..6f799d35 --- /dev/null +++ b/tests/color/strip_line_non_ws.term.svg @@ -0,0 +1,38 @@ + + + + + + + error[E0308]: mismatched types + + --> $DIR/non-whitespace-trimming.rs:4:233 + + | + + LL | ... = (); let _: () = (); let _: () = (); let _: () = 42; let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () ... + + | ^^ ^^ expected `()`, found integer + + | | + + | expected due to this + + + + diff --git a/tests/examples.rs b/tests/examples.rs index b6576629..66dd94be 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -1,3 +1,17 @@ +#[test] +fn custom_error() { + let target = "custom_error"; + let expected = snapbox::file!["../examples/custom_error.svg": TermSvg]; + assert_example(target, expected); +} + +#[test] +fn custom_level() { + let target = "custom_level"; + let expected = snapbox::file!["../examples/custom_level.svg": TermSvg]; + assert_example(target, expected); +} + #[test] fn expected_type() { let target = "expected_type"; @@ -19,6 +33,20 @@ fn format() { assert_example(target, expected); } +#[test] +fn highlight_source() { + let target = "highlight_source"; + let expected = snapbox::file!["../examples/highlight_source.svg": TermSvg]; + assert_example(target, expected); +} + +#[test] +fn highlight_title() { + let target = "highlight_title"; + let expected = snapbox::file!["../examples/highlight_title.svg": TermSvg]; + assert_example(target, expected); +} + #[test] fn multislice() { let target = "multislice"; diff --git a/tests/fixtures/deserialize.rs b/tests/fixtures/deserialize.rs deleted file mode 100644 index 3ddef798..00000000 --- a/tests/fixtures/deserialize.rs +++ /dev/null @@ -1,166 +0,0 @@ -use serde::{Deserialize, Deserializer, Serialize}; -use std::ops::Range; - -use annotate_snippets::renderer::DEFAULT_TERM_WIDTH; -use annotate_snippets::{Annotation, Level, Message, Renderer, Snippet}; - -#[derive(Deserialize)] -pub(crate) struct Fixture<'a> { - #[serde(default)] - pub(crate) renderer: RendererDef, - #[serde(borrow)] - pub(crate) message: MessageDef<'a>, -} - -#[derive(Deserialize)] -pub struct MessageDef<'a> { - #[serde(with = "LevelDef")] - pub level: Level, - #[serde(borrow)] - pub title: &'a str, - #[serde(default)] - #[serde(borrow)] - pub id: Option<&'a str>, - #[serde(default)] - #[serde(borrow)] - pub footer: Vec>, - #[serde(deserialize_with = "deserialize_snippets")] - #[serde(borrow)] - pub snippets: Vec>, -} - -impl<'a> From> for Message<'a> { - fn from(val: MessageDef<'a>) -> Self { - let MessageDef { - level, - title, - id, - footer, - snippets, - } = val; - let mut message = level.title(title); - if let Some(id) = id { - message = message.id(id); - } - message = message.snippets(snippets); - message = message.footers(footer.into_iter().map(Into::into)); - message - } -} - -fn deserialize_snippets<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - struct Wrapper<'a>( - #[serde(with = "SnippetDef")] - #[serde(borrow)] - SnippetDef<'a>, - ); - - let v = Vec::deserialize(deserializer)?; - Ok(v.into_iter().map(|Wrapper(a)| a.into()).collect()) -} - -#[derive(Deserialize)] -pub struct SnippetDef<'a> { - #[serde(borrow)] - pub source: &'a str, - pub line_start: usize, - #[serde(borrow)] - pub origin: Option<&'a str>, - #[serde(deserialize_with = "deserialize_annotations")] - #[serde(borrow)] - pub annotations: Vec>, - #[serde(default)] - pub fold: bool, -} - -impl<'a> From> for Snippet<'a> { - fn from(val: SnippetDef<'a>) -> Self { - let SnippetDef { - source, - line_start, - origin, - annotations, - fold, - } = val; - let mut snippet = Snippet::source(source).line_start(line_start).fold(fold); - if let Some(origin) = origin { - snippet = snippet.origin(origin); - } - snippet = snippet.annotations(annotations); - snippet - } -} - -fn deserialize_annotations<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - struct Wrapper<'a>(#[serde(borrow)] AnnotationDef<'a>); - - let v = Vec::deserialize(deserializer)?; - Ok(v.into_iter().map(|Wrapper(a)| a.into()).collect()) -} - -#[derive(Serialize, Deserialize)] -pub struct AnnotationDef<'a> { - pub range: Range, - #[serde(borrow)] - pub label: &'a str, - #[serde(with = "LevelDef")] - pub level: Level, -} - -impl<'a> From> for Annotation<'a> { - fn from(val: AnnotationDef<'a>) -> Self { - let AnnotationDef { - range, - label, - level, - } = val; - level.span(range).label(label) - } -} - -#[derive(Serialize, Deserialize)] -pub(crate) struct LabelDef<'a> { - #[serde(with = "LevelDef")] - pub(crate) level: Level, - #[serde(borrow)] - pub(crate) label: &'a str, -} - -#[allow(dead_code)] -#[derive(Serialize, Deserialize)] -#[serde(remote = "Level")] -enum LevelDef { - Error, - Warning, - Info, - Note, - Help, -} - -#[derive(Default, Deserialize)] -pub struct RendererDef { - #[serde(default)] - anonymized_line_numbers: bool, - #[serde(default)] - term_width: Option, -} - -impl From for Renderer { - fn from(val: RendererDef) -> Self { - let RendererDef { - anonymized_line_numbers, - term_width, - } = val; - Renderer::plain() - .anonymized_line_numbers(anonymized_line_numbers) - .term_width(term_width.unwrap_or(DEFAULT_TERM_WIDTH)) - } -} diff --git a/tests/fixtures/main.rs b/tests/fixtures/main.rs deleted file mode 100644 index 81d0246c..00000000 --- a/tests/fixtures/main.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod deserialize; - -use crate::deserialize::Fixture; -use annotate_snippets::{Message, Renderer}; -use snapbox::data::DataFormat; -use snapbox::Data; -use std::error::Error; - -fn main() { - #[cfg(not(windows))] - tryfn::Harness::new("tests/fixtures/", setup, test) - .select(["*/*.toml"]) - .test(); -} - -fn setup(input_path: std::path::PathBuf) -> tryfn::Case { - let name = input_path.file_name().unwrap().to_str().unwrap().to_owned(); - let expected = Data::read_from(&input_path.with_extension("svg"), None); - tryfn::Case { - name, - fixture: input_path, - expected, - } -} - -fn test(input_path: &std::path::Path) -> Result> { - let src = std::fs::read_to_string(input_path)?; - let (renderer, message): (Renderer, Message<'_>) = - toml::from_str(&src).map(|a: Fixture<'_>| (a.renderer.into(), a.message.into()))?; - let actual = renderer.render(message).to_string(); - Ok(Data::from(actual).coerce_to(DataFormat::TermSvg)) -} diff --git a/tests/fixtures/no-color/ann_eof.svg b/tests/fixtures/no-color/ann_eof.svg deleted file mode 100644 index c8900d03..00000000 --- a/tests/fixtures/no-color/ann_eof.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error: expected `.`, `=` - - --> Cargo.toml:1:5 - - | - - 1 | asdf - - | ^ - - | - - - - diff --git a/tests/fixtures/no-color/ann_eof.toml b/tests/fixtures/no-color/ann_eof.toml deleted file mode 100644 index 313d2204..00000000 --- a/tests/fixtures/no-color/ann_eof.toml +++ /dev/null @@ -1,12 +0,0 @@ -[message] -level = "Error" -title = "expected `.`, `=`" - -[[message.snippets]] -source = "asdf" -line_start = 1 -origin = "Cargo.toml" -[[message.snippets.annotations]] -label = "" -level = "Error" -range = [4, 4] diff --git a/tests/fixtures/no-color/ann_insertion.svg b/tests/fixtures/no-color/ann_insertion.svg deleted file mode 100644 index b15b81b4..00000000 --- a/tests/fixtures/no-color/ann_insertion.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error: expected `.`, `=` - - --> Cargo.toml:1:3 - - | - - 1 | asf - - | ^ 'd' belongs here - - | - - - - diff --git a/tests/fixtures/no-color/ann_insertion.toml b/tests/fixtures/no-color/ann_insertion.toml deleted file mode 100644 index ffd2140b..00000000 --- a/tests/fixtures/no-color/ann_insertion.toml +++ /dev/null @@ -1,12 +0,0 @@ -[message] -level = "Error" -title = "expected `.`, `=`" - -[[message.snippets]] -source = "asf" -line_start = 1 -origin = "Cargo.toml" -[[message.snippets.annotations]] -label = "'d' belongs here" -level = "Error" -range = [2, 2] diff --git a/tests/fixtures/no-color/ann_multiline.svg b/tests/fixtures/no-color/ann_multiline.svg deleted file mode 100644 index f4b4433b..00000000 --- a/tests/fixtures/no-color/ann_multiline.svg +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - error[E0027]: pattern does not mention fields `lineno`, `content` - - --> src/display_list.rs:139:32 - - | - - 139 | if let DisplayLine::Source { - - | ________________________________^ - - 140 | | ref mut inline_marks, - - 141 | | } = body[body_idx] - - | |_________________________^ missing fields `lineno`, `content` - - | - - - - diff --git a/tests/fixtures/no-color/ann_multiline.toml b/tests/fixtures/no-color/ann_multiline.toml deleted file mode 100644 index 671b5344..00000000 --- a/tests/fixtures/no-color/ann_multiline.toml +++ /dev/null @@ -1,18 +0,0 @@ -[message] -level = "Error" -id = "E0027" -title = "pattern does not mention fields `lineno`, `content`" - -[[message.snippets]] -source = """ - if let DisplayLine::Source { - ref mut inline_marks, - } = body[body_idx] -""" -line_start = 139 -origin = "src/display_list.rs" -fold = false -[[message.snippets.annotations]] -label = "missing fields `lineno`, `content`" -level = "Error" -range = [31, 128] diff --git a/tests/fixtures/no-color/ann_multiline2.svg b/tests/fixtures/no-color/ann_multiline2.svg deleted file mode 100644 index 49c2c4b7..00000000 --- a/tests/fixtures/no-color/ann_multiline2.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - error[E####]: spacing error found - - --> foo.txt:26:12 - - | - - 26 | This is an example - - | ^^^^^^^ this should not be on separate lines - - 27 | of an edge case of an annotation overflowing - - 28 | to exactly one character on next line. - - | - - - - diff --git a/tests/fixtures/no-color/ann_multiline2.toml b/tests/fixtures/no-color/ann_multiline2.toml deleted file mode 100644 index afb3aa9c..00000000 --- a/tests/fixtures/no-color/ann_multiline2.toml +++ /dev/null @@ -1,18 +0,0 @@ -[message] -level = "Error" -id = "E####" -title = "spacing error found" - -[[message.snippets]] -source = """ -This is an example -of an edge case of an annotation overflowing -to exactly one character on next line. -""" -line_start = 26 -origin = "foo.txt" -fold = false -[[message.snippets.annotations]] -label = "this should not be on separate lines" -level = "Error" -range = [11, 19] diff --git a/tests/fixtures/no-color/ann_removed_nl.svg b/tests/fixtures/no-color/ann_removed_nl.svg deleted file mode 100644 index c8900d03..00000000 --- a/tests/fixtures/no-color/ann_removed_nl.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error: expected `.`, `=` - - --> Cargo.toml:1:5 - - | - - 1 | asdf - - | ^ - - | - - - - diff --git a/tests/fixtures/no-color/ann_removed_nl.toml b/tests/fixtures/no-color/ann_removed_nl.toml deleted file mode 100644 index b681c293..00000000 --- a/tests/fixtures/no-color/ann_removed_nl.toml +++ /dev/null @@ -1,12 +0,0 @@ -[message] -level = "Error" -title = "expected `.`, `=`" - -[[message.snippets]] -source = "asdf" -line_start = 1 -origin = "Cargo.toml" -[[message.snippets.annotations]] -label = "" -level = "Error" -range = [4, 5] diff --git a/tests/fixtures/no-color/ensure-emoji-highlight-width.svg b/tests/fixtures/no-color/ensure-emoji-highlight-width.svg deleted file mode 100644 index 0840805e..00000000 --- a/tests/fixtures/no-color/ensure-emoji-highlight-width.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error: invalid character ` ` in package name: `haha this isn't a valid name 🐛`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) - - --> <file>:7:1 - - | - - 7 | "haha this isn't a valid name 🐛" = { package = "libc", version = "0.1" } - - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - | - - - - diff --git a/tests/fixtures/no-color/ensure-emoji-highlight-width.toml b/tests/fixtures/no-color/ensure-emoji-highlight-width.toml deleted file mode 100644 index 7af05ff1..00000000 --- a/tests/fixtures/no-color/ensure-emoji-highlight-width.toml +++ /dev/null @@ -1,15 +0,0 @@ -[message] -title = "invalid character ` ` in package name: `haha this isn't a valid name 🐛`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters)" -level = "Error" - - -[[message.snippets]] -source = """ -"haha this isn't a valid name 🐛" = { package = "libc", version = "0.1" } -""" -line_start = 7 -origin = "" -[[message.snippets.annotations]] -label = "" -level = "Error" -range = [0, 35] diff --git a/tests/fixtures/no-color/fold_ann_multiline.svg b/tests/fixtures/no-color/fold_ann_multiline.svg deleted file mode 100644 index f82fe25d..00000000 --- a/tests/fixtures/no-color/fold_ann_multiline.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - error[E0308]: mismatched types - - --> src/format.rs:51:6 - - | - - 51 | ) -> Option<String> { - - | -------------- expected `std::option::Option<std::string::String>` because of return type - - 52 | / for ann in annotations { - - 53 | | match (ann.range.0, ann.range.1) { - - ... | - - 71 | | } - - 72 | | } - - | |_____^ expected enum `std::option::Option`, found () - - | - - - - diff --git a/tests/fixtures/no-color/fold_ann_multiline.toml b/tests/fixtures/no-color/fold_ann_multiline.toml deleted file mode 100644 index 09fc7d44..00000000 --- a/tests/fixtures/no-color/fold_ann_multiline.toml +++ /dev/null @@ -1,41 +0,0 @@ -[message] -level = "Error" -id = "E0308" -title = "mismatched types" - -[[message.snippets]] -source = """ -) -> Option { - for ann in annotations { - match (ann.range.0, ann.range.1) { - (None, None) => continue, - (Some(start), Some(end)) if start > end_index || end < start_index => continue, - (Some(start), Some(end)) if start >= start_index && end <= end_index => { - let label = if let Some(ref label) = ann.label { - format!(" {}", label) - } else { - String::from("") - }; - - return Some(format!( - "{}{}{}", - " ".repeat(start - start_index), - "^".repeat(end - start), - label - )); - } - _ => continue, - } - } -""" -line_start = 51 -origin = "src/format.rs" -fold = true -[[message.snippets.annotations]] -label = "expected `std::option::Option` because of return type" -level = "Warning" -range = [5, 19] -[[message.snippets.annotations]] -label = "expected enum `std::option::Option`, found ()" -level = "Error" -range = [22, 766] diff --git a/tests/fixtures/no-color/fold_bad_origin_line.svg b/tests/fixtures/no-color/fold_bad_origin_line.svg deleted file mode 100644 index 13a08344..00000000 --- a/tests/fixtures/no-color/fold_bad_origin_line.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error - - --> path/to/error.rs:3:1 - - | - - 3 | invalid syntax - - | -------------- error here - - | - - - - diff --git a/tests/fixtures/no-color/fold_bad_origin_line.toml b/tests/fixtures/no-color/fold_bad_origin_line.toml deleted file mode 100644 index 1e81a713..00000000 --- a/tests/fixtures/no-color/fold_bad_origin_line.toml +++ /dev/null @@ -1,17 +0,0 @@ -[message] -level = "Error" -title = "" - -[[message.snippets]] -source = """ - - -invalid syntax -""" -line_start = 1 -origin = "path/to/error.rs" -fold = true -[[message.snippets.annotations]] -label = "error here" -level = "Warning" -range = [2,16] diff --git a/tests/fixtures/no-color/fold_leading.svg b/tests/fixtures/no-color/fold_leading.svg deleted file mode 100644 index 72887a28..00000000 --- a/tests/fixtures/no-color/fold_leading.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error[E0308]: invalid type: integer `20`, expected a bool - - --> Cargo.toml:11:13 - - | - - 11 | workspace = 20 - - | ^^ - - | - - - - diff --git a/tests/fixtures/no-color/fold_leading.toml b/tests/fixtures/no-color/fold_leading.toml deleted file mode 100644 index e3fc696a..00000000 --- a/tests/fixtures/no-color/fold_leading.toml +++ /dev/null @@ -1,26 +0,0 @@ -[message] -level = "Error" -id = "E0308" -title = "invalid type: integer `20`, expected a bool" - -[[message.snippets]] -source = """ -[workspace] - -[package] -name = "hello" -version = "1.0.0" -license = "MIT" -rust-version = "1.70" -edition = "2021" - -[lints] -workspace = 20 -""" -line_start = 1 -origin = "Cargo.toml" -fold = true -[[message.snippets.annotations]] -label = "" -level = "Error" -range = [132, 134] diff --git a/tests/fixtures/no-color/fold_trailing.svg b/tests/fixtures/no-color/fold_trailing.svg deleted file mode 100644 index 15c98502..00000000 --- a/tests/fixtures/no-color/fold_trailing.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error[E0308]: invalid type: integer `20`, expected a lints table - - --> Cargo.toml:1:9 - - | - - 1 | lints = 20 - - | ^^ - - | - - - - diff --git a/tests/fixtures/no-color/fold_trailing.toml b/tests/fixtures/no-color/fold_trailing.toml deleted file mode 100644 index 8ee4c051..00000000 --- a/tests/fixtures/no-color/fold_trailing.toml +++ /dev/null @@ -1,25 +0,0 @@ -[message] -level = "Error" -id = "E0308" -title = "invalid type: integer `20`, expected a lints table" - -[[message.snippets]] -source = """ -lints = 20 - -[workspace] - -[package] -name = "hello" -version = "1.0.0" -license = "MIT" -rust-version = "1.70" -edition = "2021" -""" -line_start = 1 -origin = "Cargo.toml" -fold = true -[[message.snippets.annotations]] -label = "" -level = "Error" -range = [8, 10] diff --git a/tests/fixtures/no-color/issue_9.svg b/tests/fixtures/no-color/issue_9.svg deleted file mode 100644 index af22d82d..00000000 --- a/tests/fixtures/no-color/issue_9.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - error: expected one of `.`, `;`, `?`, or an operator, found `for` - - --> /code/rust/src/test/ui/annotate-snippet/suggestion.rs:4:5 - - | - - 4 | let x = vec![1]; - - | - move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait - - | - - 7 | let y = x; - - | - value moved here - - | - - 9 | x; - - | ^ value used here after move - - | - - - - diff --git a/tests/fixtures/no-color/issue_9.toml b/tests/fixtures/no-color/issue_9.toml deleted file mode 100644 index 1f35243c..00000000 --- a/tests/fixtures/no-color/issue_9.toml +++ /dev/null @@ -1,28 +0,0 @@ -[message] -level = "Error" -title = "expected one of `.`, `;`, `?`, or an operator, found `for`" - -[[message.snippets]] -source = "let x = vec![1];" -line_start = 4 -origin = "/code/rust/src/test/ui/annotate-snippet/suggestion.rs" -[[message.snippets.annotations]] -label = "move occurs because `x` has type `std::vec::Vec`, which does not implement the `Copy` trait" -level = "Warning" -range = [4, 5] - -[[message.snippets]] -source = "let y = x;" -line_start = 7 -[[message.snippets.annotations]] -label = "value moved here" -level = "Warning" -range = [8, 9] - -[[message.snippets]] -source = "x;" -line_start = 9 -[[message.snippets.annotations]] -label = "value used here after move" -level = "Error" -range = [0, 1] diff --git a/tests/fixtures/no-color/multiple_annotations.svg b/tests/fixtures/no-color/multiple_annotations.svg deleted file mode 100644 index 18bca93e..00000000 --- a/tests/fixtures/no-color/multiple_annotations.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - error - - | - - 96 | fn add_title_line(result: &mut Vec<String>, main_annotation: Option<&Annotation>) { - - 97 | if let Some(annotation) = main_annotation { - - | ^^^^^^^^^^ Variable defined here - - 98 | result.push(format_title_line( - - 99 | &annotation.annotation_type, - - | ^^^^^^^^^^ Referenced here - - 100 | None, - - 101 | &annotation.label, - - | ^^^^^^^^^^ Referenced again here - - 102 | )); - - 103 | } - - 104 | } - - | - - - - diff --git a/tests/fixtures/no-color/multiple_annotations.toml b/tests/fixtures/no-color/multiple_annotations.toml deleted file mode 100644 index 842b137e..00000000 --- a/tests/fixtures/no-color/multiple_annotations.toml +++ /dev/null @@ -1,29 +0,0 @@ -[message] -level = "Error" -title = "" - -[[message.snippets]] -source = """ -fn add_title_line(result: &mut Vec, main_annotation: Option<&Annotation>) { - if let Some(annotation) = main_annotation { - result.push(format_title_line( - &annotation.annotation_type, - None, - &annotation.label, - )); - } -} -""" -line_start = 96 -[[message.snippets.annotations]] -label = "Variable defined here" -level = "Error" -range = [100, 110] -[[message.snippets.annotations]] -label = "Referenced here" -level = "Error" -range = [184, 194] -[[message.snippets.annotations]] -label = "Referenced again here" -level = "Error" -range = [243, 253] diff --git a/tests/fixtures/no-color/simple.svg b/tests/fixtures/no-color/simple.svg deleted file mode 100644 index ae7b03cf..00000000 --- a/tests/fixtures/no-color/simple.svg +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - error: expected one of `.`, `;`, `?`, or an operator, found `for` - - --> src/format_color.rs:171:9 - - | - - 169 | }) - - | - expected one of `.`, `;`, `?`, or an operator here - - 170 | - - 171 | for line in &self.body { - - | ^^^ unexpected token - - | - - - - diff --git a/tests/fixtures/no-color/simple.toml b/tests/fixtures/no-color/simple.toml deleted file mode 100644 index 76b5bac6..00000000 --- a/tests/fixtures/no-color/simple.toml +++ /dev/null @@ -1,19 +0,0 @@ -[message] -level = "Error" -title = "expected one of `.`, `;`, `?`, or an operator, found `for`" - -[[message.snippets]] -source = """ - }) - - for line in &self.body {""" -line_start = 169 -origin = "src/format_color.rs" -[[message.snippets.annotations]] -label = "unexpected token" -level = "Error" -range = [20, 23] -[[message.snippets.annotations]] -label = "expected one of `.`, `;`, `?`, or an operator here" -level = "Warning" -range = [10, 11] diff --git a/tests/fixtures/no-color/strip_line.svg b/tests/fixtures/no-color/strip_line.svg deleted file mode 100644 index b1fd8a6d..00000000 --- a/tests/fixtures/no-color/strip_line.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error[E0308]: mismatched types - - --> $DIR/whitespace-trimming.rs:4:193 - - | - - LL | ... let _: () = 42; - - | ^^ expected (), found integer - - | - - - - diff --git a/tests/fixtures/no-color/strip_line.toml b/tests/fixtures/no-color/strip_line.toml deleted file mode 100644 index 459cbe1c..00000000 --- a/tests/fixtures/no-color/strip_line.toml +++ /dev/null @@ -1,18 +0,0 @@ -[message] -level = "Error" -id = "E0308" -title = "mismatched types" - -[[message.snippets]] -source = " let _: () = 42;" -line_start = 4 -origin = "$DIR/whitespace-trimming.rs" - -[[message.snippets.annotations]] -label = "expected (), found integer" -level = "Error" -range = [192, 194] - -[renderer] -color = false -anonymized_line_numbers = true diff --git a/tests/fixtures/no-color/strip_line_char.svg b/tests/fixtures/no-color/strip_line_char.svg deleted file mode 100644 index 15296a14..00000000 --- a/tests/fixtures/no-color/strip_line_char.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - error[E0308]: mismatched types - - --> $DIR/whitespace-trimming.rs:4:193 - - | - - LL | ... let _: () = 42ñ - - | ^^ expected (), found integer - - | - - - - diff --git a/tests/fixtures/no-color/strip_line_char.toml b/tests/fixtures/no-color/strip_line_char.toml deleted file mode 100644 index dedefd5a..00000000 --- a/tests/fixtures/no-color/strip_line_char.toml +++ /dev/null @@ -1,18 +0,0 @@ -[message] -level = "Error" -id = "E0308" -title = "mismatched types" - -[[message.snippets]] -source = " let _: () = 42ñ" -line_start = 4 -origin = "$DIR/whitespace-trimming.rs" - -[[message.snippets.annotations]] -label = "expected (), found integer" -level = "Error" -range = [192, 194] - -[renderer] -color = false -anonymized_line_numbers = true diff --git a/tests/fixtures/no-color/strip_line_non_ws.svg b/tests/fixtures/no-color/strip_line_non_ws.svg deleted file mode 100644 index f1977dc5..00000000 --- a/tests/fixtures/no-color/strip_line_non_ws.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - error[E0308]: mismatched types - - --> $DIR/non-whitespace-trimming.rs:4:242 - - | - - LL | ... = (); let _: () = (); let _: () = (); let _: () = 42; let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () ... - - | ^^ ^^ expected `()`, found integer - - | | - - | expected due to this - - | - - - - diff --git a/tests/fixtures/no-color/strip_line_non_ws.toml b/tests/fixtures/no-color/strip_line_non_ws.toml deleted file mode 100644 index 06ecad85..00000000 --- a/tests/fixtures/no-color/strip_line_non_ws.toml +++ /dev/null @@ -1,25 +0,0 @@ -[message] -level = "Error" -id = "E0308" -title = "mismatched types" - -[[message.snippets]] -source = """ - let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = 42; let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); -""" -line_start = 4 -origin = "$DIR/non-whitespace-trimming.rs" - -[[message.snippets.annotations]] -label = "expected `()`, found integer" -level = "Error" -range = [241, 243] - -[[message.snippets.annotations]] -label = "expected due to this" -level = "Error" -range = [236, 238] - - -[renderer] -anonymized_line_numbers = true diff --git a/tests/formatter.rs b/tests/formatter.rs index 7f914de9..75cf8532 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -1,14 +1,17 @@ -use annotate_snippets::{Level, Renderer, Snippet}; +use annotate_snippets::{Annotation, AnnotationKind, Group, Level, Patch, Renderer, Snippet}; +use annotate_snippets::renderer::OutputTheme; use snapbox::{assert_data_eq, str}; #[test] fn test_i_29() { - let snippets = Level::Error.title("oops").snippet( - Snippet::source("First line\r\nSecond oops line") - .origin("") - .annotation(Level::Error.span(19..23).label("oops")) - .fold(true), + let snippets = Level::ERROR.header("oops").group( + Group::new().element( + Snippet::source("First line\r\nSecond oops line") + .origin("") + .annotation(AnnotationKind::Primary.span(19..23).label("oops")) + .fold(true), + ), ); let expected = str![[r#" error: oops @@ -16,150 +19,160 @@ error: oops | 2 | Second oops line | ^^^^ oops - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(snippets).to_string(), expected); + assert_data_eq!(renderer.render(snippets), expected); } #[test] fn test_point_to_double_width_characters() { - let snippets = Level::Error.title("").snippet( - Snippet::source("こんにちは、世界") - .origin("") - .annotation(Level::Error.span(18..24).label("world")), + let snippets = Level::ERROR.header("").group( + Group::new().element( + Snippet::source("こんにちは、世界") + .origin("") + .annotation(AnnotationKind::Primary.span(18..24).label("world")), + ), ); let expected = str![[r#" -error +error: --> :1:7 | 1 | こんにちは、世界 | ^^^^ world - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(snippets).to_string(), expected); + assert_data_eq!(renderer.render(snippets), expected); } #[test] fn test_point_to_double_width_characters_across_lines() { - let snippets = Level::Error.title("").snippet( - Snippet::source("おはよう\nございます") - .origin("") - .annotation(Level::Error.span(6..22).label("Good morning")), + let snippets = Level::ERROR.header("").group( + Group::new().element( + Snippet::source("おはよう\nございます") + .origin("") + .annotation(AnnotationKind::Primary.span(6..22).label("Good morning")), + ), ); let expected = str![[r#" -error +error: --> :1:3 | 1 | おはよう | _____^ 2 | | ございます | |______^ Good morning - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(snippets).to_string(), expected); + assert_data_eq!(renderer.render(snippets), expected); } #[test] fn test_point_to_double_width_characters_multiple() { - let snippets = Level::Error.title("").snippet( - Snippet::source("お寿司\n食べたい🍣") - .origin("") - .annotation(Level::Error.span(0..9).label("Sushi1")) - .annotation(Level::Note.span(16..22).label("Sushi2")), + let snippets = Level::ERROR.header("").group( + Group::new().element( + Snippet::source("お寿司\n食べたい🍣") + .origin("") + .annotation(AnnotationKind::Primary.span(0..9).label("Sushi1")) + .annotation(AnnotationKind::Context.span(16..22).label("Sushi2")), + ), ); let expected = str![[r#" -error +error: --> :1:1 | 1 | お寿司 | ^^^^^^ Sushi1 2 | 食べたい🍣 - | ---- note: Sushi2 - | + | ---- Sushi2 "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(snippets).to_string(), expected); + assert_data_eq!(renderer.render(snippets), expected); } #[test] fn test_point_to_double_width_characters_mixed() { - let snippets = Level::Error.title("").snippet( - Snippet::source("こんにちは、新しいWorld!") - .origin("") - .annotation(Level::Error.span(18..32).label("New world")), + let snippets = Level::ERROR.header("").group( + Group::new().element( + Snippet::source("こんにちは、新しいWorld!") + .origin("") + .annotation(AnnotationKind::Primary.span(18..32).label("New world")), + ), ); let expected = str![[r#" -error +error: --> :1:7 | 1 | こんにちは、新しいWorld! | ^^^^^^^^^^^ New world - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(snippets).to_string(), expected); + assert_data_eq!(renderer.render(snippets), expected); } #[test] fn test_format_title() { - let input = Level::Error.title("This is a title").id("E0001"); + let input = Level::ERROR.header("This is a title").id("E0001"); let expected = str![r#"error[E0001]: This is a title"#]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn test_format_snippet_only() { let source = "This is line 1\nThis is line 2"; - let input = Level::Error - .title("") - .snippet(Snippet::source(source).line_start(5402)); + let input = Level::ERROR + .header("") + .group(Group::new().element(Snippet::>::source(source).line_start(5402))); let expected = str![[r#" -error +error: | 5402 | This is line 1 5403 | This is line 2 - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn test_format_snippets_continuation() { let src_0 = "This is slice 1"; let src_1 = "This is slice 2"; - let input = Level::Error - .title("") - .snippet(Snippet::source(src_0).line_start(5402).origin("file1.rs")) - .snippet(Snippet::source(src_1).line_start(2).origin("file2.rs")); + let input = Level::ERROR.header("").group( + Group::new() + .element( + Snippet::>::source(src_0) + .line_start(5402) + .origin("file1.rs"), + ) + .element( + Snippet::>::source(src_1) + .line_start(2) + .origin("file2.rs"), + ), + ); let expected = str![[r#" -error +error: --> file1.rs | 5402 | This is slice 1 | - ::: file2.rs + ::: file2.rs:2 | 2 | This is slice 2 - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] @@ -169,34 +182,38 @@ fn test_format_snippet_annotation_standalone() { let source = [line_1, line_2].join("\n"); // In line 2 let range = 22..24; - let input = Level::Error.title("").snippet( - Snippet::source(&source) - .line_start(5402) - .annotation(Level::Info.span(range.clone()).label("Test annotation")), + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(&source).line_start(5402).annotation( + AnnotationKind::Context + .span(range.clone()) + .label("Test annotation"), + ), + ), ); let expected = str![[r#" -error +error: | 5402 | This is line 1 5403 | This is line 2 - | -- info: Test annotation - | + | -- Test annotation "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn test_format_footer_title() { - let input = Level::Error - .title("") - .footer(Level::Error.title("This __is__ a title")); + let input = Level::ERROR + .header("") + .group(Group::new().element(Level::ERROR.title("This __is__ a title"))); let expected = str![[r#" -error - = error: This __is__ a title +error: + | + = error: This __is__ a title "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] @@ -204,116 +221,121 @@ error fn test_i26() { let source = "short"; let label = "label"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .line_start(0) - .annotation(Level::Error.span(0..source.len() + 2).label(label)), + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source).line_start(0).annotation( + AnnotationKind::Primary + .span(0..source.len() + 2) + .label(label), + ), + ), ); let renderer = Renderer::plain(); - let _ = renderer.render(input).to_string(); + let _ = renderer.render(input); } #[test] fn test_source_content() { let source = "This is an example\nof content lines"; - let input = Level::Error - .title("") - .snippet(Snippet::source(source).line_start(56)); + let input = Level::ERROR + .header("") + .group(Group::new().element(Snippet::>::source(source).line_start(56))); let expected = str![[r#" -error +error: | 56 | This is an example 57 | of content lines - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn test_source_annotation_standalone_singleline() { let source = "tests"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .line_start(1) - .annotation(Level::Help.span(0..5).label("Example string")), + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .annotation(AnnotationKind::Context.span(0..5).label("Example string")), + ), ); let expected = str![[r#" -error +error: | 1 | tests - | ----- help: Example string - | + | ----- Example string "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn test_source_annotation_standalone_multiline() { let source = "tests"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .line_start(1) - .annotation(Level::Help.span(0..5).label("Example string")) - .annotation(Level::Help.span(0..5).label("Second line")), + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .annotation(AnnotationKind::Context.span(0..5).label("Example string")) + .annotation(AnnotationKind::Context.span(0..5).label("Second line")), + ), ); let expected = str![[r#" -error +error: | 1 | tests | ----- | | - | help: Example string - | help: Second line - | + | Example string + | Second line "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn test_only_source() { - let input = Level::Error - .title("") - .snippet(Snippet::source("").origin("file.rs")); + let input = Level::ERROR + .header("") + .group(Group::new().element(Snippet::>::source("").origin("file.rs"))); let expected = str![[r#" -error ---> file.rs - | - | +error: + --> file.rs + | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn test_anon_lines() { let source = "This is an example\nof content lines\n\nabc"; - let input = Level::Error - .title("") - .snippet(Snippet::source(source).line_start(56)); + let input = Level::ERROR + .header("") + .group(Group::new().element(Snippet::>::source(source).line_start(56))); let expected = str![[r#" -error +error: | LL | This is an example LL | of content lines LL | LL | abc - | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(true); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn issue_130() { - let input = Level::Error.title("dummy").snippet( - Snippet::source("foo\nbar\nbaz") - .origin("file/path") - .line_start(3) - .fold(true) - .annotation(Level::Error.span(4..11)), // bar\nbaz + let input = Level::ERROR.header("dummy").group( + Group::new().element( + Snippet::source("foo\nbar\nbaz") + .origin("file/path") + .line_start(3) + .fold(true) + .annotation(AnnotationKind::Primary.span(4..11)), + ), // bar\nbaz ); let expected = str![[r#" @@ -323,10 +345,9 @@ error: dummy 4 | / bar 5 | | baz | |___^ - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] @@ -335,299 +356,323 @@ fn unterminated_string_multiline() { a\" // ... "; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .fold(true) - .annotation(Level::Error.span(0..10)), // 1..10 works + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .fold(true) + .annotation(AnnotationKind::Primary.span(0..10)), + ), // 1..10 works ); let expected = str![[r#" -error +error: --> file/path:3:1 | 3 | / a" 4 | | // ... | |_______^ - | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn char_and_nl_annotate_char() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(0..2)), // a\r + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(0..2)), + ), // a\r ); let expected = str![[r#" -error +error: --> file/path:3:1 | 3 | a | ^ 4 | b - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn char_eol_annotate_char() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(0..3)), // a\r\n + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(0..3)), + ), // a\r\n ); let expected = str![[r#" -error +error: --> file/path:3:1 | 3 | a | ^ 4 | b - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn char_eol_annotate_char_double_width() { - let snippets = Level::Error.title("").snippet( - Snippet::source("こん\r\nにちは\r\n世界") - .origin("") - .annotation(Level::Error.span(3..8)), // ん\r\n + let snippets = Level::ERROR.header("").group( + Group::new().element( + Snippet::source("こん\r\nにちは\r\n世界") + .origin("") + .annotation(AnnotationKind::Primary.span(3..8)), + ), // ん\r\n ); let expected = str![[r#" -error +error: --> :1:2 | 1 | こん | ^^ 2 | にちは 3 | 世界 - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(snippets).to_string(), expected); + assert_data_eq!(renderer.render(snippets), expected); } #[test] fn annotate_eol() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(1..2)), // \r + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(1..2)), + ), // \r ); let expected = str![[r#" -error +error: --> file/path:3:2 | 3 | a | ^ 4 | b - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn annotate_eol2() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(1..3)), // \r\n + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(1..3)), + ), // \r\n ); let expected = str![[r#" -error +error: --> file/path:3:2 | 3 | a | ^ 4 | b - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn annotate_eol3() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(2..3)), // \n + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(2..3)), + ), // \n ); let expected = str![[r#" -error - --> file/path:3:2 +error: + --> file/path:3:3 | 3 | a | ^ 4 | b - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn annotate_eol4() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(2..2)), // \n + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(2..2)), + ), // \n ); let expected = str![[r#" -error - --> file/path:3:2 +error: + --> file/path:3:3 | 3 | a | ^ 4 | b - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn annotate_eol_double_width() { - let snippets = Level::Error.title("").snippet( - Snippet::source("こん\r\nにちは\r\n世界") - .origin("") - .annotation(Level::Error.span(7..8)), // \n + let snippets = Level::ERROR.header("").group( + Group::new().element( + Snippet::source("こん\r\nにちは\r\n世界") + .origin("") + .annotation(AnnotationKind::Primary.span(7..8)), + ), // \n ); let expected = str![[r#" -error - --> :1:3 +error: + --> :1:4 | 1 | こん | ^ 2 | にちは 3 | 世界 - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(snippets).to_string(), expected); + assert_data_eq!(renderer.render(snippets), expected); } #[test] fn multiline_eol_start() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(1..4)), // \r\nb + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(1..4)), + ), // \r\nb ); let expected = str![[r#" -error +error: --> file/path:3:2 | 3 | a | __^ 4 | | b | |_^ - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiline_eol_start2() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(2..4)), // \nb + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(2..4)), + ), // \nb ); let expected = str![[r#" -error - --> file/path:3:2 +error: + --> file/path:3:3 | 3 | a | __^ 4 | | b | |_^ - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiline_eol_start3() { let source = "a\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(1..3)), // \nb + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(1..3)), + ), // \nb ); let expected = str![[r#" -error +error: --> file/path:3:2 | 3 | a | __^ 4 | | b | |_^ - |"#]]; +"#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiline_eol_start_double_width() { - let snippets = Level::Error.title("").snippet( - Snippet::source("こん\r\nにちは\r\n世界") - .origin("") - .annotation(Level::Error.span(7..11)), // \r\nに + let snippets = Level::ERROR.header("").group( + Group::new().element( + Snippet::source("こん\r\nにちは\r\n世界") + .origin("") + .annotation(AnnotationKind::Primary.span(7..11)), + ), // \r\nに ); let expected = str![[r#" -error - --> :1:3 +error: + --> :1:4 | 1 | こん | _____^ 2 | | にちは | |__^ 3 | 世界 - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(snippets).to_string(), expected); + assert_data_eq!(renderer.render(snippets), expected); } #[test] fn multiline_eol_start_eol_end() { let source = "a\nb\nc"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(1..4)), // \nb\n + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(1..4)), + ), // \nb\n ); let expected = str![[r#" -error +error: --> file/path:3:2 | 3 | a @@ -635,136 +680,140 @@ error 4 | | b | |__^ 5 | c - | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiline_eol_start_eol_end2() { let source = "a\r\nb\r\nc"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(2..5)), // \nb\r + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(2..5)), + ), // \nb\r ); let expected = str![[r#" -error - --> file/path:3:2 +error: + --> file/path:3:3 | 3 | a | __^ 4 | | b | |__^ 5 | c - | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiline_eol_start_eol_end3() { let source = "a\r\nb\r\nc"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(2..6)), // \nb\r\n + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(2..6)), + ), // \nb\r\n ); let expected = str![[r#" -error - --> file/path:3:2 +error: + --> file/path:3:3 | 3 | a | __^ 4 | | b | |__^ 5 | c - | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiline_eol_start_eof_end() { let source = "a\r\nb"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(1..5)), // \r\nb(EOF) + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(1..5)), + ), // \r\nb(EOF) ); let expected = str![[r#" -error +error: --> file/path:3:2 | 3 | a | __^ 4 | | b | |__^ - | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiline_eol_start_eof_end_double_width() { let source = "ん\r\nに"; - let input = Level::Error.title("").snippet( - Snippet::source(source) - .origin("file/path") - .line_start(3) - .annotation(Level::Error.span(3..9)), // \r\nに(EOF) + let input = Level::ERROR.header("").group( + Group::new().element( + Snippet::source(source) + .origin("file/path") + .line_start(3) + .annotation(AnnotationKind::Primary.span(3..9)), + ), // \r\nに(EOF) ); let expected = str![[r#" -error +error: --> file/path:3:2 | 3 | ん | ___^ 4 | | に | |___^ - | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn two_single_line_same_line() { let source = r#"bar = { version = "0.1.0", optional = true }"#; - let input = Level::Error.title("unused optional dependency").snippet( - Snippet::source(source) - .origin("Cargo.toml") - .line_start(4) - .annotation( - Level::Error - .span(0..3) - .label("I need this to be really long so I can test overlaps"), - ) - .annotation( - Level::Info - .span(27..42) - .label("This should also be long but not too long"), - ), + let input = Level::ERROR.header("unused optional dependency").group( + Group::new().element( + Snippet::source(source) + .origin("Cargo.toml") + .line_start(4) + .annotation( + AnnotationKind::Primary + .span(0..3) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + AnnotationKind::Context + .span(27..42) + .label("This should also be long but not too long"), + ), + ), ); let expected = str![[r#" error: unused optional dependency --> Cargo.toml:4:1 | 4 | bar = { version = "0.1.0", optional = true } - | ^^^ --------------- info: This should also be long but not too long + | ^^^ --------------- This should also be long but not too long | | | I need this to be really long so I can test overlaps - | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] @@ -774,19 +823,21 @@ this is another line so is this bar = { version = "0.1.0", optional = true } "#; - let input = Level::Error.title("unused optional dependency").snippet( - Snippet::source(source) - .line_start(4) - .annotation( - Level::Error - .span(41..119) - .label("I need this to be really long so I can test overlaps"), - ) - .annotation( - Level::Info - .span(27..42) - .label("This should also be long but not too long"), - ), + let input = Level::ERROR.header("unused optional dependency").group( + Group::new().element( + Snippet::source(source) + .line_start(4) + .annotation( + AnnotationKind::Primary + .span(41..119) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + AnnotationKind::Context + .span(27..42) + .label("This should also be long but not too long"), + ), + ), ); let expected = str![[r#" error: unused optional dependency @@ -794,15 +845,14 @@ error: unused optional dependency 4 | bar = { version = "0.1.0", optional = true } | ____________________________--------------^ | | | - | | info: This should also be long but not too long + | | This should also be long but not too long 5 | | this is another line 6 | | so is this 7 | | bar = { version = "0.1.0", optional = true } | |__________________________________________^ I need this to be really long so I can test overlaps - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] @@ -812,24 +862,26 @@ this is another line so is this bar = { version = "0.1.0", optional = true } "#; - let input = Level::Error.title("unused optional dependency").snippet( - Snippet::source(source) - .line_start(4) - .annotation( - Level::Error - .span(41..119) - .label("I need this to be really long so I can test overlaps"), - ) - .annotation( - Level::Error - .span(8..102) - .label("I need this to be really long so I can test overlaps"), - ) - .annotation( - Level::Info - .span(27..42) - .label("This should also be long but not too long"), - ), + let input = Level::ERROR.header("unused optional dependency").group( + Group::new().element( + Snippet::source(source) + .line_start(4) + .annotation( + AnnotationKind::Primary + .span(41..119) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + AnnotationKind::Primary + .span(8..102) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + AnnotationKind::Context + .span(27..42) + .label("This should also be long but not too long"), + ), + ), ); let expected = str![[r#" error: unused optional dependency @@ -837,7 +889,7 @@ error: unused optional dependency 4 | bar = { version = "0.1.0", optional = true } | _________^__________________--------------^ | | | | - | |_________| info: This should also be long but not too long + | |_________| This should also be long but not too long | || 5 | || this is another line 6 | || so is this @@ -845,10 +897,9 @@ error: unused optional dependency | ||_________________________^________________^ I need this to be really long so I can test overlaps | |__________________________| | I need this to be really long so I can test overlaps - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] @@ -859,29 +910,31 @@ so is this bar = { version = "0.1.0", optional = true } this is another line "#; - let input = Level::Error.title("unused optional dependency").snippet( - Snippet::source(source) - .line_start(4) - .annotation( - Level::Error - .span(41..119) - .label("I need this to be really long so I can test overlaps"), - ) - .annotation( - Level::Error - .span(8..102) - .label("I need this to be really long so I can test overlaps"), - ) - .annotation( - Level::Error - .span(48..126) - .label("I need this to be really long so I can test overlaps"), - ) - .annotation( - Level::Info - .span(27..42) - .label("This should also be long but not too long"), - ), + let input = Level::ERROR.header("unused optional dependency").group( + Group::new().element( + Snippet::source(source) + .line_start(4) + .annotation( + AnnotationKind::Primary + .span(41..119) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + AnnotationKind::Primary + .span(8..102) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + AnnotationKind::Primary + .span(48..126) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + AnnotationKind::Context + .span(27..42) + .label("This should also be long but not too long"), + ), + ), ); let expected = str![[r#" error: unused optional dependency @@ -889,7 +942,7 @@ error: unused optional dependency 4 | bar = { version = "0.1.0", optional = true } | __________^__________________--------------^ | | | | - | |__________| info: This should also be long but not too long + | |__________| This should also be long but not too long | || 5 | || this is another line | || ____^ @@ -900,8 +953,1435 @@ error: unused optional dependency | | I need this to be really long so I can test overlaps 8 | | this is another line | |____^ I need this to be really long so I can test overlaps +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn origin_correct_start_line() { + let source = "aaa\nbbb\nccc\nddd\n"; + let input = Level::ERROR.header("title").group( + Group::new().element( + Snippet::source(source) + .origin("origin.txt") + .fold(false) + .annotation(AnnotationKind::Primary.span(8..8 + 3).label("annotation")), + ), + ); + + let expected = str![[r#" +error: title + --> origin.txt:3:1 + | +1 | aaa +2 | bbb +3 | ccc + | ^^^ annotation +4 | ddd +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn origin_correct_mid_line() { + let source = "aaa\nbbb\nccc\nddd\n"; + let input = Level::ERROR.header("title").group( + Group::new().element( + Snippet::source(source) + .origin("origin.txt") + .fold(false) + .annotation( + AnnotationKind::Primary + .span(8 + 1..8 + 3) + .label("annotation"), + ), + ), + ); + + let expected = str![[r#" +error: title + --> origin.txt:3:2 | +1 | aaa +2 | bbb +3 | ccc + | ^^ annotation +4 | ddd "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn two_suggestions_same_span() { + let source = r#" A.foo();"#; + let input_new = Level::ERROR + .header("expected value, found enum `A`") + .id("E0423") + .group( + Group::new().element( + Snippet::source(source) + .fold(true) + .annotation(AnnotationKind::Primary.span(4..5)), + ), + ) + .group( + Group::new() + .element( + Level::HELP + .title("you might have meant to use one of the following enum variants"), + ) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(4..5, "(A::Tuple())")), + ) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(4..5, "A::Unit")), + ), + ); + + let expected = str![[r#" +error[E0423]: expected value, found enum `A` + | +LL | A.foo(); + | ^ + | +help: you might have meant to use one of the following enum variants + | +LL - A.foo(); +LL + (A::Tuple()).foo(); + | +LL | A::Unit.foo(); + | ++++++ +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn two_suggestions_same_span2() { + let source = r#" +mod banana { + pub struct Chaenomeles; + + pub trait Apple { + fn pick(&self) {} + } + impl Apple for Chaenomeles {} + + pub trait Peach { + fn pick(&self, a: &mut ()) {} + } + impl Peach for Box {} + impl Peach for Chaenomeles {} +} + +fn main() { + banana::Chaenomeles.pick() +}"#; + let input_new = + Level::ERROR + .header("no method named `pick` found for struct `Chaenomeles` in the current scope") + .id("E0599") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .fold(true) + .annotation( + AnnotationKind::Context + .span(18..40) + .label("method `pick` not found for this struct"), + ) + .annotation( + AnnotationKind::Primary + .span(318..322) + .label("method not found in `Chaenomeles`"), + ), + ), + ) + .group( + Group::new() + .element(Level::HELP.title( + "the following traits which provide `pick` are implemented but not in scope; perhaps you want to import one of them", + )) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(1..1, "use banana::Apple;\n")), + ) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(1..1, "use banana::Peach;\n")), + ), + ); + let expected = str![[r#" +error[E0599]: no method named `pick` found for struct `Chaenomeles` in the current scope + | +LL | pub struct Chaenomeles; + | ---------------------- method `pick` not found for this struct +... +LL | banana::Chaenomeles.pick() + | ^^^^ method not found in `Chaenomeles` + | +help: the following traits which provide `pick` are implemented but not in scope; perhaps you want to import one of them + | +LL + use banana::Apple; + | +LL + use banana::Peach; + | +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn single_line_non_overlapping_suggestions() { + let source = r#" A.foo();"#; + + let input_new = Level::ERROR + .header("expected value, found enum `A`") + .id("E0423") + .group( + Group::new().element( + Snippet::source(source) + .fold(true) + .line_start(1) + .annotation(AnnotationKind::Primary.span(4..5)), + ), + ) + .group( + Group::new() + .element(Level::HELP.title("make these changes and things will work")) + .element( + Snippet::source(source) + .fold(true) + .fold(true) + .patch(Patch::new(4..5, "(A::Tuple())")) + .patch(Patch::new(6..9, "bar")), + ), + ); + + let expected = str![[r#" +error[E0423]: expected value, found enum `A` + | +LL | A.foo(); + | ^ + | +help: make these changes and things will work + | +LL - A.foo(); +LL + (A::Tuple()).bar(); + | +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn single_line_non_overlapping_suggestions2() { + let source = r#" ThisIsVeryLong.foo();"#; + let input_new = Level::ERROR + .header("Found `ThisIsVeryLong`") + .id("E0423") + .group( + Group::new().element( + Snippet::source(source) + .fold(true) + .line_start(1) + .annotation(AnnotationKind::Primary.span(4..18)), + ), + ) + .group( + Group::new() + .element(Level::HELP.title("make these changes and things will work")) + .element( + Snippet::source(source) + .fold(true) + .fold(true) + .patch(Patch::new(4..18, "(A::Tuple())")) + .patch(Patch::new(19..22, "bar")), + ), + ); + + let expected = str![[r#" +error[E0423]: Found `ThisIsVeryLong` + | +LL | ThisIsVeryLong.foo(); + | ^^^^^^^^^^^^^^ + | +help: make these changes and things will work + | +LL - ThisIsVeryLong.foo(); +LL + (A::Tuple()).bar(); + | +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn multiple_replacements() { + let source = r#" + let y = || { + self.bar(); + }; + self.qux(); + y(); +"#; + + let input_new = Level::ERROR + .header("cannot borrow `*self` as mutable because it is also borrowed as immutable") + .id("E0502") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .fold(true) + .annotation( + AnnotationKind::Primary + .span(49..59) + .label("mutable borrow occurs here"), + ) + .annotation( + AnnotationKind::Primary + .span(13..15) + .label("immutable borrow occurs here"), + ) + .annotation( + AnnotationKind::Primary + .span(26..30) + .label("first borrow occurs due to use of `*self` in closure"), + ) + .annotation( + AnnotationKind::Primary + .span(65..66) + .label("immutable borrow later used here"), + ), + ), + ) + .group( + Group::new() + .element( + Level::HELP + .title("try explicitly pass `&Self` into the Closure as an argument"), + ) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(14..14, "this: &Self")) + .patch(Patch::new(26..30, "this")) + .patch(Patch::new(66..68, "(self)")), + ), + ); + let expected = str![[r#" +error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable + | +LL | let y = || { + | ^^ immutable borrow occurs here +LL | self.bar(); + | ^^^^ first borrow occurs due to use of `*self` in closure +LL | }; +LL | self.qux(); + | ^^^^^^^^^^ mutable borrow occurs here +LL | y(); + | ^ immutable borrow later used here + | +help: try explicitly pass `&Self` into the Closure as an argument + | +LL ~ let y = |this: &Self| { +LL ~ this.bar(); +LL | }; +LL | self.qux(); +LL ~ y(self); + | +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn multiple_replacements2() { + let source = r#" +fn test1() { + let mut chars = "Hello".chars(); + for _c in chars.by_ref() { + chars.next(); + } +} + +fn main() { + test1(); +}"#; + + let input_new = Level::ERROR + .header("cannot borrow `chars` as mutable more than once at a time") + .id("E0499") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .fold(true) + .annotation( + AnnotationKind::Context + .span(65..70) + .label("first mutable borrow occurs here"), + ) + .annotation( + AnnotationKind::Primary + .span(90..95) + .label("second mutable borrow occurs here"), + ) + .annotation( + AnnotationKind::Context + .span(65..79) + .label("first borrow later used here"), + ), + ), + ) + .group( + Group::new() + .element( + Level::HELP + .title("if you want to call `next` on a iterator within the loop, consider using `while let`") + ) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new( + 55..59, + "let iter = chars.by_ref();\n while let Some(", + )) + .patch(Patch::new(61..79, ") = iter.next()")) + .patch(Patch::new(90..95, "iter")), + ), + ); + + let expected = str![[r#" +error[E0499]: cannot borrow `chars` as mutable more than once at a time + | +LL | for _c in chars.by_ref() { + | -------------- + | | + | first mutable borrow occurs here + | first borrow later used here +LL | chars.next(); + | ^^^^^ second mutable borrow occurs here + | +help: if you want to call `next` on a iterator within the loop, consider using `while let` + | +LL ~ let iter = chars.by_ref(); +LL ~ while let Some(_c) = iter.next() { +LL ~ iter.next(); + | +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn diff_format() { + let source = r#" +use st::cell::Cell; + +mod bar { + pub fn bar() { bar::baz(); } + + fn baz() {} +} + +use bas::bar; + +struct Foo { + bar: st::cell::Cell +} + +fn main() {}"#; + + let input_new = Level::ERROR + .header("failed to resolve: use of undeclared crate or module `st`") + .id("E0433") + .group( + Group::new().element( + Snippet::source(source).line_start(1).fold(true).annotation( + AnnotationKind::Primary + .span(122..124) + .label("use of undeclared crate or module `st`"), + ), + ), + ) + .group( + Group::new() + .element(Level::HELP.title("there is a crate or module with a similar name")) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(122..124, "std")), + ), + ) + .group( + Group::new() + .element(Level::HELP.title("consider importing this module")) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(1..1, "use std::cell;\n")), + ), + ) + .group( + Group::new() + .element(Level::HELP.title("if you import `cell`, refer to it directly")) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(122..126, "")), + ), + ); + let expected = str![[r#" +error[E0433]: failed to resolve: use of undeclared crate or module `st` + | +LL | bar: st::cell::Cell + | ^^ use of undeclared crate or module `st` + | +help: there is a crate or module with a similar name + | +LL | bar: std::cell::Cell + | + +help: consider importing this module + | +LL + use std::cell; + | +help: if you import `cell`, refer to it directly + | +LL - bar: st::cell::Cell +LL + bar: cell::Cell + | +"#]]; + + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn multiline_removal() { + let source = r#" +struct Wrapper(T); + +fn foo(foo: Wrapper) + +where + T + : + ? + Sized +{ + // +} + +fn main() {}"#; + + let input_new = Level::ERROR + .header("the size for values of type `T` cannot be known at compilation time") + .id("E0277") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .fold(true) + .annotation( + AnnotationKind::Primary + .span(39..49) + .label("doesn't have a size known at compile-time"), + ) + .annotation( + AnnotationKind::Context + .span(31..32) + .label("this type parameter needs to be `Sized`"), + ), + ), + ) + .group( + Group::new() + .element(Level::HELP.title( + "consider removing the `?Sized` bound to make the type parameter `Sized`", + )) + .element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(52..86, "")), + ), + ); + let expected = str![[r#" +error[E0277]: the size for values of type `T` cannot be known at compilation time + | +LL | fn foo(foo: Wrapper) + | - ^^^^^^^^^^ doesn't have a size known at compile-time + | | + | this type parameter needs to be `Sized` + | +help: consider removing the `?Sized` bound to make the type parameter `Sized` + | +LL - where +LL - T +LL - : +LL - ? +LL - Sized + | +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn multiline_replacement() { + let source = r#" +struct Wrapper(T); + +fn foo(foo: Wrapper) + +and where + T + : + ? + Sized +{ + // +} + +fn main() {}"#; + let input_new = Level::ERROR + .header("the size for values of type `T` cannot be known at compilation time") + .id("E0277") + .group(Group::new().element(Snippet::source(source) + .line_start(1) + .origin("$DIR/removal-of-multiline-trait-bound-in-where-clause.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(39..49) + .label("doesn't have a size known at compile-time"), + ) + .annotation( + AnnotationKind::Context + .span(31..32) + .label("this type parameter needs to be `Sized`"), + ))) + .group(Group::new().element( + Level::NOTE + .title("required by an implicit `Sized` bound in `Wrapper`") + ).element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/removal-of-multiline-trait-bound-in-where-clause.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(16..17) + .label("required by the implicit `Sized` requirement on this type parameter in `Wrapper`"), + ) + )) + .group(Group::new().element( + Level::HELP + .title("you could relax the implicit `Sized` bound on `T` if it were used through indirection like `&T` or `Box`") + ) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/removal-of-multiline-trait-bound-in-where-clause.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(16..17) + .label("this could be changed to `T: ?Sized`..."), + ) + .annotation( + AnnotationKind::Context + .span(19..20) + .label("...if indirection were used here: `Box`"), + ) + + )) + .group(Group::new().element( + Level::HELP + .title("consider removing the `?Sized` bound to make the type parameter `Sized`") + ).element( + Snippet::source(source) + .fold(true) + .patch(Patch::new(56..90, "")) + .patch(Patch::new(90..90, "+ Send")) + , + )); + let expected = str![[r#" +error[E0277]: the size for values of type `T` cannot be known at compilation time + --> $DIR/removal-of-multiline-trait-bound-in-where-clause.rs:4:16 + | +LL | fn foo(foo: Wrapper) + | - ^^^^^^^^^^ doesn't have a size known at compile-time + | | + | this type parameter needs to be `Sized` + | +note: required by an implicit `Sized` bound in `Wrapper` + --> $DIR/removal-of-multiline-trait-bound-in-where-clause.rs:2:16 + | +LL | struct Wrapper(T); + | ^ required by the implicit `Sized` requirement on this type parameter in `Wrapper` +help: you could relax the implicit `Sized` bound on `T` if it were used through indirection like `&T` or `Box` + --> $DIR/removal-of-multiline-trait-bound-in-where-clause.rs:2:16 + | +LL | struct Wrapper(T); + | ^ - ...if indirection were used here: `Box` + | | + | this could be changed to `T: ?Sized`... +help: consider removing the `?Sized` bound to make the type parameter `Sized` + | +LL ~ and +LL ~ + Send{ + | +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn multiline_removal2() { + let source = r#" +cargo +fuzzy +pizza +jumps +crazy +quack +zappy +"#; + + let input_new = Level::ERROR + .header("the size for values of type `T` cannot be known at compilation time") + .id("E0277") + .group( + Group::new() + .element(Level::HELP.title( + "consider removing the `?Sized` bound to make the type parameter `Sized`", + )) + .element( + Snippet::source(source) + .line_start(7) + .fold(true) + .patch(Patch::new(3..21, "")) + .patch(Patch::new(22..40, "")), + ), + ); + let expected = str![[r#" +error[E0277]: the size for values of type `T` cannot be known at compilation time + | +help: consider removing the `?Sized` bound to make the type parameter `Sized` + | +8 - cargo +9 - fuzzy +10 - pizza +11 - jumps +8 + campy + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn e0271() { + let source = r#" +trait Future { + type Error; +} + +impl Future for Result { + type Error = E; +} + +impl Future for Option { + type Error = (); +} + +struct Foo; + +fn foo() -> Box> { + Box::new( + Ok::<_, ()>( + Err::<(), _>( + Ok::<_, ()>( + Err::<(), _>( + Ok::<_, ()>( + Err::<(), _>(Some(5)) + ) + ) + ) + ) + ) + ) +} +fn main() { +} +"#; + + let input_new = Level::ERROR + .header("type mismatch resolving `>, ...>>, ...>>, ...> as Future>::Error == Foo`") + .id("E0271") + .group(Group::new().element(Snippet::source(source) + .line_start(4) + .origin("$DIR/E0271.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(208..510) + .label("type mismatch resolving `, ...>>, ...> as Future>::Error == Foo`"), + ))) + .group(Group::new().element( + Level::NOTE.title("expected this to be `Foo`") + ).element( + Snippet::source(source) + .line_start(4) + .origin("$DIR/E0271.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(89..90)) + ).element( + Level::NOTE + .title("required for the cast from `Box>, ()>>, ()>>, ()>>` to `Box<(dyn Future + 'static)>`") + , + )); + + let expected = str![[r#" +error[E0271]: type mismatch resolving `>, ...>>, ...>>, ...> as Future>::Error == Foo` + ╭▸ $DIR/E0271.rs:20:5 + │ +LL │ ┏ Box::new( +LL │ ┃ Ok::<_, ()>( +LL │ ┃ Err::<(), _>( +LL │ ┃ Ok::<_, ()>( + ‡ ┃ +LL │ ┃ ) + │ ┗━━━━━┛ type mismatch resolving `, ...>>, ...> as Future>::Error == Foo` + ╰╴ +note: expected this to be `Foo` + ╭▸ $DIR/E0271.rs:10:18 + │ +LL │ type Error = E; + │ ━ + ╰ note: required for the cast from `Box>, ()>>, ()>>, ()>>` to `Box<(dyn Future + 'static)>` +"#]]; + let renderer = Renderer::plain() + .term_width(40) + .theme(OutputTheme::Unicode) + .anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn e0271_2() { + let source = r#" +trait Future { + type Error; +} + +impl Future for Result { + type Error = E; +} + +impl Future for Option { + type Error = (); +} + +struct Foo; + +fn foo() -> Box> { + Box::new( + Ok::<_, ()>( + Err::<(), _>( + Ok::<_, ()>( + Err::<(), _>( + Ok::<_, ()>( + Err::<(), _>(Some(5)) + ) + ) + ) + ) + ) + ) +} +fn main() { +} +"#; + + let input_new = Level::ERROR + .header("type mismatch resolving `>, ...>>, ...>>, ...> as Future>::Error == Foo`") + .id("E0271") + .group(Group::new().element(Snippet::source(source) + .line_start(4) + .origin("$DIR/E0271.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(208..510) + .label("type mismatch resolving `, ...>>, ...> as Future>::Error == Foo`"), + ))) + .group(Group::new().element( + Level::NOTE.title("expected this to be `Foo`") + ).element( + Snippet::source(source) + .line_start(4) + .origin("$DIR/E0271.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(89..90)) + ).element( + Level::NOTE + .title("required for the cast from `Box>, ()>>, ()>>, ()>>` to `Box<(dyn Future + 'static)>`") + ).element( + Level::NOTE.title("a second note"), + )); + + let expected = str![[r#" +error[E0271]: type mismatch resolving `>, ...>>, ...>>, ...> as Future>::Error == Foo` + ╭▸ $DIR/E0271.rs:20:5 + │ +LL │ ┏ Box::new( +LL │ ┃ Ok::<_, ()>( +LL │ ┃ Err::<(), _>( +LL │ ┃ Ok::<_, ()>( + ‡ ┃ +LL │ ┃ ) + │ ┗━━━━━┛ type mismatch resolving `, ...>>, ...> as Future>::Error == Foo` + ╰╴ +note: expected this to be `Foo` + ╭▸ $DIR/E0271.rs:10:18 + │ +LL │ type Error = E; + │ ━ + ├ note: required for the cast from `Box>, ()>>, ()>>, ()>>` to `Box<(dyn Future + 'static)>` + ╰ note: a second note +"#]]; + let renderer = Renderer::plain() + .term_width(40) + .theme(OutputTheme::Unicode) + .anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn long_e0308() { + let source = r#" +mod a { + // Force the "short path for unique types" machinery to trip up + pub struct Atype; + pub struct Btype; + pub struct Ctype; +} + +mod b { + pub struct Atype(T, K); + pub struct Btype(T, K); + pub struct Ctype(T, K); +} + +use b::*; + +fn main() { + let x: Atype< + Btype< + Ctype< + Atype< + Btype< + Ctype< + Atype< + Btype< + Ctype, + i32 + >, + i32 + >, + i32 + >, + i32 + >, + i32 + >, + i32 + >, + i32 + >, + i32 + > = Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok( + Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok( + Ok("") + )))))))))))))))))))))))))))))) + )))))))))))))))))))))))))))))); + //~^^^^^ ERROR E0308 + + let _ = Some(Ok(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some( + Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some( + Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some(Some( + Some(Some(Some(Some(Some(Some(Some(Some(Some(""))))))))) + ))))))))))))))))) + )))))))))))))))))) + ))))))))))))))))) == Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok( + Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok( + Ok(Ok(Ok(Ok(Ok(Ok(Ok(""))))))) + )))))))))))))))))))))))))))))) + )))))))))))))))))))))))); + //~^^^^^ ERROR E0308 + + let x: Atype< + Btype< + Ctype< + Atype< + Btype< + Ctype< + Atype< + Btype< + Ctype, + i32 + >, + i32 + >, + i32 + >, + i32 + >, + i32 + >, + i32 + >, + i32 + >, + i32 + > = (); + //~^ ERROR E0308 + + let _: () = Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok( + Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok( + Ok(Ok(Ok(Ok(Ok(Ok(Ok(""))))))) + )))))))))))))))))))))))))))))) + )))))))))))))))))))))))); + //~^^^^^ ERROR E0308 +} +"#; + + let input_new = Level::ERROR + .header("mismatched types") + .id("E0308") + .group(Group::new().element( + Snippet::source(source) + .line_start(7) + .origin("$DIR/long-E0308.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(719..1001) + .label("expected `Atype, i32>, i32>`, found `Result, _>, _>`"), + ) + .annotation( + AnnotationKind::Context + .span(293..716) + .label("expected due to this"), + ) + ).element( + Level::NOTE + .title("expected struct `Atype, i32>`\n found enum `Result, _>`") + ).element( + Level::NOTE + .title("the full name for the type has been written to '$TEST_BUILD_DIR/$FILE.long-type-hash.txt'") + ).element( + Level::NOTE + .title("consider using `--verbose` to print the full type name to the console") + , + )); + + let expected = str![[r#" +error[E0308]: mismatched types + ╭▸ $DIR/long-E0308.rs:48:9 + │ +LL │ let x: Atype< + │ ┌─────────────┘ +LL │ │ Btype< +LL │ │ Ctype< +LL │ │ Atype< + ‡ │ +LL │ │ i32 +LL │ │ > = Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(O… + │ │┏━━━━━│━━━┛ + │ └┃─────┤ + │ ┃ expected due to this +LL │ ┃ Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(Ok(O… +LL │ ┃ Ok("") +LL │ ┃ )))))))))))))))))))))))))))))) +LL │ ┃ )))))))))))))))))))))))))))))); + │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ expected `Atype, i32>, i32>`, found `Result, _>, _>` + ├ note: expected struct `Atype, i32>` + │ found enum `Result, _>` + ├ note: the full name for the type has been written to '$TEST_BUILD_DIR/$FILE.long-type-hash.txt' + ╰ note: consider using `--verbose` to print the full type name to the console +"#]]; + let renderer = Renderer::plain() + .term_width(60) + .theme(OutputTheme::Unicode) + .anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +#[test] +fn highlighting() { + let source = r#" +use core::pin::Pin; +use core::future::Future; +use core::any::Any; + +fn query(_: fn(Box<(dyn Any + Send + '_)>) -> Pin, String>> + Send + 'static +)>>) {} + +fn wrapped_fn<'a>(_: Box<(dyn Any + Send)>) -> Pin, String>> + Send + 'static +)>> { + Box::pin(async { Err("nope".into()) }) +} + +fn main() { + query(wrapped_fn); +} +"#; + + let input_new = Level::ERROR + .header("mismatched types") + .id("E0308") + .group(Group::new().element( + Snippet::source(source) + .line_start(7) + .origin("$DIR/unicode-output.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(430..440) + .label("one type is more general than the other"), + ) + .annotation( + AnnotationKind::Context + .span(424..429) + .label("arguments to this function are incorrect"), + ), + ).element( + Level::NOTE + .title("expected fn pointer `for<'a> fn(Box<(dyn Any + Send + 'a)>) -> Pin<_>`\n found fn item `fn(Box<(dyn Any + Send + 'static)>) -> Pin<_> {wrapped_fn}`") + , + )) + .group(Group::new().element( + Level::NOTE.title("function defined here"), + ).element( + Snippet::source(source) + .line_start(7) + .origin("$DIR/unicode-output.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(77..210)) + .annotation(AnnotationKind::Context.span(71..76)), + )); + + let expected = str![[r#" +error[E0308]: mismatched types + ╭▸ $DIR/unicode-output.rs:23:11 + │ +LL │ query(wrapped_fn); + │ ┬──── ━━━━━━━━━━ one type is more general than the other + │ │ + │ arguments to this function are incorrect + │ + ╰ note: expected fn pointer `for<'a> fn(Box<(dyn Any + Send + 'a)>) -> Pin<_>` + found fn item `fn(Box<(dyn Any + Send + 'static)>) -> Pin<_> {wrapped_fn}` +note: function defined here + ╭▸ $DIR/unicode-output.rs:12:10 + │ +LL │ fn query(_: fn(Box<(dyn Any + Send + '_)>) -> Pin, String>> + Send + 'static +LL │ ┃ )>>) {} + ╰╴┗━━━┛ +"#]]; + let renderer = Renderer::plain() + .theme(OutputTheme::Unicode) + .anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input_new), expected); +} + +// This tests that an ellipsis is not inserted into Unicode text when a line +// wasn't actually trimmed. +// +// This is a regression test where `...` was inserted because the code wasn't +// properly accounting for the *rendered* length versus the length in bytes in +// all cases. +#[test] +fn unicode_cut_handling() { + let source = "version = \"0.1.0\"\n# Ensure that the spans from toml handle utf-8 correctly\nauthors = [\n { name = \"Z\u{351}\u{36b}\u{343}\u{36a}\u{302}\u{36b}\u{33d}\u{34f}\u{334}\u{319}\u{324}\u{31e}\u{349}\u{35a}\u{32f}\u{31e}\u{320}\u{34d}A\u{36b}\u{357}\u{334}\u{362}\u{335}\u{31c}\u{330}\u{354}L\u{368}\u{367}\u{369}\u{358}\u{320}G\u{311}\u{357}\u{30e}\u{305}\u{35b}\u{341}\u{334}\u{33b}\u{348}\u{34d}\u{354}\u{339}O\u{342}\u{30c}\u{30c}\u{358}\u{328}\u{335}\u{339}\u{33b}\u{31d}\u{333}\", email = 1 }\n]\n"; + let input = Level::ERROR.header("title").group( + Group::new().element( + Snippet::source(source) + .fold(false) + .annotation(AnnotationKind::Primary.span(85..228).label("annotation")), + ), + ); + let expected = str![[r#" +error: title + | +1 | version = "0.1.0" +2 | # Ensure that the spans from toml handle utf-8 correctly +3 | authors = [ + | ___________^ +4 | | { name = "Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘", email = 1 } +5 | | ] + | |_^ annotation +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn unicode_cut_handling2() { + let source = "/*这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。*/?"; + let input = Level::ERROR + .header("expected item, found `?`") + .group( + Group::new().element( + Snippet::source(source) + .fold(false) + .annotation(AnnotationKind::Primary.span(499..500).label("expected item")) + ).element( + Level::NOTE.title("for a full list of items that can appear in modules, see ") + ) + ); + + let expected = str![[r#" +error: expected item, found `?` + | +1 | ...的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。*/? + | ^ expected item + = note: for a full list of items that can appear in modules, see +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn unicode_cut_handling3() { + let source = "/*这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。这是宽的。*/?"; + let input = Level::ERROR + .header("expected item, found `?`") + .group( + Group::new().element( + Snippet::source(source) + .fold(false) + .annotation(AnnotationKind::Primary.span(251..254).label("expected item")) + ).element( + Level::NOTE.title("for a full list of items that can appear in modules, see ") + ) + ); + + let expected = str![[r#" +error: expected item, found `?` + | +1 | ...。这是宽的。这是宽的。这是宽的... + | ^^ expected item + = note: for a full list of items that can appear in modules, see +"#]]; + + let renderer = Renderer::plain().term_width(43); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn unicode_cut_handling4() { + let source = "/*aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa*/?"; + let input = Level::ERROR + .header("expected item, found `?`") + .group( + Group::new().element( + Snippet::source(source) + .fold(false) + .annotation(AnnotationKind::Primary.span(334..335).label("expected item")) + ).element( + Level::NOTE.title("for a full list of items that can appear in modules, see ") + ) + ); + + let expected = str![[r#" +error: expected item, found `?` + | +1 | ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa*/? + | ^ expected item + = note: for a full list of items that can appear in modules, see +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn diagnostic_width() { + let source = r##"// ignore-tidy-linelength + +fn main() { + let _: &str = "🦀☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓ ☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♏♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚈⚉4🦀☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♏♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚈⚉4🦀🦀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♏♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚈⚉4"; let _: () = 42; let _: &str = "🦀☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓ ☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♏♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚈⚉4🦀☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♏♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚈⚉4🦀🦀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♏♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚈⚉4"; +//~^ ERROR mismatched types +} +"##; + let input = Level::ERROR.header("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(source) + .origin("$DIR/non-whitespace-trimming-unicode.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(1207..1209) + .label("expected `()`, found integer"), + ) + .annotation( + AnnotationKind::Context + .span(1202..1204) + .label("expected due to this"), + ), + ), + ); + + let expected = str![[r#" +error[E0308]: mismatched types + --> $DIR/non-whitespace-trimming-unicode.rs:4:415 + | +LL | ...♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚈⚉4"; let _: () = 42; let _: &str = "🦀☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓ ☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷... + | -- ^^ expected `()`, found integer + | | + | expected due to this +"#]]; + + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn diagnostic_width2() { + let source = r##"//@ revisions: ascii unicode +//@[unicode] compile-flags: -Zunstable-options --error-format=human-unicode +// ignore-tidy-linelength + +fn main() { + let unicode_is_fun = "؁‱ஹ௸௵꧄.ဪ꧅⸻𒈙𒐫﷽𒌄𒈟𒍼𒁎𒀱𒌧𒅃 𒈓𒍙𒊎𒄡𒅌𒁏𒀰𒐪𒐩𒈙𒐫𪚥"; + let _ = "ༀ༁༂༃༄༅༆༇༈༉༊་༌།༎༏༐༑༒༓༔༕༖༗༘༙༚༛༜༝༞༟༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳༴༵༶༷༸༹༺༻༼༽༾༿ཀཁགགྷངཅཆཇ཈ཉཊཋཌཌྷཎཏཐདདྷནཔཕབབྷམཙཚཛཛྷཝཞཟའཡརལཤཥསཧཨཀྵཪཫཬ཭཮཯཰ཱཱཱིིུུྲྀཷླྀཹེཻོཽཾཿ྄ཱྀྀྂྃ྅྆྇ྈྉྊྋྌྍྎྏྐྑྒྒྷྔྕྖྗ྘ྙྚྛྜྜྷྞྟྠྡྡྷྣྤྥྦྦྷྨྩྪྫྫྷྭྮྯྰྱྲླྴྵྶྷྸྐྵྺྻྼ྽྾྿࿀࿁࿂࿃࿄࿅࿆࿇࿈࿉࿊࿋࿌࿍࿎࿏࿐࿑࿒࿓࿔࿕࿖࿗࿘࿙࿚"; let _a = unicode_is_fun + " really fun!"; + //[ascii]~^ ERROR cannot add `&str` to `&str` +} +"##; + let input = Level::ERROR + .header("cannot add `&str` to `&str`") + .id("E0369") + .group( + Group::new() + .element( + Snippet::source(source) + .origin("$DIR/non-1-width-unicode-multiline-label.rs") + .fold(true) + .annotation(AnnotationKind::Context.span(970..984).label("&str")) + .annotation(AnnotationKind::Context.span(987..1001).label("&str")) + .annotation( + AnnotationKind::Primary + .span(985..986) + .label("`+` cannot be used to concatenate two `&str` strings"), + ), + ) + .element( + Level::NOTE + .title("string concatenation requires an owned `String` on the left"), + ), + ) + .group( + Group::new() + .element(Level::HELP.title("create an owned `String` from a string reference")) + .element( + Snippet::source(source) + .origin("$DIR/non-1-width-unicode-multiline-label.rs") + .fold(true) + .patch(Patch::new(984..984, ".to_owned()")), + ), + ); + + let expected = str![[r#" +error[E0369]: cannot add `&str` to `&str` + ╭▸ $DIR/non-1-width-unicode-multiline-label.rs:7:260 + │ +LL │ …࿆࿇࿈࿉࿊࿋࿌࿍࿎࿏࿐࿑࿒࿓࿔࿕࿖࿗࿘࿙࿚"; let _a = unicode_is_fun + " really fun!"; + │ ┬───────────── ┯ ────────────── &str + │ │ │ + │ │ `+` cannot be used to concatenate two `&str` strings + │ &str + │ + ╰ note: string concatenation requires an owned `String` on the left +help: create an owned `String` from a string reference + ╭╴ +LL │ let _ = "ༀ༁༂༃༄༅༆༇༈༉༊་༌།༎༏༐༑༒༓༔༕༖༗༘༙༚༛༜༝༞༟༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳༴༵༶༷༸༹༺༻༼༽༾༿ཀཁགགྷངཅཆཇ཈ཉཊཋཌཌྷཎཏཐདདྷནཔཕབབྷམཙཚཛཛྷཝཞཟའཡརལཤཥསཧཨཀྵཪཫཬ཭཮཯཰ཱཱཱིིུུྲྀཷླྀཹེཻོཽཾཿ྄ཱྀྀྂྃ྅྆྇ྈྉྊྋྌྍྎྏྐྑྒྒྷྔྕྖྗ྘ྙྚྛྜྜྷྞྟྠྡྡྷྣྤྥྦྦྷྨྩྪྫྫྷྭྮྯྰྱྲླྴྵྶྷྸྐྵྺྻྼ྽྾྿࿀࿁࿂࿃࿄࿅࿆࿇࿈࿉࿊࿋࿌࿍࿎࿏࿐࿑࿒࿓࿔࿕࿖࿗࿘࿙࿚"; let _a = unicode_is_fun.to_owned() + " really fun!"; + ╰╴ +++++++++++ +"#]]; + + let renderer = Renderer::plain() + .anonymized_line_numbers(true) + .theme(OutputTheme::Unicode); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn macros_not_utf8() { + let source = r##"//@ error-pattern: did not contain valid UTF-8 +//@ reference: input.encoding.utf8 +//@ reference: input.encoding.invalid + +fn foo() { + include!("not-utf8.bin"); +} +"##; + let bin_source = "�|�\u{0002}!5�cc\u{0015}\u{0002}�Ӻi��WWj�ȥ�'�}�\u{0012}�J�ȉ��W�\u{001e}O�@����\u{001c}w�V���LO����\u{0014}[ \u{0003}_�'���SQ�~ذ��ų&��-\t��lN~��!@␌ _#���kQ��h�\u{001d}�:�\u{001c}\u{0007}�"; + let input = Level::ERROR + .header("couldn't read `$DIR/not-utf8.bin`: stream did not contain valid UTF-8") + .group( + Group::new().element( + Snippet::source(source) + .origin("$DIR/not-utf8.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(136..160)), + ), + ) + .group( + Group::new() + .element(Level::NOTE.title("byte `193` is not valid utf-8")) + .element( + Snippet::source(bin_source) + .origin("$DIR/not-utf8.bin") + .fold(true) + .annotation(AnnotationKind::Primary.span(0..0)), + ) + .element(Level::NOTE.title("this error originates in the macro `include` (in Nightly builds, run with -Z macro-backtrace for more info)")), + ); + + let expected = str![[r#" +error: couldn't read `$DIR/not-utf8.bin`: stream did not contain valid UTF-8 + --> $DIR/not-utf8.rs:6:5 + | +LL | include!("not-utf8.bin"); + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | +note: byte `193` is not valid utf-8 + --> $DIR/not-utf8.bin:1:1 + | +LL | �|�␂!5�cc␕␂�Ӻi��WWj�ȥ�'�}�␒�J�ȉ��W�␞O�@����␜w�V���LO����␔[ ␃_�'���SQ�~ذ��ų&��- ��lN~��!@␌ _#���kQ��h�␝�:�␜␇� + | ^ + = note: this error originates in the macro `include` (in Nightly builds, run with -Z macro-backtrace for more info) +"#]]; + + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); } diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs index 54b73211..69986292 100644 --- a/tests/rustc_tests.rs +++ b/tests/rustc_tests.rs @@ -2,7 +2,7 @@ //! //! [parser-tests]: https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_parse/src/parser/tests.rs -use annotate_snippets::{Level, Renderer, Snippet}; +use annotate_snippets::{AnnotationKind, Group, Level, Origin, Renderer, Snippet}; use snapbox::{assert_data_eq, str}; @@ -12,12 +12,14 @@ fn ends_on_col0() { fn foo() { } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(10..13).label("test")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(10..13).label("test")), + ), ); let expected = str![[r#" @@ -28,10 +30,9 @@ error: foo | __________^ 3 | | } | |_^ test - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn ends_on_col2() { @@ -41,12 +42,14 @@ fn foo() { } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(10..17).label("test")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(10..17).label("test")), + ), ); let expected = str![[r#" @@ -55,14 +58,12 @@ error: foo | 2 | fn foo() { | __________^ -3 | | -4 | | +... | 5 | | } | |___^ test - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn non_nested() { @@ -73,17 +74,23 @@ fn foo() { X2 Y2 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..32).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(17..35) - .label("`Y` is a good letter too"), - ), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..32) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(17..35) + .label("`Y` is a good letter too"), + ), + ), ); let expected = str![[r#" @@ -98,10 +105,9 @@ error: foo | ||____^__- `Y` is a good letter too | |_____| | `X` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn nested() { @@ -111,17 +117,23 @@ fn foo() { Y1 X1 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..27).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(17..24) - .label("`Y` is a good letter too"), - ), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..27) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(17..24) + .label("`Y` is a good letter too"), + ), + ), ); let expected = str![[r#" @@ -135,10 +147,9 @@ error: foo | ||____-__^ `X` is a good letter | |____| | `Y` is a good letter too - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn different_overlap() { @@ -150,17 +161,23 @@ fn foo() { X3 Y3 Z3 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(17..38).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(31..49) - .label("`Y` is a good letter too"), - ), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(17..38) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(31..49) + .label("`Y` is a good letter too"), + ), + ), ); let expected = str![[r#" @@ -175,10 +192,9 @@ error: foo | ||____^ `X` is a good letter 6 | | X3 Y3 Z3 | |____- `Y` is a good letter too - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn triple_overlap() { @@ -189,18 +205,24 @@ fn foo() { X2 Y2 Z2 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..38).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(17..41) - .label("`Y` is a good letter too"), - ) - .annotation(Level::Warning.span(20..44).label("`Z` label")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..38) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(17..41) + .label("`Y` is a good letter too"), + ) + .annotation(AnnotationKind::Context.span(20..44).label("`Z` label")), + ), ); let expected = str![[r#" @@ -217,10 +239,9 @@ error: foo | ||_____|__| | |______| `Y` is a good letter too | `X` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn triple_exact_overlap() { @@ -231,18 +252,24 @@ fn foo() { X2 Y2 Z2 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..38).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(14..38) - .label("`Y` is a good letter too"), - ) - .annotation(Level::Warning.span(14..38).label("`Z` label")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..38) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(14..38) + .label("`Y` is a good letter too"), + ) + .annotation(AnnotationKind::Context.span(14..38).label("`Z` label")), + ), ); // This should have a `^` but we currently don't support the idea of a @@ -254,15 +281,14 @@ error: foo 3 | / X0 Y0 Z0 4 | | X1 Y1 Z1 5 | | X2 Y2 Z2 - | | - - | |____| - | `X` is a good letter - | `Y` is a good letter too + | | ^ + | | | + | | `X` is a good letter + | |____`Y` is a good letter too | `Z` label - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn minimum_depth() { @@ -274,18 +300,24 @@ fn foo() { X3 Y3 Z3 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(17..27).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(28..44) - .label("`Y` is a good letter too"), - ) - .annotation(Level::Warning.span(36..52).label("`Z`")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(17..27) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(28..44) + .label("`Y` is a good letter too"), + ) + .annotation(AnnotationKind::Context.span(36..52).label("`Z`")), + ), ); let expected = str![[r#" @@ -304,10 +336,9 @@ error: foo | | 6 | | X3 Y3 Z3 | |_______- `Z` - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn non_overlapping() { @@ -319,17 +350,23 @@ fn foo() { X3 Y3 Z3 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..27).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(39..55) - .label("`Y` is a good letter too"), - ), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..27) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(39..55) + .label("`Y` is a good letter too"), + ), + ), ); let expected = str![[r#" @@ -343,10 +380,9 @@ error: foo | ______- 6 | | X3 Y3 Z3 | |__________- `Y` is a good letter too - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn overlapping_start_and_end() { @@ -358,17 +394,23 @@ fn foo() { X3 Y3 Z3 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(17..27).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(31..55) - .label("`Y` is a good letter too"), - ), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(17..27) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(31..55) + .label("`Y` is a good letter too"), + ), + ), ); let expected = str![[r#" @@ -384,10 +426,9 @@ error: foo 5 | | X2 Y2 Z2 6 | | X3 Y3 Z3 | |__________- `Y` is a good letter too - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiple_labels_primary_without_message() { @@ -396,14 +437,20 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(18..25).label("")) - .annotation(Level::Warning.span(14..27).label("`a` is a good letter")) - .annotation(Level::Warning.span(22..23).label("")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(18..25).label("")) + .annotation( + AnnotationKind::Context + .span(14..27) + .label("`a` is a good letter"), + ) + .annotation(AnnotationKind::Context.span(22..23).label("")), + ), ); let expected = str![[r#" @@ -412,10 +459,9 @@ error: foo | 3 | a { b { c } d } | ----^^^^-^^-- `a` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiple_labels_secondary_without_message() { @@ -424,13 +470,19 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..27).label("`a` is a good letter")) - .annotation(Level::Warning.span(18..25).label("")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..27) + .label("`a` is a good letter"), + ) + .annotation(AnnotationKind::Context.span(18..25).label("")), + ), ); let expected = str![[r#" @@ -439,10 +491,9 @@ error: foo | 3 | a { b { c } d } | ^^^^-------^^ `a` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiple_labels_primary_without_message_2() { @@ -451,14 +502,20 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(18..25).label("`b` is a good letter")) - .annotation(Level::Warning.span(14..27).label("")) - .annotation(Level::Warning.span(22..23).label("")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(18..25) + .label("`b` is a good letter"), + ) + .annotation(AnnotationKind::Context.span(14..27).label("")) + .annotation(AnnotationKind::Context.span(22..23).label("")), + ), ); let expected = str![[r#" @@ -469,10 +526,9 @@ error: foo | ----^^^^-^^-- | | | `b` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiple_labels_secondary_without_message_2() { @@ -481,13 +537,19 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..27).label("")) - .annotation(Level::Warning.span(18..25).label("`b` is a good letter")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(14..27).label("")) + .annotation( + AnnotationKind::Context + .span(18..25) + .label("`b` is a good letter"), + ), + ), ); let expected = str![[r#" @@ -498,10 +560,9 @@ error: foo | ^^^^-------^^ | | | `b` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiple_labels_secondary_without_message_3() { @@ -510,13 +571,19 @@ fn foo() { a bc d } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..18).label("`a` is a good letter")) - .annotation(Level::Warning.span(18..22).label("")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..18) + .label("`a` is a good letter"), + ) + .annotation(AnnotationKind::Context.span(18..22).label("")), + ), ); let expected = str![[r#" @@ -527,10 +594,9 @@ error: foo | ^^^^---- | | | `a` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiple_labels_without_message() { @@ -539,13 +605,15 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..27).label("")) - .annotation(Level::Warning.span(18..25).label("")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(14..27).label("")) + .annotation(AnnotationKind::Context.span(18..25).label("")), + ), ); let expected = str![[r#" @@ -554,10 +622,9 @@ error: foo | 3 | a { b { c } d } | ^^^^-------^^ - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiple_labels_without_message_2() { @@ -566,14 +633,16 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(18..25).label("")) - .annotation(Level::Warning.span(14..27).label("")) - .annotation(Level::Warning.span(22..23).label("")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(18..25).label("")) + .annotation(AnnotationKind::Context.span(14..27).label("")) + .annotation(AnnotationKind::Context.span(22..23).label("")), + ), ); let expected = str![[r#" @@ -582,10 +651,9 @@ error: foo | 3 | a { b { c } d } | ----^^^^-^^-- - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn multiple_labels_with_message() { @@ -594,13 +662,23 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..27).label("`a` is a good letter")) - .annotation(Level::Warning.span(18..25).label("`b` is a good letter")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..27) + .label("`a` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(18..25) + .label("`b` is a good letter"), + ), + ), ); let expected = str![[r#" @@ -612,10 +690,9 @@ error: foo | | | | | `b` is a good letter | `a` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn ingle_label_with_message() { @@ -624,12 +701,18 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..27).label("`a` is a good letter")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(14..27) + .label("`a` is a good letter"), + ), + ), ); let expected = str![[r#" @@ -638,10 +721,9 @@ error: foo | 3 | a { b { c } d } | ^^^^^^^^^^^^^ `a` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn single_label_without_message() { @@ -650,12 +732,14 @@ fn foo() { a { b { c } d } } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(14..27).label("")), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(14..27).label("")), + ), ); let expected = str![[r#" @@ -664,10 +748,9 @@ error: foo | 3 | a { b { c } d } | ^^^^^^^^^^^^^ - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn long_snippet() { @@ -689,17 +772,23 @@ fn foo() { X3 Y3 Z3 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(17..27).label("`X` is a good letter")) - .annotation( - Level::Warning - .span(31..76) - .label("`Y` is a good letter too"), - ), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(17..27) + .label("`X` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(31..76) + .label("`Y` is a good letter too"), + ), + ), ); let expected = str![[r#" @@ -713,14 +802,15 @@ error: foo | ||____| | | `X` is a good letter 5 | | 1 + 6 | | 2 + 7 | | 3 ... | 15 | | X2 Y2 Z2 16 | | X3 Y3 Z3 | |__________- `Y` is a good letter too - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); } #[test] fn long_snippet_multiple_spans() { @@ -742,17 +832,23 @@ fn foo() { X3 Y3 Z3 } "#; - let input = Level::Error.title("foo").snippet( - Snippet::source(source) - .line_start(1) - .origin("test.rs") - .fold(true) - .annotation(Level::Error.span(17..73).label("`Y` is a good letter")) - .annotation( - Level::Warning - .span(37..56) - .label("`Z` is a good letter too"), - ), + let input = Level::ERROR.header("foo").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(17..73) + .label("`Y` is a good letter"), + ) + .annotation( + AnnotationKind::Context + .span(37..56) + .label("`Z` is a good letter too"), + ), + ), ); let expected = str![[r#" @@ -771,13 +867,1017 @@ error: foo 10 | || 6 11 | || X2 Y2 Z2 | ||__________- `Z` is a good letter too -12 | | 7 ... | 15 | | 10 16 | | X3 Y3 Z3 | |________^ `Y` is a good letter - | "#]]; let renderer = Renderer::plain(); - assert_data_eq!(renderer.render(input).to_string(), expected); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn issue_91334() { + let source = r#"// Regression test for the ICE described in issue #91334. + +//@ error-pattern: this file contains an unclosed delimiter + +#![feature(coroutines)] + +fn f(){||yield(((){), +"#; + let input = Level::ERROR + .header("this file contains an unclosed delimiter") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/issue-91334.rs") + .fold(true) + .annotation( + AnnotationKind::Context + .span(151..152) + .label("unclosed delimiter"), + ) + .annotation( + AnnotationKind::Context + .span(159..160) + .label("unclosed delimiter"), + ) + .annotation( + AnnotationKind::Context + .span(164..164) + .label("missing open `(` for this delimiter"), + ) + .annotation(AnnotationKind::Primary.span(167..167)), + ), + ); + let expected = str![[r#" +error: this file contains an unclosed delimiter + --> $DIR/issue-91334.rs:7:23 + | +LL | fn f(){||yield(((){), + | - - - ^ + | | | | + | | | missing open `(` for this delimiter + | | unclosed delimiter + | unclosed delimiter +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn issue_114529_illegal_break_with_value() { + // tests/ui/typeck/issue-114529-illegal-break-with-value.rs + let source = r#"// Regression test for issue #114529 +// Tests that we do not ICE during const eval for a +// break-with-value in contexts where it is illegal + +#[allow(while_true)] +fn main() { + [(); { + while true { + break 9; //~ ERROR `break` with value from a `while` loop + }; + 51 + }]; + + [(); { + while let Some(v) = Some(9) { + break v; //~ ERROR `break` with value from a `while` loop + }; + 51 + }]; + + while true { + break (|| { //~ ERROR `break` with value from a `while` loop + let local = 9; + }); + } +} +"#; + let input = Level::ERROR + .header("`break` with value from a `while` loop") + .id("E0571") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/issue-114529-illegal-break-with-value.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(483..581) + .label("can only break with a value inside `loop` or breakable block"), + ) + .annotation( + AnnotationKind::Context + .span(462..472) + .label("you can't `break` with a value in a `while` loop"), + ), + ), + ) + .group( + Group::new() + .element( + Level::HELP + .title("use `break` on its own without a value inside this `while` loop"), + ) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/issue-114529-illegal-break-with-value.rs") + .fold(true) + .annotation(AnnotationKind::Context.span(483..581).label("break")), + ), + ); + let expected = str![[r#" +error[E0571]: `break` with value from a `while` loop + --> $DIR/issue-114529-illegal-break-with-value.rs:22:9 + | +LL | while true { + | ---------- you can't `break` with a value in a `while` loop +LL | / break (|| { //~ ERROR `break` with value from a `while` loop +LL | | let local = 9; +LL | | }); + | |__________^ can only break with a value inside `loop` or breakable block + | +help: use `break` on its own without a value inside this `while` loop + --> $DIR/issue-114529-illegal-break-with-value.rs:22:9 + | +LL | / break (|| { //~ ERROR `break` with value from a `while` loop +LL | | let local = 9; +LL | | }); + | |__________- break +"#]]; + + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn primitive_reprs_should_have_correct_length() { + // tests/ui/transmutability/enums/repr/primitive_reprs_should_have_correct_length.rs + let source = r#"//! An enum with a primitive repr should have exactly the size of that primitive. + +#![crate_type = "lib"] +#![feature(transmutability)] +#![allow(dead_code)] + +mod assert { + use std::mem::{Assume, TransmuteFrom}; + + pub fn is_transmutable() + where + Dst: TransmuteFrom + {} +} + +#[repr(C)] +struct Zst; + +#[derive(Clone, Copy)] +#[repr(i8)] enum V0i8 { V } +#[repr(u8)] enum V0u8 { V } +#[repr(i16)] enum V0i16 { V } +#[repr(u16)] enum V0u16 { V } +#[repr(i32)] enum V0i32 { V } +#[repr(u32)] enum V0u32 { V } +#[repr(i64)] enum V0i64 { V } +#[repr(u64)] enum V0u64 { V } +#[repr(isize)] enum V0isize { V } +#[repr(usize)] enum V0usize { V } + +fn n8() { + type Smaller = Zst; + type Analog = u8; + type Larger = u16; + + fn i_should_have_correct_length() { + type Current = V0i8; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } + + fn u_should_have_correct_length() { + type Current = V0u8; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } +} + +fn n16() { + type Smaller = u8; + type Analog = u16; + type Larger = u32; + + fn i_should_have_correct_length() { + type Current = V0i16; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } + + fn u_should_have_correct_length() { + type Current = V0u16; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } +} + +fn n32() { + type Smaller = u16; + type Analog = u32; + type Larger = u64; + + fn i_should_have_correct_length() { + type Current = V0i32; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } + + fn u_should_have_correct_length() { + type Current = V0u32; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } +} + +fn n64() { + type Smaller = u32; + type Analog = u64; + type Larger = u128; + + fn i_should_have_correct_length() { + type Current = V0i64; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } + + fn u_should_have_correct_length() { + type Current = V0u64; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } +} + +fn nsize() { + type Smaller = u8; + type Analog = usize; + type Larger = [usize; 2]; + + fn i_should_have_correct_length() { + type Current = V0isize; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } + + fn u_should_have_correct_length() { + type Current = V0usize; + + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + assert::is_transmutable::(); + assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + } +} +"#; + let input = + Level::ERROR + .header("`V0usize` cannot be safely transmuted into `[usize; 2]`") + .id("E0277") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/primitive_reprs_should_have_correct_length.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(4375..4381).label( + "the size of `V0usize` is smaller than the size of `[usize; 2]`", + )), + ), + ) + .group( + Group::new() + .element(Level::NOTE.title("required by a bound in `is_transmutable`")) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/primitive_reprs_should_have_correct_length.rs") + .fold(true) + .annotation( + AnnotationKind::Context + .span(225..240) + .label("required by a bound in this function"), + ) + .annotation( + AnnotationKind::Primary + .span(276..470) + .label("required by this bound in `is_transmutable`"), + ), + ), + ); + let expected = str![[r#" +error[E0277]: `V0usize` cannot be safely transmuted into `[usize; 2]` + --> $DIR/primitive_reprs_should_have_correct_length.rs:144:44 + | +LL | assert::is_transmutable::(); //~ ERROR cannot be safely transmuted + | ^^^^^^ the size of `V0usize` is smaller than the size of `[usize; 2]` + | +note: required by a bound in `is_transmutable` + --> $DIR/primitive_reprs_should_have_correct_length.rs:12:14 + | +LL | pub fn is_transmutable() + | --------------- required by a bound in this function +LL | where +LL | Dst: TransmuteFrom + | |__________^ required by this bound in `is_transmutable` +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn align_fail() { + // tests/ui/transmutability/alignment/align-fail.rs + let source = r#"//@ check-fail +#![feature(transmutability)] + +mod assert { + use std::mem::{Assume, TransmuteFrom}; + + pub fn is_maybe_transmutable() + where + Dst: TransmuteFrom + {} +} + +fn main() { + assert::is_maybe_transmutable::<&'static [u8; 0], &'static [u16; 0]>(); //~ ERROR `&[u8; 0]` cannot be safely transmuted into `&[u16; 0]` +} +"#; + let input = Level::ERROR + .header("`&[u8; 0]` cannot be safely transmuted into `&[u16; 0]`") + .id("E027s7") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .fold(true) + .origin("$DIR/align-fail.rs") + .annotation( + AnnotationKind::Primary + .span(442..459) + .label("the minimum alignment of `&[u8; 0]` (1) should be greater than that of `&[u16; 0]` (2)") + ), + ), + ); + let expected = str![[r#" +error[E027s7]: `&[u8; 0]` cannot be safely transmuted into `&[u16; 0]` + --> $DIR/align-fail.rs:21:55 + | +LL | ...ic [u8; 0], &'static [u16; 0]>(); //~ ERROR `&[u8; 0]` cannot be safely transmuted into `&[u16; 0]` + | ^^^^^^^^^^^^^^^^^ the minimum alignment of `&[u8; 0]` (1) should be greater than that of `&[u16; 0]` (2) +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn missing_semicolon() { + // tests/ui/suggestions/missing-semicolon.rs + let source = r#"//@ run-rustfix +#![allow(dead_code, unused_variables, path_statements)] +fn a() { + let x = 5; + let y = x //~ ERROR expected function + () //~ ERROR expected `;`, found `}` +} + +fn b() { + let x = 5; + let y = x //~ ERROR expected function + (); +} +fn c() { + let x = 5; + x //~ ERROR expected function + () +} +fn d() { // ok + let x = || (); + x + () +} +fn e() { // ok + let x = || (); + x + (); +} +fn f() + { + let y = 5 //~ ERROR expected function + () //~ ERROR expected `;`, found `}` +} +fn g() { + 5 //~ ERROR expected function + (); +} +fn main() {} +"#; + let input = Level::ERROR + .header("expected function, found `{integer}`") + .id("E0618") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/missing-semicolon.rs") + .fold(true) + .annotation( + AnnotationKind::Context + .span(108..144) + .label("call expression requires function"), + ) + .annotation( + AnnotationKind::Context + .span(89..90) + .label("`x` has type `{integer}`"), + ) + .annotation(AnnotationKind::Context.span(109..109).label( + "help: consider using a semicolon here to finish the statement: `;`", + )) + .annotation(AnnotationKind::Primary.span(108..109)), + ), + ); + let expected = str![[r#" +error[E0618]: expected function, found `{integer}` + --> $DIR/missing-semicolon.rs:5:13 + | +LL | let x = 5; + | - `x` has type `{integer}` +LL | let y = x //~ ERROR expected function + | ^- help: consider using a semicolon here to finish the statement: `;` + | _____________| + | | +LL | | () //~ ERROR expected `;`, found `}` + | |______- call expression requires function +"#]]; + + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn nested_macro_rules() { + // tests/ui/proc-macro/nested-macro-rules.rs + let source = r#"//@ run-pass +//@ aux-build:nested-macro-rules.rs +//@ proc-macro: test-macros.rs +//@ compile-flags: -Z span-debug -Z macro-backtrace +//@ edition:2018 + +#![no_std] // Don't load unnecessary hygiene information from std +#![warn(non_local_definitions)] + +extern crate std; + +extern crate nested_macro_rules; +extern crate test_macros; + +use test_macros::{print_bang, print_attr}; + +use nested_macro_rules::FirstStruct; +struct SecondStruct; + +fn main() { + nested_macro_rules::inner_macro!(print_bang, print_attr); + + nested_macro_rules::outer_macro!(SecondStruct, SecondAttrStruct); + //~^ WARN non-local `macro_rules!` definition + inner_macro!(print_bang, print_attr); +} +"#; + + let aux_source = r#"pub struct FirstStruct; + +#[macro_export] +macro_rules! outer_macro { + ($name:ident, $attr_struct_name:ident) => { + #[macro_export] + macro_rules! inner_macro { + ($bang_macro:ident, $attr_macro:ident) => { + $bang_macro!($name); + #[$attr_macro] struct $attr_struct_name {} + } + } + } +} + +outer_macro!(FirstStruct, FirstAttrStruct); +"#; + let input = Level::WARNING + .header("non-local `macro_rules!` definition, `#[macro_export]` macro should be written at top level module") + .group( + Group::new() + .element( + Snippet::source(aux_source) + .line_start(1) + .origin("$DIR/auxiliary/nested-macro-rules.rs") + .fold(true) + .annotation( + AnnotationKind::Context + .span(41..65) + .label("in this expansion of `nested_macro_rules::outer_macro!`"), + ) + .annotation(AnnotationKind::Primary.span(148..350)), + ) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/nested-macro-rules.rs") + .fold(true) + .annotation( + AnnotationKind::Context + .span(510..574) + .label("in this macro invocation"), + ), + ) + .element( + Level::HELP + .title("remove the `#[macro_export]` or move this `macro_rules!` outside the of the current function `main`") + ) + .element( + Level::NOTE + .title("a `macro_rules!` definition is non-local if it is nested inside an item and has a `#[macro_export]` attribute") + ), + ) + .group( + Group::new() + .element(Level::NOTE.title("the lint level is defined here")) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/nested-macro-rules.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(224..245)), + ), + ); + let expected = str![[r#" +warning: non-local `macro_rules!` definition, `#[macro_export]` macro should be written at top level module + --> $DIR/auxiliary/nested-macro-rules.rs:7:9 + | +LL | macro_rules! outer_macro { + | ------------------------ in this expansion of `nested_macro_rules::outer_macro!` +... +LL | / macro_rules! inner_macro { +LL | | ($bang_macro:ident, $attr_macro:ident) => { +LL | | $bang_macro!($name); +LL | | #[$attr_macro] struct $attr_struct_name {} +LL | | } +LL | | } + | |_________^ + | + ::: $DIR/nested-macro-rules.rs:23:5 + | +LL | nested_macro_rules::outer_macro!(SecondStruct, SecondAttrStruct); + | ---------------------------------------------------------------- in this macro invocation + | + = help: remove the `#[macro_export]` or move this `macro_rules!` outside the of the current function `main` + = note: a `macro_rules!` definition is non-local if it is nested inside an item and has a `#[macro_export]` attribute +note: the lint level is defined here + --> $DIR/nested-macro-rules.rs:8:9 + | +LL | #![warn(non_local_definitions)] + | ^^^^^^^^^^^^^^^^^^^^^ +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn method_on_ambiguous_numeric_type() { + // tests/ui/methods/method-on-ambiguous-numeric-type.rs + let source = r#"//@ aux-build:macro-in-other-crate.rs + +#[macro_use] extern crate macro_in_other_crate; + +macro_rules! local_mac { + ($ident:ident) => { let $ident = 42; } +} +macro_rules! local_mac_tt { + ($tt:tt) => { let $tt = 42; } +} + +fn main() { + let x = 2.0.neg(); + //~^ ERROR can't call method `neg` on ambiguous numeric type `{float}` + + let y = 2.0; + let x = y.neg(); + //~^ ERROR can't call method `neg` on ambiguous numeric type `{float}` + println!("{:?}", x); + + for i in 0..100 { + println!("{}", i.pow(2)); + //~^ ERROR can't call method `pow` on ambiguous numeric type `{integer}` + } + + local_mac!(local_bar); + local_bar.pow(2); + //~^ ERROR can't call method `pow` on ambiguous numeric type `{integer}` + + local_mac_tt!(local_bar_tt); + local_bar_tt.pow(2); + //~^ ERROR can't call method `pow` on ambiguous numeric type `{integer}` +} + +fn qux() { + mac!(bar); + bar.pow(2); + //~^ ERROR can't call method `pow` on ambiguous numeric type `{integer}` +} +"#; + + let aux_source = r#"#[macro_export] +macro_rules! mac { + ($ident:ident) => { let $ident = 42; } +} + +#[macro_export] +macro_rules! inline { + () => () +} +"#; + let input = Level::ERROR + .header("can't call method `pow` on ambiguous numeric type `{integer}`") + .id("E0689") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/method-on-ambiguous-numeric-type.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(916..919)), + ), + ) + .group( + Group::new() + .element(Level::HELP.title("you must specify a type for this binding, like `i32`")) + .element( + Snippet::source(aux_source) + .line_start(1) + .origin("$DIR/auxiliary/macro-in-other-crate.rs") + .fold(true) + .annotation(AnnotationKind::Context.span(69..69).label(": i32")), + ), + ); + let expected = str![[r#" +error[E0689]: can't call method `pow` on ambiguous numeric type `{integer}` + --> $DIR/method-on-ambiguous-numeric-type.rs:37:9 + | +LL | bar.pow(2); + | ^^^ + | +help: you must specify a type for this binding, like `i32` + --> $DIR/auxiliary/macro-in-other-crate.rs:3:35 + | +LL | ($ident:ident) => { let $ident = 42; } + | - : i32 +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn issue_42234_unknown_receiver_type() { + // tests/ui/span/issue-42234-unknown-receiver-type.rs + let source = r#"//@ revisions: full generic_arg +#![cfg_attr(generic_arg, feature(generic_arg_infer))] + +// When the type of a method call's receiver is unknown, the span should point +// to the receiver (and not the entire call, as was previously the case before +// the fix of which this tests). + +fn shines_a_beacon_through_the_darkness() { + let x: Option<_> = None; //~ ERROR type annotations needed + x.unwrap().method_that_could_exist_on_some_type(); +} + +fn courier_to_des_moines_and_points_west(data: &[u32]) -> String { + data.iter() + .sum::<_>() //~ ERROR type annotations needed + .to_string() +} + +fn main() {} +"#; + + let input = Level::ERROR + .header("type annotations needed") + .id("E0282") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/issue-42234-unknown-receiver-type.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(536..539).label( + "cannot infer type of the type parameter `S` declared on the method `sum`", + )), + ), + ); + let expected = str![[r#" +error[E0282]: type annotations needed + --> $DIR/issue-42234-unknown-receiver-type.rs:15:10 + | +LL | .sum::<_>() //~ ERROR type annotations needed + | ^^^ cannot infer type of the type parameter `S` declared on the method `sum` +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn pattern_usefulness_empty_match() { + // tests/ui/pattern/usefulness/empty-match.rs + let source = r##"//@ revisions: normal exhaustive_patterns +// +// This tests a match with no arms on various types. +#![feature(never_type)] +#![cfg_attr(exhaustive_patterns, feature(exhaustive_patterns))] +#![deny(unreachable_patterns)] + +fn nonempty(arrayN_of_empty: [!; N]) { + macro_rules! match_no_arms { + ($e:expr) => { + match $e {} + }; + } + macro_rules! match_guarded_arm { + ($e:expr) => { + match $e { + _ if false => {} + } + }; + } + + struct NonEmptyStruct1; + struct NonEmptyStruct2(bool); + union NonEmptyUnion1 { + foo: (), + } + union NonEmptyUnion2 { + foo: (), + bar: !, + } + enum NonEmptyEnum1 { + Foo(bool), + } + enum NonEmptyEnum2 { + Foo(bool), + Bar, + } + enum NonEmptyEnum5 { + V1, + V2, + V3, + V4, + V5, + } + let array0_of_empty: [!; 0] = []; + + match_no_arms!(0u8); //~ ERROR type `u8` is non-empty + match_no_arms!(0i8); //~ ERROR type `i8` is non-empty + match_no_arms!(0usize); //~ ERROR type `usize` is non-empty + match_no_arms!(0isize); //~ ERROR type `isize` is non-empty + match_no_arms!(NonEmptyStruct1); //~ ERROR type `NonEmptyStruct1` is non-empty + match_no_arms!(NonEmptyStruct2(true)); //~ ERROR type `NonEmptyStruct2` is non-empty + match_no_arms!((NonEmptyUnion1 { foo: () })); //~ ERROR type `NonEmptyUnion1` is non-empty + match_no_arms!((NonEmptyUnion2 { foo: () })); //~ ERROR type `NonEmptyUnion2` is non-empty + match_no_arms!(NonEmptyEnum1::Foo(true)); //~ ERROR `NonEmptyEnum1::Foo(_)` not covered + match_no_arms!(NonEmptyEnum2::Foo(true)); //~ ERROR `NonEmptyEnum2::Foo(_)` and `NonEmptyEnum2::Bar` not covered + match_no_arms!(NonEmptyEnum5::V1); //~ ERROR `NonEmptyEnum5::V1`, `NonEmptyEnum5::V2`, `NonEmptyEnum5::V3` and 2 more not covered + match_no_arms!(array0_of_empty); //~ ERROR type `[!; 0]` is non-empty + match_no_arms!(arrayN_of_empty); //~ ERROR type `[!; N]` is non-empty + + match_guarded_arm!(0u8); //~ ERROR `0_u8..=u8::MAX` not covered + match_guarded_arm!(0i8); //~ ERROR `i8::MIN..=i8::MAX` not covered + match_guarded_arm!(0usize); //~ ERROR `0_usize..` not covered + match_guarded_arm!(0isize); //~ ERROR `_` not covered + match_guarded_arm!(NonEmptyStruct1); //~ ERROR `NonEmptyStruct1` not covered + match_guarded_arm!(NonEmptyStruct2(true)); //~ ERROR `NonEmptyStruct2(_)` not covered + match_guarded_arm!((NonEmptyUnion1 { foo: () })); //~ ERROR `NonEmptyUnion1 { .. }` not covered + match_guarded_arm!((NonEmptyUnion2 { foo: () })); //~ ERROR `NonEmptyUnion2 { .. }` not covered + match_guarded_arm!(NonEmptyEnum1::Foo(true)); //~ ERROR `NonEmptyEnum1::Foo(_)` not covered + match_guarded_arm!(NonEmptyEnum2::Foo(true)); //~ ERROR `NonEmptyEnum2::Foo(_)` and `NonEmptyEnum2::Bar` not covered + match_guarded_arm!(NonEmptyEnum5::V1); //~ ERROR `NonEmptyEnum5::V1`, `NonEmptyEnum5::V2`, `NonEmptyEnum5::V3` and 2 more not covered + match_guarded_arm!(array0_of_empty); //~ ERROR `[]` not covered + match_guarded_arm!(arrayN_of_empty); //~ ERROR `[]` not covered +} + +fn main() {} +"##; + + let input = Level::ERROR + .header( + "non-exhaustive patterns: `NonEmptyEnum5::V1`, `NonEmptyEnum5::V2`, `NonEmptyEnum5::V3` and 2 more not covered" + ) + .id("E0004") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/empty-match.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(2911..2928) + .label("patterns `NonEmptyEnum5::V1`, `NonEmptyEnum5::V2`, `NonEmptyEnum5::V3` and 2 more not covered") + ), + ), + ) + .group( + Group::new() + .element(Level::NOTE.title("`NonEmptyEnum5` defined here")) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/empty-match.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(818..831)) + .annotation(AnnotationKind::Context.span(842..844).label("not covered")) + .annotation(AnnotationKind::Context.span(854..856).label("not covered")) + .annotation(AnnotationKind::Context.span(866..868).label("not covered")) + .annotation(AnnotationKind::Context.span(878..880).label("not covered")) + .annotation(AnnotationKind::Context.span(890..892).label("not covered")) + ) + .element(Level::NOTE.title("the matched value is of type `NonEmptyEnum5`")) + .element(Level::NOTE.title("match arms with guards don't count towards exhaustivity")) + ) + .group( + Group::new() + .element( + Level::HELP + .title("ensure that all possible cases are being handled by adding a match arm with a wildcard pattern as shown, or multiple match arms") + ) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/empty-match.rs") + .fold(true) + .annotation(AnnotationKind::Context.span(485..485).label(",\n _ => todo!()")) + ) + ); + let expected = str![[r#" +error[E0004]: non-exhaustive patterns: `NonEmptyEnum5::V1`, `NonEmptyEnum5::V2`, `NonEmptyEnum5::V3` and 2 more not covered + --> $DIR/empty-match.rs:71:24 + | +LL | match_guarded_arm!(NonEmptyEnum5::V1); //~ ERROR `NonEmptyEnum5::V1`, `NonEmptyEnum5::V2`, `NonEmptyEnum5::V3` and 2 more not covered + | ^^^^^^^^^^^^^^^^^ patterns `NonEmptyEnum5::V1`, `NonEmptyEnum5::V2`, `NonEmptyEnum5::V3` and 2 more not covered + | +note: `NonEmptyEnum5` defined here + --> $DIR/empty-match.rs:38:10 + | +LL | enum NonEmptyEnum5 { + | ^^^^^^^^^^^^^ +LL | V1, + | -- not covered +LL | V2, + | -- not covered +LL | V3, + | -- not covered +LL | V4, + | -- not covered +LL | V5, + | -- not covered + = note: the matched value is of type `NonEmptyEnum5` + = note: match arms with guards don't count towards exhaustivity +help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern as shown, or multiple match arms + --> $DIR/empty-match.rs:17:33 + | +LL | _ if false => {} + | - , + _ => todo!() +"#]]; + let renderer = Renderer::plain() + .anonymized_line_numbers(true) + .term_width(annotate_snippets::renderer::DEFAULT_TERM_WIDTH + 4); + assert_data_eq!(renderer.render(input), expected); +} + +#[test] +fn object_fail() { + // tests/ui/traits/alias/object-fail.rs + let source = r#"#![feature(trait_alias)] + +trait EqAlias = Eq; +trait IteratorAlias = Iterator; + +fn main() { + let _: &dyn EqAlias = &123; + //~^ ERROR the trait alias `EqAlias` is not dyn compatible [E0038] + let _: &dyn IteratorAlias = &vec![123].into_iter(); + //~^ ERROR must be specified +} +"#; + let input = Level::ERROR + .header("the trait alias `EqAlias` is not dyn compatible") + .id("E0038") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/object-fail.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(107..114) + .label("`EqAlias` is not dyn compatible"), + ), + ), + ) + .group( + Group::new() + .element( + Level::NOTE + .title("for a trait to be dyn compatible it needs to allow building a vtable\nfor more information, visit ")) + .element( + Origin::new("$SRC_DIR/core/src/cmp.rs") + .line(334) + .char_column(14) + .primary(true) + .label("...because it uses `Self` as a type parameter") + + ) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/object-fail.rs") + .fold(true) + .annotation( + AnnotationKind::Context + .span(32..39) + .label("this trait is not dyn compatible..."), + ), + ), + ); + let expected = str![[r#" +error[E0038]: the trait alias `EqAlias` is not dyn compatible + --> $DIR/object-fail.rs:7:17 + | +LL | let _: &dyn EqAlias = &123; + | ^^^^^^^ `EqAlias` is not dyn compatible + | +note: for a trait to be dyn compatible it needs to allow building a vtable + for more information, visit + --> $SRC_DIR/core/src/cmp.rs:334:14 + | + = note: ...because it uses `Self` as a type parameter + | + ::: $DIR/object-fail.rs:3:7 + | +LL | trait EqAlias = Eq; + | ------- this trait is not dyn compatible... +"#]]; + + let renderer = Renderer::plain().anonymized_line_numbers(true); + assert_data_eq!(renderer.render(input), expected); }