Skip to content

Commit 059ec69

Browse files
committed
Auto merge of #13226 - ehuss:fix-empty-span, r=weihanglo
rustfix: Support inserting new lines. If rustfix received a suggestion which inserts new lines without replacing existing lines, it would ignore the suggestion. This is because `parse_snippet` would immediately return if the `lines` to replace was empty. The solution here is to just drop the code which messes with the original text line. `cargo fix` (and compiletest) currently do not use this. This was originally added back in the days when rustfix supported an interactive UI which showed color highlighting of what it looks like with the replacement. My feeling is that when we add something like this back in, I would prefer to instead use a real diff library and display instead of trying to do various text manipulation for display. This particular code has generally been buggy, and has been a problem several times. The included test fails without this fix because the changes do not apply, and the code cannot compile. This also includes a little bit of cleanup for the `parse_and_replace` test. My feeling is that this test could use further improvements.
2 parents 903a509 + 3d3e1b3 commit 059ec69

File tree

8 files changed

+70
-93
lines changed

8 files changed

+70
-93
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pulldown-cmark = { version = "0.9.3", default-features = false }
7777
rand = "0.8.5"
7878
regex = "1.10.2"
7979
rusqlite = { version = "0.30.0", features = ["bundled"] }
80-
rustfix = { version = "0.7.0", path = "crates/rustfix" }
80+
rustfix = { version = "0.8.0", path = "crates/rustfix" }
8181
same-file = "1.0.6"
8282
security-framework = "2.9.2"
8383
semver = { version = "1.0.20", features = ["serde"] }

