Skip to content

Commit 5021f32

Browse files
committed
test: another update to add back a caret
This change also requires some shuffling to the offsets we generate for the diagnostic. Previously, we were generating an empty range immediately *after* the line terminator and immediate before the first byte of the subsequent line. How this is rendered is somewhat open to interpretation, but the new version of `annotate-snippets` chooses to render this at the end of the preceding line instead of the beginning of the following line. In this case, we want the diagnostic to point to the beginning of the following line. So we either need to change `annotate-snippets` to render such spans at the beginning of the following line, or we need to change our span to point to the first full character in the following line. The latter will force `annotate-snippets` to move the caret to the proper location. I ended up deciding to change our spans instead of changing how `annotate-snippets` renders empty spans after a line terminator. While I didn't investigate it, my guess is that they probably had good reason for doing so, and it doesn't necessarily strike me as _wrong_. Furthermore, fixing up our spans seems like a good idea regardless, and was pretty easy to do.
1 parent 75b4ed5 commit 5021f32

File tree

4 files changed

+96
-12
lines changed

4 files changed

+96
-12
lines changed

crates/ruff_linter/src/checkers/logical_lines.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use ruff_python_codegen::Stylist;
33
use ruff_python_index::Indexer;
44
use ruff_python_parser::{TokenKind, Tokens};
55
use ruff_source_file::LineRanges;
6-
use ruff_text_size::{Ranged, TextRange};
6+
use ruff_text_size::{Ranged, TextRange, TextSize};
77

88
use crate::line_width::IndentWidth;
99
use crate::registry::{AsRule, Rule};
@@ -161,7 +161,13 @@ pub(crate) fn check_logical_lines(
161161
let range = if first_token.kind() == TokenKind::Indent {
162162
first_token.range()
163163
} else {
164-
TextRange::new(locator.line_start(first_token.start()), first_token.start())
164+
let mut range =
165+
TextRange::new(locator.line_start(first_token.start()), first_token.start());
166+
if range.is_empty() {
167+
let end = locator.ceil_char_boundary(range.start() + TextSize::from(1));
168+
range = TextRange::new(range.start(), end);
169+
}
170+
range
165171
};
166172

167173
let indent_level = expand_indent(locator.slice(range), settings.tab_size);

crates/ruff_linter/src/locator.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,86 @@ impl<'a> Locator<'a> {
118118
}
119119
}
120120

121+
/// Finds the closest [`TextSize`] not less than the offset given for which
122+
/// `is_char_boundary` is `true`. Unless the offset given is greater than
123+
/// the length of the underlying contents, in which case, the length of the
124+
/// contents is returned.
125+
///
126+
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
127+
///
128+
/// # Examples
129+
///
130+
/// From `std`:
131+
///
132+
/// ```
133+
/// use ruff_text_size::{Ranged, TextSize};
134+
/// use ruff_linter::Locator;
135+
///
136+
/// let locator = Locator::new("❤️🧡💛💚💙💜");
137+
/// assert_eq!(locator.text_len(), TextSize::from(26));
138+
/// assert!(!locator.contents().is_char_boundary(13));
139+
///
140+
/// let closest = locator.ceil_char_boundary(TextSize::from(13));
141+
/// assert_eq!(closest, TextSize::from(14));
142+
/// assert_eq!(&locator.contents()[..closest.to_usize()], "❤️🧡💛");
143+
/// ```
144+
///
145+
/// Additional examples:
146+
///
147+
/// ```
148+
/// use ruff_text_size::{Ranged, TextRange, TextSize};
149+
/// use ruff_linter::Locator;
150+
///
151+
/// let locator = Locator::new("Hello");
152+
///
153+
/// assert_eq!(
154+
/// locator.ceil_char_boundary(TextSize::from(0)),
155+
/// TextSize::from(0)
156+
/// );
157+
///
158+
/// assert_eq!(
159+
/// locator.ceil_char_boundary(TextSize::from(5)),
160+
/// TextSize::from(5)
161+
/// );
162+
///
163+
/// assert_eq!(
164+
/// locator.ceil_char_boundary(TextSize::from(6)),
165+
/// TextSize::from(5)
166+
/// );
167+
///
168+
/// let locator = Locator::new("α");
169+
///
170+
/// assert_eq!(
171+
/// locator.ceil_char_boundary(TextSize::from(0)),
172+
/// TextSize::from(0)
173+
/// );
174+
///
175+
/// assert_eq!(
176+
/// locator.ceil_char_boundary(TextSize::from(1)),
177+
/// TextSize::from(2)
178+
/// );
179+
///
180+
/// assert_eq!(
181+
/// locator.ceil_char_boundary(TextSize::from(2)),
182+
/// TextSize::from(2)
183+
/// );
184+
///
185+
/// assert_eq!(
186+
/// locator.ceil_char_boundary(TextSize::from(3)),
187+
/// TextSize::from(2)
188+
/// );
189+
/// ```
190+
pub fn ceil_char_boundary(&self, offset: TextSize) -> TextSize {
191+
let upper_bound = offset
192+
.to_u32()
193+
.saturating_add(4)
194+
.min(self.text_len().to_u32());
195+
(offset.to_u32()..upper_bound)
196+
.map(TextSize::from)
197+
.find(|offset| self.contents.is_char_boundary(offset.to_usize()))
198+
.unwrap_or_else(|| TextSize::from(upper_bound))
199+
}
200+
121201
/// Take the source code between the given [`TextRange`].
122202
#[inline]
123203
pub fn slice<T: Ranged>(&self, ranged: T) -> &'a str {

crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
---
22
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
3-
snapshot_kind: text
43
---
54
E11.py:9:1: E112 Expected an indented block
65
|
76
7 | #: E112
87
8 | if False:
98
9 | print()
10-
| E112
9+
| ^ E112
1110
10 | #: E113
1211
11 | print()
1312
|
@@ -47,7 +46,7 @@ E11.py:45:1: E112 Expected an indented block
4746
43 | #: E112
4847
44 | if False: #
4948
45 | print()
50-
| E112
49+
| ^ E112
5150
46 | #:
5251
47 | if False:
5352
|

crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
3-
snapshot_kind: text
43
---
54
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
65
|
@@ -37,7 +36,7 @@ E11.py:30:1: E115 Expected an indented block (comment)
3736
28 | def start(self):
3837
29 | if True:
3938
30 | # try:
40-
| E115
39+
| ^ E115
4140
31 | # self.master.start()
4241
32 | # except MasterExit:
4342
|
@@ -47,7 +46,7 @@ E11.py:31:1: E115 Expected an indented block (comment)
4746
29 | if True:
4847
30 | # try:
4948
31 | # self.master.start()
50-
| E115
49+
| ^ E115
5150
32 | # except MasterExit:
5251
33 | # self.shutdown()
5352
|
@@ -57,7 +56,7 @@ E11.py:32:1: E115 Expected an indented block (comment)
5756
30 | # try:
5857
31 | # self.master.start()
5958
32 | # except MasterExit:
60-
| E115
59+
| ^ E115
6160
33 | # self.shutdown()
6261
34 | # finally:
6362
|
@@ -67,7 +66,7 @@ E11.py:33:1: E115 Expected an indented block (comment)
6766
31 | # self.master.start()
6867
32 | # except MasterExit:
6968
33 | # self.shutdown()
70-
| E115
69+
| ^ E115
7170
34 | # finally:
7271
35 | # sys.exit()
7372
|
@@ -77,7 +76,7 @@ E11.py:34:1: E115 Expected an indented block (comment)
7776
32 | # except MasterExit:
7877
33 | # self.shutdown()
7978
34 | # finally:
80-
| E115
79+
| ^ E115
8180
35 | # sys.exit()
8281
36 | self.master.start()
8382
|
@@ -87,7 +86,7 @@ E11.py:35:1: E115 Expected an indented block (comment)
8786
33 | # self.shutdown()
8887
34 | # finally:
8988
35 | # sys.exit()
90-
| E115
89+
| ^ E115
9190
36 | self.master.start()
9291
37 | #: E117
9392
|

0 commit comments

Comments
 (0)