mirror of
https://github.com/rust-lang/cargo
synced 2024-08-27 19:29:21 +00:00
Auto merge of #13005 - ehuss:rustfix, r=weihanglo
Migrate rustfix to the cargo repo This migrates the `rustfix` crate from https://github.com/rust-lang/rustfix/ to the cargo repo. The cargo team has been responsible for the client-side of `cargo fix`, and it can make it easier to maintain with all our tooling and tracking here. This crate is used by some external parties (like the compiler), so it will need to be maintained like an "ecosystem" package, but hopefully there shouldn't be any outside requirements (I haven't seen any in several years). After merging, I'll follow up with some things to address in the future, such as: - Migrating issues from the other repo. - Opening new issues for some cleanup tasks, such as adding documentation, fixing the `#[ignore]` annotations, fixing testing on windows, maybe migrating the test code to use different dependencies, various code cleanup. - Archiving the repo.
This commit is contained in:
commit
eed300d5e8
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -2885,14 +2885,16 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|||
|
||||
[[package]]
|
||||
name = "rustfix"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecd2853d9e26988467753bd9912c3a126f642d05d229a4b53f5752ee36c56481"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
"proptest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -75,7 +75,7 @@ pulldown-cmark = { version = "0.9.3", default-features = false }
|
|||
rand = "0.8.5"
|
||||
regex = "1.9.3"
|
||||
rusqlite = { version = "0.29.0", features = ["bundled"] }
|
||||
rustfix = "0.6.1"
|
||||
rustfix = { version = "0.6.2", path = "crates/rustfix" }
|
||||
same-file = "1.0.6"
|
||||
security-framework = "2.9.2"
|
||||
semver = { version = "1.0.20", features = ["serde"] }
|
||||
|
|
33
crates/rustfix/Cargo.toml
Normal file
33
crates/rustfix/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "rustfix"
|
||||
version = "0.6.2"
|
||||
authors = [
|
||||
"Pascal Hertleif <killercup@gmail.com>",
|
||||
"Oliver Schneider <oli-obk@users.noreply.github.com>",
|
||||
]
|
||||
rust-version = "1.70.0" # MSRV:3
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage = "https://github.com/rust-lang/cargo"
|
||||
repository = "https://github.com/rust-lang/cargo"
|
||||
description = "Automatically apply the suggestions made by rustc"
|
||||
documentation = "https://docs.rs/rustfix"
|
||||
exclude = [
|
||||
"examples/*",
|
||||
"tests/*",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
similar = "2.2.1"
|
||||
tempfile.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
79
crates/rustfix/Changelog.md
Normal file
79
crates/rustfix/Changelog.md
Normal file
|
@ -0,0 +1,79 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.6] - 2019-07-16
|
||||
|
||||
### Changed
|
||||
|
||||
Internal changes:
|
||||
|
||||
- Change example to automatically determine filename
|
||||
- Migrate to Rust 2018
|
||||
- use `derive` feature over `serde_derive` crate
|
||||
|
||||
## [0.4.5] - 2019-03-26
|
||||
|
||||
### Added
|
||||
|
||||
- Implement common traits for Diagnostic and related types
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix out of bounds access in parse_snippet
|
||||
|
||||
## [0.4.4] - 2018-12-13
|
||||
|
||||
### Added
|
||||
|
||||
- Make Diagnostic::rendered public.
|
||||
|
||||
### Changed
|
||||
|
||||
- Revert faulty "Allow multiple solutions in a suggestion"
|
||||
|
||||
## [0.4.3] - 2018-12-09 - *yanked!*
|
||||
|
||||
### Added
|
||||
|
||||
- Allow multiple solutions in a suggestion
|
||||
|
||||
### Changed
|
||||
|
||||
- use `RUSTC` environment var if present
|
||||
|
||||
## [0.4.2] - 2018-07-31
|
||||
|
||||
### Added
|
||||
|
||||
- Expose an interface to apply fixes on-by-one
|
||||
|
||||
### Changed
|
||||
|
||||
- Handle invalid snippets instead of panicking
|
||||
|
||||
## [0.4.1] - 2018-07-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Ignore duplicate replacements
|
||||
|
||||
## [0.4.0] - 2018-05-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Filter by machine applicability by default
|
||||
|
||||
[Unreleased]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.6...HEAD
|
||||
[0.4.6]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.5...rustfix-0.4.6
|
||||
[0.4.5]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.4...rustfix-0.4.5
|
||||
[0.4.4]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.3...rustfix-0.4.4
|
||||
[0.4.3]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.2...rustfix-0.4.3
|
||||
[0.4.2]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.1...rustfix-0.4.2
|
||||
[0.4.1]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.0...rustfix-0.4.1
|
||||
[0.4.0]: https://github.com/rust-lang-nursery/rustfix/compare/rustfix-0.4.0
|
1
crates/rustfix/LICENSE-APACHE
Symbolic link
1
crates/rustfix/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
1
crates/rustfix/LICENSE-MIT
Symbolic link
1
crates/rustfix/LICENSE-MIT
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-MIT
|
29
crates/rustfix/Readme.md
Normal file
29
crates/rustfix/Readme.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# rustfix
|
||||
|
||||
[![Latest Version](https://img.shields.io/crates/v/rustfix.svg)](https://crates.io/crates/rustfix)
|
||||
[![Rust Documentation](https://docs.rs/rustfix/badge.svg)](https://docs.rs/rustfix)
|
||||
|
||||
Rustfix is a library defining useful structures that represent fix suggestions from rustc.
|
||||
|
||||
This is a low-level library. You pass it the JSON output from `rustc`, and you can then use it to apply suggestions to in-memory strings. This library doesn't execute commands, or read or write from the filesystem.
|
||||
|
||||
If you are looking for the [`cargo fix`] implementation, the core of it is located in [`cargo::ops::fix`].
|
||||
|
||||
[`cargo fix`]: https://doc.rust-lang.org/cargo/commands/cargo-fix.html
|
||||
[`cargo::ops::fix`]: https://github.com/rust-lang/cargo/blob/master/src/cargo/ops/fix.rs
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally
|
||||
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||
license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
44
crates/rustfix/examples/fix-json.rs
Normal file
44
crates/rustfix/examples/fix-json.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
#![allow(clippy::disallowed_methods, clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use anyhow::Error;
|
||||
use std::io::{stdin, BufReader, Read};
|
||||
use std::{collections::HashMap, collections::HashSet, env, fs};
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
let suggestions_file = env::args().nth(1).expect("USAGE: fix-json <file or -->");
|
||||
let suggestions = if suggestions_file == "--" {
|
||||
let mut buffer = String::new();
|
||||
BufReader::new(stdin()).read_to_string(&mut buffer)?;
|
||||
buffer
|
||||
} else {
|
||||
fs::read_to_string(&suggestions_file)?
|
||||
};
|
||||
let suggestions = rustfix::get_suggestions_from_json(
|
||||
&suggestions,
|
||||
&HashSet::new(),
|
||||
rustfix::Filter::Everything,
|
||||
)?;
|
||||
|
||||
let mut files = HashMap::new();
|
||||
for suggestion in suggestions {
|
||||
let file = suggestion.solutions[0].replacements[0]
|
||||
.snippet
|
||||
.file_name
|
||||
.clone();
|
||||
files.entry(file).or_insert_with(Vec::new).push(suggestion);
|
||||
}
|
||||
|
||||
for (source_file, suggestions) in &files {
|
||||
let source = fs::read_to_string(source_file)?;
|
||||
let mut fix = rustfix::CodeFix::new(&source);
|
||||
for suggestion in suggestions.iter().rev() {
|
||||
if let Err(e) = fix.apply(suggestion) {
|
||||
eprintln!("Failed to apply suggestion to {}: {}", source_file, e);
|
||||
}
|
||||
}
|
||||
let fixes = fix.finish()?;
|
||||
fs::write(source_file, fixes)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
8
crates/rustfix/proptest-regressions/replace.txt
Normal file
8
crates/rustfix/proptest-regressions/replace.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
xs 358148376 3634975642 2528447681 3675516813 # shrinks to ref s = ""
|
||||
xs 3127423015 3362740891 2605681441 2390162043 # shrinks to ref data = "", ref replacements = [(0..0, [])]
|
89
crates/rustfix/src/diagnostics.rs
Normal file
89
crates/rustfix/src/diagnostics.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
//! Rustc Diagnostic JSON Output
|
||||
//!
|
||||
//! The following data types are copied from [rust-lang/rust](https://github.com/rust-lang/rust/blob/de78655bca47cac8e783dbb563e7e5c25c1fae40/src/libsyntax/json.rs)
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct Diagnostic {
|
||||
/// The primary error message.
|
||||
pub message: String,
|
||||
pub code: Option<DiagnosticCode>,
|
||||
/// "error: internal compiler error", "error", "warning", "note", "help".
|
||||
level: String,
|
||||
pub spans: Vec<DiagnosticSpan>,
|
||||
/// Associated diagnostic messages.
|
||||
pub children: Vec<Diagnostic>,
|
||||
/// The message as rustc would render it. Currently this is only
|
||||
/// `Some` for "suggestions", but eventually it will include all
|
||||
/// snippets.
|
||||
pub rendered: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct DiagnosticSpan {
|
||||
pub file_name: String,
|
||||
pub byte_start: u32,
|
||||
pub byte_end: u32,
|
||||
/// 1-based.
|
||||
pub line_start: usize,
|
||||
pub line_end: usize,
|
||||
/// 1-based, character offset.
|
||||
pub column_start: usize,
|
||||
pub column_end: usize,
|
||||
/// Is this a "primary" span -- meaning the point, or one of the points,
|
||||
/// where the error occurred?
|
||||
pub is_primary: bool,
|
||||
/// Source text from the start of line_start to the end of line_end.
|
||||
pub text: Vec<DiagnosticSpanLine>,
|
||||
/// Label that should be placed at this location (if any)
|
||||
label: Option<String>,
|
||||
/// If we are suggesting a replacement, this will contain text
|
||||
/// that should be sliced in atop this span. You may prefer to
|
||||
/// load the fully rendered version from the parent `Diagnostic`,
|
||||
/// however.
|
||||
pub suggested_replacement: Option<String>,
|
||||
pub suggestion_applicability: Option<Applicability>,
|
||||
/// Macro invocations that created the code at this span, if any.
|
||||
expansion: Option<Box<DiagnosticSpanMacroExpansion>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
|
||||
pub enum Applicability {
|
||||
MachineApplicable,
|
||||
HasPlaceholders,
|
||||
MaybeIncorrect,
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct DiagnosticSpanLine {
|
||||
pub text: String,
|
||||
|
||||
/// 1-based, character offset in self.text.
|
||||
pub highlight_start: usize,
|
||||
|
||||
pub highlight_end: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Hash)]
|
||||
struct DiagnosticSpanMacroExpansion {
|
||||
/// span where macro was applied to generate this code; note that
|
||||
/// this may itself derive from a macro (if
|
||||
/// `span.expansion.is_some()`)
|
||||
span: DiagnosticSpan,
|
||||
|
||||
/// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]")
|
||||
macro_decl_name: String,
|
||||
|
||||
/// span where macro was defined (if known)
|
||||
def_site_span: Option<DiagnosticSpan>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct DiagnosticCode {
|
||||
/// The code itself.
|
||||
pub code: String,
|
||||
/// An explanation for the code.
|
||||
explanation: Option<String>,
|
||||
}
|
272
crates/rustfix/src/lib.rs
Normal file
272
crates/rustfix/src/lib.rs
Normal file
|
@ -0,0 +1,272 @@
|
|||
//! Library for applying diagnostic suggestions to source code.
|
||||
//!
|
||||
//! This is a low-level library. You pass it the JSON output from `rustc`, and
|
||||
//! you can then use it to apply suggestions to in-memory strings. This
|
||||
//! library doesn't execute commands, or read or write from the filesystem.
|
||||
//!
|
||||
//! If you are looking for the [`cargo fix`] implementation, the core of it is
|
||||
//! located in [`cargo::ops::fix`].
|
||||
//!
|
||||
//! [`cargo fix`]: https://doc.rust-lang.org/cargo/commands/cargo-fix.html
|
||||
//! [`cargo::ops::fix`]: https://github.com/rust-lang/cargo/blob/master/src/cargo/ops/fix.rs
|
||||
//!
|
||||
//! The general outline of how to use this library is:
|
||||
//!
|
||||
//! 1. Call `rustc` and collect the JSON data.
|
||||
//! 2. Pass the json data to [`get_suggestions_from_json`].
|
||||
//! 3. Create a [`CodeFix`] with the source of a file to modify.
|
||||
//! 4. Call [`CodeFix::apply`] to apply a change.
|
||||
//! 5. Write the source back to disk.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Range;
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
pub mod diagnostics;
|
||||
use crate::diagnostics::{Diagnostic, DiagnosticSpan};
|
||||
mod replace;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Filter {
|
||||
MachineApplicableOnly,
|
||||
Everything,
|
||||
}
|
||||
|
||||
pub fn get_suggestions_from_json<S: ::std::hash::BuildHasher>(
|
||||
input: &str,
|
||||
only: &HashSet<String, S>,
|
||||
filter: Filter,
|
||||
) -> serde_json::error::Result<Vec<Suggestion>> {
|
||||
let mut result = Vec::new();
|
||||
for cargo_msg in serde_json::Deserializer::from_str(input).into_iter::<Diagnostic>() {
|
||||
// One diagnostic line might have multiple suggestions
|
||||
result.extend(collect_suggestions(&cargo_msg?, only, filter));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct LinePosition {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LinePosition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}", self.line, self.column)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct LineRange {
|
||||
pub start: LinePosition,
|
||||
pub end: LinePosition,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LineRange {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}-{}", self.start, self.end)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
/// An error/warning and possible solutions for fixing it
|
||||
pub struct Suggestion {
|
||||
pub message: String,
|
||||
pub snippets: Vec<Snippet>,
|
||||
pub solutions: Vec<Solution>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Solution {
|
||||
pub message: String,
|
||||
pub replacements: Vec<Replacement>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Snippet {
|
||||
pub file_name: String,
|
||||
pub line_range: LineRange,
|
||||
pub range: Range<usize>,
|
||||
/// leading surrounding text, text to replace, trailing surrounding text
|
||||
///
|
||||
/// This split is useful for higlighting the part that gets replaced
|
||||
pub text: (String, String, String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Replacement {
|
||||
pub snippet: Snippet,
|
||||
pub replacement: String,
|
||||
}
|
||||
|
||||
fn parse_snippet(span: &DiagnosticSpan) -> Option<Snippet> {
|
||||
// unindent the snippet
|
||||
let indent = span
|
||||
.text
|
||||
.iter()
|
||||
.map(|line| {
|
||||
let indent = line
|
||||
.text
|
||||
.chars()
|
||||
.take_while(|&c| char::is_whitespace(c))
|
||||
.count();
|
||||
std::cmp::min(indent, line.highlight_start - 1)
|
||||
})
|
||||
.min()?;
|
||||
|
||||
let text_slice = span.text[0].text.chars().collect::<Vec<char>>();
|
||||
|
||||
// We subtract `1` because these highlights are 1-based
|
||||
// Check the `min` so that it doesn't attempt to index out-of-bounds when
|
||||
// the span points to the "end" of the line. For example, a line of
|
||||
// "foo\n" with a highlight_start of 5 is intended to highlight *after*
|
||||
// the line. This needs to compensate since the newline has been removed
|
||||
// from the text slice.
|
||||
let start = (span.text[0].highlight_start - 1).min(text_slice.len());
|
||||
let end = (span.text[0].highlight_end - 1).min(text_slice.len());
|
||||
let lead = text_slice[indent..start].iter().collect();
|
||||
let mut body: String = text_slice[start..end].iter().collect();
|
||||
|
||||
for line in span.text.iter().take(span.text.len() - 1).skip(1) {
|
||||
body.push('\n');
|
||||
body.push_str(&line.text[indent..]);
|
||||
}
|
||||
let mut tail = String::new();
|
||||
let last = &span.text[span.text.len() - 1];
|
||||
|
||||
// If we get a DiagnosticSpanLine where highlight_end > text.len(), we prevent an 'out of
|
||||
// bounds' access by making sure the index is within the array bounds.
|
||||
// `saturating_sub` is used in case of an empty file
|
||||
let last_tail_index = last.highlight_end.min(last.text.len()).saturating_sub(1);
|
||||
let last_slice = last.text.chars().collect::<Vec<char>>();
|
||||
|
||||
if span.text.len() > 1 {
|
||||
body.push('\n');
|
||||
body.push_str(
|
||||
&last_slice[indent..last_tail_index]
|
||||
.iter()
|
||||
.collect::<String>(),
|
||||
);
|
||||
}
|
||||
tail.push_str(&last_slice[last_tail_index..].iter().collect::<String>());
|
||||
Some(Snippet {
|
||||
file_name: span.file_name.clone(),
|
||||
line_range: LineRange {
|
||||
start: LinePosition {
|
||||
line: span.line_start,
|
||||
column: span.column_start,
|
||||
},
|
||||
end: LinePosition {
|
||||
line: span.line_end,
|
||||
column: span.column_end,
|
||||
},
|
||||
},
|
||||
range: (span.byte_start as usize)..(span.byte_end as usize),
|
||||
text: (lead, body, tail),
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_span(span: &DiagnosticSpan) -> Option<Replacement> {
|
||||
let snippet = parse_snippet(span)?;
|
||||
let replacement = span.suggested_replacement.clone()?;
|
||||
Some(Replacement {
|
||||
snippet,
|
||||
replacement,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collect_suggestions<S: ::std::hash::BuildHasher>(
|
||||
diagnostic: &Diagnostic,
|
||||
only: &HashSet<String, S>,
|
||||
filter: Filter,
|
||||
) -> Option<Suggestion> {
|
||||
if !only.is_empty() {
|
||||
if let Some(ref code) = diagnostic.code {
|
||||
if !only.contains(&code.code) {
|
||||
// This is not the code we are looking for
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
// No code, probably a weird builtin warning/error
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let snippets = diagnostic.spans.iter().filter_map(parse_snippet).collect();
|
||||
|
||||
let solutions: Vec<_> = diagnostic
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
let replacements: Vec<_> = child
|
||||
.spans
|
||||
.iter()
|
||||
.filter(|span| {
|
||||
use crate::diagnostics::Applicability::*;
|
||||
use crate::Filter::*;
|
||||
|
||||
match (filter, &span.suggestion_applicability) {
|
||||
(MachineApplicableOnly, Some(MachineApplicable)) => true,
|
||||
(MachineApplicableOnly, _) => false,
|
||||
(Everything, _) => true,
|
||||
}
|
||||
})
|
||||
.filter_map(collect_span)
|
||||
.collect();
|
||||
if !replacements.is_empty() {
|
||||
Some(Solution {
|
||||
message: child.message.clone(),
|
||||
replacements,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if solutions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Suggestion {
|
||||
message: diagnostic.message.clone(),
|
||||
snippets,
|
||||
solutions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeFix {
|
||||
data: replace::Data,
|
||||
}
|
||||
|
||||
impl CodeFix {
|
||||
pub fn new(s: &str) -> CodeFix {
|
||||
CodeFix {
|
||||
data: replace::Data::new(s.as_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&mut self, suggestion: &Suggestion) -> Result<(), Error> {
|
||||
for sol in &suggestion.solutions {
|
||||
for r in &sol.replacements {
|
||||
self.data
|
||||
.replace_range(r.snippet.range.clone(), r.replacement.as_bytes())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn finish(&self) -> Result<String, Error> {
|
||||
Ok(String::from_utf8(self.data.to_vec())?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_suggestions(code: &str, suggestions: &[Suggestion]) -> Result<String, Error> {
|
||||
let mut fix = CodeFix::new(code);
|
||||
for suggestion in suggestions.iter().rev() {
|
||||
fix.apply(suggestion)?;
|
||||
}
|
||||
fix.finish()
|
||||
}
|
335
crates/rustfix/src/replace.rs
Normal file
335
crates/rustfix/src/replace.rs
Normal file
|
@ -0,0 +1,335 @@
|
|||
//! A small module giving you a simple container that allows easy and cheap
|
||||
//! replacement of parts of its content, with the ability to prevent changing
|
||||
//! the same parts multiple times.
|
||||
|
||||
use anyhow::{anyhow, ensure, Error};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum State {
|
||||
Initial,
|
||||
Replaced(Rc<[u8]>),
|
||||
Inserted(Rc<[u8]>),
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn is_inserted(&self) -> bool {
|
||||
matches!(*self, State::Inserted(..))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Span {
|
||||
/// Start of this span in parent data
|
||||
start: usize,
|
||||
/// up to end excluding
|
||||
end: usize,
|
||||
data: State,
|
||||
}
|
||||
|
||||
/// A container that allows easily replacing chunks of its data
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Data {
|
||||
original: Vec<u8>,
|
||||
parts: Vec<Span>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
/// Create a new data container from a slice of bytes
|
||||
pub fn new(data: &[u8]) -> Self {
|
||||
Data {
|
||||
original: data.into(),
|
||||
parts: vec![Span {
|
||||
data: State::Initial,
|
||||
start: 0,
|
||||
end: data.len(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
/// Render this data as a vector of bytes
|
||||
pub fn to_vec(&self) -> Vec<u8> {
|
||||
if self.original.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.parts.iter().fold(Vec::new(), |mut acc, d| {
|
||||
match d.data {
|
||||
State::Initial => acc.extend_from_slice(&self.original[d.start..d.end]),
|
||||
State::Replaced(ref d) | State::Inserted(ref d) => acc.extend_from_slice(d),
|
||||
};
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
/// Replace a chunk of data with the given slice, erroring when this part
|
||||
/// was already changed previously.
|
||||
pub fn replace_range(
|
||||
&mut self,
|
||||
range: std::ops::Range<usize>,
|
||||
data: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
let exclusive_end = range.end;
|
||||
|
||||
ensure!(
|
||||
range.start <= exclusive_end,
|
||||
"Invalid range {}..{}, start is larger than end",
|
||||
range.start,
|
||||
range.end
|
||||
);
|
||||
|
||||
ensure!(
|
||||
exclusive_end <= self.original.len(),
|
||||
"Invalid range {}..{} given, original data is only {} byte long",
|
||||
range.start,
|
||||
range.end,
|
||||
self.original.len()
|
||||
);
|
||||
|
||||
let insert_only = range.start == range.end;
|
||||
|
||||
// Since we error out when replacing an already replaced chunk of data,
|
||||
// we can take some shortcuts here. For example, there can be no
|
||||
// overlapping replacements -- we _always_ split a chunk of 'initial'
|
||||
// data into three[^empty] parts, and there can't ever be two 'initial'
|
||||
// parts touching.
|
||||
//
|
||||
// [^empty]: Leading and trailing ones might be empty if we replace
|
||||
// the whole chunk. As an optimization and without loss of generality we
|
||||
// don't add empty parts.
|
||||
let new_parts = {
|
||||
let index_of_part_to_split = self
|
||||
.parts
|
||||
.iter()
|
||||
.position(|p| !p.data.is_inserted() && p.start <= range.start && p.end >= range.end)
|
||||
.ok_or_else(|| {
|
||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||
let slices = self
|
||||
.parts
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
p.start,
|
||||
p.end,
|
||||
match p.data {
|
||||
State::Initial => "initial",
|
||||
State::Replaced(..) => "replaced",
|
||||
State::Inserted(..) => "inserted",
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
tracing::debug!(
|
||||
"no single slice covering {}..{}, current slices: {:?}",
|
||||
range.start,
|
||||
range.end,
|
||||
slices,
|
||||
);
|
||||
}
|
||||
|
||||
anyhow!(
|
||||
"Could not replace range {}..{} in file \
|
||||
-- maybe parts of it were already replaced?",
|
||||
range.start,
|
||||
range.end,
|
||||
)
|
||||
})?;
|
||||
|
||||
let part_to_split = &self.parts[index_of_part_to_split];
|
||||
|
||||
// If this replacement matches exactly the part that we would
|
||||
// otherwise split then we ignore this for now. This means that you
|
||||
// can replace the exact same range with the exact same content
|
||||
// multiple times and we'll process and allow it.
|
||||
//
|
||||
// This is currently done to alleviate issues like
|
||||
// rust-lang/rust#51211 although this clause likely wants to be
|
||||
// removed if that's fixed deeper in the compiler.
|
||||
if part_to_split.start == range.start && part_to_split.end == range.end {
|
||||
if let State::Replaced(ref replacement) = part_to_split.data {
|
||||
if &**replacement == data {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(
|
||||
part_to_split.data == State::Initial,
|
||||
"Cannot replace slice of data that was already replaced"
|
||||
);
|
||||
|
||||
let mut new_parts = Vec::with_capacity(self.parts.len() + 2);
|
||||
|
||||
// Previous parts
|
||||
if let Some(ps) = self.parts.get(..index_of_part_to_split) {
|
||||
new_parts.extend_from_slice(ps);
|
||||
}
|
||||
|
||||
// Keep initial data on left side of part
|
||||
if range.start > part_to_split.start {
|
||||
new_parts.push(Span {
|
||||
start: part_to_split.start,
|
||||
end: range.start,
|
||||
data: State::Initial,
|
||||
});
|
||||
}
|
||||
|
||||
// New part
|
||||
new_parts.push(Span {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
data: if insert_only {
|
||||
State::Inserted(data.into())
|
||||
} else {
|
||||
State::Replaced(data.into())
|
||||
},
|
||||
});
|
||||
|
||||
// Keep initial data on right side of part
|
||||
if range.end < part_to_split.end {
|
||||
new_parts.push(Span {
|
||||
start: range.end,
|
||||
end: part_to_split.end,
|
||||
data: State::Initial,
|
||||
});
|
||||
}
|
||||
|
||||
// Following parts
|
||||
if let Some(ps) = self.parts.get(index_of_part_to_split + 1..) {
|
||||
new_parts.extend_from_slice(ps);
|
||||
}
|
||||
|
||||
new_parts
|
||||
};
|
||||
|
||||
self.parts = new_parts;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn str(i: &[u8]) -> &str {
|
||||
::std::str::from_utf8(i).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_at_beginning() {
|
||||
let mut d = Data::new(b"foo bar baz");
|
||||
d.replace_range(0..0, b"oh no ").unwrap();
|
||||
assert_eq!("oh no foo bar baz", str(&d.to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_at_end() {
|
||||
let mut d = Data::new(b"foo bar baz");
|
||||
d.replace_range(11..11, b" oh no").unwrap();
|
||||
assert_eq!("foo bar baz oh no", str(&d.to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_some_stuff() {
|
||||
let mut d = Data::new(b"foo bar baz");
|
||||
d.replace_range(4..7, b"lol").unwrap();
|
||||
assert_eq!("foo lol baz", str(&d.to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_a_single_char() {
|
||||
let mut d = Data::new(b"let y = true;");
|
||||
d.replace_range(4..5, b"mut y").unwrap();
|
||||
assert_eq!("let mut y = true;", str(&d.to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_multiple_lines() {
|
||||
let mut d = Data::new(b"lorem\nipsum\ndolor");
|
||||
|
||||
d.replace_range(6..11, b"lol").unwrap();
|
||||
assert_eq!("lorem\nlol\ndolor", str(&d.to_vec()));
|
||||
|
||||
d.replace_range(12..17, b"lol").unwrap();
|
||||
assert_eq!("lorem\nlol\nlol", str(&d.to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_multiple_lines_with_insert_only() {
|
||||
let mut d = Data::new(b"foo!");
|
||||
|
||||
d.replace_range(3..3, b"bar").unwrap();
|
||||
assert_eq!("foobar!", str(&d.to_vec()));
|
||||
|
||||
d.replace_range(0..3, b"baz").unwrap();
|
||||
assert_eq!("bazbar!", str(&d.to_vec()));
|
||||
|
||||
d.replace_range(3..4, b"?").unwrap();
|
||||
assert_eq!("bazbar?", str(&d.to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_invalid_range() {
|
||||
let mut d = Data::new(b"foo!");
|
||||
|
||||
assert!(d.replace_range(2..1, b"bar").is_err());
|
||||
assert!(d.replace_range(0..3, b"bar").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_to_vec_roundtrip() {
|
||||
let s = "";
|
||||
assert_eq!(s.as_bytes(), Data::new(s.as_bytes()).to_vec().as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Cannot replace slice of data that was already replaced")]
|
||||
fn replace_overlapping_stuff_errs() {
|
||||
let mut d = Data::new(b"foo bar baz");
|
||||
|
||||
d.replace_range(4..7, b"lol").unwrap();
|
||||
assert_eq!("foo lol baz", str(&d.to_vec()));
|
||||
|
||||
d.replace_range(4..7, b"lol2").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "original data is only 3 byte long")]
|
||||
fn broken_replacements() {
|
||||
let mut d = Data::new(b"foo");
|
||||
d.replace_range(4..8, b"lol").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_same_twice() {
|
||||
let mut d = Data::new(b"foo");
|
||||
d.replace_range(0..1, b"b").unwrap();
|
||||
d.replace_range(0..1, b"b").unwrap();
|
||||
assert_eq!("boo", str(&d.to_vec()));
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn new_to_vec_roundtrip(ref s in "\\PC*") {
|
||||
assert_eq!(s.as_bytes(), Data::new(s.as_bytes()).to_vec().as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn replace_random_chunks(
|
||||
ref data in "\\PC*",
|
||||
ref replacements in prop::collection::vec(
|
||||
(any::<::std::ops::Range<usize>>(), any::<Vec<u8>>()),
|
||||
1..1337,
|
||||
)
|
||||
) {
|
||||
let mut d = Data::new(data.as_bytes());
|
||||
for &(ref range, ref bytes) in replacements {
|
||||
let _ = d.replace_range(range.clone(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
crates/rustfix/tests/edge-cases/empty.json
Normal file
42
crates/rustfix/tests/edge-cases/empty.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"message": "`main` function not found in crate `empty`",
|
||||
"code": {
|
||||
"code": "E0601",
|
||||
"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"
|
||||
},
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "empty.rs",
|
||||
"byte_start": 0,
|
||||
"byte_end": 0,
|
||||
"line_start": 0,
|
||||
"line_end": 0,
|
||||
"column_start": 1,
|
||||
"column_end": 1,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": "",
|
||||
"highlight_start": 1,
|
||||
"highlight_end": 1
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "consider adding a `main` function to `empty.rs`",
|
||||
"code": null,
|
||||
"level": "note",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"rendered": "error[E0601]: `main` function not found in crate `empty`\n |\n = note: consider adding a `main` function to `empty.rs`\n\n"
|
||||
}
|
0
crates/rustfix/tests/edge-cases/empty.rs
Normal file
0
crates/rustfix/tests/edge-cases/empty.rs
Normal file
60
crates/rustfix/tests/edge-cases/indented_whitespace.json
Normal file
60
crates/rustfix/tests/edge-cases/indented_whitespace.json
Normal file
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"message": "non-ASCII whitespace symbol '\\u{a0}' is not skipped",
|
||||
"code": null,
|
||||
"level": "warning",
|
||||
"spans":
|
||||
[
|
||||
{
|
||||
"file_name": "lib.rs",
|
||||
"byte_start": 26,
|
||||
"byte_end": 28,
|
||||
"line_start": 2,
|
||||
"line_end": 2,
|
||||
"column_start": 1,
|
||||
"column_end": 2,
|
||||
"is_primary": false,
|
||||
"text":
|
||||
[
|
||||
{
|
||||
"text": " indented\";",
|
||||
"highlight_start": 1,
|
||||
"highlight_end": 2
|
||||
}
|
||||
],
|
||||
"label": "non-ASCII whitespace symbol '\\u{a0}' is not skipped",
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
},
|
||||
{
|
||||
"file_name": "lib.rs",
|
||||
"byte_start": 24,
|
||||
"byte_end": 28,
|
||||
"line_start": 1,
|
||||
"line_end": 2,
|
||||
"column_start": 25,
|
||||
"column_end": 2,
|
||||
"is_primary": true,
|
||||
"text":
|
||||
[
|
||||
{
|
||||
"text": "pub static FOO: &str = \"\\",
|
||||
"highlight_start": 25,
|
||||
"highlight_end": 26
|
||||
},
|
||||
{
|
||||
"text": " indented\";",
|
||||
"highlight_start": 1,
|
||||
"highlight_end": 2
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children":
|
||||
[],
|
||||
"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"
|
||||
}
|
33
crates/rustfix/tests/edge-cases/no_main.json
Normal file
33
crates/rustfix/tests/edge-cases/no_main.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"message": "`main` function not found in crate `no_main`",
|
||||
"code": {
|
||||
"code": "E0601",
|
||||
"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"
|
||||
},
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "no_main.rs",
|
||||
"byte_start": 26,
|
||||
"byte_end": 26,
|
||||
"line_start": 1,
|
||||
"line_end": 1,
|
||||
"column_start": 27,
|
||||
"column_end": 27,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": "// This file has no main.",
|
||||
"highlight_start": 27,
|
||||
"highlight_end": 27
|
||||
}
|
||||
],
|
||||
"label": "consider adding a `main` function to `no_main.rs`",
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"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"
|
||||
}
|
1
crates/rustfix/tests/edge-cases/no_main.rs
Normal file
1
crates/rustfix/tests/edge-cases/no_main.rs
Normal file
|
@ -0,0 +1 @@
|
|||
// This file has no main.
|
43
crates/rustfix/tests/edge-cases/out_of_bounds.recorded.json
Normal file
43
crates/rustfix/tests/edge-cases/out_of_bounds.recorded.json
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"message": "unterminated double quote string",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/tab_2.rs",
|
||||
"byte_start": 485,
|
||||
"byte_end": 526,
|
||||
"line_start": 12,
|
||||
"line_end": 13,
|
||||
"column_start": 7,
|
||||
"column_end": 3,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " \"\"\"; //~ ERROR unterminated double quote",
|
||||
"highlight_start": 7,
|
||||
"highlight_end": 45
|
||||
},
|
||||
{
|
||||
"text": "}",
|
||||
"highlight_start": 1,
|
||||
"highlight_end": 3
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "aborting due to previous error",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": "error: aborting due to previous error\n\n"
|
||||
}
|
59
crates/rustfix/tests/edge-cases/utf8_idents.recorded.json
Normal file
59
crates/rustfix/tests/edge-cases/utf8_idents.recorded.json
Normal file
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"message": "expected one of `,`, `:`, `=`, or `>`, found `'β`",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/utf8_idents.rs",
|
||||
"byte_start": 14,
|
||||
"byte_end": 14,
|
||||
"line_start": 2,
|
||||
"line_end": 2,
|
||||
"column_start": 6,
|
||||
"column_end": 6,
|
||||
"is_primary": false,
|
||||
"text": [
|
||||
{
|
||||
"text": " γ //~ ERROR non-ascii idents are not fully supported",
|
||||
"highlight_start": 6,
|
||||
"highlight_end": 6
|
||||
}
|
||||
],
|
||||
"label": "expected one of `,`, `:`, `=`, or `>` here",
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
},
|
||||
{
|
||||
"file_name": "./tests/everything/utf8_idents.rs",
|
||||
"byte_start": 145,
|
||||
"byte_end": 148,
|
||||
"line_start": 4,
|
||||
"line_end": 4,
|
||||
"column_start": 5,
|
||||
"column_end": 7,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " 'β, //~ ERROR non-ascii idents are not fully supported",
|
||||
"highlight_start": 5,
|
||||
"highlight_end": 7
|
||||
}
|
||||
],
|
||||
"label": "unexpected token",
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "aborting due to previous error",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": "error: aborting due to previous error\n\n"
|
||||
}
|
25
crates/rustfix/tests/edge_cases.rs
Normal file
25
crates/rustfix/tests/edge_cases.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use rustfix;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
|
||||
macro_rules! expect_empty_json_test {
|
||||
($name:ident, $file:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let json = fs::read_to_string(concat!("./tests/edge-cases/", $file)).unwrap();
|
||||
let expected_suggestions = rustfix::get_suggestions_from_json(
|
||||
&json,
|
||||
&HashSet::new(),
|
||||
rustfix::Filter::Everything,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(expected_suggestions.is_empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
expect_empty_json_test! {out_of_bounds_test, "out_of_bounds.recorded.json"}
|
||||
expect_empty_json_test! {utf8_identifiers_test, "utf8_idents.recorded.json"}
|
||||
expect_empty_json_test! {empty, "empty.json"}
|
||||
expect_empty_json_test! {no_main, "no_main.json"}
|
||||
expect_empty_json_test! {indented_whitespace, "indented_whitespace.json"}
|
2
crates/rustfix/tests/edition/.gitignore
vendored
Normal file
2
crates/rustfix/tests/edition/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.recorded.json
|
||||
*.recorded.rs
|
2
crates/rustfix/tests/everything/.gitignore
vendored
Normal file
2
crates/rustfix/tests/everything/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.recorded.json
|
||||
*.recorded.rs
|
10
crates/rustfix/tests/everything/E0178.fixed.rs
Normal file
10
crates/rustfix/tests/everything/E0178.fixed.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
trait Foo {}
|
||||
|
||||
struct Bar<'a> {
|
||||
w: &'a (dyn Foo + Send),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
}
|
70
crates/rustfix/tests/everything/E0178.json
Normal file
70
crates/rustfix/tests/everything/E0178.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"message": "expected a path on the left-hand side of `+`, not `&'a Foo`",
|
||||
"code": {
|
||||
"code": "E0178",
|
||||
"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"
|
||||
},
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/E0178.rs",
|
||||
"byte_start": 60,
|
||||
"byte_end": 74,
|
||||
"line_start": 6,
|
||||
"line_end": 6,
|
||||
"column_start": 8,
|
||||
"column_end": 22,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " w: &'a Foo + Send,",
|
||||
"highlight_start": 8,
|
||||
"highlight_end": 22
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "try adding parentheses",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/E0178.rs",
|
||||
"byte_start": 60,
|
||||
"byte_end": 74,
|
||||
"line_start": 6,
|
||||
"line_end": 6,
|
||||
"column_start": 8,
|
||||
"column_end": 22,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " w: &'a Foo + Send,",
|
||||
"highlight_start": 8,
|
||||
"highlight_end": 22
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "&'a (Foo + Send)",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "aborting due to previous error",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": "error: aborting due to previous error\n\n"
|
||||
}
|
10
crates/rustfix/tests/everything/E0178.rs
Normal file
10
crates/rustfix/tests/everything/E0178.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
trait Foo {}
|
||||
|
||||
struct Bar<'a> {
|
||||
w: &'a dyn Foo + Send,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// Point at the captured immutable outer variable
|
||||
|
||||
fn foo(mut f: Box<dyn FnMut()>) {
|
||||
f();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut y = true;
|
||||
foo(Box::new(move || y = false) as Box<_>); //~ ERROR cannot assign to captured outer variable
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"message": "cannot assign to captured outer variable in an `FnMut` closure",
|
||||
"code": {
|
||||
"code": "E0594",
|
||||
"explanation": null
|
||||
},
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/closure-immutable-outer-variable.rs",
|
||||
"byte_start": 615,
|
||||
"byte_end": 624,
|
||||
"line_start": 19,
|
||||
"line_end": 19,
|
||||
"column_start": 26,
|
||||
"column_end": 35,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " foo(Box::new(move || y = false) as Box<_>); //~ ERROR cannot assign to captured outer variable",
|
||||
"highlight_start": 26,
|
||||
"highlight_end": 35
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "consider making `y` mutable",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/closure-immutable-outer-variable.rs",
|
||||
"byte_start": 580,
|
||||
"byte_end": 581,
|
||||
"line_start": 18,
|
||||
"line_end": 18,
|
||||
"column_start": 9,
|
||||
"column_end": 10,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let y = true;",
|
||||
"highlight_start": 9,
|
||||
"highlight_end": 10
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "mut y",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "aborting due to previous error",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": "error: aborting due to previous error\n\n"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// Point at the captured immutable outer variable
|
||||
|
||||
fn foo(mut f: Box<FnMut()>) {
|
||||
f();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let y = true;
|
||||
foo(Box::new(move || y = false) as Box<_>); //~ ERROR cannot assign to captured outer variable
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
fn main() {
|
||||
// insert only fix, adds `,` to first match arm
|
||||
// why doesnt this replace 1 with 1,?
|
||||
match &Some(3) {
|
||||
&None => 1,
|
||||
&Some(x) => x,
|
||||
};
|
||||
}
|
68
crates/rustfix/tests/everything/handle-insert-only.json
Normal file
68
crates/rustfix/tests/everything/handle-insert-only.json
Normal file
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"message": "expected one of `,`, `.`, `?`, `}`, or an operator, found `=>`",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/handle-insert-only.rs",
|
||||
"byte_start": 163,
|
||||
"byte_end": 165,
|
||||
"line_start": 6,
|
||||
"line_end": 6,
|
||||
"column_start": 18,
|
||||
"column_end": 20,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " &Some(x) => x,",
|
||||
"highlight_start": 18,
|
||||
"highlight_end": 20
|
||||
}
|
||||
],
|
||||
"label": "expected one of `,`, `.`, `?`, `}`, or an operator here",
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "missing a comma here to end this `match` arm",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/handle-insert-only.rs",
|
||||
"byte_start": 145,
|
||||
"byte_end": 145,
|
||||
"line_start": 5,
|
||||
"line_end": 5,
|
||||
"column_start": 19,
|
||||
"column_end": 19,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " &None => 1",
|
||||
"highlight_start": 19,
|
||||
"highlight_end": 19
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": ",",
|
||||
"suggestion_applicability": "Unspecified",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "aborting due to previous error",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": "error: aborting due to previous error\n\n"
|
||||
}
|
8
crates/rustfix/tests/everything/handle-insert-only.rs
Normal file
8
crates/rustfix/tests/everything/handle-insert-only.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
fn main() {
|
||||
// insert only fix, adds `,` to first match arm
|
||||
// why doesnt this replace 1 with 1,?
|
||||
match &Some(3) {
|
||||
&None => 1
|
||||
&Some(x) => x,
|
||||
};
|
||||
}
|
7
crates/rustfix/tests/everything/lt-generic-comp.fixed.rs
Normal file
7
crates/rustfix/tests/everything/lt-generic-comp.fixed.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
fn main() {
|
||||
let x = 5i64;
|
||||
|
||||
if (x as u32) < 4 {
|
||||
println!("yay");
|
||||
}
|
||||
}
|
87
crates/rustfix/tests/everything/lt-generic-comp.json
Normal file
87
crates/rustfix/tests/everything/lt-generic-comp.json
Normal file
|
@ -0,0 +1,87 @@
|
|||
{
|
||||
"message": "`<` is interpreted as a start of generic arguments for `u32`, not a comparison",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/lt-generic-comp.rs",
|
||||
"byte_start": 49,
|
||||
"byte_end": 50,
|
||||
"line_start": 4,
|
||||
"line_end": 4,
|
||||
"column_start": 19,
|
||||
"column_end": 20,
|
||||
"is_primary": false,
|
||||
"text": [
|
||||
{
|
||||
"text": " if x as u32 < 4 {",
|
||||
"highlight_start": 19,
|
||||
"highlight_end": 20
|
||||
}
|
||||
],
|
||||
"label": "interpreted as generic arguments",
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
},
|
||||
{
|
||||
"file_name": "./tests/everything/lt-generic-comp.rs",
|
||||
"byte_start": 47,
|
||||
"byte_end": 48,
|
||||
"line_start": 4,
|
||||
"line_end": 4,
|
||||
"column_start": 17,
|
||||
"column_end": 18,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " if x as u32 < 4 {",
|
||||
"highlight_start": 17,
|
||||
"highlight_end": 18
|
||||
}
|
||||
],
|
||||
"label": "not interpreted as comparison",
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "try comparing the cast value",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/lt-generic-comp.rs",
|
||||
"byte_start": 38,
|
||||
"byte_end": 46,
|
||||
"line_start": 4,
|
||||
"line_end": 4,
|
||||
"column_start": 8,
|
||||
"column_end": 16,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " if x as u32 < 4 {",
|
||||
"highlight_start": 8,
|
||||
"highlight_end": 16
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "(x as u32)",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "aborting due to previous error",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": "error: aborting due to previous error\n\n"
|
||||
}
|
7
crates/rustfix/tests/everything/lt-generic-comp.rs
Normal file
7
crates/rustfix/tests/everything/lt-generic-comp.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
fn main() {
|
||||
let x = 5i64;
|
||||
|
||||
if x as u32 < 4 {
|
||||
println!("yay");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
use std::collections::{HashSet};
|
||||
|
||||
fn main() {
|
||||
let _: HashSet<()>;
|
||||
}
|
114
crates/rustfix/tests/everything/multiple-solutions.json
Normal file
114
crates/rustfix/tests/everything/multiple-solutions.json
Normal file
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"message": "unused imports: `HashMap`, `VecDeque`",
|
||||
"code": {
|
||||
"code": "unused_imports",
|
||||
"explanation": null
|
||||
},
|
||||
"level": "warning",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "src/main.rs",
|
||||
"byte_start": 23,
|
||||
"byte_end": 30,
|
||||
"line_start": 1,
|
||||
"line_end": 1,
|
||||
"column_start": 24,
|
||||
"column_end": 31,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": "use std::collections::{HashMap, HashSet, VecDeque};",
|
||||
"highlight_start": 24,
|
||||
"highlight_end": 31
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
},
|
||||
{
|
||||
"file_name": "src/main.rs",
|
||||
"byte_start": 41,
|
||||
"byte_end": 49,
|
||||
"line_start": 1,
|
||||
"line_end": 1,
|
||||
"column_start": 42,
|
||||
"column_end": 50,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": "use std::collections::{HashMap, HashSet, VecDeque};",
|
||||
"highlight_start": 42,
|
||||
"highlight_end": 50
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": null,
|
||||
"suggestion_applicability": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "#[warn(unused_imports)] on by default",
|
||||
"code": null,
|
||||
"level": "note",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
},
|
||||
{
|
||||
"message": "remove the unused imports",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "src/main.rs",
|
||||
"byte_start": 23,
|
||||
"byte_end": 32,
|
||||
"line_start": 1,
|
||||
"line_end": 1,
|
||||
"column_start": 24,
|
||||
"column_end": 33,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": "use std::collections::{HashMap, HashSet, VecDeque};",
|
||||
"highlight_start": 24,
|
||||
"highlight_end": 33
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "",
|
||||
"suggestion_applicability": "MachineApplicable",
|
||||
"expansion": null
|
||||
},
|
||||
{
|
||||
"file_name": "src/main.rs",
|
||||
"byte_start": 39,
|
||||
"byte_end": 49,
|
||||
"line_start": 1,
|
||||
"line_end": 1,
|
||||
"column_start": 40,
|
||||
"column_end": 50,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": "use std::collections::{HashMap, HashSet, VecDeque};",
|
||||
"highlight_start": 40,
|
||||
"highlight_end": 50
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "",
|
||||
"suggestion_applicability": "MachineApplicable",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
5
crates/rustfix/tests/everything/multiple-solutions.rs
Normal file
5
crates/rustfix/tests/everything/multiple-solutions.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
fn main() {
|
||||
let _: HashSet<()>;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
let _x = 42;
|
||||
}
|
70
crates/rustfix/tests/everything/replace-only-one-char.json
Normal file
70
crates/rustfix/tests/everything/replace-only-one-char.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"message": "unused variable: `x`",
|
||||
"code": {
|
||||
"code": "unused_variables",
|
||||
"explanation": null
|
||||
},
|
||||
"level": "warning",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "replace-only-one-char.rs",
|
||||
"byte_start": 20,
|
||||
"byte_end": 21,
|
||||
"line_start": 2,
|
||||
"line_end": 2,
|
||||
"column_start": 9,
|
||||
"column_end": 10,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let x = 42;",
|
||||
"highlight_start": 9,
|
||||
"highlight_end": 10
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "#[warn(unused_variables)] on by default",
|
||||
"code": null,
|
||||
"level": "note",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
},
|
||||
{
|
||||
"message": "consider using `_x` instead",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "replace-only-one-char.rs",
|
||||
"byte_start": 20,
|
||||
"byte_end": 21,
|
||||
"line_start": 2,
|
||||
"line_end": 2,
|
||||
"column_start": 9,
|
||||
"column_end": 10,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let x = 42;",
|
||||
"highlight_start": 9,
|
||||
"highlight_end": 10
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "_x",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
3
crates/rustfix/tests/everything/replace-only-one-char.rs
Normal file
3
crates/rustfix/tests/everything/replace-only-one-char.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
let x = 42;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
let x: &[u8] = b"foo"; //~ ERROR mismatched types
|
||||
let y: &[u8; 4] = b"baaa"; //~ ERROR mismatched types
|
||||
let z: &str = "foo"; //~ ERROR mismatched types
|
||||
}
|
218
crates/rustfix/tests/everything/str-lit-type-mismatch.json
Normal file
218
crates/rustfix/tests/everything/str-lit-type-mismatch.json
Normal file
|
@ -0,0 +1,218 @@
|
|||
{
|
||||
"message": "mismatched types",
|
||||
"code": {
|
||||
"code": "E0308",
|
||||
"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"
|
||||
},
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/str-lit-type-mismatch.rs",
|
||||
"byte_start": 499,
|
||||
"byte_end": 504,
|
||||
"line_start": 13,
|
||||
"line_end": 13,
|
||||
"column_start": 20,
|
||||
"column_end": 25,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let x: &[u8] = \"foo\"; //~ ERROR mismatched types",
|
||||
"highlight_start": 20,
|
||||
"highlight_end": 25
|
||||
}
|
||||
],
|
||||
"label": "expected slice, found str",
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "expected type `&[u8]`\n found type `&'static str`",
|
||||
"code": null,
|
||||
"level": "note",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
},
|
||||
{
|
||||
"message": "consider adding a leading `b`",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/str-lit-type-mismatch.rs",
|
||||
"byte_start": 499,
|
||||
"byte_end": 504,
|
||||
"line_start": 13,
|
||||
"line_end": 13,
|
||||
"column_start": 20,
|
||||
"column_end": 25,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let x: &[u8] = \"foo\"; //~ ERROR mismatched types",
|
||||
"highlight_start": 20,
|
||||
"highlight_end": 25
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "b\"foo\"",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "mismatched types",
|
||||
"code": {
|
||||
"code": "E0308",
|
||||
"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"
|
||||
},
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/str-lit-type-mismatch.rs",
|
||||
"byte_start": 555,
|
||||
"byte_end": 561,
|
||||
"line_start": 14,
|
||||
"line_end": 14,
|
||||
"column_start": 23,
|
||||
"column_end": 29,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let y: &[u8; 4] = \"baaa\"; //~ ERROR mismatched types",
|
||||
"highlight_start": 23,
|
||||
"highlight_end": 29
|
||||
}
|
||||
],
|
||||
"label": "expected array of 4 elements, found str",
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "expected type `&[u8; 4]`\n found type `&'static str`",
|
||||
"code": null,
|
||||
"level": "note",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
},
|
||||
{
|
||||
"message": "consider adding a leading `b`",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/str-lit-type-mismatch.rs",
|
||||
"byte_start": 555,
|
||||
"byte_end": 561,
|
||||
"line_start": 14,
|
||||
"line_end": 14,
|
||||
"column_start": 23,
|
||||
"column_end": 29,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let y: &[u8; 4] = \"baaa\"; //~ ERROR mismatched types",
|
||||
"highlight_start": 23,
|
||||
"highlight_end": 29
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "b\"baaa\"",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "mismatched types",
|
||||
"code": {
|
||||
"code": "E0308",
|
||||
"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"
|
||||
},
|
||||
"level": "error",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/str-lit-type-mismatch.rs",
|
||||
"byte_start": 608,
|
||||
"byte_end": 614,
|
||||
"line_start": 15,
|
||||
"line_end": 15,
|
||||
"column_start": 19,
|
||||
"column_end": 25,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let z: &str = b\"foo\"; //~ ERROR mismatched types",
|
||||
"highlight_start": 19,
|
||||
"highlight_end": 25
|
||||
}
|
||||
],
|
||||
"label": "expected str, found array of 3 elements",
|
||||
"suggested_replacement": null,
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"message": "expected type `&str`\n found type `&'static [u8; 3]`",
|
||||
"code": null,
|
||||
"level": "note",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
},
|
||||
{
|
||||
"message": "consider removing the leading `b`",
|
||||
"code": null,
|
||||
"level": "help",
|
||||
"spans": [
|
||||
{
|
||||
"file_name": "./tests/everything/str-lit-type-mismatch.rs",
|
||||
"byte_start": 608,
|
||||
"byte_end": 614,
|
||||
"line_start": 15,
|
||||
"line_end": 15,
|
||||
"column_start": 19,
|
||||
"column_end": 25,
|
||||
"is_primary": true,
|
||||
"text": [
|
||||
{
|
||||
"text": " let z: &str = b\"foo\"; //~ ERROR mismatched types",
|
||||
"highlight_start": 19,
|
||||
"highlight_end": 25
|
||||
}
|
||||
],
|
||||
"label": null,
|
||||
"suggested_replacement": "\"foo\"",
|
||||
"expansion": null
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"rendered": null
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"message": "aborting due to 3 previous errors",
|
||||
"code": null,
|
||||
"level": "error",
|
||||
"spans": [],
|
||||
"children": [],
|
||||
"rendered": "error: aborting due to 3 previous errors\n\n"
|
||||
}
|
5
crates/rustfix/tests/everything/str-lit-type-mismatch.rs
Normal file
5
crates/rustfix/tests/everything/str-lit-type-mismatch.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
let x: &[u8] = "foo"; //~ ERROR mismatched types
|
||||
let y: &[u8; 4] = "baaa"; //~ ERROR mismatched types
|
||||
let z: &str = b"foo"; //~ ERROR mismatched types
|
||||
}
|
247
crates/rustfix/tests/parse_and_replace.rs
Normal file
247
crates/rustfix/tests/parse_and_replace.rs
Normal file
|
@ -0,0 +1,247 @@
|
|||
#![allow(clippy::disallowed_methods, clippy::print_stdout, clippy::print_stderr)]
|
||||
#![cfg(not(windows))] // TODO: should fix these tests on Windows
|
||||
|
||||
use anyhow::{anyhow, ensure, Context, Error};
|
||||
use rustfix::apply_suggestions;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use tempfile::tempdir;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
mod fixmode {
|
||||
pub const EVERYTHING: &str = "yolo";
|
||||
pub const EDITION: &str = "edition";
|
||||
}
|
||||
|
||||
mod settings {
|
||||
// can be set as env var to debug
|
||||
pub const CHECK_JSON: &str = "RUSTFIX_TEST_CHECK_JSON";
|
||||
pub const RECORD_JSON: &str = "RUSTFIX_TEST_RECORD_JSON";
|
||||
pub const RECORD_FIXED_RUST: &str = "RUSTFIX_TEST_RECORD_FIXED_RUST";
|
||||
}
|
||||
|
||||
fn compile(file: &Path, mode: &str) -> Result<Output, Error> {
|
||||
let tmp = tempdir()?;
|
||||
|
||||
let mut args: Vec<OsString> = vec![
|
||||
file.into(),
|
||||
"--error-format=json".into(),
|
||||
"--emit=metadata".into(),
|
||||
"--crate-name=rustfix_test".into(),
|
||||
"--out-dir".into(),
|
||||
tmp.path().into(),
|
||||
];
|
||||
|
||||
if mode == fixmode::EDITION {
|
||||
args.push("--edition=2018".into());
|
||||
}
|
||||
|
||||
let res = Command::new(env::var_os("RUSTC").unwrap_or("rustc".into()))
|
||||
.args(&args)
|
||||
.env("CLIPPY_DISABLE_DOCS_LINKS", "true")
|
||||
.env_remove("RUST_LOG")
|
||||
.output()?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn compile_and_get_json_errors(file: &Path, mode: &str) -> Result<String, Error> {
|
||||
let res = compile(file, mode)?;
|
||||
let stderr = String::from_utf8(res.stderr)?;
|
||||
if stderr.contains("is only accepted on the nightly compiler") {
|
||||
panic!("rustfix tests require a nightly compiler");
|
||||
}
|
||||
|
||||
match res.status.code() {
|
||||
Some(0) | Some(1) | Some(101) => Ok(stderr),
|
||||
_ => Err(anyhow!(
|
||||
"failed with status {:?}: {}",
|
||||
res.status.code(),
|
||||
stderr
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn compiles_without_errors(file: &Path, mode: &str) -> Result<(), Error> {
|
||||
let res = compile(file, mode)?;
|
||||
|
||||
match res.status.code() {
|
||||
Some(0) => Ok(()),
|
||||
_ => {
|
||||
info!(
|
||||
"file {:?} failed to compile:\n{}",
|
||||
file,
|
||||
String::from_utf8(res.stderr)?
|
||||
);
|
||||
Err(anyhow!(
|
||||
"failed with status {:?} (`env RUST_LOG=parse_and_replace=info` for more info)",
|
||||
res.status.code(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_file(path: &Path) -> Result<String, Error> {
|
||||
use std::io::Read;
|
||||
|
||||
let mut buffer = String::new();
|
||||
let mut file = fs::File::open(path)?;
|
||||
file.read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn diff(expected: &str, actual: &str) -> String {
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut res = String::new();
|
||||
let diff = TextDiff::from_lines(expected.trim(), actual.trim());
|
||||
|
||||
let mut different = false;
|
||||
for op in diff.ops() {
|
||||
for change in diff.iter_changes(op) {
|
||||
let prefix = match change.tag() {
|
||||
ChangeTag::Equal => continue,
|
||||
ChangeTag::Insert => "+",
|
||||
ChangeTag::Delete => "-",
|
||||
};
|
||||
if !different {
|
||||
write!(
|
||||
&mut res,
|
||||
"differences found (+ == actual, - == expected):\n"
|
||||
)
|
||||
.unwrap();
|
||||
different = true;
|
||||
}
|
||||
write!(&mut res, "{} {}", prefix, change.value()).unwrap();
|
||||
}
|
||||
}
|
||||
if different {
|
||||
write!(&mut res, "").unwrap();
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn test_rustfix_with_file<P: AsRef<Path>>(file: P, mode: &str) -> Result<(), Error> {
|
||||
let file: &Path = file.as_ref();
|
||||
let json_file = file.with_extension("json");
|
||||
let fixed_file = file.with_extension("fixed.rs");
|
||||
|
||||
let filter_suggestions = if mode == fixmode::EVERYTHING {
|
||||
rustfix::Filter::Everything
|
||||
} else {
|
||||
rustfix::Filter::MachineApplicableOnly
|
||||
};
|
||||
|
||||
debug!("next up: {:?}", file);
|
||||
let code = read_file(file).context(format!("could not read {}", file.display()))?;
|
||||
let errors = compile_and_get_json_errors(file, mode)
|
||||
.context(format!("could compile {}", file.display()))?;
|
||||
let suggestions =
|
||||
rustfix::get_suggestions_from_json(&errors, &HashSet::new(), filter_suggestions)
|
||||
.context("could not load suggestions")?;
|
||||
|
||||
if std::env::var(settings::RECORD_JSON).is_ok() {
|
||||
use std::io::Write;
|
||||
let mut recorded_json = fs::File::create(&file.with_extension("recorded.json")).context(
|
||||
format!("could not create recorded.json for {}", file.display()),
|
||||
)?;
|
||||
recorded_json.write_all(errors.as_bytes())?;
|
||||
}
|
||||
|
||||
if std::env::var(settings::CHECK_JSON).is_ok() {
|
||||
let expected_json = read_file(&json_file).context(format!(
|
||||
"could not load json fixtures for {}",
|
||||
file.display()
|
||||
))?;
|
||||
let expected_suggestions =
|
||||
rustfix::get_suggestions_from_json(&expected_json, &HashSet::new(), filter_suggestions)
|
||||
.context("could not load expected suggestions")?;
|
||||
|
||||
ensure!(
|
||||
expected_suggestions == suggestions,
|
||||
"got unexpected suggestions from clippy:\n{}",
|
||||
diff(
|
||||
&format!("{:?}", expected_suggestions),
|
||||
&format!("{:?}", suggestions)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let fixed = apply_suggestions(&code, &suggestions)
|
||||
.context(format!("could not apply suggestions to {}", file.display()))?;
|
||||
|
||||
if std::env::var(settings::RECORD_FIXED_RUST).is_ok() {
|
||||
use std::io::Write;
|
||||
let mut recorded_rust = fs::File::create(&file.with_extension("recorded.rs"))?;
|
||||
recorded_rust.write_all(fixed.as_bytes())?;
|
||||
}
|
||||
|
||||
let expected_fixed =
|
||||
read_file(&fixed_file).context(format!("could read fixed file for {}", file.display()))?;
|
||||
ensure!(
|
||||
fixed.trim() == expected_fixed.trim(),
|
||||
"file {} doesn't look fixed:\n{}",
|
||||
file.display(),
|
||||
diff(fixed.trim(), expected_fixed.trim())
|
||||
);
|
||||
|
||||
compiles_without_errors(&fixed_file, mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_fixture_files(p: &str) -> Result<Vec<PathBuf>, Error> {
|
||||
Ok(fs::read_dir(&p)?
|
||||
.into_iter()
|
||||
.map(|e| e.unwrap().path())
|
||||
.filter(|p| p.is_file())
|
||||
.filter(|p| {
|
||||
let x = p.to_string_lossy();
|
||||
x.ends_with(".rs") && !x.ends_with(".fixed.rs") && !x.ends_with(".recorded.rs")
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn assert_fixtures(dir: &str, mode: &str) {
|
||||
let files = get_fixture_files(&dir)
|
||||
.context(format!("couldn't load dir `{}`", dir))
|
||||
.unwrap();
|
||||
let mut failures = 0;
|
||||
|
||||
for file in &files {
|
||||
if let Err(err) = test_rustfix_with_file(file, mode) {
|
||||
println!("failed: {}", file.display());
|
||||
warn!("{:?}", err);
|
||||
failures += 1;
|
||||
}
|
||||
info!("passed: {:?}", file);
|
||||
}
|
||||
|
||||
if failures > 0 {
|
||||
panic!(
|
||||
"{} out of {} fixture asserts failed\n\
|
||||
(run with `env RUST_LOG=parse_and_replace=info` to get more details)",
|
||||
failures,
|
||||
files.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn everything() {
|
||||
tracing_subscriber::fmt::init();
|
||||
assert_fixtures("./tests/everything", fixmode::EVERYTHING);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires custom rustc build"]
|
||||
fn edition() {
|
||||
tracing_subscriber::fmt::init();
|
||||
assert_fixtures("./tests/edition", fixmode::EDITION);
|
||||
}
|
|
@ -166,6 +166,7 @@ fn bump_check(args: &clap::ArgMatches, config: &cargo::util::Config) -> CargoRes
|
|||
let mut cmd = ProcessBuilder::new("cargo");
|
||||
cmd.arg("semver-checks")
|
||||
.arg("--workspace")
|
||||
.args(&["--exclude", "rustfix"]) // FIXME: Remove once 1.76 is stable
|
||||
.arg("--baseline-rev")
|
||||
.arg(referenced_commit.id().to_string());
|
||||
config.shell().status("Running", &cmd)?;
|
||||
|
|
|
@ -21,6 +21,7 @@ TO_PUBLISH = [
|
|||
'credential/cargo-credential-wincred',
|
||||
'credential/cargo-credential-1password',
|
||||
'credential/cargo-credential-macos-keychain',
|
||||
'crates/rustfix',
|
||||
'crates/cargo-platform',
|
||||
'crates/cargo-util',
|
||||
'crates/crates-io',
|
||||
|
|
|
@ -33,10 +33,9 @@ relevant are:
|
|||
* [`rust-lang/crates.io`] --- Home for the [crates.io] website.
|
||||
|
||||
Issues with [`cargo fix`] can be tricky to know where they should be filed,
|
||||
since the fixes are driven by `rustc`, processed by [`rustfix`], and the
|
||||
front-interface is implemented in Cargo. Feel free to file in the Cargo issue
|
||||
tracker, and it will get moved to one of the other issue trackers if
|
||||
necessary.
|
||||
since the fixes are driven by `rustc`, and the front-interface is implemented
|
||||
in Cargo. Feel free to file in the Cargo issue tracker, and it will get moved
|
||||
to the [`rust-lang/rust`] issue tracker if necessary.
|
||||
|
||||
[Process]: process/index.md
|
||||
[security policy]: https://www.rust-lang.org/security.html
|
||||
|
@ -51,7 +50,6 @@ necessary.
|
|||
[`rustup`]: https://rust-lang.github.io/rustup/
|
||||
[`rust-lang/crates.io`]: https://github.com/rust-lang/crates.io
|
||||
[crates.io]: https://crates.io/
|
||||
[`rustfix`]: https://github.com/rust-lang/rustfix/
|
||||
[`cargo fix`]: https://doc.rust-lang.org/cargo/commands/cargo-fix.html
|
||||
|
||||
## Issue labels
|
||||
|
|
|
@ -262,6 +262,7 @@ trigger_files = [
|
|||
"src/bin/cargo/commands/fix.rs",
|
||||
"src/cargo/ops/fix.rs",
|
||||
"src/cargo/util/lockserver.rs",
|
||||
"crates/rustfix/",
|
||||
]
|
||||
|
||||
[autolabel."Command-generate-lockfile"]
|
||||
|
|
Loading…
Reference in a new issue