diff --git a/crates/ruff_linter/src/checkers/logical_lines.rs b/crates/ruff_linter/src/checkers/logical_lines.rs index 1933889387b92e..487327a397bfb1 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 c75cfd4d485698..5aeaced1b31375 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 c25b450d50cb87..032451e9ba548a 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 565fc858e328b1..e0ce2a827cbe95 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 |