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.
This commit is contained in:
Andrew Gallant 2025-01-14 11:55:14 -05:00 committed by Andrew Gallant
parent 75b4ed5ad1
commit 5021f32449
4 changed files with 96 additions and 12 deletions

View file

@ -3,7 +3,7 @@ use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::line_width::IndentWidth;
use crate::registry::{AsRule, Rule};
@ -161,7 +161,13 @@ pub(crate) fn check_logical_lines(
let range = if first_token.kind() == TokenKind::Indent {
first_token.range()
} else {
TextRange::new(locator.line_start(first_token.start()), first_token.start())
let mut range =
TextRange::new(locator.line_start(first_token.start()), first_token.start());
if range.is_empty() {
let end = locator.ceil_char_boundary(range.start() + TextSize::from(1));
range = TextRange::new(range.start(), end);
}
range
};
let indent_level = expand_indent(locator.slice(range), settings.tab_size);

View file

@ -118,6 +118,86 @@ impl<'a> Locator<'a> {
}
}
/// Finds the closest [`TextSize`] not less than the offset given for which
/// `is_char_boundary` is `true`. Unless the offset given is greater than
/// the length of the underlying contents, in which case, the length of the
/// contents is returned.
///
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
///
/// # Examples
///
/// From `std`:
///
/// ```
/// use ruff_text_size::{Ranged, TextSize};
/// use ruff_linter::Locator;
///
/// let locator = Locator::new("❤️🧡💛💚💙💜");
/// assert_eq!(locator.text_len(), TextSize::from(26));
/// assert!(!locator.contents().is_char_boundary(13));
///
/// let closest = locator.ceil_char_boundary(TextSize::from(13));
/// assert_eq!(closest, TextSize::from(14));
/// assert_eq!(&locator.contents()[..closest.to_usize()], "❤️🧡💛");
/// ```
///
/// Additional examples:
///
/// ```
/// use ruff_text_size::{Ranged, TextRange, TextSize};
/// use ruff_linter::Locator;
///
/// let locator = Locator::new("Hello");
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(5)),
/// TextSize::from(5)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(6)),
/// TextSize::from(5)
/// );
///
/// let locator = Locator::new("α");
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(1)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(2)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(3)),
/// TextSize::from(2)
/// );
/// ```
pub fn ceil_char_boundary(&self, offset: TextSize) -> TextSize {
let upper_bound = offset
.to_u32()
.saturating_add(4)
.min(self.text_len().to_u32());
(offset.to_u32()..upper_bound)
.map(TextSize::from)
.find(|offset| self.contents.is_char_boundary(offset.to_usize()))
.unwrap_or_else(|| TextSize::from(upper_bound))
}
/// Take the source code between the given [`TextRange`].
#[inline]
pub fn slice<T: Ranged>(&self, ranged: T) -> &'a str {

View file

@ -1,13 +1,12 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
snapshot_kind: text
---
E11.py:9:1: E112 Expected an indented block
|
7 | #: E112
8 | if False:
9 | print()
| E112
| ^ E112
10 | #: E113
11 | print()
|
@ -47,7 +46,7 @@ E11.py:45:1: E112 Expected an indented block
43 | #: E112
44 | if False: #
45 | print()
| E112
| ^ E112
46 | #:
47 | if False:
|

View file

@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
snapshot_kind: text
---
E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
|
@ -37,7 +36,7 @@ E11.py:30:1: E115 Expected an indented block (comment)
28 | def start(self):
29 | if True:
30 | # try:
| E115
| ^ E115
31 | # self.master.start()
32 | # except MasterExit:
|
@ -47,7 +46,7 @@ E11.py:31:1: E115 Expected an indented block (comment)
29 | if True:
30 | # try:
31 | # self.master.start()
| E115
| ^ E115
32 | # except MasterExit:
33 | # self.shutdown()
|
@ -57,7 +56,7 @@ E11.py:32:1: E115 Expected an indented block (comment)
30 | # try:
31 | # self.master.start()
32 | # except MasterExit:
| E115
| ^ E115
33 | # self.shutdown()
34 | # finally:
|
@ -67,7 +66,7 @@ E11.py:33:1: E115 Expected an indented block (comment)
31 | # self.master.start()
32 | # except MasterExit:
33 | # self.shutdown()
| E115
| ^ E115
34 | # finally:
35 | # sys.exit()
|
@ -77,7 +76,7 @@ E11.py:34:1: E115 Expected an indented block (comment)
32 | # except MasterExit:
33 | # self.shutdown()
34 | # finally:
| E115
| ^ E115
35 | # sys.exit()
36 | self.master.start()
|
@ -87,7 +86,7 @@ E11.py:35:1: E115 Expected an indented block (comment)
33 | # self.shutdown()
34 | # finally:
35 | # sys.exit()
| E115
| ^ E115
36 | self.master.start()
37 | #: E117
|