crates/rustfix/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rustfix"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
authors = [
55
"Pascal Hertleif <killercup@gmail.com>",
66
"Oliver Schneider <oli-obk@users.noreply.github.com>",

crates/rustfix/src/lib.rs

Lines changed: 6 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,6 @@ pub struct Snippet {
104104
pub file_name: String,
105105
pub line_range: LineRange,
106106
pub range: Range<usize>,
107-
/// leading surrounding text, text to replace, trailing surrounding text
108-
///
109-
/// This split is useful for highlighting the part that gets replaced
110-
pub text: (String, String, String),
111107
}
112108

113109
/// Represents a replacement of a `snippet`.
@@ -119,58 +115,9 @@ pub struct Replacement {
119115
pub replacement: String,
120116
}
121117

122-
/// Parses a [`Snippet`] from a diagnostic span item.
123-
fn parse_snippet(span: &DiagnosticSpan) -> Option<Snippet> {
124-
// unindent the snippet
125-
let indent = span
126-
.text
127-
.iter()
128-
.map(|line| {
129-
let indent = line
130-
.text
131-
.chars()
132-
.take_while(|&c| char::is_whitespace(c))
133-
.count();
134-
std::cmp::min(indent, line.highlight_start - 1)
135-
})
136-
.min()?;
137-
138-
let text_slice = span.text[0].text.chars().collect::<Vec<char>>();
139-
140-
// We subtract `1` because these highlights are 1-based
141-
// Check the `min` so that it doesn't attempt to index out-of-bounds when
142-
// the span points to the "end" of the line. For example, a line of
143-
// "foo\n" with a highlight_start of 5 is intended to highlight *after*
144-
// the line. This needs to compensate since the newline has been removed
145-
// from the text slice.
146-
let start = (span.text[0].highlight_start - 1).min(text_slice.len());
147-
let end = (span.text[0].highlight_end - 1).min(text_slice.len());
148-
let lead = text_slice[indent..start].iter().collect();
149-
let mut body: String = text_slice[start..end].iter().collect();
150-
151-
for line in span.text.iter().take(span.text.len() - 1).skip(1) {
152-
body.push('\n');
153-
body.push_str(&line.text[indent..]);
154-
}
155-
let mut tail = String::new();
156-
let last = &span.text[span.text.len() - 1];
157-
158-
// If we get a DiagnosticSpanLine where highlight_end > text.len(), we prevent an 'out of
159-
// bounds' access by making sure the index is within the array bounds.
160-
// `saturating_sub` is used in case of an empty file
161-
let last_tail_index = last.highlight_end.min(last.text.len()).saturating_sub(1);
162-
let last_slice = last.text.chars().collect::<Vec<char>>();
163-
164-
if span.text.len() > 1 {
165-
body.push('\n');
166-
body.push_str(
167-
&last_slice[indent..last_tail_index]
168-
.iter()
169-
.collect::<String>(),
170-
);
171-
}
172-
tail.push_str(&last_slice[last_tail_index..].iter().collect::<String>());
173-
Some(Snippet {
118+
/// Converts a [`DiagnosticSpan`] to a [`Snippet`].
119+
fn span_to_snippet(span: &DiagnosticSpan) -> Snippet {
120+
Snippet {
174121
file_name: span.file_name.clone(),
175122
line_range: LineRange {
176123
start: LinePosition {
@@ -183,13 +130,12 @@ fn parse_snippet(span: &DiagnosticSpan) -> Option<Snippet> {
183130
},
184131
},
185132
range: (span.byte_start as usize)..(span.byte_end as usize),
186-
text: (lead, body, tail),
187-
})
133+
}
188134
}
189135

190136
/// Converts a [`DiagnosticSpan`] into a [`Replacement`].
191137
fn collect_span(span: &DiagnosticSpan) -> Option<Replacement> {
192-
let snippet = parse_snippet(span)?;
138+
let snippet = span_to_snippet(span);
193139
let replacement = span.suggested_replacement.clone()?;
194140
Some(Replacement {
195141
snippet,
@@ -217,7 +163,7 @@ pub fn collect_suggestions<S: ::std::hash::BuildHasher>(
217163
}
218164
}
219165

220-
let snippets = diagnostic.spans.iter().filter_map(parse_snippet).collect();
166+
let snippets = diagnostic.spans.iter().map(span_to_snippet).collect();
221167

222168
let solutions: Vec<_> = diagnostic
223169
.children
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use a::f;
2+
3+
mod a {
4+
pub fn f() {}
5+
}
6+
7+
fn main() {
8+
f();
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{"$message_type":"diagnostic","message":"cannot find function `f` in this scope","code":{"code":"E0425","explanation":"An unresolved name was used.\n\nErroneous code examples:\n\n```compile_fail,E0425\nsomething_that_doesnt_exist::foo;\n// error: unresolved name `something_that_doesnt_exist::foo`\n\n// or:\n\ntrait Foo {\n fn bar() {\n Self; // error: unresolved name `Self`\n }\n}\n\n// or:\n\nlet x = unknown_variable; // error: unresolved name `unknown_variable`\n```\n\nPlease verify that the name wasn't misspelled and ensure that the\nidentifier being referred to is valid for the given situation. Example:\n\n```\nenum something_that_does_exist {\n Foo,\n}\n```\n\nOr:\n\n```\nmod something_that_does_exist {\n pub static foo : i32 = 0i32;\n}\n\nsomething_that_does_exist::foo; // ok!\n```\n\nOr:\n\n```\nlet unknown_variable = 12u32;\nlet x = unknown_variable; // ok!\n```\n\nIf the item is not defined in the current module, it must be imported using a\n`use` statement, like so:\n\n```\n# mod foo { pub fn bar() {} }\n# fn main() {\nuse foo::bar;\nbar();\n# }\n```\n\nIf the item you are importing is not defined in some super-module of the\ncurrent module, then it must also be declared as public (e.g., `pub fn`).\n"},"level":"error","spans":[{"file_name":"./tests/everything/use-insert.rs","byte_start":45,"byte_end":46,"line_start":6,"line_end":6,"column_start":5,"column_end":6,"is_primary":true,"text":[{"text":" f();","highlight_start":5,"highlight_end":6}],"label":"not found in this scope","suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"consider importing this function","code":null,"level":"help","spans":[{"file_name":"./tests/everything/use-insert.rs","byte_start":0,"byte_end":0,"line_start":1,"line_end":1,"column_start":1,"column_end":1,"is_primary":true,"text":[],"label":null,"suggested_replacement":"use a::f;\n\n","suggestion_applicability":"MaybeIncorrect","expansion":null}],"children":[],"rendered":null}],"rendered":"error[E0425]: cannot find function `f` in this scope\n --> ./tests/everything/use-insert.rs:6:5\n |\n6 | f();\n | ^ not found in this scope\n |\nhelp: consider importing this function\n |\n1 + use a::f;\n |\n\n"}
2+
{"$message_type":"diagnostic","message":"aborting due to 1 previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"error: aborting due to 1 previous error\n\n"}
3+
{"$message_type":"diagnostic","message":"For more information about this error, try `rustc --explain E0425`.","code":null,"level":"failure-note","spans":[],"children":[],"rendered":"For more information about this error, try `rustc --explain E0425`.\n"}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
mod a {
2+
pub fn f() {}
3+
}
4+
5+
fn main() {
6+
f();
7+
}

crates/rustfix/tests/parse_and_replace.rs

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
//! Tests that verify rustfix applies the appropriate changes to a file.
2+
//!
3+
//! This test works by reading a series of `*.rs` files in the
4+
//! `tests/everything` directory. For each `.rs` file, it runs `rustc` to
5+
//! collect JSON diagnostics from the file. It feeds that JSON data into
6+
//! rustfix and applies the recommended suggestions to the `.rs` file. It then
7+
//! compares the result with the corresponding `.fixed.rs` file. If they don't
8+
//! match, then the test fails.
9+
//!
10+
//! There are several debugging environment variables for this test that you can set:
11+
//!
12+
//! - `RUST_LOG=parse_and_replace=debug`: Print debug information.
13+
//! - `RUSTFIX_TEST_BLESS=test-name.rs`: When given the name of a test, this
14+
//! will overwrite the `.json` and `.fixed.rs` files with the expected
15+
//! values. This can be used when adding a new test.
16+
//! - `RUSTFIX_TEST_RECORD_JSON=1`: Records the JSON output to
17+
//! `*.recorded.json` files. You can then move that to `.json` or whatever
18+
//! you need.
19+
//! - `RUSTFIX_TEST_RECORD_FIXED_RUST=1`: Records the fixed result to
20+
//! `*.recorded.rs` files. You can then move that to `.rs` or whatever you
21+
//! need.
22+
123
#![allow(clippy::disallowed_methods, clippy::print_stdout, clippy::print_stderr)]
224

325
use anyhow::{anyhow, ensure, Context, Error};
@@ -20,6 +42,7 @@ mod settings {
2042
pub const CHECK_JSON: &str = "RUSTFIX_TEST_CHECK_JSON";
2143
pub const RECORD_JSON: &str = "RUSTFIX_TEST_RECORD_JSON";
2244
pub const RECORD_FIXED_RUST: &str = "RUSTFIX_TEST_RECORD_FIXED_RUST";
45+
pub const BLESS: &str = "RUSTFIX_TEST_BLESS";
2346
}
2447

2548
fn compile(file: &Path) -> Result<Output, Error> {
@@ -79,15 +102,6 @@ fn compiles_without_errors(file: &Path) -> Result<(), Error> {
79102
}
80103
}
81104

82-
fn read_file(path: &Path) -> Result<String, Error> {
83-
use std::io::Read;
84-
85-
let mut buffer = String::new();
86-
let mut file = fs::File::open(path)?;
87-
file.read_to_string(&mut buffer)?;
88-
Ok(buffer)
89-
}
90-
91105
fn diff(expected: &str, actual: &str) -> String {
92106
use similar::{ChangeTag, TextDiff};
93107
use std::fmt::Write;
@@ -104,11 +118,7 @@ fn diff(expected: &str, actual: &str) -> String {
104118
ChangeTag::Delete => "-",
105119
};
106120
if !different {
107-
write!(
108-
&mut res,
109-
"differences found (+ == actual, - == expected):\n"
110-
)
111-
.unwrap();
121+
writeln!(&mut res, "differences found (+ == actual, - == expected):").unwrap();
112122
different = true;
113123
}
114124
write!(&mut res, "{} {}", prefix, change.value()).unwrap();
@@ -133,23 +143,19 @@ fn test_rustfix_with_file<P: AsRef<Path>>(file: P, mode: &str) -> Result<(), Err
133143
};
134144

135145
debug!("next up: {:?}", file);
136-
let code = read_file(file).context(format!("could not read {}", file.display()))?;
146+
let code = fs::read_to_string(file)?;
137147
let errors =
138148
compile_and_get_json_errors(file).context(format!("could compile {}", file.display()))?;
139149
let suggestions =
140150
rustfix::get_suggestions_from_json(&errors, &HashSet::new(), filter_suggestions)
141151
.context("could not load suggestions")?;
142152

143153
if std::env::var(settings::RECORD_JSON).is_ok() {
144-
use std::io::Write;
145-
let mut recorded_json = fs::File::create(&file.with_extension("recorded.json")).context(
146-
format!("could not create recorded.json for {}", file.display()),
147-
)?;
148-
recorded_json.write_all(errors.as_bytes())?;
154+
fs::write(file.with_extension("recorded.json"), &errors)?;
149155
}
150156

151157
if std::env::var(settings::CHECK_JSON).is_ok() {
152-
let expected_json = read_file(&json_file).context(format!(
158+
let expected_json = fs::read_to_string(&json_file).context(format!(
153159
"could not load json fixtures for {}",
154160
file.display()
155161
))?;
@@ -168,16 +174,23 @@ fn test_rustfix_with_file<P: AsRef<Path>>(file: P, mode: &str) -> Result<(), Err
168174
}
169175

170176
let fixed = apply_suggestions(&code, &suggestions)
171-
.context(format!("could not apply suggestions to {}", file.display()))?;
177+
.context(format!("could not apply suggestions to {}", file.display()))?
178+
.replace('\r', "");
172179

173180
if std::env::var(settings::RECORD_FIXED_RUST).is_ok() {
174-
use std::io::Write;
175-
let mut recorded_rust = fs::File::create(&file.with_extension("recorded.rs"))?;
176-
recorded_rust.write_all(fixed.as_bytes())?;
181+
fs::write(file.with_extension("recorded.rs"), &fixed)?;
182+
}
183+
184+
if let Some(bless_name) = std::env::var_os(settings::BLESS) {
185+
if bless_name == file.file_name().unwrap() {
186+
std::fs::write(&json_file, &errors)?;
187+
std::fs::write(&fixed_file, &fixed)?;
188+
}
177189
}
178190

179-
let expected_fixed =
180-
read_file(&fixed_file).context(format!("could read fixed file for {}", file.display()))?;
191+
let expected_fixed = fs::read_to_string(&fixed_file)
192+
.context(format!("could read fixed file for {}", file.display()))?
193+
.replace('\r', "");
181194
ensure!(
182195
fixed.trim() == expected_fixed.trim(),
183196
"file {} doesn't look fixed:\n{}",
@@ -191,8 +204,7 @@ fn test_rustfix_with_file<P: AsRef<Path>>(file: P, mode: &str) -> Result<(), Err
191204
}
192205

193206
fn get_fixture_files(p: &str) -> Result<Vec<PathBuf>, Error> {
194-
Ok(fs::read_dir(&p)?
195-
.into_iter()
207+
Ok(fs::read_dir(p)?
196208
.map(|e| e.unwrap().path())
197209
.filter(|p| p.is_file())
198210
.filter(|p| {
@@ -203,7 +215,7 @@ fn get_fixture_files(p: &str) -> Result<Vec<PathBuf>, Error> {
203215
}
204216

205217
fn assert_fixtures(dir: &str, mode: &str) {
206-
let files = get_fixture_files(&dir)
218+
let files = get_fixture_files(dir)
207219
.context(format!("couldn't load dir `{}`", dir))
208220
.unwrap();
209221
let mut failures = 0;

0 commit comments

Comments
 (0)