diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 454b4a6121a353..57992738d1383f 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -858,7 +858,7 @@ where .rules .enabled(Rule::ModuleImportNotAtTopOfFile) { - pycodestyle::rules::module_import_not_at_top_of_file(self, stmt); + pycodestyle::rules::module_import_not_at_top_of_file(self, stmt, self.locator); } if self.settings.rules.enabled(Rule::GlobalStatement) { @@ -1102,7 +1102,7 @@ where .rules .enabled(Rule::ModuleImportNotAtTopOfFile) { - pycodestyle::rules::module_import_not_at_top_of_file(self, stmt); + pycodestyle::rules::module_import_not_at_top_of_file(self, stmt, self.locator); } if self.settings.rules.enabled(Rule::GlobalStatement) { diff --git a/crates/ruff/src/checkers/logical_lines.rs b/crates/ruff/src/checkers/logical_lines.rs index 9a98bd6a0edb4d..a9d6e955917f8b 100644 --- a/crates/ruff/src/checkers/logical_lines.rs +++ b/crates/ruff/src/checkers/logical_lines.rs @@ -1,5 +1,4 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::Location; use rustpython_parser::lexer::LexResult; use ruff_diagnostics::{Diagnostic, Fix}; @@ -136,7 +135,9 @@ pub fn check_logical_lines( } } if line.flags().contains(TokenFlags::COMMENT) { - for (range, kind) in whitespace_before_comment(&line.tokens(), locator) { + for (range, kind) in + whitespace_before_comment(&line.tokens(), locator, prev_line.is_none()) + { if settings.rules.enabled(kind.rule()) { diagnostics.push(Diagnostic { kind, @@ -161,8 +162,7 @@ pub fn check_logical_lines( // Extract the indentation level. let Some(start_loc) = line.first_token_location() else { continue; }; - let start_line = - locator.slice(TextRange::new(Location::new(start_loc.row(), 0), start_loc)); + let start_line = locator.slice(TextRange::new(locator.line_start(start_loc), start_loc)); let indent_level = expand_indent(start_line); let indent_size = 4; diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index ea68250d5227c4..f6cff44a92dd99 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -373,7 +373,7 @@ fn diagnostics_to_messages( let file = once_cell::unsync::Lazy::new(|| { let mut builder = SourceFileBuilder::new(&path.to_string_lossy()); if settings.show_source { - builder.set_source_text(locator.contents()); + builder.set_source_code(&locator.to_source_code()) } builder.finish() diff --git a/crates/ruff/src/message/grouped.rs b/crates/ruff/src/message/grouped.rs index ad4dbd98121eb8..0ffd0e97803cb5 100644 --- a/crates/ruff/src/message/grouped.rs +++ b/crates/ruff/src/message/grouped.rs @@ -9,7 +9,7 @@ use colored::Colorize; use ruff_python_ast::source_code::OneIndexed; use std::fmt::{Display, Formatter}; use std::io::Write; -use std::num::{NonZeroU32, NonZeroUsize}; +use std::num::NonZeroUsize; #[derive(Default)] pub struct GroupedEmitter { diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 0d9f7494e05870..b0ee8d08b91f75 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -186,7 +186,7 @@ pub fn add_noqa( path: &Path, diagnostics: &[Diagnostic], contents: &str, - commented_lines: &[usize], + commented_lines: &[TextRange], noqa_line_for: &IntMap, line_ending: LineEnding, ) -> Result { @@ -204,7 +204,7 @@ pub fn add_noqa( fn add_noqa_inner( diagnostics: &[Diagnostic], contents: &str, - commented_lines: &[usize], + commented_lines: &[TextRange], noqa_line_for: &IntMap, line_ending: LineEnding, ) -> (usize, String) { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs index 7f88ca9af4d68c..5ff56baa225c43 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs @@ -1,4 +1,3 @@ -use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; diff --git a/crates/ruff/src/rules/flake8_executable/helpers.rs b/crates/ruff/src/rules/flake8_executable/helpers.rs index 92ce48ac6ab8c1..524a3eeb076164 100644 --- a/crates/ruff/src/rules/flake8_executable/helpers.rs +++ b/crates/ruff/src/rules/flake8_executable/helpers.rs @@ -12,7 +12,7 @@ use ruff_text_size::{TextLen, TextSize}; static SHEBANG_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(?P\s*)#!(?P.*)").unwrap()); -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum ShebangDirective<'a> { None, // whitespace length, start of shebang, end, shebang contents @@ -72,11 +72,8 @@ mod tests { #[test] fn shebang_extract_match() { - assert!(matches!( - extract_shebang("not a match"), - ShebangDirective::None - )); - assert!(matches!( + assert_eq!(extract_shebang("not a match"), ShebangDirective::None); + assert_eq!( extract_shebang("#!/usr/bin/env python"), ShebangDirective::Match( TextSize::from(0), @@ -84,8 +81,8 @@ mod tests { TextSize::from(21), "/usr/bin/env python" ) - )); - assert!(matches!( + ); + assert_eq!( extract_shebang(" #!/usr/bin/env python"), ShebangDirective::Match( TextSize::from(2), @@ -93,10 +90,10 @@ mod tests { TextSize::from(23), "/usr/bin/env python" ) - )); - assert!(matches!( + ); + assert_eq!( extract_shebang("print('test') #!/usr/bin/python"), ShebangDirective::None - )); + ); } } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs index 183113128bf541..eb464f6d44fc39 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -1,5 +1,6 @@ +use ruff_text_size::TextLen; use rustpython_parser::ast::{ - Constant, Excepthandler, ExcepthandlerKind, ExprKind, Located, Location, Stmt, StmtKind, + Constant, Excepthandler, ExcepthandlerKind, ExprKind, Located, Stmt, StmtKind, }; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; @@ -97,10 +98,10 @@ pub fn suppressible_exception( &checker.importer, checker.locator, )?; - let try_ending = stmt.location.with_col_offset(3); // size of "try" + let try_ending = stmt.start() + "try".text_len(); let replace_try = Edit::replacement( format!("with {binding}({exception})"), - stmt.location, + stmt.start(), try_ending, ); let handler_line_begin = checker.locator.line_start(handler.start()); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs index 6f6816ecb348a2..8310e99e5c4361 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs @@ -1,3 +1,4 @@ +use ruff_text_size::TextSize; use rustpython_parser::ast::Location; use super::{LogicalLine, Whitespace}; @@ -111,10 +112,7 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine) -> Vec<(Location, Diagno TokenKind::Lbrace | TokenKind::Lpar | TokenKind::Lsqb => { if !matches!(line.trailing_whitespace(&token), Whitespace::None) { let end = token.end(); - diagnostics.push(( - Location::new(end.row(), end.column()), - WhitespaceAfterOpenBracket.into(), - )); + diagnostics.push((end, WhitespaceAfterOpenBracket.into())); } } TokenKind::Rbrace @@ -135,10 +133,8 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine) -> Vec<(Location, Diagno { if !matches!(last_token, Some(TokenKind::Comma)) { let start = token.start(); - diagnostics.push(( - Location::new(start.row(), start.column() - offset), - diagnostic_kind, - )); + diagnostics + .push((start - TextSize::try_from(offset).unwrap(), diagnostic_kind)); } } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs index 97494b53360227..4513b57cd68aea 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -1,5 +1,5 @@ use bitflags::bitflags; -use ruff_text_size::TextRange; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::Location; use rustpython_parser::lexer::LexResult; use std::fmt::{Debug, Formatter}; @@ -248,8 +248,8 @@ impl<'a> LogicalLine<'a> { Whitespace::leading(self.text_after(token)) } - /// Returns the whitespace and whitespace character-length *before* the `token` - pub fn leading_whitespace(&self, token: &LogicalLineToken<'a>) -> (Whitespace, usize) { + /// Returns the whitespace and whitespace byte-length *before* the `token` + pub fn leading_whitespace(&self, token: &LogicalLineToken<'a>) -> (Whitespace, TextSize) { Whitespace::trailing(self.text_before(token)) } @@ -515,26 +515,28 @@ impl Whitespace { } } - fn trailing(content: &str) -> (Self, usize) { - let mut count = 0; + fn trailing(content: &str) -> (Self, TextSize) { + let mut len = TextSize::default(); + let mut count = 0usize; for c in content.chars().rev() { if c == '\t' { - return (Self::Tab, count + 1); + return (Self::Tab, len + c.text_len()); } else if matches!(c, '\n' | '\r') { // Indent - return (Self::None, 0); + return (Self::None, TextSize::default()); } else if c.is_whitespace() { count += 1; + len += c.text_len(); } else { break; } } match count { - 0 => (Self::None, 0), - 1 => (Self::Single, count), - _ => (Self::Many, count), + 0 => (Self::None, TextSize::default()), + 1 => (Self::Single, len), + _ => (Self::Many, len), } } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs index b0a61519cd2c47..3ab63d0d5192e5 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::TextSize; use super::{LogicalLine, Whitespace}; use ruff_diagnostics::DiagnosticKind; @@ -123,7 +123,7 @@ impl Violation for MultipleSpacesAfterOperator { } /// E221, E222, E223, E224 -pub(crate) fn space_around_operator(line: &LogicalLine) -> Vec<(Location, DiagnosticKind)> { +pub(crate) fn space_around_operator(line: &LogicalLine) -> Vec<(TextSize, DiagnosticKind)> { let mut diagnostics = vec![]; let mut after_operator = false; @@ -135,17 +135,11 @@ pub(crate) fn space_around_operator(line: &LogicalLine) -> Vec<(Location, Diagno match line.leading_whitespace(&token) { (Whitespace::Tab, offset) => { let start = token.start(); - diagnostics.push(( - Location::new(start.row(), start.column() - offset), - TabBeforeOperator.into(), - )); + diagnostics.push((start - offset, TabBeforeOperator.into())); } (Whitespace::Many, offset) => { let start = token.start(); - diagnostics.push(( - Location::new(start.row(), start.column() - offset), - MultipleSpacesBeforeOperator.into(), - )); + diagnostics.push((start - offset, MultipleSpacesBeforeOperator.into())); } _ => {} } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs index efaae2343b8842..9d0575fdd494a6 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs @@ -120,17 +120,11 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine) -> Vec<(Location, D match line.leading_whitespace(&token) { (Whitespace::Tab, offset) => { let start = token.start(); - diagnostics.push(( - Location::new(start.row(), start.column() - offset), - TabBeforeKeyword.into(), - )); + diagnostics.push((start - offset, TabBeforeKeyword.into())); } (Whitespace::Many, offset) => { let start = token.start(); - diagnostics.push(( - Location::new(start.row(), start.column() - offset), - MultipleSpacesBeforeKeyword.into(), - )); + diagnostics.push((start - offset, MultipleSpacesBeforeKeyword.into())); } _ => {} } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs index faf3b96f7196b6..17bedf165ff8c3 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs @@ -4,8 +4,7 @@ use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; use ruff_python_ast::token_kind::TokenKind; -use ruff_text_size::TextRange; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextRange, TextSize}; /// ## What it does /// Checks if inline comments are separated by at least two spaces. @@ -140,24 +139,24 @@ impl Violation for MultipleLeadingHashesForBlockComment { pub(crate) fn whitespace_before_comment( tokens: &LogicalLineTokens, locator: &Locator, + is_first_row: bool, ) -> Vec<(TextRange, DiagnosticKind)> { let mut diagnostics = vec![]; - let mut prev_end = Location::new(0, 0); + let mut prev_end = TextSize::default(); for token in tokens { let kind = token.kind(); if let TokenKind::Comment = kind { let (start, end) = token.range(); - let line = locator.slice(TextRange::new( - Location::new(start.row(), 0), - Location::new(start.row(), start.column()), - )); + let line = locator.slice(TextRange::new(locator.line_start(start), start)); let text = locator.slice(TextRange::new(start, end)); let is_inline_comment = !line.trim().is_empty(); if is_inline_comment { - if prev_end.row() == start.row() && start.column() < prev_end.column() + 2 { + if start < prev_end + TextSize::from(2) + && !locator.contains_line_break(TextRange::new(start, prev_end)) + { diagnostics.push(( TextRange::new(prev_end, start), TooFewSpacesBeforeInlineComment.into(), @@ -183,7 +182,7 @@ pub(crate) fn whitespace_before_comment( .push((TextRange::new(start, end), NoSpaceAfterInlineComment.into())); } } else if let Some(bad_prefix) = bad_prefix { - if bad_prefix != '!' || start.row() > 1 { + if bad_prefix != '!' || !is_first_row { if bad_prefix != '#' { diagnostics .push((TextRange::new(start, end), NoSpaceAfterBlockComment.into())); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs index dfff016b876f67..e6475617e4fea0 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs @@ -1,7 +1,7 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use ruff_text_size::TextRange; +use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::Location; use super::LogicalLineTokens; @@ -57,10 +57,8 @@ pub(crate) fn whitespace_before_parameters( && (pre_pre_kind != Some(TokenKind::Class)) && token.start() != prev_end { - let start = Location::new(prev_end.row(), prev_end.column()); - let end = token.end(); - let end = Location::new(end.row(), end.column() - 1); - + let start = prev_end; + let end = token.end() - TextSize::from(1); let kind: WhitespaceBeforeParameters = WhitespaceBeforeParameters { bracket: kind }; let mut diagnostic = Diagnostic::new(kind, TextRange::new(start, end)); diff --git a/crates/ruff/src/rules/pycodestyle/rules/tab_indentation.rs b/crates/ruff/src/rules/pycodestyle/rules/tab_indentation.rs index 324ef2c2e5eae5..5298378b9015f1 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/tab_indentation.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/tab_indentation.rs @@ -1,4 +1,4 @@ -use ruff_text_size::{TextLen, TextRange}; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -22,44 +22,25 @@ pub fn tab_indentation( ) -> Option { let indent = leading_space(line); if let Some(tab_index) = indent.find('\t') { + let tab_offset = line_range.start() + TextSize::try_from(tab_index).unwrap(); + // If the tab character is within a multi-line string, abort. - if let Ok(range_index) = string_ranges.binary_search_by(|range| { - let start = range.location.row(); - let end = range.end_location.row(); - if start > lineno { - std::cmp::Ordering::Greater - } else if end < lineno { + if let Ok(_) = string_ranges.binary_search_by(|range| { + if tab_offset < range.start() { std::cmp::Ordering::Less - } else { + } else if range.contains(tab_offset) { std::cmp::Ordering::Equal + } else { + std::cmp::Ordering::Greater } }) { - let string_range = &string_ranges[range_index]; - let start = string_range.location; - let end = string_range.end_location; - - // Tab is contained in the string range by virtue of lines. - if lineno != start.row() && lineno != end.row() { - return None; - } - - let tab_column = line[..tab_index].chars().count(); - - // Tab on first line of the quoted range, following the quote. - if lineno == start.row() && tab_column > start.column() { - return None; - } - - // Tab on last line of the quoted range, preceding the quote. - if lineno == end.row() && tab_column < end.column() { - return None; - } + None + } else { + Some(Diagnostic::new( + TabIndentation, + TextRange::at(line_range.start(), indent.text_len()), + )) } - - Some(Diagnostic::new( - TabIndentation, - TextRange::at(line_range.start(), indent.text_len()), - )) } else { None } diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs index a598054869aa73..a885ebf4111ba2 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs @@ -1,7 +1,6 @@ use once_cell::sync::Lazy; use regex::Regex; -use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; diff --git a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs index 0620b4079616f9..19bc74ae43b2a7 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs @@ -185,19 +185,20 @@ pub fn invalid_string_characters( for line in UniversalNewlineIterator::from(text) { for (column, match_) in line.match_indices(&['\x08', '\x1A', '\x1B', '\0', '\u{200b}']) { - let (replacement, rule): (&str, DiagnosticKind) = match match_.chars().next().unwrap() { + let c = match_.chars().next().unwrap(); + let (replacement, rule): (&str, DiagnosticKind) = match c { '\x08' => ("\\b", InvalidCharacterBackspace.into()), '\x1A' => ("\\x1A", InvalidCharacterSub.into()), '\x1B' => ("\\x1B", InvalidCharacterEsc.into()), '\0' => ("\\0", InvalidCharacterNul.into()), '\u{200b}' => ("\\u200b", InvalidCharacterZeroWidthSpace.into()), _ => { - char_offset += 1; continue; } }; + let location = offset + TextSize::try_from(column).unwrap(); - let range = TextRange::at(location, match_.chars().next().unwrap().text_len()); + let range = TextRange::at(location, c.text_len()); let mut diagnostic = Diagnostic::new(rule, range); if autofix { @@ -208,7 +209,6 @@ pub fn invalid_string_characters( )); } diagnostics.push(diagnostic); - char_offset += 1; } offset += line.text_len(); diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs index edb64d95dfe27e..5c17ba2bb380fe 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs @@ -163,7 +163,7 @@ pub fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) FunctionCallInDataclassDefaultArgument { name: compose_call_path(func), }, - Range::from(expr), + expr.range(), )); } } @@ -182,7 +182,7 @@ pub fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { if is_mutable_expr(value) { checker .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, Range::from(value))); + .push(Diagnostic::new(MutableDataclassDefault, value.range())); } } } diff --git a/crates/ruff_python_ast/src/source_code/locator.rs b/crates/ruff_python_ast/src/source_code/locator.rs index f0cba58fbff67f..058b51e16003fe 100644 --- a/crates/ruff_python_ast/src/source_code/locator.rs +++ b/crates/ruff_python_ast/src/source_code/locator.rs @@ -1,15 +1,40 @@ //! Struct used to efficiently slice source code at (row, column) Locations. +use crate::source_code::{LineIndex, OneIndexed, SourceCode}; +use once_cell::unsync::OnceCell; use ruff_text_size::{TextLen, TextRange, TextSize}; use std::ops::Add; pub struct Locator<'a> { contents: &'a str, + index: OnceCell, } impl<'a> Locator<'a> { pub const fn new(contents: &'a str) -> Self { - Self { contents } + Self { + contents, + index: OnceCell::new(), + } + } + + #[deprecated( + note = "This is expensive, avoid using outside of the diagnostic phase. Prefer the other `Locator` methods instead." + )] + pub fn compute_line_index(&self, offset: TextSize) -> OneIndexed { + self.to_index().line_index(offset) + } + + fn to_index(&self) -> &LineIndex { + self.index + .get_or_init(|| LineIndex::from_source_text(self.contents)) + } + + pub fn to_source_code(&self) -> SourceCode { + SourceCode { + index: self.to_index(), + text: self.contents, + } } /// Computes the start position of the line of `offset`.