From 5021f3244909c0513ea434f1d7f29535b2cfca34 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 14 Jan 2025 11:55:14 -0500 Subject: [PATCH] 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. --- .../ruff_linter/src/checkers/logical_lines.rs | 10 ++- crates/ruff_linter/src/locator.rs | 80 +++++++++++++++++++ ...ules__pycodestyle__tests__E112_E11.py.snap | 5 +- ...ules__pycodestyle__tests__E115_E11.py.snap | 13 ++- 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/src/checkers/logical_lines.rs b/crates/ruff_linter/src/checkers/logical_lines.rs index 1933889387..487327a397 100644 --- a/crates/ruff_linter/src/checkers/logical_lines.rs +++ b/crates/ruff_linter/src/checkers/logical_lines.rs @@ -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); diff --git a/crates/ruff_linter/src/locator.rs b/crates/ruff_linter/src/locator.rs index c75cfd4d48..5aeaced1b3 100644 --- a/crates/ruff_linter/src/locator.rs +++ b/crates/ruff_linter/src/locator.rs @@ -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(&self, ranged: T) -> &'a str { diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap index c25b450d50..032451e9ba 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap @@ -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: | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap index 565fc858e3..e0ce2a827c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap @@ -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 |