Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Render tabs as 4 spaces in diagnostics #4132

Merged
merged 1 commit into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 64 additions & 11 deletions crates/ruff/src/message/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use bitflags::bitflags;
use colored::Colorize;
use ruff_diagnostics::DiagnosticKind;
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use ruff_text_size::TextRange;
use ruff_text_size::{TextRange, TextSize};
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::io::Write;

Expand Down Expand Up @@ -172,6 +173,7 @@ impl Display for MessageCodeFrame<'_> {
};

let source_code = file.to_source_code();

let content_start_index = source_code.line_index(range.start());
let mut start_index = content_start_index.saturating_sub(2);

Expand Down Expand Up @@ -200,26 +202,23 @@ impl Display for MessageCodeFrame<'_> {
let start_offset = source_code.line_start(start_index);
let end_offset = source_code.line_end(end_index);

let source_text = source_code.slice(TextRange::new(start_offset, end_offset));

let annotation_start_offset = range.start() - start_offset;
let annotation_end_offset = range.end() - start_offset;
let source = replace_whitespace(
source_code.slice(TextRange::new(start_offset, end_offset)),
range - start_offset,
);

let start_char = source_text[TextRange::up_to(annotation_start_offset)]
let start_char = source.text[TextRange::up_to(source.annotation_range.start())]
.chars()
.count();

let char_length = source_text
[TextRange::new(annotation_start_offset, annotation_end_offset)]
.chars()
.count();
let char_length = source.text[source.annotation_range].chars().count();

let label = kind.rule().noqa_code().to_string();

