Skip to content

Span off-by-one error in diagnostics #58270

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
estebank opened this issue Feb 7, 2019 · 11 comments · Fixed by #60160 or #60250
Closed

Span off-by-one error in diagnostics #58270

estebank opened this issue Feb 7, 2019 · 11 comments · Fixed by #60160 or #60250
Labels
A-diagnostics Area: Messages for errors, warnings, and lints E-easy Call for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue. E-mentor Call for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion.

Comments

@estebank
Copy link
Contributor

estebank commented Feb 7, 2019

When two diagnostic spans are of the same width/location (effectively the same) and only one of them has a label, the output is the same as for a single one. There's a corner case where having a zero-width span (which get's rendered as a 1-char-wide span) fall in the same place as a 1-char-wide span without a label, resulting in the following output:

{foo(bar {}}
    -      ^
    |      |
    |      help: `)` may belong here
    |
    unclosed delimiter

This should instead be

{foo(bar {}}
    -      ^ help: `)` may belong here
    |
    unclosed delimiter
@estebank estebank added E-easy Call for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue. A-diagnostics Area: Messages for errors, warnings, and lints labels Feb 7, 2019
@relchar
Copy link

relchar commented Feb 7, 2019

Happy to take this on if no one else has. It'll be my first time contributing to Rust.

@estebank
Copy link
Contributor Author

estebank commented Feb 8, 2019

@relchar that'd be awesome!

The actual case that made the bug evident is being introduced in #57944 (if you look at https://github.com/rust-lang/rust/pull/57944/files#diff-beb01736b1c52103f2dcd2eac341b810R4).

The following is the code that turns small suggestions into an inline label:

if let Some((sugg, rest)) = db.suggestions.split_first() {
if rest.is_empty() &&
// don't display multi-suggestions as labels
sugg.substitutions.len() == 1 &&
// don't display multipart suggestions as labels
sugg.substitutions[0].parts.len() == 1 &&
// don't display long messages as labels
sugg.msg.split_whitespace().count() < 10 &&
// don't display multiline suggestions as labels
!sugg.substitutions[0].parts[0].snippet.contains('\n') {
let substitution = &sugg.substitutions[0].parts[0].snippet.trim();
let msg = if substitution.len() == 0 || !sugg.show_code_when_inline {
// This substitution is only removal or we explicitly don't want to show the
// code inline, don't show it
format!("help: {}", sugg.msg)
} else {
format!("help: {}: `{}`", sugg.msg, substitution)
};
primary_span.push_span_label(sugg.substitutions[0].parts[0].span, msg);
} else {
// if there are multiple suggestions, print them all in full
// to be consistent. We could try to figure out if we can
// make one (or the first one) inline, but that would give
// undue importance to a semi-random suggestion
suggestions = &db.suggestions;
}
}

the following is the code where we account for zero-width spans

// Watch out for "empty spans". If we get a span like 6..6, we
// want to just display a `^` at 6, so convert that to
// 6..7. This is degenerate input, but it's best to degrade
// gracefully -- and the parser likes to supply a span like
// that for EOF, in particular.
if lo.col_display == hi.col_display && lo.line == hi.line {
hi.col_display += 1;
}

and finally here is the logic for displaying spans, which is a bit verbose, but at least it has some comments that explain what is supposed to be going on (this is where I would expect the fix to consider zero-width spans as a 1 char wide span to be made)

// First, figure out where each label will be positioned.
//
// In the case where you have the following annotations:
//
// vec.push(vec.pop().unwrap());
// -------- - previous borrow ends here [C]
// ||
// |this makes no sense [B]
// previous borrow of `vec` occurs here [A]
//
// `annotations_position` will hold [(2, A), (1, B), (0, C)].
//
// We try, when possible, to stick the rightmost annotation at the end
// of the highlight line:
//
// vec.push(vec.pop().unwrap());
// --- --- - previous borrow ends here
//
// But sometimes that's not possible because one of the other
// annotations overlaps it. For example, from the test
// `span_overlap_label`, we have the following annotations
// (written on distinct lines for clarity):
//
// fn foo(x: u32) {
// --------------
// -
//
// In this case, we can't stick the rightmost-most label on
// the highlight line, or we would get:
//
// fn foo(x: u32) {
// -------- x_span
// |
// fn_span
//
// which is totally weird. Instead we want:
//
// fn foo(x: u32) {
// --------------
// | |
// | x_span
// fn_span
//
// which is...less weird, at least. In fact, in general, if
// the rightmost span overlaps with any other span, we should
// use the "hang below" version, so we can at least make it
// clear where the span *starts*. There's an exception for this
// logic, when the labels do not have a message:
//
// fn foo(x: u32) {
// --------------
// |
// x_span
//
// instead of:
//
// fn foo(x: u32) {
// --------------
// | |
// | x_span
// <EMPTY LINE>
//
let mut annotations_position = vec![];
let mut line_len = 0;
let mut p = 0;
for (i, annotation) in annotations.iter().enumerate() {
for (j, next) in annotations.iter().enumerate() {
if overlaps(next, annotation, 0) // This label overlaps with another one and both
&& annotation.has_label() // take space (they have text and are not
&& j > i // multiline lines).
&& p == 0 // We're currently on the first line, move the label one line down
{
// This annotation needs a new line in the output.
p += 1;
break;
}
}
annotations_position.push((p, annotation));
for (j, next) in annotations.iter().enumerate() {
if j > i {
let l = if let Some(ref label) = next.label {
label.len() + 2
} else {
0
};
if (overlaps(next, annotation, l) // Do not allow two labels to be in the same
// line if they overlap including padding, to
// avoid situations like:
//
// fn foo(x: u32) {
// -------^------
// | |
// fn_spanx_span
//
&& annotation.has_label() // Both labels must have some text, otherwise
&& next.has_label()) // they are not overlapping.
// Do not add a new line if this annotation
// or the next are vertical line placeholders.
|| (annotation.takes_space() // If either this or the next annotation is
&& next.has_label()) // multiline start/end, move it to a new line
|| (annotation.has_label() // so as not to overlap the orizontal lines.
&& next.takes_space())
|| (annotation.takes_space() && next.takes_space())
|| (overlaps(next, annotation, l)
&& next.end_col <= annotation.end_col
&& next.has_label()
&& p == 0) // Avoid #42595.
{
// This annotation needs a new line in the output.
p += 1;
break;
}
}
}
if line_len < p {
line_len = p;
}
}
if line_len != 0 {
line_len += 1;
}

Feel free to reach out to anyone in the team either here or at https://rust-lang.zulipchat.com/#narrow/stream/147480-t-compiler.2Fdiagnostics

Please take a look at The Forge, which has a lot of relevant information on how to work with the rustc codebase, particularly how to run tests (usually, ./x.py test src/test/ui --stage 1 --bless).

@gs0510
Copy link

gs0510 commented Feb 27, 2019

@relchar Are you working on this? If not can I work on this issue? Thanks!

@relchar
Copy link

relchar commented Feb 27, 2019 via email

@saleemjaffer
Copy link
Contributor

@gs0510 Are you working on this? Please let me know if I can work on this.

@oli-obk oli-obk added the E-mentor Call for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion. label Mar 13, 2019
@gs0510
Copy link

gs0510 commented Mar 13, 2019

Hi @saleemjaffer, yes I'm working on it. :) I will have a PR open by EOD today.

@xldenis
Copy link
Contributor

xldenis commented Apr 16, 2019

@gs0510 have you had a chance to work on this? Or would you like me to take over?

@gs0510
Copy link

gs0510 commented Apr 16, 2019

hey @xldenis, please feel free to take over. Thanks!

@xldenis
Copy link
Contributor

xldenis commented Apr 16, 2019

@estebank is there any reason to ever keep a zero width annotation with no label?

@estebank
Copy link
Contributor Author

@xldenis zero width annotations are not always zero width conceptually. They mark a place in between two characters, and are often used for suggestions to mark a place where we'll add code or by diagnostics to point at a place in between two tokens. Sometimes having a label doesn't increase the understanding of the error. In the example shown above, we have a suggestion to add ) in between the two } characters and we also have the primary span pointing at the closing }. Does that make sense?

@xldenis
Copy link
Contributor

xldenis commented Apr 18, 2019

yea it does, I need to do some more debugging and investigating but I think I'll be able to come up with a fix for this edge-case soon :)

xldenis added a commit to xldenis/rust that referenced this issue Apr 22, 2019
Centril added a commit to Centril/rust that referenced this issue Apr 25, 2019
…annotation, r=estebank

Fix rust-lang#58270, fix off-by-one error in error diagnostics.

This fixes rust-lang#58270 by checking if two diagnostics overlap completely when we're calculating the line offset for each message.
bors added a commit that referenced this issue Apr 25, 2019
Rollup of 6 pull requests

Successful merges:

 - #59560 (MIR generation cleanup)
 - #59697 (tweak unresolved label suggestion)
 - #60038 (Add codegen test for PGO instrumentation.)
 - #60160 (Fix #58270, fix off-by-one error in error diagnostics.)
 - #60185 (Reexport IntErrorKind in std)
 - #60243 (Add regression test for #53249.)

Failed merges:

r? @ghost
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
A-diagnostics Area: Messages for errors, warnings, and lints E-easy Call for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue. E-mentor Call for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion.
Projects
None yet
6 participants