diff --git a/crates/ruff_linter/src/directives.rs b/crates/ruff_linter/src/directives.rs index 87095f9ac6..d3e8a0a342 100644 --- a/crates/ruff_linter/src/directives.rs +++ b/crates/ruff_linter/src/directives.rs @@ -156,10 +156,10 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer // the inner f-strings. let mut last_fstring_range: TextRange = TextRange::default(); for fstring_range in indexer.fstring_ranges().values() { - if !locator.contains_line_break(*fstring_range) { + if !locator.contains_line_break(fstring_range.range()) { continue; } - if last_fstring_range.contains_range(*fstring_range) { + if last_fstring_range.contains_range(fstring_range.range()) { continue; } let new_range = TextRange::new( diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index e98f33bcf7..b808c37768 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -728,23 +728,65 @@ impl<'a> NoqaDirectives<'a> { } } +pub enum Completeness { + Complete, + Incomplete, +} + +pub struct NoqaOffset { + range: TextRange, + completeness: Completeness, +} + +impl NoqaOffset { + pub fn new(range: TextRange, completeness: Completeness) -> Self { + Self { + range, + completeness, + } + } + + pub fn completeness(&self) -> Completeness { + self.completeness + } + + pub const fn is_complete(&self) -> bool { + matches!(self.completeness, Completeness::Complete) + } +} + +impl Ranged for NoqaOffset { + fn range(&self) -> TextRange { + self.range + } +} + +impl From for NoqaOffset { + fn from(range: TextRange) -> Self { + Self { + range, + completeness: Completeness::Complete, + } + } +} + /// Remaps offsets falling into one of the ranges to instead check for a noqa comment on the /// line specified by the offset. #[derive(Debug, Default, PartialEq, Eq)] pub struct NoqaMapping { - ranges: Vec, + offsets: Vec, } impl NoqaMapping { pub(crate) fn with_capacity(capacity: usize) -> Self { Self { - ranges: Vec::with_capacity(capacity), + offsets: Vec::with_capacity(capacity), } } /// Returns the re-mapped position or `position` if no mapping exists. - pub(crate) fn resolve(&self, offset: TextSize) -> TextSize { - let index = self.ranges.binary_search_by(|range| { + pub(crate) fn resolve(&self, offset: TextSize) -> Option { + let index = self.offsets.binary_search_by(|range| { if range.end() < offset { std::cmp::Ordering::Less } else if range.contains(offset) { @@ -755,30 +797,36 @@ impl NoqaMapping { }); if let Ok(index) = index { - self.ranges[index].end() + let offset = self.offsets[index]; + if offset.is_complete() { + Some(offset.end()) + } else { + None + } } else { - offset + Some(offset) } } - pub(crate) fn push_mapping(&mut self, range: TextRange) { - if let Some(last_range) = self.ranges.last_mut() { + pub(crate) fn push_mapping(&mut self, offset: Into) { + let offset: NoqaOffset = offset.into(); + if let Some(last_offset) = self.offsets.last_mut() { // Strictly sorted insertion - if last_range.end() < range.start() { + if last_offset.end() < offset.start() { // OK - } else if range.end() < last_range.start() { + } else if offset.end() < last_offset.start() { // Incoming range is strictly before the last range which violates // the function's contract. - panic!("Ranges must be inserted in sorted order") + panic!("Offsets must be inserted in sorted order") } else { // Here, it's guaranteed that `last_range` and `range` overlap // in some way. We want to merge them into a single range. - *last_range = last_range.cover(range); + *last_offset = last_offset.range().cover(offset.range()); return; } } - self.ranges.push(range); + self.offsets.push(offset); } } diff --git a/crates/ruff_python_index/src/fstring_ranges.rs b/crates/ruff_python_index/src/fstring_ranges.rs index bdc31258bb..854d82769e 100644 --- a/crates/ruff_python_index/src/fstring_ranges.rs +++ b/crates/ruff_python_index/src/fstring_ranges.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use ruff_python_parser::Tok; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange, TextSize}; /// Stores the ranges of all f-strings in a file sorted by [`TextRange::start`]. /// There can be multiple overlapping ranges for nested f-strings. @@ -10,21 +10,21 @@ use ruff_text_size::{TextRange, TextSize}; #[derive(Debug)] pub struct FStringRanges { // Mapping from the f-string start location to its range. - raw: BTreeMap, + raw: BTreeMap, } impl FStringRanges { /// Return the [`TextRange`] of the innermost f-string at the given offset. - pub fn innermost(&self, offset: TextSize) -> Option { + pub fn innermost(&self, offset: TextSize) -> Option<&FStringRange> { self.raw .range(..=offset) .rev() .find(|(_, range)| range.contains(offset)) - .map(|(_, range)| *range) + .map(|(_, range)| range) } /// Return the [`TextRange`] of the outermost f-string at the given offset. - pub fn outermost(&self, offset: TextSize) -> Option { + pub fn outermost(&self, offset: TextSize) -> Option<&FStringRange> { // Explanation of the algorithm: // // ```python @@ -50,7 +50,7 @@ impl FStringRanges { .skip_while(|(_, range)| !range.contains(offset)) .take_while(|(_, range)| range.contains(offset)) .last() - .map(|(_, range)| *range) + .map(|(_, range)| range) } /// Returns an iterator over all f-string [`TextRange`] sorted by their @@ -59,7 +59,7 @@ impl FStringRanges { /// For nested f-strings, the outermost f-string is yielded first, moving /// inwards with each iteration. #[inline] - pub fn values(&self) -> impl Iterator + '_ { + pub fn values(&self) -> impl Iterator + '_ { self.raw.values() } @@ -73,7 +73,7 @@ impl FStringRanges { #[derive(Default)] pub(crate) struct FStringRangesBuilder { start_locations: Vec, - raw: BTreeMap, + raw: BTreeMap, } impl FStringRangesBuilder { @@ -84,14 +84,49 @@ impl FStringRangesBuilder { } Tok::FStringEnd => { if let Some(start) = self.start_locations.pop() { - self.raw.insert(start, TextRange::new(start, range.end())); + self.raw.insert( + start, + FStringRange::new(TextRange::new(start, range.end()), true), + ); } } _ => {} } } - pub(crate) fn finish(self) -> FStringRanges { + pub(crate) fn finish(mut self, end_location: TextSize) -> FStringRanges { + while let Some(start) = self.start_locations.pop() { + self.raw.insert( + start, + FStringRange::new(TextRange::new(start, end_location), false), + ); + } FStringRanges { raw: self.raw } } } + +#[derive(Debug)] +pub struct FStringRange { + range: TextRange, + complete: bool, +} + +impl FStringRange { + fn new(range: TextRange, complete: bool) -> Self { + Self { range, complete } + } + + pub fn range(&self) -> TextRange { + self.range + } + + pub fn is_complete(&self) -> bool { + self.complete + } +} + +impl Ranged for FStringRange { + fn range(&self) -> TextRange { + self.range + } +} diff --git a/crates/ruff_python_index/src/indexer.rs b/crates/ruff_python_index/src/indexer.rs index 78bf2606b2..75b8f2031f 100644 --- a/crates/ruff_python_index/src/indexer.rs +++ b/crates/ruff_python_index/src/indexer.rs @@ -72,7 +72,7 @@ impl Indexer { Self { comment_ranges: comment_ranges_builder.finish(), continuation_lines, - fstring_ranges: fstring_ranges_builder.finish(), + fstring_ranges: fstring_ranges_builder.finish(locator.text_len()), } }