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:
bors 2023-11-21 18:36:36 +00:00
commit eed300d5e8
48 changed files with 2211 additions and 10 deletions

10
Cargo.lock generated
View file

@ -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]]

View file

@ -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
View 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

View 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

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

1
crates/rustfix/LICENSE-MIT Symbolic link
View file

@ -0,0 +1 @@
../../LICENSE-MIT

29
crates/rustfix/Readme.md Normal file
View 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.

View 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(())
}

View 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, [])]

View 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
View 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()
}

View 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);
}
}
}
}

View 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"
}

View file

View 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"
}

View 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"
}

View file

@ -0,0 +1 @@
// This file has no main.

View 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"
}

View 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"
}

View 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"}

View file

@ -0,0 +1,2 @@
*.recorded.json
*.recorded.rs

View file

@ -0,0 +1,2 @@
*.recorded.json
*.recorded.rs

View file

@ -0,0 +1,10 @@
#![allow(dead_code)]
trait Foo {}
struct Bar<'a> {
w: &'a (dyn Foo + Send),
}
fn main() {
}

View 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"
}

View file

@ -0,0 +1,10 @@
#![allow(dead_code)]
trait Foo {}
struct Bar<'a> {
w: &'a dyn Foo + Send,
}
fn main() {
}

View file

@ -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
}

View file

@ -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"
}

View file

@ -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
}

View 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,
};
}

View 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"
}

View 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,
};
}

View file

@ -0,0 +1,7 @@
fn main() {
let x = 5i64;
if (x as u32) < 4 {
println!("yay");
}
}

View 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"
}

View file

@ -0,0 +1,7 @@
fn main() {
let x = 5i64;
if x as u32 < 4 {
println!("yay");
}
}

View file

@ -0,0 +1,5 @@
use std::collections::{HashSet};
fn main() {
let _: HashSet<()>;
}

View 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"
}

View file

@ -0,0 +1,5 @@
use std::collections::{HashMap, HashSet, VecDeque};
fn main() {
let _: HashSet<()>;
}

View file

@ -0,0 +1,3 @@
fn main() {
let _x = 42;
}

View 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"
}

View file

@ -0,0 +1,3 @@
fn main() {
let x = 42;
}

View file

@ -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
}

View 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"
}

View 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
}

View 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);
}

View file

@ -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)?;

View file

@ -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',

View file

@ -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

View file

@ -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"]