let snippet = Snippet {
title: None,
slices: vec![Slice {
source: source_text,
source: &source.text,
line_start: content_start_index.get(),
annotations: vec![SourceAnnotation {
label: &label,
Expand All @@ -245,6 +244,60 @@ impl Display for MessageCodeFrame<'_> {
}
}

fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
static TAB_SIZE: TextSize = TextSize::new(4);

let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut column = 0;

for (index, m) in source.match_indices(['\t', '\n', '\r']) {
match m {
"\t" => {
let tab_width = TAB_SIZE - TextSize::new(column % 4);

if index < usize::from(annotation_range.start()) {
range += tab_width - TextSize::new(1);
} else if index < usize::from(annotation_range.end()) {
range = range.add_end(tab_width - TextSize::new(1));
}

result.push_str(&source[last_end..index]);

for _ in 0..u32::from(tab_width) {
result.push(' ');
}

last_end = index + 1;
}
"\n" | "\r" => {
column = 0;
}
_ => unreachable!(),
}
}

// No tabs
if result.is_empty() {
SourceCode {
annotation_range,
text: Cow::Borrowed(source),
}
} else {
result.push_str(&source[last_end..]);
SourceCode {
annotation_range: range,
text: Cow::Owned(result),
}
}
}

struct SourceCode<'a> {
text: Cow<'a, str>,
annotation_range: TextRange,
}

#[cfg(test)]
mod tests {
use crate::message::tests::{capture_emitter_output, create_messages};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ E101.py:11:1: E101 Indentation contains mixed spaces and tabs
|
11 | def func_mixed_start_with_tab():
12 | # E101
13 | print("mixed starts with tab")
| ^^ E101
13 | print("mixed starts with tab")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, tabs were counted as two characters but rendered as multiple ones. That's why the caret was off.

| ^^^^^^ E101
14 |
15 | def func_mixed_start_with_space():
|
Expand All @@ -15,8 +15,8 @@ E101.py:15:1: E101 Indentation contains mixed spaces and tabs
|
15 | def func_mixed_start_with_space():
16 | # E101
17 | print("mixed starts with space")
| ^^^^^^^^ E101
17 | print("mixed starts with space")
| ^^^^^^^^^^^^^^^^^^^^ E101
18 |
19 | def xyz():
|
Expand All @@ -25,8 +25,8 @@ E101.py:19:1: E101 Indentation contains mixed spaces and tabs
|
19 | def xyz():
20 | # E101
21 | print("xyz");
| ^^^ E101
21 | print("xyz");
| ^^^^^^^ E101
|


Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ E11.py:42:1: E117 Over-indented
|
42 | #: E117 W191
43 | def start():
44 | print()
| E117
44 | print()
| ^^^^^^^^ E117
|


Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,34 @@ E20.py:6:15: E201 Whitespace after '('
8 | spam(ham[1], { eggs: 2})
| E201
9 | #: E201:1:6
10 | spam( ham[1], {eggs: 2})
10 | spam( ham[1], {eggs: 2})
|

E20.py:8:6: E201 Whitespace after '('
|
8 | spam(ham[1], { eggs: 2})
9 | #: E201:1:6
10 | spam( ham[1], {eggs: 2})
10 | spam( ham[1], {eggs: 2})
| E201
11 | #: E201:1:10
12 | spam(ham[ 1], {eggs: 2})
12 | spam(ham[ 1], {eggs: 2})
|

E20.py:10:10: E201 Whitespace after '('
|
10 | spam( ham[1], {eggs: 2})
10 | spam( ham[1], {eggs: 2})
11 | #: E201:1:10
12 | spam(ham[ 1], {eggs: 2})
12 | spam(ham[ 1], {eggs: 2})
| E201
13 | #: E201:1:15
14 | spam(ham[1], { eggs: 2})
14 | spam(ham[1], { eggs: 2})
|

E20.py:12:15: E201 Whitespace after '('
|
12 | spam(ham[ 1], {eggs: 2})
12 | spam(ham[ 1], {eggs: 2})
13 | #: E201:1:15
14 | spam(ham[1], { eggs: 2})
14 | spam(ham[1], { eggs: 2})
| E201
15 | #: Okay
16 | spam(ham[1], {eggs: 2})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,34 @@ E20.py:23:11: E202 Whitespace before ')'
25 | spam(ham[1 ], {eggs: 2})
| E202
26 | #: E202:1:23
27 | spam(ham[1], {eggs: 2} )
27 | spam(ham[1], {eggs: 2} )
|

E20.py:25:23: E202 Whitespace before ')'
|
25 | spam(ham[1 ], {eggs: 2})
26 | #: E202:1:23
27 | spam(ham[1], {eggs: 2} )
27 | spam(ham[1], {eggs: 2} )
| E202
28 | #: E202:1:22
29 | spam(ham[1], {eggs: 2 })
29 | spam(ham[1], {eggs: 2 })
|

E20.py:27:22: E202 Whitespace before ')'
|
27 | spam(ham[1], {eggs: 2} )
27 | spam(ham[1], {eggs: 2} )
28 | #: E202:1:22
29 | spam(ham[1], {eggs: 2 })
29 | spam(ham[1], {eggs: 2 })
| E202
30 | #: E202:1:11
31 | spam(ham[1 ], {eggs: 2})
31 | spam(ham[1 ], {eggs: 2})
|

E20.py:29:11: E202 Whitespace before ')'
|
29 | spam(ham[1], {eggs: 2 })
29 | spam(ham[1], {eggs: 2 })
30 | #: E202:1:11
31 | spam(ham[1 ], {eggs: 2})
31 | spam(ham[1 ], {eggs: 2})
| E202
32 | #: Okay
33 | spam(ham[1], {eggs: 2})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ E20.py:55:10: E203 Whitespace before ',', ';', or ':'
|
55 | x, y = y, x
56 | #: E203:1:10
57 | if x == 4 :
57 | if x == 4 :
| E203
58 | print x, y
59 | x, y = y, x
Expand All @@ -34,7 +34,7 @@ E20.py:63:15: E203 Whitespace before ',', ';', or ':'
|
63 | #: E203:2:15 E702:2:16
64 | if x == 4:
65 | print x, y ; x, y = y, x
65 | print x, y ; x, y = y, x
| E203
66 | #: E203:3:13
67 | if x == 4:
Expand All @@ -54,7 +54,7 @@ E20.py:71:13: E203 Whitespace before ',', ';', or ':'
|
71 | if x == 4:
72 | print x, y
73 | x, y = y , x
73 | x, y = y , x
| E203
74 | #: Okay
75 | if x == 4:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ E22.py:43:2: E223 Tab before operator
|
43 | #: E223
44 | foobart = 4
45 | a = 3 # aligned with tab
45 | a = 3 # aligned with tab
| E223
46 | #:
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs
E22.py:48:5: E224 Tab after operator
|
48 | #: E224
49 | a += 1
49 | a += 1
| E224
50 | b += 1000
51 | #:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ E27.py:8:3: E271 Multiple spaces after keyword
10 | if 1:
| E271
11 | #: E273
12 | True and False
12 | True and False
|

E27.py:14:6: E271 Multiple spaces after keyword
|
14 | True and False
14 | True and False
15 | #: E271
16 | a and b
| E271
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ E27.py:24:5: E272 Multiple spaces before keyword
26 | this and False
| E272
27 | #: E273
28 | a and b
28 | a and b
|


Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,28 @@ E27.py:10:9: E273 Tab after keyword
|
10 | if 1:
11 | #: E273
12 | True and False
12 | True and False
| E273
13 | #: E273 E274
14 | True and False
14 | True and False
|

E27.py:12:5: E273 Tab after keyword
|
12 | True and False
12 | True and False
13 | #: E273 E274
14 | True and False
14 | True and False
| E273
15 | #: E271
16 | a and b
|

E27.py:12:10: E273 Tab after keyword
|
12 | True and False
12 | True and False
13 | #: E273 E274
14 | True and False
| E273
14 | True and False
| E273
15 | #: E271
16 | a and b
|
Expand All @@ -35,18 +35,18 @@ E27.py:26:6: E273 Tab after keyword
|
26 | this and False
27 | #: E273
28 | a and b
28 | a and b
| E273
29 | #: E274
30 | a and b
30 | a and b
|

E27.py:30:10: E273 Tab after keyword
|
30 | a and b
30 | a and b
31 | #: E273 E274
32 | this and False
| E273
32 | this and False
| E273
33 | #: Okay
34 | from u import (a, b)
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs
---
E27.py:28:3: E274 Tab before keyword
|
28 | a and b
28 | a and b
29 | #: E274
30 | a and b
| E274
30 | a and b
| E274
31 | #: E273 E274
32 | this and False
32 | this and False
|

E27.py:30:6: E274 Tab before keyword
|
30 | a and b
30 | a and b
31 | #: E273 E274
32 | this and False
| E274
32 | this and False
| E274
33 | #: Okay
34 | from u import (a, b)
|
Expand Down
Loading