├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── Changelog.md ├── LICENSE-APACHE ├── LICENSE-MIT ├── Readme.md ├── examples └── fix-json.rs ├── proptest-regressions └── replace.txt ├── src ├── diagnostics.rs ├── lib.rs └── replace.rs └── tests ├── edge-cases ├── empty.json ├── empty.rs ├── indented_whitespace.json ├── no_main.json ├── no_main.rs ├── out_of_bounds.recorded.json └── utf8_idents.recorded.json ├── edge_cases.rs ├── edition └── .gitignore ├── everything ├── .gitignore ├── E0178.fixed.rs ├── E0178.json ├── E0178.rs ├── closure-immutable-outer-variable.fixed.rs ├── closure-immutable-outer-variable.json ├── closure-immutable-outer-variable.rs ├── handle-insert-only.fixed.rs ├── handle-insert-only.json ├── handle-insert-only.rs ├── lt-generic-comp.fixed.rs ├── lt-generic-comp.json ├── lt-generic-comp.rs ├── multiple-solutions.fixed.rs ├── multiple-solutions.json ├── multiple-solutions.rs ├── replace-only-one-char.fixed.rs ├── replace-only-one-char.json ├── replace-only-one-char.rs ├── str-lit-type-mismatch.fixed.rs ├── str-lit-type-mismatch.json └── str-lit-type-mismatch.rs └── parse_and_replace.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.rs] 17 | indent_style = space 18 | indent_size = 4 19 | 20 | [*.toml] 21 | indent_style = space 22 | indent_size = 4 23 | 24 | [*.md] 25 | trim_trailing_whitespace = false 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Test 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest] 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Install Rust (rustup) 14 | run: rustup update nightly --no-self-update && rustup default nightly 15 | shell: bash 16 | - run: cargo test --all 17 | - run: cargo test --all -- --ignored 18 | 19 | rustfmt: 20 | name: Rustfmt 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@master 24 | - name: Install Rust 25 | run: rustup update stable && rustup default stable && rustup component add rustfmt 26 | - run: cargo fmt --check 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | tests/crates/**/output.txt 4 | 5 | # Editor and IDE-specific ignores 6 | ## VSCode 7 | .vscode 8 | 9 | ## Eclipse 10 | .project 11 | 12 | ## All JetBrains IDE's 13 | *.iml 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rustfix/c433369f4aa3fe67b1399e2f6e3c4aeefc3144b7/.gitmodules -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = [ 3 | "Pascal Hertleif ", 4 | "Oliver Schneider ", 5 | ] 6 | license = "Apache-2.0/MIT" 7 | name = "rustfix" 8 | description = "Automatically apply the suggestions made by rustc" 9 | repository = "https://github.com/rust-lang-nursery/rustfix" 10 | documentation = "https://docs.rs/rustfix" 11 | edition = "2018" 12 | readme = "Readme.md" 13 | version = "0.6.1" 14 | exclude = [ 15 | "etc/*", 16 | "examples/*", 17 | "tests/*", 18 | ] 19 | 20 | [dependencies] 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | anyhow = "1.0.6" 24 | log = "0.4.1" 25 | 26 | [dev-dependencies] 27 | duct = "0.9" 28 | env_logger = "0.5.0-rc.1" 29 | log = "0.4.1" 30 | tempfile = "3" 31 | proptest = "0.7.0" 32 | similar = "0.4.0" 33 | 34 | [workspace] 35 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.4.6] - 2019-07-16 11 | 12 | ### Changed 13 | 14 | Internal changes: 15 | 16 | - Change example to automatically determine filename 17 | - Migrate to Rust 2018 18 | - use `derive` feature over `serde_derive` crate 19 | 20 | ## [0.4.5] - 2019-03-26 21 | 22 | ### Added 23 | 24 | - Implement common traits for Diagnostic and related types 25 | 26 | ### Fixed 27 | 28 | - Fix out of bounds access in parse_snippet 29 | 30 | ## [0.4.4] - 2018-12-13 31 | 32 | ### Added 33 | 34 | - Make Diagnostic::rendered public. 35 | 36 | ### Changed 37 | 38 | - Revert faulty "Allow multiple solutions in a suggestion" 39 | 40 | ## [0.4.3] - 2018-12-09 - *yanked!* 41 | 42 | ### Added 43 | 44 | - Allow multiple solutions in a suggestion 45 | 46 | ### Changed 47 | 48 | - use `RUSTC` environment var if present 49 | 50 | ## [0.4.2] - 2018-07-31 51 | 52 | ### Added 53 | 54 | - Expose an interface to apply fixes on-by-one 55 | 56 | ### Changed 57 | 58 | - Handle invalid snippets instead of panicking 59 | 60 | ## [0.4.1] - 2018-07-26 61 | 62 | ### Changed 63 | 64 | - Ignore duplicate replacements 65 | 66 | ## [0.4.0] - 2018-05-23 67 | 68 | ### Changed 69 | 70 | - Filter by machine applicability by default 71 | 72 | [Unreleased]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.6...HEAD 73 | [0.4.6]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.5...rustfix-0.4.6 74 | [0.4.5]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.4...rustfix-0.4.5 75 | [0.4.4]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.3...rustfix-0.4.4 76 | [0.4.3]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.2...rustfix-0.4.3 77 | [0.4.2]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.1...rustfix-0.4.2 78 | [0.4.1]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.0...rustfix-0.4.1 79 | [0.4.0]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.0 80 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pascal Hertleif 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Rustfix has moved 2 | 3 | The rustfix crate has moved to . 4 | Any issues about the `rustfix` library or `cargo fix` should be filed at . 5 | Any issues about the suggestions generated by the compiler should be filed at . 6 | 7 | ---- 8 | 9 | # rustfix 10 | 11 | [![Latest Version](https://img.shields.io/crates/v/rustfix.svg)](https://crates.io/crates/rustfix) 12 | [![Rust Documentation](https://docs.rs/rustfix/badge.svg)](https://docs.rs/rustfix) 13 | 14 | Rustfix is a library defining useful structures that represent fix suggestions from rustc. 15 | 16 | ## Current status 17 | 18 | Currently, rustfix is split into two crates: 19 | 20 | - `rustfix`, a library for consuming and applying suggestions in the format that `rustc` outputs (this crate) 21 | - `cargo-fix`, a binary that works as cargo subcommand and that end users will use to fix their code (maintained in the [cargo](https://github.com/rust-lang/cargo/blob/master/src/bin/cargo/commands/fix.rs) repo). 22 | 23 | 24 | The library (and therefore this repo) is considered largely feature-complete. This is because: 25 | * There is no compiler or even rust-specific logic here 26 | * New lints and suggestions come from the Rust compiler (and external lints, like [clippy]). 27 | * `rustfix` doesn't touch the filesystem to implement fixes, or read from disk 28 | 29 | [clippy]: https://github.com/rust-lang-nursery/rust-clippy 30 | 31 | ## Installation 32 | 33 | `cargo fix` is a built-in command in Cargo since Rust 1.29. There is no need to install it separately from crates.io. 34 | 35 | To use the rustfix library for use in your own fix project, add it to your `Cargo.toml`. 36 | 37 | ## Using `cargo fix --edition` to transition to Rust 2021 38 | 39 | Instructions on how to use this tool to transition a crate to Rust 2021 can be 40 | found [in the Rust Edition Guide.](https://rust-lang-nursery.github.io/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html) 41 | 42 | ## License 43 | 44 | Licensed under either of 45 | 46 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 47 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 48 | 49 | at your option. 50 | 51 | ### Contribution 52 | 53 | Unless you explicitly state otherwise, any contribution intentionally 54 | submitted for inclusion in the work by you, as defined in the Apache-2.0 55 | license, shall be dual licensed as above, without any additional terms or 56 | conditions. 57 | -------------------------------------------------------------------------------- /examples/fix-json.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use std::io::{stdin, BufReader, Read}; 3 | use std::{collections::HashMap, collections::HashSet, env, fs}; 4 | 5 | fn main() -> Result<(), Error> { 6 | let suggestions_file = env::args().nth(1).expect("USAGE: fix-json "); 7 | let suggestions = if suggestions_file == "--" { 8 | let mut buffer = String::new(); 9 | BufReader::new(stdin()).read_to_string(&mut buffer)?; 10 | buffer 11 | } else { 12 | fs::read_to_string(&suggestions_file)? 13 | }; 14 | let suggestions = rustfix::get_suggestions_from_json( 15 | &suggestions, 16 | &HashSet::new(), 17 | rustfix::Filter::Everything, 18 | )?; 19 | 20 | let mut files = HashMap::new(); 21 | for suggestion in suggestions { 22 | let file = suggestion.solutions[0].replacements[0] 23 | .snippet 24 | .file_name 25 | .clone(); 26 | files.entry(file).or_insert_with(Vec::new).push(suggestion); 27 | } 28 | 29 | for (source_file, suggestions) in &files { 30 | let source = fs::read_to_string(source_file)?; 31 | let mut fix = rustfix::CodeFix::new(&source); 32 | for suggestion in suggestions.iter().rev() { 33 | if let Err(e) = fix.apply(suggestion) { 34 | eprintln!("Failed to apply suggestion to {}: {}", source_file, e); 35 | } 36 | } 37 | let fixes = fix.finish()?; 38 | fs::write(source_file, fixes)?; 39 | } 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /proptest-regressions/replace.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | xs 358148376 3634975642 2528447681 3675516813 # shrinks to ref s = "" 8 | xs 3127423015 3362740891 2605681441 2390162043 # shrinks to ref data = "", ref replacements = [(0..0, [])] 9 | -------------------------------------------------------------------------------- /src/diagnostics.rs: -------------------------------------------------------------------------------- 1 | //! Rustc Diagnostic JSON Output 2 | //! 3 | //! The following data types are copied from [rust-lang/rust](https://github.com/rust-lang/rust/blob/de78655bca47cac8e783dbb563e7e5c25c1fae40/src/libsyntax/json.rs) 4 | 5 | use serde::Deserialize; 6 | 7 | #[derive(Clone, Deserialize, Debug, Hash, Eq, PartialEq)] 8 | pub struct Diagnostic { 9 | /// The primary error message. 10 | pub message: String, 11 | pub code: Option, 12 | /// "error: internal compiler error", "error", "warning", "note", "help". 13 | level: String, 14 | pub spans: Vec, 15 | /// Associated diagnostic messages. 16 | pub children: Vec, 17 | /// The message as rustc would render it. Currently this is only 18 | /// `Some` for "suggestions", but eventually it will include all 19 | /// snippets. 20 | pub rendered: Option, 21 | } 22 | 23 | #[derive(Clone, Deserialize, Debug, Hash, Eq, PartialEq)] 24 | pub struct DiagnosticSpan { 25 | pub file_name: String, 26 | pub byte_start: u32, 27 | pub byte_end: u32, 28 | /// 1-based. 29 | pub line_start: usize, 30 | pub line_end: usize, 31 | /// 1-based, character offset. 32 | pub column_start: usize, 33 | pub column_end: usize, 34 | /// Is this a "primary" span -- meaning the point, or one of the points, 35 | /// where the error occurred? 36 | pub is_primary: bool, 37 | /// Source text from the start of line_start to the end of line_end. 38 | pub text: Vec, 39 | /// Label that should be placed at this location (if any) 40 | label: Option, 41 | /// If we are suggesting a replacement, this will contain text 42 | /// that should be sliced in atop this span. You may prefer to 43 | /// load the fully rendered version from the parent `Diagnostic`, 44 | /// however. 45 | pub suggested_replacement: Option, 46 | pub suggestion_applicability: Option, 47 | /// Macro invocations that created the code at this span, if any. 48 | expansion: Option>, 49 | } 50 | 51 | #[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)] 52 | pub enum Applicability { 53 | MachineApplicable, 54 | HasPlaceholders, 55 | MaybeIncorrect, 56 | Unspecified, 57 | } 58 | 59 | #[derive(Clone, Deserialize, Debug, Eq, PartialEq, Hash)] 60 | pub struct DiagnosticSpanLine { 61 | pub text: String, 62 | 63 | /// 1-based, character offset in self.text. 64 | pub highlight_start: usize, 65 | 66 | pub highlight_end: usize, 67 | } 68 | 69 | #[derive(Clone, Deserialize, Debug, Eq, PartialEq, Hash)] 70 | struct DiagnosticSpanMacroExpansion { 71 | /// span where macro was applied to generate this code; note that 72 | /// this may itself derive from a macro (if 73 | /// `span.expansion.is_some()`) 74 | span: DiagnosticSpan, 75 | 76 | /// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]") 77 | macro_decl_name: String, 78 | 79 | /// span where macro was defined (if known) 80 | def_site_span: Option, 81 | } 82 | 83 | #[derive(Clone, Deserialize, Debug, Eq, PartialEq, Hash)] 84 | pub struct DiagnosticCode { 85 | /// The code itself. 86 | pub code: String, 87 | /// An explanation for the code. 88 | explanation: Option, 89 | } 90 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(rust_2018_idioms)] 2 | 3 | #[macro_use] 4 | extern crate log; 5 | #[cfg(test)] 6 | #[macro_use] 7 | extern crate proptest; 8 | 9 | use std::collections::HashSet; 10 | use std::ops::Range; 11 | 12 | use anyhow::Error; 13 | 14 | pub mod diagnostics; 15 | use crate::diagnostics::{Diagnostic, DiagnosticSpan}; 16 | mod replace; 17 | 18 | #[derive(Debug, Clone, Copy)] 19 | pub enum Filter { 20 | MachineApplicableOnly, 21 | Everything, 22 | } 23 | 24 | pub fn get_suggestions_from_json( 25 | input: &str, 26 | only: &HashSet, 27 | filter: Filter, 28 | ) -> serde_json::error::Result> { 29 | let mut result = Vec::new(); 30 | for cargo_msg in serde_json::Deserializer::from_str(input).into_iter::() { 31 | // One diagnostic line might have multiple suggestions 32 | result.extend(collect_suggestions(&cargo_msg?, only, filter)); 33 | } 34 | Ok(result) 35 | } 36 | 37 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] 38 | pub struct LinePosition { 39 | pub line: usize, 40 | pub column: usize, 41 | } 42 | 43 | impl std::fmt::Display for LinePosition { 44 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 45 | write!(f, "{}:{}", self.line, self.column) 46 | } 47 | } 48 | 49 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] 50 | pub struct LineRange { 51 | pub start: LinePosition, 52 | pub end: LinePosition, 53 | } 54 | 55 | impl std::fmt::Display for LineRange { 56 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 57 | write!(f, "{}-{}", self.start, self.end) 58 | } 59 | } 60 | 61 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 62 | /// An error/warning and possible solutions for fixing it 63 | pub struct Suggestion { 64 | pub message: String, 65 | pub snippets: Vec, 66 | pub solutions: Vec, 67 | } 68 | 69 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 70 | pub struct Solution { 71 | pub message: String, 72 | pub replacements: Vec, 73 | } 74 | 75 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 76 | pub struct Snippet { 77 | pub file_name: String, 78 | pub line_range: LineRange, 79 | pub range: Range, 80 | /// leading surrounding text, text to replace, trailing surrounding text 81 | /// 82 | /// This split is useful for higlighting the part that gets replaced 83 | pub text: (String, String, String), 84 | } 85 | 86 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 87 | pub struct Replacement { 88 | pub snippet: Snippet, 89 | pub replacement: String, 90 | } 91 | 92 | fn parse_snippet(span: &DiagnosticSpan) -> Option { 93 | // unindent the snippet 94 | let indent = span 95 | .text 96 | .iter() 97 | .map(|line| { 98 | let indent = line 99 | .text 100 | .chars() 101 | .take_while(|&c| char::is_whitespace(c)) 102 | .count(); 103 | std::cmp::min(indent, line.highlight_start - 1) 104 | }) 105 | .min()?; 106 | 107 | let text_slice = span.text[0].text.chars().collect::>(); 108 | 109 | // We subtract `1` because these highlights are 1-based 110 | // Check the `min` so that it doesn't attempt to index out-of-bounds when 111 | // the span points to the "end" of the line. For example, a line of 112 | // "foo\n" with a highlight_start of 5 is intended to highlight *after* 113 | // the line. This needs to compensate since the newline has been removed 114 | // from the text slice. 115 | let start = (span.text[0].highlight_start - 1).min(text_slice.len()); 116 | let end = (span.text[0].highlight_end - 1).min(text_slice.len()); 117 | let lead = text_slice[indent..start].iter().collect(); 118 | let mut body: String = text_slice[start..end].iter().collect(); 119 | 120 | for line in span.text.iter().take(span.text.len() - 1).skip(1) { 121 | body.push('\n'); 122 | body.push_str(&line.text[indent..]); 123 | } 124 | let mut tail = String::new(); 125 | let last = &span.text[span.text.len() - 1]; 126 | 127 | // If we get a DiagnosticSpanLine where highlight_end > text.len(), we prevent an 'out of 128 | // bounds' access by making sure the index is within the array bounds. 129 | // `saturating_sub` is used in case of an empty file 130 | let last_tail_index = last.highlight_end.min(last.text.len()).saturating_sub(1); 131 | let last_slice = last.text.chars().collect::>(); 132 | 133 | if span.text.len() > 1 { 134 | body.push('\n'); 135 | body.push_str( 136 | &last_slice[indent..last_tail_index] 137 | .iter() 138 | .collect::(), 139 | ); 140 | } 141 | tail.push_str(&last_slice[last_tail_index..].iter().collect::()); 142 | Some(Snippet { 143 | file_name: span.file_name.clone(), 144 | line_range: LineRange { 145 | start: LinePosition { 146 | line: span.line_start, 147 | column: span.column_start, 148 | }, 149 | end: LinePosition { 150 | line: span.line_end, 151 | column: span.column_end, 152 | }, 153 | }, 154 | range: (span.byte_start as usize)..(span.byte_end as usize), 155 | text: (lead, body, tail), 156 | }) 157 | } 158 | 159 | fn collect_span(span: &DiagnosticSpan) -> Option { 160 | let snippet = parse_snippet(span)?; 161 | let replacement = span.suggested_replacement.clone()?; 162 | Some(Replacement { 163 | snippet, 164 | replacement, 165 | }) 166 | } 167 | 168 | pub fn collect_suggestions( 169 | diagnostic: &Diagnostic, 170 | only: &HashSet, 171 | filter: Filter, 172 | ) -> Option { 173 | if !only.is_empty() { 174 | if let Some(ref code) = diagnostic.code { 175 | if !only.contains(&code.code) { 176 | // This is not the code we are looking for 177 | return None; 178 | } 179 | } else { 180 | // No code, probably a weird builtin warning/error 181 | return None; 182 | } 183 | } 184 | 185 | let snippets = diagnostic.spans.iter().filter_map(parse_snippet).collect(); 186 | 187 | let solutions: Vec<_> = diagnostic 188 | .children 189 | .iter() 190 | .filter_map(|child| { 191 | let replacements: Vec<_> = child 192 | .spans 193 | .iter() 194 | .filter(|span| { 195 | use crate::diagnostics::Applicability::*; 196 | use crate::Filter::*; 197 | 198 | match (filter, &span.suggestion_applicability) { 199 | (MachineApplicableOnly, Some(MachineApplicable)) => true, 200 | (MachineApplicableOnly, _) => false, 201 | (Everything, _) => true, 202 | } 203 | }) 204 | .filter_map(collect_span) 205 | .collect(); 206 | if !replacements.is_empty() { 207 | Some(Solution { 208 | message: child.message.clone(), 209 | replacements, 210 | }) 211 | } else { 212 | None 213 | } 214 | }) 215 | .collect(); 216 | 217 | if solutions.is_empty() { 218 | None 219 | } else { 220 | Some(Suggestion { 221 | message: diagnostic.message.clone(), 222 | snippets, 223 | solutions, 224 | }) 225 | } 226 | } 227 | 228 | pub struct CodeFix { 229 | data: replace::Data, 230 | } 231 | 232 | impl CodeFix { 233 | pub fn new(s: &str) -> CodeFix { 234 | CodeFix { 235 | data: replace::Data::new(s.as_bytes()), 236 | } 237 | } 238 | 239 | pub fn apply(&mut self, suggestion: &Suggestion) -> Result<(), Error> { 240 | for sol in &suggestion.solutions { 241 | for r in &sol.replacements { 242 | self.data 243 | .replace_range(r.snippet.range.clone(), r.replacement.as_bytes())?; 244 | } 245 | } 246 | Ok(()) 247 | } 248 | 249 | pub fn finish(&self) -> Result { 250 | Ok(String::from_utf8(self.data.to_vec())?) 251 | } 252 | } 253 | 254 | pub fn apply_suggestions(code: &str, suggestions: &[Suggestion]) -> Result { 255 | let mut fix = CodeFix::new(code); 256 | for suggestion in suggestions.iter().rev() { 257 | fix.apply(suggestion)?; 258 | } 259 | fix.finish() 260 | } 261 | -------------------------------------------------------------------------------- /src/replace.rs: -------------------------------------------------------------------------------- 1 | //! A small module giving you a simple container that allows easy and cheap 2 | //! replacement of parts of its content, with the ability to prevent changing 3 | //! the same parts multiple times. 4 | 5 | use anyhow::{anyhow, ensure, Error}; 6 | use std::rc::Rc; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | enum State { 10 | Initial, 11 | Replaced(Rc<[u8]>), 12 | Inserted(Rc<[u8]>), 13 | } 14 | 15 | impl State { 16 | fn is_inserted(&self) -> bool { 17 | matches!(*self, State::Inserted(..)) 18 | } 19 | } 20 | 21 | #[derive(Debug, Clone, PartialEq, Eq)] 22 | struct Span { 23 | /// Start of this span in parent data 24 | start: usize, 25 | /// up to end excluding 26 | end: usize, 27 | data: State, 28 | } 29 | 30 | /// A container that allows easily replacing chunks of its data 31 | #[derive(Debug, Clone, Default)] 32 | pub struct Data { 33 | original: Vec, 34 | parts: Vec, 35 | } 36 | 37 | impl Data { 38 | /// Create a new data container from a slice of bytes 39 | pub fn new(data: &[u8]) -> Self { 40 | Data { 41 | original: data.into(), 42 | parts: vec![Span { 43 | data: State::Initial, 44 | start: 0, 45 | end: data.len(), 46 | }], 47 | } 48 | } 49 | 50 | /// Render this data as a vector of bytes 51 | pub fn to_vec(&self) -> Vec { 52 | if self.original.is_empty() { 53 | return Vec::new(); 54 | } 55 | 56 | self.parts.iter().fold(Vec::new(), |mut acc, d| { 57 | match d.data { 58 | State::Initial => acc.extend_from_slice(&self.original[d.start..d.end]), 59 | State::Replaced(ref d) | State::Inserted(ref d) => acc.extend_from_slice(d), 60 | }; 61 | acc 62 | }) 63 | } 64 | 65 | /// Replace a chunk of data with the given slice, erroring when this part 66 | /// was already changed previously. 67 | pub fn replace_range( 68 | &mut self, 69 | range: std::ops::Range, 70 | data: &[u8], 71 | ) -> Result<(), Error> { 72 | let exclusive_end = range.end; 73 | 74 | ensure!( 75 | range.start <= exclusive_end, 76 | "Invalid range {}..{}, start is larger than end", 77 | range.start, 78 | range.end 79 | ); 80 | 81 | ensure!( 82 | exclusive_end <= self.original.len(), 83 | "Invalid range {}..{} given, original data is only {} byte long", 84 | range.start, 85 | range.end, 86 | self.original.len() 87 | ); 88 | 89 | let insert_only = range.start == range.end; 90 | 91 | // Since we error out when replacing an already replaced chunk of data, 92 | // we can take some shortcuts here. For example, there can be no 93 | // overlapping replacements -- we _always_ split a chunk of 'initial' 94 | // data into three[^empty] parts, and there can't ever be two 'initial' 95 | // parts touching. 96 | // 97 | // [^empty]: Leading and trailing ones might be empty if we replace 98 | // the whole chunk. As an optimization and without loss of generality we 99 | // don't add empty parts. 100 | let new_parts = { 101 | let index_of_part_to_split = self 102 | .parts 103 | .iter() 104 | .position(|p| !p.data.is_inserted() && p.start <= range.start && p.end >= range.end) 105 | .ok_or_else(|| { 106 | use log::Level::Debug; 107 | if log_enabled!(Debug) { 108 | let slices = self 109 | .parts 110 | .iter() 111 | .map(|p| { 112 | ( 113 | p.start, 114 | p.end, 115 | match p.data { 116 | State::Initial => "initial", 117 | State::Replaced(..) => "replaced", 118 | State::Inserted(..) => "inserted", 119 | }, 120 | ) 121 | }) 122 | .collect::>(); 123 | debug!( 124 | "no single slice covering {}..{}, current slices: {:?}", 125 | range.start, range.end, slices, 126 | ); 127 | } 128 | 129 | anyhow!( 130 | "Could not replace range {}..{} in file \ 131 | -- maybe parts of it were already replaced?", 132 | range.start, 133 | range.end, 134 | ) 135 | })?; 136 | 137 | let part_to_split = &self.parts[index_of_part_to_split]; 138 | 139 | // If this replacement matches exactly the part that we would 140 | // otherwise split then we ignore this for now. This means that you 141 | // can replace the exact same range with the exact same content 142 | // multiple times and we'll process and allow it. 143 | // 144 | // This is currently done to alleviate issues like 145 | // rust-lang/rust#51211 although this clause likely wants to be 146 | // removed if that's fixed deeper in the compiler. 147 | if part_to_split.start == range.start && part_to_split.end == range.end { 148 | if let State::Replaced(ref replacement) = part_to_split.data { 149 | if &**replacement == data { 150 | return Ok(()); 151 | } 152 | } 153 | } 154 | 155 | ensure!( 156 | part_to_split.data == State::Initial, 157 | "Cannot replace slice of data that was already replaced" 158 | ); 159 | 160 | let mut new_parts = Vec::with_capacity(self.parts.len() + 2); 161 | 162 | // Previous parts 163 | if let Some(ps) = self.parts.get(..index_of_part_to_split) { 164 | new_parts.extend_from_slice(ps); 165 | } 166 | 167 | // Keep initial data on left side of part 168 | if range.start > part_to_split.start { 169 | new_parts.push(Span { 170 | start: part_to_split.start, 171 | end: range.start, 172 | data: State::Initial, 173 | }); 174 | } 175 | 176 | // New part 177 | new_parts.push(Span { 178 | start: range.start, 179 | end: range.end, 180 | data: if insert_only { 181 | State::Inserted(data.into()) 182 | } else { 183 | State::Replaced(data.into()) 184 | }, 185 | }); 186 | 187 | // Keep initial data on right side of part 188 | if range.end < part_to_split.end { 189 | new_parts.push(Span { 190 | start: range.end, 191 | end: part_to_split.end, 192 | data: State::Initial, 193 | }); 194 | } 195 | 196 | // Following parts 197 | if let Some(ps) = self.parts.get(index_of_part_to_split + 1..) { 198 | new_parts.extend_from_slice(ps); 199 | } 200 | 201 | new_parts 202 | }; 203 | 204 | self.parts = new_parts; 205 | 206 | Ok(()) 207 | } 208 | } 209 | 210 | #[cfg(test)] 211 | mod tests { 212 | use super::*; 213 | use proptest::prelude::*; 214 | 215 | fn str(i: &[u8]) -> &str { 216 | ::std::str::from_utf8(i).unwrap() 217 | } 218 | 219 | #[test] 220 | fn insert_at_beginning() { 221 | let mut d = Data::new(b"foo bar baz"); 222 | d.replace_range(0..0, b"oh no ").unwrap(); 223 | assert_eq!("oh no foo bar baz", str(&d.to_vec())); 224 | } 225 | 226 | #[test] 227 | fn insert_at_end() { 228 | let mut d = Data::new(b"foo bar baz"); 229 | d.replace_range(11..11, b" oh no").unwrap(); 230 | assert_eq!("foo bar baz oh no", str(&d.to_vec())); 231 | } 232 | 233 | #[test] 234 | fn replace_some_stuff() { 235 | let mut d = Data::new(b"foo bar baz"); 236 | d.replace_range(4..7, b"lol").unwrap(); 237 | assert_eq!("foo lol baz", str(&d.to_vec())); 238 | } 239 | 240 | #[test] 241 | fn replace_a_single_char() { 242 | let mut d = Data::new(b"let y = true;"); 243 | d.replace_range(4..5, b"mut y").unwrap(); 244 | assert_eq!("let mut y = true;", str(&d.to_vec())); 245 | } 246 | 247 | #[test] 248 | fn replace_multiple_lines() { 249 | let mut d = Data::new(b"lorem\nipsum\ndolor"); 250 | 251 | d.replace_range(6..11, b"lol").unwrap(); 252 | assert_eq!("lorem\nlol\ndolor", str(&d.to_vec())); 253 | 254 | d.replace_range(12..17, b"lol").unwrap(); 255 | assert_eq!("lorem\nlol\nlol", str(&d.to_vec())); 256 | } 257 | 258 | #[test] 259 | fn replace_multiple_lines_with_insert_only() { 260 | let mut d = Data::new(b"foo!"); 261 | 262 | d.replace_range(3..3, b"bar").unwrap(); 263 | assert_eq!("foobar!", str(&d.to_vec())); 264 | 265 | d.replace_range(0..3, b"baz").unwrap(); 266 | assert_eq!("bazbar!", str(&d.to_vec())); 267 | 268 | d.replace_range(3..4, b"?").unwrap(); 269 | assert_eq!("bazbar?", str(&d.to_vec())); 270 | } 271 | 272 | #[test] 273 | fn replace_invalid_range() { 274 | let mut d = Data::new(b"foo!"); 275 | 276 | assert!(d.replace_range(2..1, b"bar").is_err()); 277 | assert!(d.replace_range(0..3, b"bar").is_ok()); 278 | } 279 | 280 | #[test] 281 | fn empty_to_vec_roundtrip() { 282 | let s = ""; 283 | assert_eq!(s.as_bytes(), Data::new(s.as_bytes()).to_vec().as_slice()); 284 | } 285 | 286 | #[test] 287 | #[should_panic(expected = "Cannot replace slice of data that was already replaced")] 288 | fn replace_overlapping_stuff_errs() { 289 | let mut d = Data::new(b"foo bar baz"); 290 | 291 | d.replace_range(4..7, b"lol").unwrap(); 292 | assert_eq!("foo lol baz", str(&d.to_vec())); 293 | 294 | d.replace_range(4..7, b"lol2").unwrap(); 295 | } 296 | 297 | #[test] 298 | #[should_panic(expected = "original data is only 3 byte long")] 299 | fn broken_replacements() { 300 | let mut d = Data::new(b"foo"); 301 | d.replace_range(4..8, b"lol").unwrap(); 302 | } 303 | 304 | #[test] 305 | fn replace_same_twice() { 306 | let mut d = Data::new(b"foo"); 307 | d.replace_range(0..1, b"b").unwrap(); 308 | d.replace_range(0..1, b"b").unwrap(); 309 | assert_eq!("boo", str(&d.to_vec())); 310 | } 311 | 312 | proptest! { 313 | #[test] 314 | #[ignore] 315 | fn new_to_vec_roundtrip(ref s in "\\PC*") { 316 | assert_eq!(s.as_bytes(), Data::new(s.as_bytes()).to_vec().as_slice()); 317 | } 318 | 319 | #[test] 320 | #[ignore] 321 | fn replace_random_chunks( 322 | ref data in "\\PC*", 323 | ref replacements in prop::collection::vec( 324 | (any::<::std::ops::Range>(), any::>()), 325 | 1..1337, 326 | ) 327 | ) { 328 | let mut d = Data::new(data.as_bytes()); 329 | for &(ref range, ref bytes) in replacements { 330 | let _ = d.replace_range(range.clone(), bytes); 331 | } 332 | } 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /tests/edge-cases/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "`main` function not found in crate `empty`", 3 | "code": { 4 | "code": "E0601", 5 | "explanation": "No `main` function was found in a binary crate. To fix this error, add a\n`main` function. For example:\n\n```\nfn main() {\n // Your program will start here.\n println!(\"Hello world!\");\n}\n```\n\nIf you don't know the basics of Rust, you can go look to the Rust Book to get\nstarted: https://doc.rust-lang.org/book/\n" 6 | }, 7 | "level": "error", 8 | "spans": [ 9 | { 10 | "file_name": "empty.rs", 11 | "byte_start": 0, 12 | "byte_end": 0, 13 | "line_start": 0, 14 | "line_end": 0, 15 | "column_start": 1, 16 | "column_end": 1, 17 | "is_primary": true, 18 | "text": [ 19 | { 20 | "text": "", 21 | "highlight_start": 1, 22 | "highlight_end": 1 23 | } 24 | ], 25 | "label": null, 26 | "suggested_replacement": null, 27 | "suggestion_applicability": null, 28 | "expansion": null 29 | } 30 | ], 31 | "children": [ 32 | { 33 | "message": "consider adding a `main` function to `empty.rs`", 34 | "code": null, 35 | "level": "note", 36 | "spans": [], 37 | "children": [], 38 | "rendered": null 39 | } 40 | ], 41 | "rendered": "error[E0601]: `main` function not found in crate `empty`\n |\n = note: consider adding a `main` function to `empty.rs`\n\n" 42 | } 43 | -------------------------------------------------------------------------------- /tests/edge-cases/empty.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rustfix/c433369f4aa3fe67b1399e2f6e3c4aeefc3144b7/tests/edge-cases/empty.rs -------------------------------------------------------------------------------- /tests/edge-cases/indented_whitespace.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "non-ASCII whitespace symbol '\\u{a0}' is not skipped", 3 | "code": null, 4 | "level": "warning", 5 | "spans": 6 | [ 7 | { 8 | "file_name": "lib.rs", 9 | "byte_start": 26, 10 | "byte_end": 28, 11 | "line_start": 2, 12 | "line_end": 2, 13 | "column_start": 1, 14 | "column_end": 2, 15 | "is_primary": false, 16 | "text": 17 | [ 18 | { 19 | "text": " indented\";", 20 | "highlight_start": 1, 21 | "highlight_end": 2 22 | } 23 | ], 24 | "label": "non-ASCII whitespace symbol '\\u{a0}' is not skipped", 25 | "suggested_replacement": null, 26 | "suggestion_applicability": null, 27 | "expansion": null 28 | }, 29 | { 30 | "file_name": "lib.rs", 31 | "byte_start": 24, 32 | "byte_end": 28, 33 | "line_start": 1, 34 | "line_end": 2, 35 | "column_start": 25, 36 | "column_end": 2, 37 | "is_primary": true, 38 | "text": 39 | [ 40 | { 41 | "text": "pub static FOO: &str = \"\\", 42 | "highlight_start": 25, 43 | "highlight_end": 26 44 | }, 45 | { 46 | "text": " indented\";", 47 | "highlight_start": 1, 48 | "highlight_end": 2 49 | } 50 | ], 51 | "label": null, 52 | "suggested_replacement": null, 53 | "suggestion_applicability": null, 54 | "expansion": null 55 | } 56 | ], 57 | "children": 58 | [], 59 | "rendered": "warning: non-ASCII whitespace symbol '\\u{a0}' is not skipped\n --> lib.rs:1:25\n |\n1 | pub static FOO: &str = \"\\\n | _________________________^\n2 | |  indented\";\n | | ^ non-ASCII whitespace symbol '\\u{a0}' is not skipped\n | |_|\n | \n\n" 60 | } 61 | -------------------------------------------------------------------------------- /tests/edge-cases/no_main.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "`main` function not found in crate `no_main`", 3 | "code": { 4 | "code": "E0601", 5 | "explanation": "No `main` function was found in a binary crate. To fix this error, add a\n`main` function. For example:\n\n```\nfn main() {\n // Your program will start here.\n println!(\"Hello world!\");\n}\n```\n\nIf you don't know the basics of Rust, you can go look to the Rust Book to get\nstarted: https://doc.rust-lang.org/book/\n" 6 | }, 7 | "level": "error", 8 | "spans": [ 9 | { 10 | "file_name": "no_main.rs", 11 | "byte_start": 26, 12 | "byte_end": 26, 13 | "line_start": 1, 14 | "line_end": 1, 15 | "column_start": 27, 16 | "column_end": 27, 17 | "is_primary": true, 18 | "text": [ 19 | { 20 | "text": "// This file has no main.", 21 | "highlight_start": 27, 22 | "highlight_end": 27 23 | } 24 | ], 25 | "label": "consider adding a `main` function to `no_main.rs`", 26 | "suggested_replacement": null, 27 | "suggestion_applicability": null, 28 | "expansion": null 29 | } 30 | ], 31 | "children": [], 32 | "rendered": "error[E0601]: `main` function not found in crate `no_main`\n --> no_main.rs:1:27\n |\n1 | // This file has no main.\n | ^ consider adding a `main` function to `no_main.rs`\n\n" 33 | } 34 | -------------------------------------------------------------------------------- /tests/edge-cases/no_main.rs: -------------------------------------------------------------------------------- 1 | // This file has no main. 2 | -------------------------------------------------------------------------------- /tests/edge-cases/out_of_bounds.recorded.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "unterminated double quote string", 3 | "code": null, 4 | "level": "error", 5 | "spans": [ 6 | { 7 | "file_name": "./tests/everything/tab_2.rs", 8 | "byte_start": 485, 9 | "byte_end": 526, 10 | "line_start": 12, 11 | "line_end": 13, 12 | "column_start": 7, 13 | "column_end": 3, 14 | "is_primary": true, 15 | "text": [ 16 | { 17 | "text": " \"\"\"; //~ ERROR unterminated double quote", 18 | "highlight_start": 7, 19 | "highlight_end": 45 20 | }, 21 | { 22 | "text": "}", 23 | "highlight_start": 1, 24 | "highlight_end": 3 25 | } 26 | ], 27 | "label": null, 28 | "suggested_replacement": null, 29 | "suggestion_applicability": null, 30 | "expansion": null 31 | } 32 | ], 33 | "children": [], 34 | "rendered": "error: unterminated double quote string\n --> ./tests/everything/tab_2.rs:12:7\n |\n12 | \"\"\"; //~ ERROR unterminated double quote\n | _______^\n13 | | }\n | |__^\n\n" 35 | } 36 | { 37 | "message": "aborting due to previous error", 38 | "code": null, 39 | "level": "error", 40 | "spans": [], 41 | "children": [], 42 | "rendered": "error: aborting due to previous error\n\n" 43 | } 44 | -------------------------------------------------------------------------------- /tests/edge-cases/utf8_idents.recorded.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "expected one of `,`, `:`, `=`, or `>`, found `'β`", 3 | "code": null, 4 | "level": "error", 5 | "spans": [ 6 | { 7 | "file_name": "./tests/everything/utf8_idents.rs", 8 | "byte_start": 14, 9 | "byte_end": 14, 10 | "line_start": 2, 11 | "line_end": 2, 12 | "column_start": 6, 13 | "column_end": 6, 14 | "is_primary": false, 15 | "text": [ 16 | { 17 | "text": " γ //~ ERROR non-ascii idents are not fully supported", 18 | "highlight_start": 6, 19 | "highlight_end": 6 20 | } 21 | ], 22 | "label": "expected one of `,`, `:`, `=`, or `>` here", 23 | "suggested_replacement": null, 24 | "suggestion_applicability": null, 25 | "expansion": null 26 | }, 27 | { 28 | "file_name": "./tests/everything/utf8_idents.rs", 29 | "byte_start": 145, 30 | "byte_end": 148, 31 | "line_start": 4, 32 | "line_end": 4, 33 | "column_start": 5, 34 | "column_end": 7, 35 | "is_primary": true, 36 | "text": [ 37 | { 38 | "text": " 'β, //~ ERROR non-ascii idents are not fully supported", 39 | "highlight_start": 5, 40 | "highlight_end": 7 41 | } 42 | ], 43 | "label": "unexpected token", 44 | "suggested_replacement": null, 45 | "suggestion_applicability": null, 46 | "expansion": null 47 | } 48 | ], 49 | "children": [], 50 | "rendered": "error: expected one of `,`, `:`, `=`, or `>`, found `'β`\n --> ./tests/everything/utf8_idents.rs:4:5\n |\n2 | γ //~ ERROR non-ascii idents are not fully supported\n | - expected one of `,`, `:`, `=`, or `>` here\n3 | //~^ WARN type parameter `γ` should have an upper camel case name\n4 | 'β, //~ ERROR non-ascii idents are not fully supported\n | ^^ unexpected token\n\n" 51 | } 52 | { 53 | "message": "aborting due to previous error", 54 | "code": null, 55 | "level": "error", 56 | "spans": [], 57 | "children": [], 58 | "rendered": "error: aborting due to previous error\n\n" 59 | } 60 | -------------------------------------------------------------------------------- /tests/edge_cases.rs: -------------------------------------------------------------------------------- 1 | use rustfix; 2 | use std::collections::HashSet; 3 | use std::fs; 4 | 5 | macro_rules! expect_empty_json_test { 6 | ($name:ident, $file:expr) => { 7 | #[test] 8 | fn $name() { 9 | let json = fs::read_to_string(concat!("./tests/edge-cases/", $file)).unwrap(); 10 | let expected_suggestions = rustfix::get_suggestions_from_json( 11 | &json, 12 | &HashSet::new(), 13 | rustfix::Filter::Everything, 14 | ) 15 | .unwrap(); 16 | assert!(expected_suggestions.is_empty()); 17 | } 18 | }; 19 | } 20 | 21 | expect_empty_json_test! {out_of_bounds_test, "out_of_bounds.recorded.json"} 22 | expect_empty_json_test! {utf8_identifiers_test, "utf8_idents.recorded.json"} 23 | expect_empty_json_test! {empty, "empty.json"} 24 | expect_empty_json_test! {no_main, "no_main.json"} 25 | expect_empty_json_test! {indented_whitespace, "indented_whitespace.json"} 26 | -------------------------------------------------------------------------------- /tests/edition/.gitignore: -------------------------------------------------------------------------------- 1 | *.recorded.json 2 | *.recorded.rs 3 | -------------------------------------------------------------------------------- /tests/everything/.gitignore: -------------------------------------------------------------------------------- 1 | *.recorded.json 2 | *.recorded.rs 3 | -------------------------------------------------------------------------------- /tests/everything/E0178.fixed.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | trait Foo {} 4 | 5 | struct Bar<'a> { 6 | w: &'a (dyn Foo + Send), 7 | } 8 | 9 | fn main() { 10 | } 11 | -------------------------------------------------------------------------------- /tests/everything/E0178.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "expected a path on the left-hand side of `+`, not `&'a Foo`", 3 | "code": { 4 | "code": "E0178", 5 | "explanation": "\nIn types, the `+` type operator has low precedence, so it is often necessary\nto use parentheses.\n\nFor example:\n\n```compile_fail,E0178\ntrait Foo {}\n\nstruct Bar<'a> {\n w: &'a Foo + Copy, // error, use &'a (Foo + Copy)\n x: &'a Foo + 'a, // error, use &'a (Foo + 'a)\n y: &'a mut Foo + 'a, // error, use &'a mut (Foo + 'a)\n z: fn() -> Foo + 'a, // error, use fn() -> (Foo + 'a)\n}\n```\n\nMore details can be found in [RFC 438].\n\n[RFC 438]: https://github.com/rust-lang/rfcs/pull/438\n" 6 | }, 7 | "level": "error", 8 | "spans": [ 9 | { 10 | "file_name": "./tests/everything/E0178.rs", 11 | "byte_start": 60, 12 | "byte_end": 74, 13 | "line_start": 6, 14 | "line_end": 6, 15 | "column_start": 8, 16 | "column_end": 22, 17 | "is_primary": true, 18 | "text": [ 19 | { 20 | "text": " w: &'a Foo + Send,", 21 | "highlight_start": 8, 22 | "highlight_end": 22 23 | } 24 | ], 25 | "label": null, 26 | "suggested_replacement": null, 27 | "expansion": null 28 | } 29 | ], 30 | "children": [ 31 | { 32 | "message": "try adding parentheses", 33 | "code": null, 34 | "level": "help", 35 | "spans": [ 36 | { 37 | "file_name": "./tests/everything/E0178.rs", 38 | "byte_start": 60, 39 | "byte_end": 74, 40 | "line_start": 6, 41 | "line_end": 6, 42 | "column_start": 8, 43 | "column_end": 22, 44 | "is_primary": true, 45 | "text": [ 46 | { 47 | "text": " w: &'a Foo + Send,", 48 | "highlight_start": 8, 49 | "highlight_end": 22 50 | } 51 | ], 52 | "label": null, 53 | "suggested_replacement": "&'a (Foo + Send)", 54 | "expansion": null 55 | } 56 | ], 57 | "children": [], 58 | "rendered": null 59 | } 60 | ], 61 | "rendered": "error[E0178]: expected a path on the left-hand side of `+`, not `&'a Foo`\n --> ./tests/everything/E0178.rs:6:8\n |\n6 | w: &'a Foo + Send,\n | ^^^^^^^^^^^^^^ help: try adding parentheses: `&'a (Foo + Send)`\n\nIf you want more information on this error, try using \"rustc --explain E0178\"\n" 62 | } 63 | { 64 | "message": "aborting due to previous error", 65 | "code": null, 66 | "level": "error", 67 | "spans": [], 68 | "children": [], 69 | "rendered": "error: aborting due to previous error\n\n" 70 | } 71 | -------------------------------------------------------------------------------- /tests/everything/E0178.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | trait Foo {} 4 | 5 | struct Bar<'a> { 6 | w: &'a dyn Foo + Send, 7 | } 8 | 9 | fn main() { 10 | } 11 | -------------------------------------------------------------------------------- /tests/everything/closure-immutable-outer-variable.fixed.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | // Point at the captured immutable outer variable 12 | 13 | fn foo(mut f: Box) { 14 | f(); 15 | } 16 | 17 | fn main() { 18 | let mut y = true; 19 | foo(Box::new(move || y = false) as Box<_>); //~ ERROR cannot assign to captured outer variable 20 | } 21 | -------------------------------------------------------------------------------- /tests/everything/closure-immutable-outer-variable.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "cannot assign to captured outer variable in an `FnMut` closure", 3 | "code": { 4 | "code": "E0594", 5 | "explanation": null 6 | }, 7 | "level": "error", 8 | "spans": [ 9 | { 10 | "file_name": "./tests/everything/closure-immutable-outer-variable.rs", 11 | "byte_start": 615, 12 | "byte_end": 624, 13 | "line_start": 19, 14 | "line_end": 19, 15 | "column_start": 26, 16 | "column_end": 35, 17 | "is_primary": true, 18 | "text": [ 19 | { 20 | "text": " foo(Box::new(move || y = false) as Box<_>); //~ ERROR cannot assign to captured outer variable", 21 | "highlight_start": 26, 22 | "highlight_end": 35 23 | } 24 | ], 25 | "label": null, 26 | "suggested_replacement": null, 27 | "expansion": null 28 | } 29 | ], 30 | "children": [ 31 | { 32 | "message": "consider making `y` mutable", 33 | "code": null, 34 | "level": "help", 35 | "spans": [ 36 | { 37 | "file_name": "./tests/everything/closure-immutable-outer-variable.rs", 38 | "byte_start": 580, 39 | "byte_end": 581, 40 | "line_start": 18, 41 | "line_end": 18, 42 | "column_start": 9, 43 | "column_end": 10, 44 | "is_primary": true, 45 | "text": [ 46 | { 47 | "text": " let y = true;", 48 | "highlight_start": 9, 49 | "highlight_end": 10 50 | } 51 | ], 52 | "label": null, 53 | "suggested_replacement": "mut y", 54 | "expansion": null 55 | } 56 | ], 57 | "children": [], 58 | "rendered": null 59 | } 60 | ], 61 | "rendered": "error[E0594]: cannot assign to captured outer variable in an `FnMut` closure\n --> ./tests/everything/closure-immutable-outer-variable.rs:19:26\n |\n18 | let y = true;\n | - help: consider making `y` mutable: `mut y`\n19 | foo(Box::new(move || y = false) as Box<_>); //~ ERROR cannot assign to captured outer variable\n | ^^^^^^^^^\n\nIf you want more information on this error, try using \"rustc --explain E0594\"\n" 62 | } 63 | { 64 | "message": "aborting due to previous error", 65 | "code": null, 66 | "level": "error", 67 | "spans": [], 68 | "children": [], 69 | "rendered": "error: aborting due to previous error\n\n" 70 | } 71 | -------------------------------------------------------------------------------- /tests/everything/closure-immutable-outer-variable.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | // Point at the captured immutable outer variable 12 | 13 | fn foo(mut f: Box) { 14 | f(); 15 | } 16 | 17 | fn main() { 18 | let y = true; 19 | foo(Box::new(move || y = false) as Box<_>); //~ ERROR cannot assign to captured outer variable 20 | } 21 | -------------------------------------------------------------------------------- /tests/everything/handle-insert-only.fixed.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // insert only fix, adds `,` to first match arm 3 | // why doesnt this replace 1 with 1,? 4 | match &Some(3) { 5 | &None => 1, 6 | &Some(x) => x, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /tests/everything/handle-insert-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "expected one of `,`, `.`, `?`, `}`, or an operator, found `=>`", 3 | "code": null, 4 | "level": "error", 5 | "spans": [ 6 | { 7 | "file_name": "./tests/everything/handle-insert-only.rs", 8 | "byte_start": 163, 9 | "byte_end": 165, 10 | "line_start": 6, 11 | "line_end": 6, 12 | "column_start": 18, 13 | "column_end": 20, 14 | "is_primary": true, 15 | "text": [ 16 | { 17 | "text": " &Some(x) => x,", 18 | "highlight_start": 18, 19 | "highlight_end": 20 20 | } 21 | ], 22 | "label": "expected one of `,`, `.`, `?`, `}`, or an operator here", 23 | "suggested_replacement": null, 24 | "expansion": null 25 | } 26 | ], 27 | "children": [ 28 | { 29 | "message": "missing a comma here to end this `match` arm", 30 | "code": null, 31 | "level": "help", 32 | "spans": [ 33 | { 34 | "file_name": "./tests/everything/handle-insert-only.rs", 35 | "byte_start": 145, 36 | "byte_end": 145, 37 | "line_start": 5, 38 | "line_end": 5, 39 | "column_start": 19, 40 | "column_end": 19, 41 | "is_primary": true, 42 | "text": [ 43 | { 44 | "text": " &None => 1", 45 | "highlight_start": 19, 46 | "highlight_end": 19 47 | } 48 | ], 49 | "label": null, 50 | "suggested_replacement": ",", 51 | "suggestion_applicability": "Unspecified", 52 | "expansion": null 53 | } 54 | ], 55 | "children": [], 56 | "rendered": null 57 | } 58 | ], 59 | "rendered": "error: expected one of `,`, `.`, `?`, `}`, or an operator, found `=>`\n --> ./tests/everything/handle-insert-only.rs:6:18\n |\n5 | &None => 1\n | - help: missing a comma here to end this `match` arm\n6 | &Some(x) => x,\n | ^^ expected one of `,`, `.`, `?`, `}`, or an operator here\n\n" 60 | } 61 | { 62 | "message": "aborting due to previous error", 63 | "code": null, 64 | "level": "error", 65 | "spans": [], 66 | "children": [], 67 | "rendered": "error: aborting due to previous error\n\n" 68 | } 69 | -------------------------------------------------------------------------------- /tests/everything/handle-insert-only.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // insert only fix, adds `,` to first match arm 3 | // why doesnt this replace 1 with 1,? 4 | match &Some(3) { 5 | &None => 1 6 | &Some(x) => x, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /tests/everything/lt-generic-comp.fixed.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let x = 5i64; 3 | 4 | if (x as u32) < 4 { 5 | println!("yay"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/everything/lt-generic-comp.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "`<` is interpreted as a start of generic arguments for `u32`, not a comparison", 3 | "code": null, 4 | "level": "error", 5 | "spans": [ 6 | { 7 | "file_name": "./tests/everything/lt-generic-comp.rs", 8 | "byte_start": 49, 9 | "byte_end": 50, 10 | "line_start": 4, 11 | "line_end": 4, 12 | "column_start": 19, 13 | "column_end": 20, 14 | "is_primary": false, 15 | "text": [ 16 | { 17 | "text": " if x as u32 < 4 {", 18 | "highlight_start": 19, 19 | "highlight_end": 20 20 | } 21 | ], 22 | "label": "interpreted as generic arguments", 23 | "suggested_replacement": null, 24 | "expansion": null 25 | }, 26 | { 27 | "file_name": "./tests/everything/lt-generic-comp.rs", 28 | "byte_start": 47, 29 | "byte_end": 48, 30 | "line_start": 4, 31 | "line_end": 4, 32 | "column_start": 17, 33 | "column_end": 18, 34 | "is_primary": true, 35 | "text": [ 36 | { 37 | "text": " if x as u32 < 4 {", 38 | "highlight_start": 17, 39 | "highlight_end": 18 40 | } 41 | ], 42 | "label": "not interpreted as comparison", 43 | "suggested_replacement": null, 44 | "expansion": null 45 | } 46 | ], 47 | "children": [ 48 | { 49 | "message": "try comparing the cast value", 50 | "code": null, 51 | "level": "help", 52 | "spans": [ 53 | { 54 | "file_name": "./tests/everything/lt-generic-comp.rs", 55 | "byte_start": 38, 56 | "byte_end": 46, 57 | "line_start": 4, 58 | "line_end": 4, 59 | "column_start": 8, 60 | "column_end": 16, 61 | "is_primary": true, 62 | "text": [ 63 | { 64 | "text": " if x as u32 < 4 {", 65 | "highlight_start": 8, 66 | "highlight_end": 16 67 | } 68 | ], 69 | "label": null, 70 | "suggested_replacement": "(x as u32)", 71 | "expansion": null 72 | } 73 | ], 74 | "children": [], 75 | "rendered": null 76 | } 77 | ], 78 | "rendered": "error: `<` is interpreted as a start of generic arguments for `u32`, not a comparison\n --> ./tests/everything/lt-generic-comp.rs:4:17\n |\n4 | if x as u32 < 4 {\n | -------- ^ - interpreted as generic arguments\n | | |\n | | not interpreted as comparison\n | help: try comparing the cast value: `(x as u32)`\n\n" 79 | } 80 | { 81 | "message": "aborting due to previous error", 82 | "code": null, 83 | "level": "error", 84 | "spans": [], 85 | "children": [], 86 | "rendered": "error: aborting due to previous error\n\n" 87 | } 88 | -------------------------------------------------------------------------------- /tests/everything/lt-generic-comp.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let x = 5i64; 3 | 4 | if x as u32 < 4 { 5 | println!("yay"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/everything/multiple-solutions.fixed.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashSet}; 2 | 3 | fn main() { 4 | let _: HashSet<()>; 5 | } 6 | -------------------------------------------------------------------------------- /tests/everything/multiple-solutions.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "unused imports: `HashMap`, `VecDeque`", 3 | "code": { 4 | "code": "unused_imports", 5 | "explanation": null 6 | }, 7 | "level": "warning", 8 | "spans": [ 9 | { 10 | "file_name": "src/main.rs", 11 | "byte_start": 23, 12 | "byte_end": 30, 13 | "line_start": 1, 14 | "line_end": 1, 15 | "column_start": 24, 16 | "column_end": 31, 17 | "is_primary": true, 18 | "text": [ 19 | { 20 | "text": "use std::collections::{HashMap, HashSet, VecDeque};", 21 | "highlight_start": 24, 22 | "highlight_end": 31 23 | } 24 | ], 25 | "label": null, 26 | "suggested_replacement": null, 27 | "suggestion_applicability": null, 28 | "expansion": null 29 | }, 30 | { 31 | "file_name": "src/main.rs", 32 | "byte_start": 41, 33 | "byte_end": 49, 34 | "line_start": 1, 35 | "line_end": 1, 36 | "column_start": 42, 37 | "column_end": 50, 38 | "is_primary": true, 39 | "text": [ 40 | { 41 | "text": "use std::collections::{HashMap, HashSet, VecDeque};", 42 | "highlight_start": 42, 43 | "highlight_end": 50 44 | } 45 | ], 46 | "label": null, 47 | "suggested_replacement": null, 48 | "suggestion_applicability": null, 49 | "expansion": null 50 | } 51 | ], 52 | "children": [ 53 | { 54 | "message": "#[warn(unused_imports)] on by default", 55 | "code": null, 56 | "level": "note", 57 | "spans": [], 58 | "children": [], 59 | "rendered": null 60 | }, 61 | { 62 | "message": "remove the unused imports", 63 | "code": null, 64 | "level": "help", 65 | "spans": [ 66 | { 67 | "file_name": "src/main.rs", 68 | "byte_start": 23, 69 | "byte_end": 32, 70 | "line_start": 1, 71 | "line_end": 1, 72 | "column_start": 24, 73 | "column_end": 33, 74 | "is_primary": true, 75 | "text": [ 76 | { 77 | "text": "use std::collections::{HashMap, HashSet, VecDeque};", 78 | "highlight_start": 24, 79 | "highlight_end": 33 80 | } 81 | ], 82 | "label": null, 83 | "suggested_replacement": "", 84 | "suggestion_applicability": "MachineApplicable", 85 | "expansion": null 86 | }, 87 | { 88 | "file_name": "src/main.rs", 89 | "byte_start": 39, 90 | "byte_end": 49, 91 | "line_start": 1, 92 | "line_end": 1, 93 | "column_start": 40, 94 | "column_end": 50, 95 | "is_primary": true, 96 | "text": [ 97 | { 98 | "text": "use std::collections::{HashMap, HashSet, VecDeque};", 99 | "highlight_start": 40, 100 | "highlight_end": 50 101 | } 102 | ], 103 | "label": null, 104 | "suggested_replacement": "", 105 | "suggestion_applicability": "MachineApplicable", 106 | "expansion": null 107 | } 108 | ], 109 | "children": [], 110 | "rendered": null 111 | } 112 | ], 113 | "rendered": "warning: unused imports: `HashMap`, `VecDeque`\n --> src/main.rs:1:24\n |\n1 | use std::collections::{HashMap, HashSet, VecDeque};\n | ^^^^^^^ ^^^^^^^^\n |\n = note: #[warn(unused_imports)] on by default\nhelp: remove the unused imports\n |\n1 | use std::collections::{HashSet};\n | -- --\n\n" 114 | } 115 | -------------------------------------------------------------------------------- /tests/everything/multiple-solutions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet, VecDeque}; 2 | 3 | fn main() { 4 | let _: HashSet<()>; 5 | } 6 | -------------------------------------------------------------------------------- /tests/everything/replace-only-one-char.fixed.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let _x = 42; 3 | } 4 | -------------------------------------------------------------------------------- /tests/everything/replace-only-one-char.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "unused variable: `x`", 3 | "code": { 4 | "code": "unused_variables", 5 | "explanation": null 6 | }, 7 | "level": "warning", 8 | "spans": [ 9 | { 10 | "file_name": "replace-only-one-char.rs", 11 | "byte_start": 20, 12 | "byte_end": 21, 13 | "line_start": 2, 14 | "line_end": 2, 15 | "column_start": 9, 16 | "column_end": 10, 17 | "is_primary": true, 18 | "text": [ 19 | { 20 | "text": " let x = 42;", 21 | "highlight_start": 9, 22 | "highlight_end": 10 23 | } 24 | ], 25 | "label": null, 26 | "suggested_replacement": null, 27 | "expansion": null 28 | } 29 | ], 30 | "children": [ 31 | { 32 | "message": "#[warn(unused_variables)] on by default", 33 | "code": null, 34 | "level": "note", 35 | "spans": [], 36 | "children": [], 37 | "rendered": null 38 | }, 39 | { 40 | "message": "consider using `_x` instead", 41 | "code": null, 42 | "level": "help", 43 | "spans": [ 44 | { 45 | "file_name": "replace-only-one-char.rs", 46 | "byte_start": 20, 47 | "byte_end": 21, 48 | "line_start": 2, 49 | "line_end": 2, 50 | "column_start": 9, 51 | "column_end": 10, 52 | "is_primary": true, 53 | "text": [ 54 | { 55 | "text": " let x = 42;", 56 | "highlight_start": 9, 57 | "highlight_end": 10 58 | } 59 | ], 60 | "label": null, 61 | "suggested_replacement": "_x", 62 | "expansion": null 63 | } 64 | ], 65 | "children": [], 66 | "rendered": null 67 | } 68 | ], 69 | "rendered": "warning: unused variable: `x`\n --> replace-only-one-char.rs:2:9\n |\n2 | let x = 42;\n | ^ help: consider using `_x` instead\n |\n = note: #[warn(unused_variables)] on by default\n\n" 70 | } 71 | -------------------------------------------------------------------------------- /tests/everything/replace-only-one-char.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let x = 42; 3 | } 4 | -------------------------------------------------------------------------------- /tests/everything/str-lit-type-mismatch.fixed.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | 12 | fn main() { 13 | let x: &[u8] = b"foo"; //~ ERROR mismatched types 14 | let y: &[u8; 4] = b"baaa"; //~ ERROR mismatched types 15 | let z: &str = "foo"; //~ ERROR mismatched types 16 | } 17 | -------------------------------------------------------------------------------- /tests/everything/str-lit-type-mismatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "mismatched types", 3 | "code": { 4 | "code": "E0308", 5 | "explanation": "\nThis error occurs when the compiler was unable to infer the concrete type of a\nvariable. It can occur for several cases, the most common of which is a\nmismatch in the expected type that the compiler inferred for a variable's\ninitializing expression, and the actual type explicitly assigned to the\nvariable.\n\nFor example:\n\n```compile_fail,E0308\nlet x: i32 = \"I am not a number!\";\n// ~~~ ~~~~~~~~~~~~~~~~~~~~\n// | |\n// | initializing expression;\n// | compiler infers type `&str`\n// |\n// type `i32` assigned to variable `x`\n```\n" 6 | }, 7 | "level": "error", 8 | "spans": [ 9 | { 10 | "file_name": "./tests/everything/str-lit-type-mismatch.rs", 11 | "byte_start": 499, 12 | "byte_end": 504, 13 | "line_start": 13, 14 | "line_end": 13, 15 | "column_start": 20, 16 | "column_end": 25, 17 | "is_primary": true, 18 | "text": [ 19 | { 20 | "text": " let x: &[u8] = \"foo\"; //~ ERROR mismatched types", 21 | "highlight_start": 20, 22 | "highlight_end": 25 23 | } 24 | ], 25 | "label": "expected slice, found str", 26 | "suggested_replacement": null, 27 | "expansion": null 28 | } 29 | ], 30 | "children": [ 31 | { 32 | "message": "expected type `&[u8]`\n found type `&'static str`", 33 | "code": null, 34 | "level": "note", 35 | "spans": [], 36 | "children": [], 37 | "rendered": null 38 | }, 39 | { 40 | "message": "consider adding a leading `b`", 41 | "code": null, 42 | "level": "help", 43 | "spans": [ 44 | { 45 | "file_name": "./tests/everything/str-lit-type-mismatch.rs", 46 | "byte_start": 499, 47 | "byte_end": 504, 48 | "line_start": 13, 49 | "line_end": 13, 50 | "column_start": 20, 51 | "column_end": 25, 52 | "is_primary": true, 53 | "text": [ 54 | { 55 | "text": " let x: &[u8] = \"foo\"; //~ ERROR mismatched types", 56 | "highlight_start": 20, 57 | "highlight_end": 25 58 | } 59 | ], 60 | "label": null, 61 | "suggested_replacement": "b\"foo\"", 62 | "expansion": null 63 | } 64 | ], 65 | "children": [], 66 | "rendered": null 67 | } 68 | ], 69 | "rendered": "error[E0308]: mismatched types\n --> ./tests/everything/str-lit-type-mismatch.rs:13:20\n |\n13 | let x: &[u8] = \"foo\"; //~ ERROR mismatched types\n | ^^^^^\n | |\n | expected slice, found str\n | help: consider adding a leading `b`: `b\"foo\"`\n |\n = note: expected type `&[u8]`\n found type `&'static str`\n\nIf you want more information on this error, try using \"rustc --explain E0308\"\n" 70 | } 71 | { 72 | "message": "mismatched types", 73 | "code": { 74 | "code": "E0308", 75 | "explanation": "\nThis error occurs when the compiler was unable to infer the concrete type of a\nvariable. It can occur for several cases, the most common of which is a\nmismatch in the expected type that the compiler inferred for a variable's\ninitializing expression, and the actual type explicitly assigned to the\nvariable.\n\nFor example:\n\n```compile_fail,E0308\nlet x: i32 = \"I am not a number!\";\n// ~~~ ~~~~~~~~~~~~~~~~~~~~\n// | |\n// | initializing expression;\n// | compiler infers type `&str`\n// |\n// type `i32` assigned to variable `x`\n```\n" 76 | }, 77 | "level": "error", 78 | "spans": [ 79 | { 80 | "file_name": "./tests/everything/str-lit-type-mismatch.rs", 81 | "byte_start": 555, 82 | "byte_end": 561, 83 | "line_start": 14, 84 | "line_end": 14, 85 | "column_start": 23, 86 | "column_end": 29, 87 | "is_primary": true, 88 | "text": [ 89 | { 90 | "text": " let y: &[u8; 4] = \"baaa\"; //~ ERROR mismatched types", 91 | "highlight_start": 23, 92 | "highlight_end": 29 93 | } 94 | ], 95 | "label": "expected array of 4 elements, found str", 96 | "suggested_replacement": null, 97 | "expansion": null 98 | } 99 | ], 100 | "children": [ 101 | { 102 | "message": "expected type `&[u8; 4]`\n found type `&'static str`", 103 | "code": null, 104 | "level": "note", 105 | "spans": [], 106 | "children": [], 107 | "rendered": null 108 | }, 109 | { 110 | "message": "consider adding a leading `b`", 111 | "code": null, 112 | "level": "help", 113 | "spans": [ 114 | { 115 | "file_name": "./tests/everything/str-lit-type-mismatch.rs", 116 | "byte_start": 555, 117 | "byte_end": 561, 118 | "line_start": 14, 119 | "line_end": 14, 120 | "column_start": 23, 121 | "column_end": 29, 122 | "is_primary": true, 123 | "text": [ 124 | { 125 | "text": " let y: &[u8; 4] = \"baaa\"; //~ ERROR mismatched types", 126 | "highlight_start": 23, 127 | "highlight_end": 29 128 | } 129 | ], 130 | "label": null, 131 | "suggested_replacement": "b\"baaa\"", 132 | "expansion": null 133 | } 134 | ], 135 | "children": [], 136 | "rendered": null 137 | } 138 | ], 139 | "rendered": "error[E0308]: mismatched types\n --> ./tests/everything/str-lit-type-mismatch.rs:14:23\n |\n14 | let y: &[u8; 4] = \"baaa\"; //~ ERROR mismatched types\n | ^^^^^^\n | |\n | expected array of 4 elements, found str\n | help: consider adding a leading `b`: `b\"baaa\"`\n |\n = note: expected type `&[u8; 4]`\n found type `&'static str`\n\nIf you want more information on this error, try using \"rustc --explain E0308\"\n" 140 | } 141 | { 142 | "message": "mismatched types", 143 | "code": { 144 | "code": "E0308", 145 | "explanation": "\nThis error occurs when the compiler was unable to infer the concrete type of a\nvariable. It can occur for several cases, the most common of which is a\nmismatch in the expected type that the compiler inferred for a variable's\ninitializing expression, and the actual type explicitly assigned to the\nvariable.\n\nFor example:\n\n```compile_fail,E0308\nlet x: i32 = \"I am not a number!\";\n// ~~~ ~~~~~~~~~~~~~~~~~~~~\n// | |\n// | initializing expression;\n// | compiler infers type `&str`\n// |\n// type `i32` assigned to variable `x`\n```\n" 146 | }, 147 | "level": "error", 148 | "spans": [ 149 | { 150 | "file_name": "./tests/everything/str-lit-type-mismatch.rs", 151 | "byte_start": 608, 152 | "byte_end": 614, 153 | "line_start": 15, 154 | "line_end": 15, 155 | "column_start": 19, 156 | "column_end": 25, 157 | "is_primary": true, 158 | "text": [ 159 | { 160 | "text": " let z: &str = b\"foo\"; //~ ERROR mismatched types", 161 | "highlight_start": 19, 162 | "highlight_end": 25 163 | } 164 | ], 165 | "label": "expected str, found array of 3 elements", 166 | "suggested_replacement": null, 167 | "expansion": null 168 | } 169 | ], 170 | "children": [ 171 | { 172 | "message": "expected type `&str`\n found type `&'static [u8; 3]`", 173 | "code": null, 174 | "level": "note", 175 | "spans": [], 176 | "children": [], 177 | "rendered": null 178 | }, 179 | { 180 | "message": "consider removing the leading `b`", 181 | "code": null, 182 | "level": "help", 183 | "spans": [ 184 | { 185 | "file_name": "./tests/everything/str-lit-type-mismatch.rs", 186 | "byte_start": 608, 187 | "byte_end": 614, 188 | "line_start": 15, 189 | "line_end": 15, 190 | "column_start": 19, 191 | "column_end": 25, 192 | "is_primary": true, 193 | "text": [ 194 | { 195 | "text": " let z: &str = b\"foo\"; //~ ERROR mismatched types", 196 | "highlight_start": 19, 197 | "highlight_end": 25 198 | } 199 | ], 200 | "label": null, 201 | "suggested_replacement": "\"foo\"", 202 | "expansion": null 203 | } 204 | ], 205 | "children": [], 206 | "rendered": null 207 | } 208 | ], 209 | "rendered": "error[E0308]: mismatched types\n --> ./tests/everything/str-lit-type-mismatch.rs:15:19\n |\n15 | let z: &str = b\"foo\"; //~ ERROR mismatched types\n | ^^^^^^\n | |\n | expected str, found array of 3 elements\n | help: consider removing the leading `b`: `\"foo\"`\n |\n = note: expected type `&str`\n found type `&'static [u8; 3]`\n\nIf you want more information on this error, try using \"rustc --explain E0308\"\n" 210 | } 211 | { 212 | "message": "aborting due to 3 previous errors", 213 | "code": null, 214 | "level": "error", 215 | "spans": [], 216 | "children": [], 217 | "rendered": "error: aborting due to 3 previous errors\n\n" 218 | } 219 | -------------------------------------------------------------------------------- /tests/everything/str-lit-type-mismatch.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | 12 | fn main() { 13 | let x: &[u8] = "foo"; //~ ERROR mismatched types 14 | let y: &[u8; 4] = "baaa"; //~ ERROR mismatched types 15 | let z: &str = b"foo"; //~ ERROR mismatched types 16 | } 17 | -------------------------------------------------------------------------------- /tests/parse_and_replace.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(windows))] // TODO: should fix these tests on Windows 2 | 3 | use anyhow::{anyhow, ensure, Context, Error}; 4 | use log::{debug, info, warn}; 5 | use rustfix::apply_suggestions; 6 | use std::collections::HashSet; 7 | use std::env; 8 | use std::ffi::OsString; 9 | use std::fs; 10 | use std::path::{Path, PathBuf}; 11 | use std::process::Output; 12 | use tempfile::tempdir; 13 | 14 | mod fixmode { 15 | pub const EVERYTHING: &str = "yolo"; 16 | pub const EDITION: &str = "edition"; 17 | } 18 | 19 | mod settings { 20 | // can be set as env var to debug 21 | pub const CHECK_JSON: &str = "RUSTFIX_TEST_CHECK_JSON"; 22 | pub const RECORD_JSON: &str = "RUSTFIX_TEST_RECORD_JSON"; 23 | pub const RECORD_FIXED_RUST: &str = "RUSTFIX_TEST_RECORD_FIXED_RUST"; 24 | } 25 | 26 | fn compile(file: &Path, mode: &str) -> Result { 27 | let tmp = tempdir()?; 28 | 29 | let mut args: Vec = vec![ 30 | file.into(), 31 | "--error-format=pretty-json".into(), 32 | "-Zunstable-options".into(), 33 | "--emit=metadata".into(), 34 | "--crate-name=rustfix_test".into(), 35 | "--out-dir".into(), 36 | tmp.path().into(), 37 | ]; 38 | 39 | if mode == fixmode::EDITION { 40 | args.push("--edition=2018".into()); 41 | } 42 | 43 | let res = duct::cmd(env::var_os("RUSTC").unwrap_or("rustc".into()), &args) 44 | .env("CLIPPY_DISABLE_DOCS_LINKS", "true") 45 | .env_remove("RUST_LOG") 46 | .stdout_capture() 47 | .stderr_capture() 48 | .unchecked() 49 | .run()?; 50 | 51 | Ok(res) 52 | } 53 | 54 | fn compile_and_get_json_errors(file: &Path, mode: &str) -> Result { 55 | let res = compile(file, mode)?; 56 | let stderr = String::from_utf8(res.stderr)?; 57 | if stderr.contains("is only accepted on the nightly compiler") { 58 | panic!("rustfix tests require a nightly compiler"); 59 | } 60 | 61 | match res.status.code() { 62 | Some(0) | Some(1) | Some(101) => Ok(stderr), 63 | _ => Err(anyhow!( 64 | "failed with status {:?}: {}", 65 | res.status.code(), 66 | stderr 67 | )), 68 | } 69 | } 70 | 71 | fn compiles_without_errors(file: &Path, mode: &str) -> Result<(), Error> { 72 | let res = compile(file, mode)?; 73 | 74 | match res.status.code() { 75 | Some(0) => Ok(()), 76 | _ => { 77 | info!( 78 | "file {:?} failed to compile:\n{}", 79 | file, 80 | String::from_utf8(res.stderr)? 81 | ); 82 | Err(anyhow!( 83 | "failed with status {:?} (`env RUST_LOG=parse_and_replace=info` for more info)", 84 | res.status.code(), 85 | )) 86 | } 87 | } 88 | } 89 | 90 | fn read_file(path: &Path) -> Result { 91 | use std::io::Read; 92 | 93 | let mut buffer = String::new(); 94 | let mut file = fs::File::open(path)?; 95 | file.read_to_string(&mut buffer)?; 96 | Ok(buffer) 97 | } 98 | 99 | fn diff(expected: &str, actual: &str) -> String { 100 | use similar::text::{ChangeTag, TextDiff}; 101 | use std::fmt::Write; 102 | 103 | let mut res = String::new(); 104 | let diff = TextDiff::from_lines(expected.trim(), actual.trim()); 105 | 106 | let mut different = false; 107 | for op in diff.ops() { 108 | for change in diff.iter_changes(op) { 109 | let prefix = match change.tag() { 110 | ChangeTag::Equal => continue, 111 | ChangeTag::Insert => "+", 112 | ChangeTag::Delete => "-", 113 | }; 114 | if !different { 115 | write!( 116 | &mut res, 117 | "differences found (+ == actual, - == expected):\n" 118 | ) 119 | .unwrap(); 120 | different = true; 121 | } 122 | write!(&mut res, "{} {}", prefix, change.value()).unwrap(); 123 | } 124 | } 125 | if different { 126 | write!(&mut res, "").unwrap(); 127 | } 128 | 129 | res 130 | } 131 | 132 | fn test_rustfix_with_file>(file: P, mode: &str) -> Result<(), Error> { 133 | let file: &Path = file.as_ref(); 134 | let json_file = file.with_extension("json"); 135 | let fixed_file = file.with_extension("fixed.rs"); 136 | 137 | let filter_suggestions = if mode == fixmode::EVERYTHING { 138 | rustfix::Filter::Everything 139 | } else { 140 | rustfix::Filter::MachineApplicableOnly 141 | }; 142 | 143 | debug!("next up: {:?}", file); 144 | let code = read_file(file).context(format!("could not read {}", file.display()))?; 145 | let errors = compile_and_get_json_errors(file, mode) 146 | .context(format!("could compile {}", file.display()))?; 147 | let suggestions = 148 | rustfix::get_suggestions_from_json(&errors, &HashSet::new(), filter_suggestions) 149 | .context("could not load suggestions")?; 150 | 151 | if std::env::var(settings::RECORD_JSON).is_ok() { 152 | use std::io::Write; 153 | let mut recorded_json = fs::File::create(&file.with_extension("recorded.json")).context( 154 | format!("could not create recorded.json for {}", file.display()), 155 | )?; 156 | recorded_json.write_all(errors.as_bytes())?; 157 | } 158 | 159 | if std::env::var(settings::CHECK_JSON).is_ok() { 160 | let expected_json = read_file(&json_file).context(format!( 161 | "could not load json fixtures for {}", 162 | file.display() 163 | ))?; 164 | let expected_suggestions = 165 | rustfix::get_suggestions_from_json(&expected_json, &HashSet::new(), filter_suggestions) 166 | .context("could not load expected suggestions")?; 167 | 168 | ensure!( 169 | expected_suggestions == suggestions, 170 | "got unexpected suggestions from clippy:\n{}", 171 | diff( 172 | &format!("{:?}", expected_suggestions), 173 | &format!("{:?}", suggestions) 174 | ) 175 | ); 176 | } 177 | 178 | let fixed = apply_suggestions(&code, &suggestions) 179 | .context(format!("could not apply suggestions to {}", file.display()))?; 180 | 181 | if std::env::var(settings::RECORD_FIXED_RUST).is_ok() { 182 | use std::io::Write; 183 | let mut recorded_rust = fs::File::create(&file.with_extension("recorded.rs"))?; 184 | recorded_rust.write_all(fixed.as_bytes())?; 185 | } 186 | 187 | let expected_fixed = 188 | read_file(&fixed_file).context(format!("could read fixed file for {}", file.display()))?; 189 | ensure!( 190 | fixed.trim() == expected_fixed.trim(), 191 | "file {} doesn't look fixed:\n{}", 192 | file.display(), 193 | diff(fixed.trim(), expected_fixed.trim()) 194 | ); 195 | 196 | compiles_without_errors(&fixed_file, mode)?; 197 | 198 | Ok(()) 199 | } 200 | 201 | fn get_fixture_files(p: &str) -> Result, Error> { 202 | Ok(fs::read_dir(&p)? 203 | .into_iter() 204 | .map(|e| e.unwrap().path()) 205 | .filter(|p| p.is_file()) 206 | .filter(|p| { 207 | let x = p.to_string_lossy(); 208 | x.ends_with(".rs") && !x.ends_with(".fixed.rs") && !x.ends_with(".recorded.rs") 209 | }) 210 | .collect()) 211 | } 212 | 213 | fn assert_fixtures(dir: &str, mode: &str) { 214 | let files = get_fixture_files(&dir) 215 | .context(format!("couldn't load dir `{}`", dir)) 216 | .unwrap(); 217 | let mut failures = 0; 218 | 219 | for file in &files { 220 | if let Err(err) = test_rustfix_with_file(file, mode) { 221 | println!("failed: {}", file.display()); 222 | warn!("{:?}", err); 223 | failures += 1; 224 | } 225 | info!("passed: {:?}", file); 226 | } 227 | 228 | if failures > 0 { 229 | panic!( 230 | "{} out of {} fixture asserts failed\n\ 231 | (run with `env RUST_LOG=parse_and_replace=info` to get more details)", 232 | failures, 233 | files.len(), 234 | ); 235 | } 236 | } 237 | 238 | #[test] 239 | fn everything() { 240 | let _ = env_logger::try_init(); 241 | assert_fixtures("./tests/everything", fixmode::EVERYTHING); 242 | } 243 | 244 | #[test] 245 | #[ignore = "Requires custom rustc build"] 246 | fn edition() { 247 | let _ = env_logger::try_init(); 248 | assert_fixtures("./tests/edition", fixmode::EDITION); 249 | } 250 | --------------------------------------------------------------------------------