Skip to content

Commit 1055162

Browse files
pnkfelixUbuntu
authored and
Ubuntu
committed
Tweak diagnostic for use suggestion to blank text surrounding span.
Our suggestion system is oriented around diff's to code, and in that context, if your spans are perfect, everything works out. But our spans are not always perfect, due to macros. In the specific case of issue 87613, the #[tokio::main] attribute macro works by rewriting an `async fn main` into an `fn main`, by just removing the `async` token. The problem is that the `async` text remains in the source code, and the span of the code rewritten by suggestion system happily transcribes all the text on the line of that `fn main` when generating a `use` suggestion, yielding the absurd looking `async use std::process::Command;` suggestion. This patch works around this whole problem by adding a way for suggestions to indicate that their transcriptions should not include the original source code; just its *whitespace*. This leads to a happy place where the suggested line numbers are correct and the indentation is usually also correct, while avoiding the `async use` output we would see before.
1 parent 3831aaa commit 1055162

File tree

6 files changed

+154
-6
lines changed

6 files changed

+154
-6
lines changed

compiler/rustc_errors/src/diagnostic.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ impl Diagnostic {
350350
msg: msg.to_owned(),
351351
style,
352352
applicability,
353+
transcription: Default::default(),
353354
tool_metadata: Default::default(),
354355
});
355356
self
@@ -378,6 +379,7 @@ impl Diagnostic {
378379
msg: msg.to_owned(),
379380
style: SuggestionStyle::CompletelyHidden,
380381
applicability,
382+
transcription: Default::default(),
381383
tool_metadata: Default::default(),
382384
});
383385
self
@@ -433,6 +435,7 @@ impl Diagnostic {
433435
msg: msg.to_owned(),
434436
style,
435437
applicability,
438+
transcription: Default::default(),
436439
tool_metadata: Default::default(),
437440
});
438441
self
@@ -476,6 +479,7 @@ impl Diagnostic {
476479
msg: msg.to_owned(),
477480
style: SuggestionStyle::ShowCode,
478481
applicability,
482+
transcription: Default::default(),
479483
tool_metadata: Default::default(),
480484
});
481485
self
@@ -501,6 +505,7 @@ impl Diagnostic {
501505
msg: msg.to_owned(),
502506
style: SuggestionStyle::ShowCode,
503507
applicability,
508+
transcription: Default::default(),
504509
tool_metadata: Default::default(),
505510
});
506511
self
@@ -583,6 +588,7 @@ impl Diagnostic {
583588
msg: msg.to_owned(),
584589
style: SuggestionStyle::CompletelyHidden,
585590
applicability,
591+
transcription: Default::default(),
586592
tool_metadata: ToolMetadata::new(tool_metadata),
587593
})
588594
}

compiler/rustc_errors/src/lib.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub use rustc_lint_defs::{pluralize, Applicability};
3131
use rustc_serialize::json::Json;
3232
use rustc_serialize::{Decodable, Decoder, Encodable, Encoder};
3333
use rustc_span::source_map::SourceMap;
34-
use rustc_span::{Loc, MultiSpan, Span};
34+
use rustc_span::{Loc, MultiSpan, SourceFile, Span};
3535

3636
use std::borrow::Cow;
3737
use std::hash::{Hash, Hasher};
@@ -142,6 +142,10 @@ pub struct CodeSuggestion {
142142
pub msg: String,
143143
/// Visual representation of this suggestion.
144144
pub style: SuggestionStyle,
145+
/// Role of context of spans. Usually the context around every
146+
/// span should be included in the output. But sometimes the
147+
/// suggestion stands entirely on its own.
148+
pub transcription: Transcription,
145149
/// Whether or not the suggestion is approximate
146150
///
147151
/// Sometimes we may show suggestions with placeholders,
@@ -152,6 +156,24 @@ pub struct CodeSuggestion {
152156
pub tool_metadata: ToolMetadata,
153157
}
154158

159+
#[derive(Clone, Debug, PartialEq, Hash, Encodable, Decodable)]
160+
pub enum Transcription {
161+
/// The characters in the line(s) around each span should be
162+
/// included in the output.
163+
Copy,
164+
165+
/// The span is solely providing the indentation and lexical
166+
/// context; the source code contents of the surrounding code is
167+
/// irrelevant and should not be included in the suggestion.
168+
Blank,
169+
}
170+
171+
impl Default for Transcription {
172+
fn default() -> Self {
173+
Transcription::Copy
174+
}
175+
}
176+
155177
#[derive(Clone, Debug, PartialEq, Hash, Encodable, Decodable)]
156178
/// See the docs on `CodeSuggestion::substitutions`
157179
pub struct Substitution {
@@ -193,6 +215,19 @@ impl SubstitutionPart {
193215
}
194216

195217
impl CodeSuggestion {
218+
fn get_line<'a>(&self, sf: &'a SourceFile, line_number: usize) -> Option<Cow<'a, str>> {
219+
self.transcribe(sf.get_line(line_number))
220+
}
221+
222+
fn transcribe<'a>(&self, text: Option<Cow<'a, str>>) -> Option<Cow<'a, str>> {
223+
match self.transcription {
224+
Transcription::Copy => text,
225+
Transcription::Blank => text.map(|s| {
226+
s.chars().map(|c| if c.is_whitespace() { c } else { ' ' }).collect()
227+
}),
228+
}
229+
}
230+
196231
/// Returns the assembled code suggestions, whether they should be shown with an underline
197232
/// and whether the substitution only differs in capitalization.
198233
pub fn splice_lines(
@@ -283,7 +318,7 @@ impl CodeSuggestion {
283318
let mut prev_hi = sm.lookup_char_pos(bounding_span.lo());
284319
prev_hi.col = CharPos::from_usize(0);
285320
let mut prev_line =
286-
lines.lines.get(0).and_then(|line0| sf.get_line(line0.line_index));
321+
lines.lines.get(0).and_then(|line0| self.get_line(sf, line0.line_index));
287322
let mut buf = String::new();
288323

289324
let mut line_highlight = vec![];
@@ -310,13 +345,13 @@ impl CodeSuggestion {
310345
}
311346
// push lines between the previous and current span (if any)
312347
for idx in prev_hi.line..(cur_lo.line - 1) {
313-
if let Some(line) = sf.get_line(idx) {
348+
if let Some(line) = self.get_line(sf, idx) {
314349
buf.push_str(line.as_ref());
315350
buf.push('\n');
316351
highlights.push(std::mem::take(&mut line_highlight));
317352
}
318353
}
319-
if let Some(cur_line) = sf.get_line(cur_lo.line - 1) {
354+
if let Some(cur_line) = self.get_line(sf, cur_lo.line - 1) {
320355
let end = match cur_line.char_indices().nth(cur_lo.col.to_usize()) {
321356
Some((i, _)) => i,
322357
None => cur_line.len(),
@@ -349,7 +384,7 @@ impl CodeSuggestion {
349384
acc += len as isize - (cur_hi.col.0 - cur_lo.col.0) as isize;
350385
}
351386
prev_hi = cur_hi;
352-
prev_line = sf.get_line(prev_hi.line - 1);
387+
prev_line = self.get_line(sf, prev_hi.line - 1);
353388
for line in part.snippet.split('\n').skip(1) {
354389
acc = 0;
355390
highlights.push(std::mem::take(&mut line_highlight));

compiler/rustc_resolve/src/diagnostics.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::ptr;
44
use rustc_ast::{self as ast, Path};
55
use rustc_ast_pretty::pprust;
66
use rustc_data_structures::fx::FxHashSet;
7-
use rustc_errors::{struct_span_err, Applicability, DiagnosticBuilder};
7+
use rustc_errors::{struct_span_err, Applicability, DiagnosticBuilder, Transcription};
88
use rustc_feature::BUILTIN_ATTRIBUTES;
99
use rustc_hir::def::Namespace::{self, *};
1010
use rustc_hir::def::{self, CtorKind, CtorOf, DefKind, NonMacroAttrKind};
@@ -1843,6 +1843,11 @@ crate fn show_candidates(
18431843
accessible_path_strings.into_iter().map(|a| a.0),
18441844
Applicability::Unspecified,
18451845
);
1846+
let last_idx = err.suggestions.len() - 1;
1847+
// rust#87613: for `use` suggestions, transcribed source
1848+
// code can yield incorrect suggestions. Instead, use
1849+
// source span solely to establish line number and indent.
1850+
err.suggestions[last_idx].transcription = Transcription::Blank;
18461851
} else {
18471852
msg.push(':');
18481853

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// aux-build:amputate-span.rs
2+
// edition:2018
3+
// compile-flags: --extern amputate_span
4+
5+
// This test has been crafted to ensure the following things:
6+
//
7+
// 1. There's a resolution error that prompts the compiler to suggest
8+
// adding a `use` item.
9+
//
10+
// 2. There are no `use` or `extern crate` items in the source
11+
// code. In fact, there is only one item, the `fn main`
12+
// declaration.
13+
//
14+
// 3. The single `fn main` declaration has an attribute attached to it
15+
// that just deletes the first token from the given item.
16+
//
17+
// You need all of these conditions to hold in order to replicate the
18+
// scenario that yielded issue 87613, where the compiler's suggestion
19+
// looks like:
20+
//
21+
// ```
22+
// help: consider importing this struct
23+
// |
24+
// 47 | hey */ async use std::process::Command;
25+
// | ++++++++++++++++++++++++++
26+
// ```
27+
//
28+
// The first condition is necessary to force the compiler issue a
29+
// suggestion. The second condition is necessary to force the
30+
// suggestion to be issued at a span associated with the sole
31+
// `fn`-item of this crate. The third condition is necessary in order
32+
// to yield the weird state where the associated span of the `fn`-item
33+
// does not actually cover all of the original source code of the
34+
// `fn`-item (which is why we are calling it an "amputated" span
35+
// here).
36+
//
37+
// Note that satisfying conditions 2 and 3 requires the use of the
38+
// `--extern` compile flag.
39+
//
40+
// You might ask yourself: What code would do such a thing? The
41+
// answer is: the #[tokio::main] attribute does *exactly* this (as
42+
// well as injecting some other code into the `fn main` that it
43+
// constructs).
44+
45+
#[amputate_span::drop_first_token]
46+
/* what the
47+
hey */ async fn main() {
48+
Command::new("git"); //~ ERROR [E0433]
49+
}
50+
51+
// (The /* ... */ comment in the above is not part of the original
52+
// bug. It is just meant to illustrate one particular facet of the
53+
// original non-ideal behavior, where we were transcribing the
54+
// trailing comment as part of the emitted suggestion, for better or
55+
// for worse.)
56+
57+
mod inner {
58+
#[amputate_span::drop_first_token]
59+
/* another interesting
60+
case */ async fn foo() {
61+
Command::new("git"); //~ ERROR [E0433]
62+
}
63+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
error[E0433]: failed to resolve: use of undeclared type `Command`
2+
--> $DIR/amputate-span.rs:48:5
3+
|
4+
LL | Command::new("git");
5+
| ^^^^^^^ not found in this scope
6+
|
7+
help: consider importing this struct
8+
|
9+
LL | use std::process::Command;
10+
|
11+
12+
error[E0433]: failed to resolve: use of undeclared type `Command`
13+
--> $DIR/amputate-span.rs:61:2
14+
|
15+
LL | Command::new("git");
16+
| ^^^^^^^ not found in this scope
17+
|
18+
help: consider importing this struct
19+
|
20+
LL | use std::process::Command;
21+
|
22+
23+
error: aborting due to 2 previous errors
24+
25+
For more information about this error, try `rustc --explain E0433`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// force-host
2+
// no-prefer-dynamic
3+
4+
#![crate_type = "proc-macro"]
5+
6+
extern crate proc_macro;
7+
8+
use proc_macro::TokenStream;
9+
10+
#[proc_macro_attribute]
11+
pub fn drop_first_token(attr: TokenStream, input: TokenStream) -> TokenStream {
12+
assert!(attr.is_empty());
13+
input.into_iter().skip(1).collect()
14+
}

0 commit comments

Comments
 (0)