diff --git a/.editorconfig b/.editorconfig index 6c6ab76d51..776563c1e1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,6 @@ indent_size = 2 [*.{rs,py}] indent_size = 4 + +[*.snap] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c9fe444f19..b51a3c2fa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2123,6 +2123,7 @@ dependencies = [ "ruff_diagnostics", "ruff_python_ast", "ruff_python_stdlib", + "ruff_text_size", "rustc-hash", "serde", "serde_json", @@ -2165,8 +2166,7 @@ version = "0.0.0" dependencies = [ "anyhow", "log", - "ruff_python_ast", - "rustpython-parser", + "ruff_text_size", "serde", ] @@ -2248,6 +2248,7 @@ dependencies = [ "nohash-hasher", "ruff_python_ast", "ruff_python_stdlib", + "ruff_text_size", "rustc-hash", "rustpython-parser", "smallvec", @@ -2284,11 +2285,10 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" +source = "git+https://github.com/charliermarsh/RustPython.git?rev=c3147d2c1524ebd0e90cf1c2938d770314fd5a5a#c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" dependencies = [ "schemars", "serde", - "serde_test", - "static_assertions", ] [[package]] @@ -2356,27 +2356,28 @@ dependencies = [ [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=c15f670f2c30cfae6b41a1874893590148c74bc4#c15f670f2c30cfae6b41a1874893590148c74bc4" +source = "git+https://github.com/charliermarsh/RustPython.git?rev=c3147d2c1524ebd0e90cf1c2938d770314fd5a5a#c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" dependencies = [ "num-bigint", - "rustpython-compiler-core", + "ruff_text_size", ] [[package]] name = "rustpython-common" version = "0.2.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=c15f670f2c30cfae6b41a1874893590148c74bc4#c15f670f2c30cfae6b41a1874893590148c74bc4" +source = "git+https://github.com/charliermarsh/RustPython.git?rev=c3147d2c1524ebd0e90cf1c2938d770314fd5a5a#c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" dependencies = [ "ascii", "bitflags 1.3.2", + "bstr 0.2.17", "cfg-if", + "getrandom", "hexf-parse", "itertools", "lexical-parse-float", "libc", "lock_api", "num-bigint", - "num-complex", "num-traits", "once_cell", "radium", @@ -2390,23 +2391,21 @@ dependencies = [ [[package]] name = "rustpython-compiler-core" version = "0.2.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=c15f670f2c30cfae6b41a1874893590148c74bc4#c15f670f2c30cfae6b41a1874893590148c74bc4" +source = "git+https://github.com/charliermarsh/RustPython.git?rev=c3147d2c1524ebd0e90cf1c2938d770314fd5a5a#c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" dependencies = [ "bitflags 1.3.2", - "bstr 0.2.17", "itertools", "lz4_flex", "num-bigint", "num-complex", - "serde", + "ruff_text_size", ] [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=c15f670f2c30cfae6b41a1874893590148c74bc4#c15f670f2c30cfae6b41a1874893590148c74bc4" +source = "git+https://github.com/charliermarsh/RustPython.git?rev=c3147d2c1524ebd0e90cf1c2938d770314fd5a5a#c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" dependencies = [ - "ahash", "anyhow", "itertools", "lalrpop", @@ -2416,10 +2415,10 @@ dependencies = [ "num-traits", "phf", "phf_codegen", + "ruff_text_size", "rustc-hash", "rustpython-ast", "rustpython-compiler-core", - "serde", "tiny-keccak", "unic-emoji-char", "unic-ucd-ident", @@ -2568,15 +2567,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_test" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c95a500e3923258f7fc3a16bf29934e403aef5ca1096e184d85e3b1926675e8" -dependencies = [ - "serde", -] - [[package]] name = "shellexpand" version = "3.1.0" diff --git a/Cargo.toml b/Cargo.toml index a962b5720d..cc5b1275c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,12 +30,10 @@ path-absolutize = { version = "3.0.14" } proc-macro2 = { version = "1.0.51" } quote = { version = "1.0.23" } regex = { version = "1.7.1" } +ruff_text_size = { git = "https://github.com/charliermarsh/RustPython.git", rev = "c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" } rustc-hash = { version = "1.1.0" } -rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "c15f670f2c30cfae6b41a1874893590148c74bc4" } -rustpython-parser = { features = [ - "lalrpop", - "serde", -], git = "https://github.com/RustPython/RustPython.git", rev = "c15f670f2c30cfae6b41a1874893590148c74bc4" } +rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" } +rustpython-parser = { git = "https://github.com/charliermarsh/RustPython.git", rev = "c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" } schemars = { version = "0.8.12" } serde = { version = "1.0.152", features = ["derive"] } serde_json = { version = "1.0.93", features = ["preserve_order"] } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 591bab9123..3fc2629da9 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -17,11 +17,11 @@ name = "ruff" ruff_cache = { path = "../ruff_cache" } ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] } ruff_macros = { path = "../ruff_macros" } -ruff_python_ast = { path = "../ruff_python_ast" } +ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] } ruff_python_semantic = { path = "../ruff_python_semantic" } ruff_python_stdlib = { path = "../ruff_python_stdlib" } ruff_rustpython = { path = "../ruff_rustpython" } -ruff_text_size = { path = "../ruff_text_size" } +ruff_text_size = { workspace = true } annotate-snippets = { version = "0.9.1", features = ["color"] } anyhow = { workspace = true } diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W505.py b/crates/ruff/resources/test/fixtures/pycodestyle/W505.py index 62f5faae72..d8085da989 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/W505.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W505.py @@ -9,6 +9,9 @@ def f(): # Here's a standalone comment that's over the limit. + x = 2 + # Another standalone that is preceded by a newline and indent toke and is over the limit. + print("Here's a string that's over the limit, but it's not a docstring.") diff --git a/crates/ruff/src/autofix/actions.rs b/crates/ruff/src/autofix/actions.rs index 64d588addf..283c9298a9 100644 --- a/crates/ruff/src/autofix/actions.rs +++ b/crates/ruff/src/autofix/actions.rs @@ -4,12 +4,12 @@ use itertools::Itertools; use libcst_native::{ Codegen, CodegenState, ImportNames, ParenthesizableWhitespace, SmallStatement, Statement, }; -use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind}; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Stmt, StmtKind}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::Edit; use ruff_python_ast::helpers; -use ruff_python_ast::helpers::to_absolute; use ruff_python_ast::imports::{AnyImport, Import}; use ruff_python_ast::newlines::NewlineWithTrailingNewline; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; @@ -102,20 +102,17 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result /// Return the location of a trailing semicolon following a `Stmt`, if it's part /// of a multi-statement line. -fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option { - let contents = locator.after(stmt.end_location.unwrap()); - for (row, line) in NewlineWithTrailingNewline::from(contents).enumerate() { - let trimmed = line.trim(); +fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option { + let contents = locator.after(stmt.end()); + + for line in NewlineWithTrailingNewline::from(contents) { + let trimmed = line.trim_start(); + if trimmed.starts_with(';') { - let column = line - .char_indices() - .find_map(|(column, char)| if char == ';' { Some(column) } else { None }) - .unwrap(); - return Some(to_absolute( - Location::new(row + 1, column), - stmt.end_location.unwrap(), - )); + let colon_offset = line.text_len() - trimmed.text_len(); + return Some(stmt.end() + line.start() + colon_offset); } + if !trimmed.starts_with('\\') { break; } @@ -124,42 +121,36 @@ fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option { } /// Find the next valid break for a `Stmt` after a semicolon. -fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location { - let start_location = Location::new(semicolon.row(), semicolon.column() + 1); - let contents = locator.after(start_location); - for (row, line) in NewlineWithTrailingNewline::from(contents).enumerate() { +fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize { + let start_location = semicolon + TextSize::from(1); + + let contents = &locator.contents()[usize::from(start_location)..]; + for line in NewlineWithTrailingNewline::from(contents) { let trimmed = line.trim(); // Skip past any continuations. if trimmed.starts_with('\\') { continue; } - return if trimmed.is_empty() { - // If the line is empty, then despite the previous statement ending in a - // semicolon, we know that it's not a multi-statement line. - to_absolute(Location::new(row + 1, 0), start_location) - } else { - // Otherwise, find the start of the next statement. (Or, anything that isn't - // whitespace.) - let column = line - .char_indices() - .find_map(|(column, char)| { - if char.is_whitespace() { - None - } else { - Some(column) - } - }) - .unwrap(); - to_absolute(Location::new(row + 1, column), start_location) - }; + + return start_location + + if trimmed.is_empty() { + // If the line is empty, then despite the previous statement ending in a + // semicolon, we know that it's not a multi-statement line. + line.start() + } else { + // Otherwise, find the start of the next statement. (Or, anything that isn't + // whitespace.) + let relative_offset = line.find(|c: char| !c.is_whitespace()).unwrap(); + line.start() + TextSize::try_from(relative_offset).unwrap() + }; } - Location::new(start_location.row() + 1, 0) + + locator.line_end(start_location) } /// Return `true` if a `Stmt` occurs at the end of a file. fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool { - let contents = locator.after(stmt.end_location.unwrap()); - contents.is_empty() + stmt.end() == locator.contents().text_len() } /// Return the `Fix` to use when deleting a `Stmt`. @@ -190,33 +181,23 @@ pub fn delete_stmt( { // If removing this node would lead to an invalid syntax tree, replace // it with a `pass`. - Ok(Edit::replacement( - "pass".to_string(), - stmt.location, - stmt.end_location.unwrap(), - )) + Ok(Edit::range_replacement("pass".to_string(), stmt.range())) } else { Ok(if let Some(semicolon) = trailing_semicolon(stmt, locator) { let next = next_stmt_break(semicolon, locator); - Edit::deletion(stmt.location, next) - } else if helpers::match_leading_content(stmt, locator) { - Edit::deletion(stmt.location, stmt.end_location.unwrap()) - } else if helpers::preceded_by_continuation(stmt, indexer) { - if is_end_of_file(stmt, locator) && stmt.location.column() == 0 { + Edit::deletion(stmt.start(), next) + } else if helpers::has_leading_content(stmt, locator) { + Edit::range_deletion(stmt.range()) + } else if helpers::preceded_by_continuation(stmt, indexer, locator) { + if is_end_of_file(stmt, locator) && locator.is_at_start_of_line(stmt.start()) { // Special-case: a file can't end in a continuation. - Edit::replacement( - stylist.line_ending().to_string(), - stmt.location, - stmt.end_location.unwrap(), - ) + Edit::range_replacement(stylist.line_ending().to_string(), stmt.range()) } else { - Edit::deletion(stmt.location, stmt.end_location.unwrap()) + Edit::range_deletion(stmt.range()) } } else { - Edit::deletion( - Location::new(stmt.location.row(), 0), - Location::new(stmt.end_location.unwrap().row() + 1, 0), - ) + let range = locator.full_lines_range(stmt.range()); + Edit::range_deletion(range) }) } } @@ -231,7 +212,7 @@ pub fn remove_unused_imports<'a>( indexer: &Indexer, stylist: &Stylist, ) -> Result { - let module_text = locator.slice(stmt); + let module_text = locator.slice(stmt.range()); let mut tree = match_module(module_text)?; let Some(Statement::Simple(body)) = tree.body.first_mut() else { @@ -337,11 +318,7 @@ pub fn remove_unused_imports<'a>( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - stmt.location, - stmt.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), stmt.range())) } } @@ -353,9 +330,8 @@ pub fn remove_unused_imports<'a>( /// For this behavior, set `remove_parentheses` to `true`. pub fn remove_argument( locator: &Locator, - call_at: Location, - expr_at: Location, - expr_end: Location, + call_at: TextSize, + expr_range: TextRange, args: &[Expr], keywords: &[Keyword], remove_parentheses: bool, @@ -374,13 +350,13 @@ pub fn remove_argument( if n_arguments == 1 { // Case 1: there is only one argument. let mut count: usize = 0; - for (start, tok, end) in lexer::lex_located(contents, Mode::Module, call_at).flatten() { + for (tok, range) in lexer::lex_located(contents, Mode::Module, call_at).flatten() { if matches!(tok, Tok::Lpar) { if count == 0 { fix_start = Some(if remove_parentheses { - start + range.start() } else { - Location::new(start.row(), start.column() + 1) + range.start() + TextSize::from(1) }); } count += 1; @@ -390,9 +366,9 @@ pub fn remove_argument( count -= 1; if count == 0 { fix_end = Some(if remove_parentheses { - end + range.end() } else { - Location::new(end.row(), end.column() - 1) + range.end() - TextSize::from(1) }); break; } @@ -400,27 +376,27 @@ pub fn remove_argument( } } else if args .iter() - .map(|node| node.location) - .chain(keywords.iter().map(|node| node.location)) - .any(|location| location > expr_at) + .map(Expr::start) + .chain(keywords.iter().map(Keyword::start)) + .any(|location| location > expr_range.start()) { // Case 2: argument or keyword is _not_ the last node. let mut seen_comma = false; - for (start, tok, end) in lexer::lex_located(contents, Mode::Module, call_at).flatten() { + for (tok, range) in lexer::lex_located(contents, Mode::Module, call_at).flatten() { if seen_comma { if matches!(tok, Tok::NonLogicalNewline) { // Also delete any non-logical newlines after the comma. continue; } fix_end = Some(if matches!(tok, Tok::Newline) { - end + range.end() } else { - start + range.start() }); break; } - if start == expr_at { - fix_start = Some(start); + if range.start() == expr_range.start() { + fix_start = Some(range.start()); } if fix_start.is_some() && matches!(tok, Tok::Comma) { seen_comma = true; @@ -429,13 +405,13 @@ pub fn remove_argument( } else { // Case 3: argument or keyword is the last node, so we have to find the last // comma in the stmt. - for (start, tok, _) in lexer::lex_located(contents, Mode::Module, call_at).flatten() { - if start == expr_at { - fix_end = Some(expr_end); + for (tok, range) in lexer::lex_located(contents, Mode::Module, call_at).flatten() { + if range.start() == expr_range.start() { + fix_end = Some(expr_range.end()); break; } if matches!(tok, Tok::Comma) { - fix_start = Some(start); + fix_start = Some(range.start()); } } } @@ -482,11 +458,8 @@ pub fn get_or_import_symbol( // // By adding this no-op edit, we force the `unused-imports` fix to conflict with the // `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass. - let import_edit = Edit::replacement( - locator.slice(source).to_string(), - source.location, - source.end_location.unwrap(), - ); + let import_edit = + Edit::range_replacement(locator.slice(source.range()).to_string(), source.range()); Ok((import_edit, binding)) } else { if let Some(stmt) = importer.get_import_from(module) { @@ -527,8 +500,8 @@ pub fn get_or_import_symbol( #[cfg(test)] mod tests { use anyhow::Result; + use ruff_text_size::TextSize; use rustpython_parser as parser; - use rustpython_parser::ast::Location; use ruff_python_ast::source_code::Locator; @@ -546,19 +519,13 @@ mod tests { let program = parser::parse_program(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!( - trailing_semicolon(stmt, &locator), - Some(Location::new(1, 5)) - ); + assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(5))); let contents = "x = 1 ; y = 1"; let program = parser::parse_program(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!( - trailing_semicolon(stmt, &locator), - Some(Location::new(1, 6)) - ); + assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(6))); let contents = r#" x = 1 \ @@ -568,10 +535,7 @@ x = 1 \ let program = parser::parse_program(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!( - trailing_semicolon(stmt, &locator), - Some(Location::new(2, 2)) - ); + assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(10))); Ok(()) } @@ -581,15 +545,15 @@ x = 1 \ let contents = "x = 1; y = 1"; let locator = Locator::new(contents); assert_eq!( - next_stmt_break(Location::new(1, 4), &locator), - Location::new(1, 5) + next_stmt_break(TextSize::from(4), &locator), + TextSize::from(5) ); let contents = "x = 1 ; y = 1"; let locator = Locator::new(contents); assert_eq!( - next_stmt_break(Location::new(1, 5), &locator), - Location::new(1, 6) + next_stmt_break(TextSize::from(5), &locator), + TextSize::from(6) ); let contents = r#" @@ -599,8 +563,8 @@ x = 1 \ .trim(); let locator = Locator::new(contents); assert_eq!( - next_stmt_break(Location::new(2, 2), &locator), - Location::new(2, 4) + next_stmt_break(TextSize::from(10), &locator), + TextSize::from(12) ); } } diff --git a/crates/ruff/src/autofix/mod.rs b/crates/ruff/src/autofix/mod.rs index 71a2547c68..50283071e7 100644 --- a/crates/ruff/src/autofix/mod.rs +++ b/crates/ruff/src/autofix/mod.rs @@ -1,12 +1,11 @@ use std::collections::BTreeSet; use itertools::Itertools; +use ruff_text_size::{TextRange, TextSize}; use rustc_hash::FxHashMap; -use rustpython_parser::ast::Location; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::linter::FixTable; use crate::registry::{AsRule, Rule}; @@ -33,7 +32,7 @@ fn apply_fixes<'a>( locator: &'a Locator<'a>, ) -> (String, FixTable) { let mut output = String::with_capacity(locator.len()); - let mut last_pos: Option = None; + let mut last_pos: Option = None; let mut applied: BTreeSet<&Edit> = BTreeSet::default(); let mut fixed = FxHashMap::default(); @@ -57,7 +56,7 @@ fn apply_fixes<'a>( // Best-effort approach: if this fix overlaps with a fix we've already applied, // skip it. if last_pos.map_or(false, |last_pos| { - fix.min_location() + fix.min_start() .map_or(false, |fix_location| last_pos >= fix_location) }) { continue; @@ -65,14 +64,14 @@ fn apply_fixes<'a>( for edit in fix.edits() { // Add all contents from `last_pos` to `fix.location`. - let slice = locator.slice(Range::new(last_pos.unwrap_or_default(), edit.location())); + let slice = locator.slice(TextRange::new(last_pos.unwrap_or_default(), edit.start())); output.push_str(slice); // Add the patch itself. output.push_str(edit.content().unwrap_or_default()); // Track that the edit was applied. - last_pos = Some(edit.end_location()); + last_pos = Some(edit.end()); applied.insert(edit); } @@ -88,8 +87,8 @@ fn apply_fixes<'a>( /// Compare two fixes. fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering { - fix1.min_location() - .cmp(&fix2.min_location()) + fix1.min_start() + .cmp(&fix2.min_start()) .then_with(|| match (&rule1, &rule2) { // Apply `EndsInPeriod` fixes before `NewLineAfterLastParagraph` fixes. (Rule::EndsInPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less, @@ -100,7 +99,7 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi #[cfg(test)] mod tests { - use rustpython_parser::ast::Location; + use ruff_text_size::TextSize; use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Edit; @@ -114,8 +113,7 @@ mod tests { .map(|edit| Diagnostic { // The choice of rule here is arbitrary. kind: MissingNewlineAtEndOfFile.into(), - location: edit.location(), - end_location: edit.end_location(), + range: edit.range(), fix: edit.into(), parent: None, }) @@ -142,8 +140,8 @@ class A(object): ); let diagnostics = create_diagnostics([Edit::replacement( "Bar".to_string(), - Location::new(1, 8), - Location::new(1, 14), + TextSize::new(8), + TextSize::new(14), )]); let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); assert_eq!( @@ -166,8 +164,7 @@ class A(object): "# .trim(), ); - let diagnostics = - create_diagnostics([Edit::deletion(Location::new(1, 7), Location::new(1, 15))]); + let diagnostics = create_diagnostics([Edit::deletion(TextSize::new(7), TextSize::new(15))]); let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); assert_eq!( contents, @@ -190,8 +187,8 @@ class A(object, object, object): .trim(), ); let diagnostics = create_diagnostics([ - Edit::deletion(Location::new(1, 8), Location::new(1, 16)), - Edit::deletion(Location::new(1, 22), Location::new(1, 30)), + Edit::deletion(TextSize::from(8), TextSize::from(16)), + Edit::deletion(TextSize::from(22), TextSize::from(30)), ]); let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); @@ -216,12 +213,8 @@ class A(object): .trim(), ); let diagnostics = create_diagnostics([ - Edit::deletion(Location::new(1, 7), Location::new(1, 15)), - Edit::replacement( - "ignored".to_string(), - Location::new(1, 9), - Location::new(1, 11), - ), + Edit::deletion(TextSize::from(7), TextSize::from(15)), + Edit::replacement("ignored".to_string(), TextSize::from(9), TextSize::from(11)), ]); let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); assert_eq!( diff --git a/crates/ruff/src/checkers/ast/deferred.rs b/crates/ruff/src/checkers/ast/deferred.rs index 70b3834a38..44d607fbc3 100644 --- a/crates/ruff/src/checkers/ast/deferred.rs +++ b/crates/ruff/src/checkers/ast/deferred.rs @@ -1,7 +1,7 @@ use ruff_python_semantic::scope::ScopeStack; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, Stmt}; -use ruff_python_ast::types::Range; use ruff_python_ast::types::RefEquality; use ruff_python_semantic::analyze::visibility::{Visibility, VisibleScope}; @@ -16,7 +16,7 @@ type Context<'a> = (ScopeStack, Vec>); #[derive(Default)] pub struct Deferred<'a> { pub definitions: Vec<(Definition<'a>, Visibility, Context<'a>)>, - pub string_type_definitions: Vec<(Range, &'a str, AnnotationContext, Context<'a>)>, + pub string_type_definitions: Vec<(TextRange, &'a str, AnnotationContext, Context<'a>)>, pub type_definitions: Vec<(&'a Expr, AnnotationContext, Context<'a>)>, pub functions: Vec<(&'a Stmt, Context<'a>, VisibleScope)>, pub lambdas: Vec<(&'a Expr, Context<'a>)>, diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 536ab26abe..b3f850d67d 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3,20 +3,19 @@ use std::path::Path; use itertools::Itertools; use log::error; -use nohash_hasher::IntMap; +use ruff_text_size::{TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; use rustpython_common::cformat::{CFormatError, CFormatErrorType}; use rustpython_parser::ast::{ Arg, Arguments, Comprehension, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, - ExprKind, KeywordData, Located, Location, Operator, Pattern, PatternKind, Stmt, StmtKind, - Suite, + ExprKind, KeywordData, Located, Operator, Pattern, PatternKind, Stmt, StmtKind, Suite, }; use ruff_diagnostics::Diagnostic; use ruff_python_ast::all::{extract_all_names, AllNamesFlags}; use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path}; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; -use ruff_python_ast::types::{Node, Range, RefEquality}; +use ruff_python_ast::types::{Node, RefEquality}; use ruff_python_ast::typing::parse_type_annotation; use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor}; use ruff_python_ast::{branch_detection, cast, helpers, str, visitor}; @@ -39,6 +38,7 @@ use crate::docstrings::definition::{ }; use crate::fs::relativize_path; use crate::importer::Importer; +use crate::noqa::NoqaMapping; use crate::registry::{AsRule, Rule}; use crate::rules::{ flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap, @@ -67,7 +67,7 @@ pub struct Checker<'a> { autofix: flags::Autofix, noqa: flags::Noqa, pub settings: &'a Settings, - pub noqa_line_for: &'a IntMap, + pub noqa_line_for: &'a NoqaMapping, pub locator: &'a Locator<'a>, pub stylist: &'a Stylist<'a>, pub indexer: &'a Indexer, @@ -85,7 +85,7 @@ impl<'a> Checker<'a> { #[allow(clippy::too_many_arguments)] pub fn new( settings: &'a Settings, - noqa_line_for: &'a IntMap, + noqa_line_for: &'a NoqaMapping, autofix: flags::Autofix, noqa: flags::Noqa, path: &'a Path, @@ -126,7 +126,7 @@ impl<'a> Checker<'a> { } /// Return `true` if a `Rule` is disabled by a `noqa` directive. - pub fn rule_is_ignored(&self, code: Rule, lineno: usize) -> bool { + pub fn rule_is_ignored(&self, code: Rule, offset: TextSize) -> bool { // TODO(charlie): `noqa` directives are mostly enforced in `check_lines.rs`. // However, in rare cases, we need to check them here. For example, when // removing unused imports, we create a single fix that's applied to all @@ -137,7 +137,7 @@ impl<'a> Checker<'a> { if !self.noqa.to_bool() { return false; } - noqa::rule_is_ignored(code, lineno, self.noqa_line_for, self.locator) + noqa::rule_is_ignored(code, offset, self.noqa_line_for, self.locator) } } @@ -221,13 +221,13 @@ where match &stmt.node { StmtKind::Global { names } => { let scope_index = self.ctx.scope_id(); - let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); + let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); if !scope_index.is_global() { // Add the binding to the current scope. let context = self.ctx.execution_context(); let exceptions = self.ctx.exceptions(); let scope = &mut self.ctx.scopes[scope_index]; - let usage = Some((scope.id, Range::from(stmt))); + let usage = Some((scope.id, stmt.range())); for (name, range) in names.iter().zip(ranges.iter()) { let id = self.ctx.bindings.push(Binding { kind: BindingKind::Global, @@ -252,12 +252,12 @@ where } StmtKind::Nonlocal { names } => { let scope_index = self.ctx.scope_id(); - let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); + let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); if !scope_index.is_global() { let context = self.ctx.execution_context(); let exceptions = self.ctx.exceptions(); let scope = &mut self.ctx.scopes[scope_index]; - let usage = Some((scope.id, Range::from(stmt))); + let usage = Some((scope.id, stmt.range())); for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. let id = self.ctx.bindings.push(Binding { @@ -695,7 +695,7 @@ where runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(stmt), + range: stmt.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -880,7 +880,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) { @@ -909,9 +909,9 @@ where kind: BindingKind::FutureImportation, runtime_usage: None, // Always mark `__future__` imports as used. - synthetic_usage: Some((self.ctx.scope_id(), Range::from(alias))), + synthetic_usage: Some((self.ctx.scope_id(), alias.range())), typing_usage: None, - range: Range::from(alias), + range: alias.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -923,7 +923,7 @@ where { self.diagnostics.push(Diagnostic::new( pyflakes::rules::LateFutureImport, - Range::from(stmt), + stmt.range(), )); } } else if alias.node.name.contains('.') && alias.node.asname.is_none() { @@ -941,7 +941,7 @@ where runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(alias), + range: alias.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -964,12 +964,12 @@ where kind: BindingKind::Importation(Importation { name, full_name }), runtime_usage: None, synthetic_usage: if is_explicit_reexport { - Some((self.ctx.scope_id(), Range::from(alias))) + Some((self.ctx.scope_id(), alias.range())) } else { None }, typing_usage: None, - range: Range::from(alias), + range: alias.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -1135,7 +1135,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) { @@ -1220,9 +1220,9 @@ where kind: BindingKind::FutureImportation, runtime_usage: None, // Always mark `__future__` imports as used. - synthetic_usage: Some((self.ctx.scope_id(), Range::from(alias))), + synthetic_usage: Some((self.ctx.scope_id(), alias.range())), typing_usage: None, - range: Range::from(alias), + range: alias.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -1242,7 +1242,7 @@ where { self.diagnostics.push(Diagnostic::new( pyflakes::rules::LateFutureImport, - Range::from(stmt), + stmt.range(), )); } } else if alias.node.name == "*" { @@ -1265,7 +1265,7 @@ where module.as_deref(), ), }, - Range::from(stmt), + stmt.range(), )); } } @@ -1279,7 +1279,7 @@ where pyflakes::rules::UndefinedLocalWithImportStar { name: helpers::format_import_from(*level, module.as_deref()), }, - Range::from(stmt), + stmt.range(), )); } } else { @@ -1313,12 +1313,12 @@ where }), runtime_usage: None, synthetic_usage: if is_explicit_reexport { - Some((self.ctx.scope_id(), Range::from(alias))) + Some((self.ctx.scope_id(), alias.range())) } else { None }, typing_usage: None, - range: Range::from(alias), + range: alias.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -2004,7 +2004,7 @@ where runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(stmt), + range: stmt.range(), source: Some(RefEquality(stmt)), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -2067,7 +2067,7 @@ where runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(*stmt), + range: stmt.range(), source: Some(RefEquality(stmt)), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -2228,7 +2228,7 @@ where runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(stmt), + range: stmt.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -2261,7 +2261,7 @@ where } = &expr.node { self.deferred.string_type_definitions.push(( - Range::from(expr), + expr.range(), value, (self.ctx.in_annotation, self.ctx.in_type_checking_block), (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), @@ -2336,7 +2336,7 @@ where elts, check_too_many_expressions, check_two_starred_expressions, - Range::from(expr), + expr.range(), ) { self.diagnostics.push(diagnostic); } @@ -2369,7 +2369,7 @@ where ExprContext::Store => { if self.settings.rules.enabled(Rule::AmbiguousVariableName) { if let Some(diagnostic) = - pycodestyle::rules::ambiguous_variable_name(id, Range::from(expr)) + pycodestyle::rules::ambiguous_variable_name(id, expr.range()) { self.diagnostics.push(diagnostic); } @@ -2455,7 +2455,7 @@ where { if attr == "format" { // "...".format(...) call - let location = Range::from(expr); + let location = expr.range(); match pyflakes::format::FormatSummary::try_from(value.as_ref()) { Err(e) => { if self @@ -2895,14 +2895,14 @@ where func, args, keywords, - Range::from(expr), + expr.range(), ); } if self.settings.rules.enabled(Rule::CallDatetimeToday) { - flake8_datetimez::rules::call_datetime_today(self, func, Range::from(expr)); + flake8_datetimez::rules::call_datetime_today(self, func, expr.range()); } if self.settings.rules.enabled(Rule::CallDatetimeUtcnow) { - flake8_datetimez::rules::call_datetime_utcnow(self, func, Range::from(expr)); + flake8_datetimez::rules::call_datetime_utcnow(self, func, expr.range()); } if self .settings @@ -2912,7 +2912,7 @@ where flake8_datetimez::rules::call_datetime_utcfromtimestamp( self, func, - Range::from(expr), + expr.range(), ); } if self @@ -2925,7 +2925,7 @@ where func, args, keywords, - Range::from(expr), + expr.range(), ); } if self.settings.rules.enabled(Rule::CallDatetimeFromtimestamp) { @@ -2934,7 +2934,7 @@ where func, args, keywords, - Range::from(expr), + expr.range(), ); } if self @@ -2946,14 +2946,14 @@ where self, func, args, - Range::from(expr), + expr.range(), ); } if self.settings.rules.enabled(Rule::CallDateToday) { - flake8_datetimez::rules::call_date_today(self, func, Range::from(expr)); + flake8_datetimez::rules::call_date_today(self, func, expr.range()); } if self.settings.rules.enabled(Rule::CallDateFromtimestamp) { - flake8_datetimez::rules::call_date_fromtimestamp(self, func, Range::from(expr)); + flake8_datetimez::rules::call_date_fromtimestamp(self, func, expr.range()); } // pygrep-hooks @@ -3207,7 +3207,7 @@ where Rule::PercentFormatStarRequiresSequence, Rule::PercentFormatUnsupportedFormatCharacter, ]) { - let location = Range::from(expr); + let location = expr.range(); match pyflakes::cformat::CFormatSummary::try_from(value.as_str()) { Err(CFormatError { typ: CFormatErrorType::UnsupportedFormatChar(c), @@ -3309,7 +3309,7 @@ where } if self.settings.rules.enabled(Rule::PrintfStringFormatting) { - pyupgrade::rules::printf_string_formatting(self, expr, right); + pyupgrade::rules::printf_string_formatting(self, expr, right, self.locator); } if self.settings.rules.enabled(Rule::BadStringFormatType) { pylint::rules::bad_string_format_type(self, expr, right); @@ -3417,7 +3417,7 @@ where left, ops, comparators, - Range::from(expr), + expr.range(), ); } @@ -3495,7 +3495,7 @@ where } => { if self.ctx.in_type_definition && !self.ctx.in_literal && !self.ctx.in_f_string { self.deferred.string_type_definitions.push(( - Range::from(expr), + expr.range(), value, (self.ctx.in_annotation, self.ctx.in_type_checking_block), (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), @@ -3506,10 +3506,9 @@ where .rules .enabled(Rule::HardcodedBindAllInterfaces) { - if let Some(diagnostic) = flake8_bandit::rules::hardcoded_bind_all_interfaces( - value, - &Range::from(expr), - ) { + if let Some(diagnostic) = + flake8_bandit::rules::hardcoded_bind_all_interfaces(value, expr.range()) + { self.diagnostics.push(diagnostic); } } @@ -3979,13 +3978,12 @@ where if self.ctx.scope().defines(name.as_str()) { self.handle_node_store( name, - &Expr::new( - name_range.location, - name_range.end_location, + &Expr::with_range( ExprKind::Name { id: name.to_string(), ctx: ExprContext::Store, }, + name_range, ), ); } @@ -3993,13 +3991,12 @@ where let definition = self.ctx.scope().get(name.as_str()).copied(); self.handle_node_store( name, - &Expr::new( - name_range.location, - name_range.end_location, + &Expr::with_range( ExprKind::Name { id: name.to_string(), ctx: ExprContext::Store, }, + name_range, ), ); @@ -4108,7 +4105,7 @@ where runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(arg), + range: arg.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -4117,7 +4114,7 @@ where if self.settings.rules.enabled(Rule::AmbiguousVariableName) { if let Some(diagnostic) = - pycodestyle::rules::ambiguous_variable_name(&arg.node.arg, Range::from(arg)) + pycodestyle::rules::ambiguous_variable_name(&arg.node.arg, arg.range()) { self.diagnostics.push(diagnostic); } @@ -4152,7 +4149,7 @@ where runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(pattern), + range: pattern.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -4220,10 +4217,13 @@ impl<'a> Checker<'a> { ); if binding.kind.is_loop_var() && existing_is_import { if self.settings.rules.enabled(Rule::ImportShadowedByLoopVar) { + #[allow(deprecated)] + let line = self.locator.compute_line_index(existing.range.start()); + self.diagnostics.push(Diagnostic::new( pyflakes::rules::ImportShadowedByLoopVar { name: name.to_string(), - line: existing.range.location.row(), + line, }, binding.range, )); @@ -4239,10 +4239,13 @@ impl<'a> Checker<'a> { )) { if self.settings.rules.enabled(Rule::RedefinedWhileUnused) { + #[allow(deprecated)] + let line = self.locator.compute_line_index(existing.range.start()); + let mut diagnostic = Diagnostic::new( pyflakes::rules::RedefinedWhileUnused { name: name.to_string(), - line: existing.range.location.row(), + line, }, matches!( binding.kind, @@ -4257,9 +4260,9 @@ impl<'a> Checker<'a> { ); if let Some(parent) = binding.source.as_ref() { if matches!(parent.node, StmtKind::ImportFrom { .. }) - && parent.location.row() != binding.range.location.row() + && parent.range().contains_range(binding.range) { - diagnostic.set_parent(parent.location); + diagnostic.set_parent(parent.start()); } } self.diagnostics.push(diagnostic); @@ -4327,9 +4330,9 @@ impl<'a> Checker<'a> { { let id = self.ctx.bindings.push(Binding { kind: BindingKind::Builtin, - range: Range::default(), + range: TextRange::default(), runtime_usage: None, - synthetic_usage: Some((ScopeId::global(), Range::default())), + synthetic_usage: Some((ScopeId::global(), TextRange::default())), typing_usage: None, source: None, context: ExecutionContext::Runtime, @@ -4363,7 +4366,7 @@ impl<'a> Checker<'a> { if let Some(index) = scope.get(id.as_str()) { // Mark the binding as used. let context = self.ctx.execution_context(); - self.ctx.bindings[*index].mark_used(scope_id, Range::from(expr), context); + self.ctx.bindings[*index].mark_used(scope_id, expr.range(), context); if self.ctx.bindings[*index].kind.is_annotation() && self.ctx.in_deferred_string_type_definition.is_none() @@ -4394,7 +4397,7 @@ impl<'a> Checker<'a> { if let Some(index) = scope.get(full_name) { self.ctx.bindings[*index].mark_used( scope_id, - Range::from(expr), + expr.range(), context, ); } @@ -4411,7 +4414,7 @@ impl<'a> Checker<'a> { if let Some(index) = scope.get(full_name.as_str()) { self.ctx.bindings[*index].mark_used( scope_id, - Range::from(expr), + expr.range(), context, ); } @@ -4451,7 +4454,7 @@ impl<'a> Checker<'a> { name: id.to_string(), sources, }, - Range::from(expr), + expr.range(), )); } return; @@ -4482,7 +4485,7 @@ impl<'a> Checker<'a> { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedName { name: id.clone() }, - Range::from(expr), + expr.range(), )); } } @@ -4557,7 +4560,7 @@ impl<'a> Checker<'a> { runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(expr), + range: expr.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -4577,7 +4580,7 @@ impl<'a> Checker<'a> { runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(expr), + range: expr.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -4594,7 +4597,7 @@ impl<'a> Checker<'a> { runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(expr), + range: expr.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -4676,7 +4679,7 @@ impl<'a> Checker<'a> { runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(expr), + range: expr.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -4693,7 +4696,7 @@ impl<'a> Checker<'a> { runtime_usage: None, synthetic_usage: None, typing_usage: None, - range: Range::from(expr), + range: expr.range(), source: Some(*self.ctx.current_stmt()), context: self.ctx.execution_context(), exceptions: self.ctx.exceptions(), @@ -4721,7 +4724,7 @@ impl<'a> Checker<'a> { pyflakes::rules::UndefinedName { name: id.to_string(), }, - Range::from(expr), + expr.range(), )); } @@ -4948,9 +4951,9 @@ impl<'a> Checker<'a> { } // Mark anything referenced in `__all__` as used. - let all_bindings: Option<(Vec, Range)> = { + let all_bindings: Option<(Vec, TextRange)> = { let global_scope = self.ctx.global_scope(); - let all_names: Option<(&Vec<&str>, Range)> = global_scope + let all_names: Option<(&Vec<&str>, TextRange)> = global_scope .get("__all__") .map(|index| &self.ctx.bindings[*index]) .and_then(|binding| match &binding.kind { @@ -4980,7 +4983,7 @@ impl<'a> Checker<'a> { } // Extract `__all__` names from the global scope. - let all_names: Option<(&[&str], Range)> = self + let all_names: Option<(&[&str], TextRange)> = self .ctx .global_scope() .get("__all__") @@ -5023,7 +5026,7 @@ impl<'a> Checker<'a> { // F822 if self.settings.rules.enabled(Rule::UndefinedExport) { if !self.path.ends_with("__init__.py") { - if let Some((names, range)) = &all_names { + if let Some((names, range)) = all_names { diagnostics .extend(pyflakes::rules::undefined_export(names, range, scope)); } @@ -5107,10 +5110,13 @@ impl<'a> Checker<'a> { if let Some(indices) = self.ctx.shadowed_bindings.get(index) { for index in indices { let rebound = &self.ctx.bindings[*index]; + #[allow(deprecated)] + let line = self.locator.compute_line_index(binding.range.start()); + let mut diagnostic = Diagnostic::new( pyflakes::rules::RedefinedWhileUnused { name: (*name).to_string(), - line: binding.range.location.row(), + line, }, matches!( rebound.kind, @@ -5126,9 +5132,9 @@ impl<'a> Checker<'a> { ); if let Some(parent) = &rebound.source { if matches!(parent.node, StmtKind::ImportFrom { .. }) - && parent.location.row() != rebound.range.location.row() + && parent.range().contains_range(rebound.range) { - diagnostic.set_parent(parent.location); + diagnostic.set_parent(parent.start()); } }; diagnostics.push(diagnostic); @@ -5178,7 +5184,7 @@ impl<'a> Checker<'a> { if self.settings.rules.enabled(Rule::UnusedImport) { // Collect all unused imports by location. (Multiple unused imports at the same // location indicates an `import from`.) - type UnusedImport<'a> = (&'a str, &'a Range); + type UnusedImport<'a> = (&'a str, &'a TextRange); type BindingContext<'a, 'b> = ( &'a RefEquality<'b, Stmt>, Option<&'a RefEquality<'b, Stmt>>, @@ -5213,16 +5219,16 @@ impl<'a> Checker<'a> { let exceptions = binding.exceptions; let child: &Stmt = defined_by.into(); - let diagnostic_lineno = binding.range.location.row(); - let parent_lineno = if matches!(child.node, StmtKind::ImportFrom { .. }) { - Some(child.location.row()) + let diagnostic_offset = binding.range.start(); + let parent_offset = if matches!(child.node, StmtKind::ImportFrom { .. }) { + Some(child.start()) } else { None }; - if self.rule_is_ignored(Rule::UnusedImport, diagnostic_lineno) - || parent_lineno.map_or(false, |parent_lineno| { - self.rule_is_ignored(Rule::UnusedImport, parent_lineno) + if self.rule_is_ignored(Rule::UnusedImport, diagnostic_offset) + || parent_offset.map_or(false, |parent_offset| { + self.rule_is_ignored(Rule::UnusedImport, parent_offset) }) { ignored @@ -5241,7 +5247,7 @@ impl<'a> Checker<'a> { self.settings.ignore_init_module_imports && self.path.ends_with("__init__.py"); for ((defined_by, defined_in, exceptions), unused_imports) in unused .into_iter() - .sorted_by_key(|((defined_by, ..), ..)| defined_by.location) + .sorted_by_key(|((defined_by, ..), ..)| defined_by.start()) { let child: &Stmt = defined_by.into(); let parent: Option<&Stmt> = defined_in.map(Into::into); @@ -5291,9 +5297,9 @@ impl<'a> Checker<'a> { *range, ); if matches!(child.node, StmtKind::ImportFrom { .. }) { - diagnostic.set_parent(child.location); + diagnostic.set_parent(child.start()); } - if let Some(fix) = fix.as_ref() { + if let Some(fix) = &fix { diagnostic.set_fix(fix.clone()); } diagnostics.push(diagnostic); @@ -5301,7 +5307,7 @@ impl<'a> Checker<'a> { } for ((defined_by, .., exceptions), unused_imports) in ignored .into_iter() - .sorted_by_key(|((defined_by, ..), ..)| defined_by.location) + .sorted_by_key(|((defined_by, ..), ..)| defined_by.start()) { let child: &Stmt = defined_by.into(); let multiple = unused_imports.len() > 1; @@ -5323,7 +5329,7 @@ impl<'a> Checker<'a> { *range, ); if matches!(child.node, StmtKind::ImportFrom { .. }) { - diagnostic.set_parent(child.location); + diagnostic.set_parent(child.start()); } diagnostics.push(diagnostic); } @@ -5451,30 +5457,33 @@ impl<'a> Checker<'a> { // Extract a `Docstring` from a `Definition`. let expr = definition.docstring.unwrap(); - let contents = self.locator.slice(expr); - let indentation = self.locator.slice(Range::new( - Location::new(expr.location.row(), 0), - Location::new(expr.location.row(), expr.location.column()), + let contents = self.locator.slice(expr.range()); + + let indentation = self.locator.slice(TextRange::new( + self.locator.line_start(expr.start()), + expr.start(), )); if pydocstyle::helpers::should_ignore_docstring(contents) { + #[allow(deprecated)] + let location = self.locator.compute_source_location(expr.start()); warn_user!( - "Docstring at {}:{}:{} contains implicit string concatenation; ignoring...", - relativize_path(self.path), - expr.location.row(), - expr.location.column() + 1 - ); + "Docstring at {}:{}:{} contains implicit string concatenation; ignoring...", + relativize_path(self.path), + location.row, + location.column + ); continue; } // SAFETY: Safe for docstrings that pass `should_ignore_docstring`. - let body = str::raw_contents(contents).unwrap(); + let body_range = str::raw_contents_range(contents).unwrap(); let docstring = Docstring { kind: definition.kind, expr, contents, indentation, - body, + body_range, }; if !pydocstyle::rules::not_empty(self, &docstring) { @@ -5624,7 +5633,7 @@ pub fn check_ast( locator: &Locator, stylist: &Stylist, indexer: &Indexer, - noqa_line_for: &IntMap, + noqa_line_for: &NoqaMapping, settings: &Settings, autofix: flags::Autofix, noqa: flags::Noqa, diff --git a/crates/ruff/src/checkers/imports.rs b/crates/ruff/src/checkers/imports.rs index ca1ffc6813..ad0cba81f2 100644 --- a/crates/ruff/src/checkers/imports.rs +++ b/crates/ruff/src/checkers/imports.rs @@ -30,13 +30,11 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> for stmt in blocks.iter().flat_map(|block| &block.imports) { match &stmt.node { StmtKind::Import { names } => { - module_imports.extend(names.iter().map(|name| { - ModuleImport::new( - name.node.name.clone(), - stmt.location, - stmt.end_location.unwrap(), - ) - })); + module_imports.extend( + names + .iter() + .map(|name| ModuleImport::new(name.node.name.clone(), stmt.range())), + ); } StmtKind::ImportFrom { module, @@ -61,11 +59,7 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Cow::Owned(module_path[..module_path.len() - level].join(".")) }; module_imports.extend(names.iter().map(|name| { - ModuleImport::new( - format!("{}.{}", module, name.node.name), - name.location, - name.end_location.unwrap(), - ) + ModuleImport::new(format!("{}.{}", module, name.node.name), name.range()) })); } _ => panic!("Expected StmtKind::Import | StmtKind::ImportFrom"), diff --git a/crates/ruff/src/checkers/logical_lines.rs b/crates/ruff/src/checkers/logical_lines.rs index cade19ab72..88d0f8694a 100644 --- a/crates/ruff/src/checkers/logical_lines.rs +++ b/crates/ruff/src/checkers/logical_lines.rs @@ -1,9 +1,9 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::TextRange; use rustpython_parser::lexer::LexResult; use ruff_diagnostics::{Diagnostic, Fix}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; +use ruff_python_ast::token_kind::TokenKind; use crate::registry::{AsRule, Rule}; use crate::rules::pycodestyle::rules::logical_lines::{ @@ -63,8 +63,7 @@ pub fn check_logical_lines( if settings.rules.enabled(kind.rule()) { diagnostics.push(Diagnostic { kind, - location, - end_location: location, + range: TextRange::empty(location), fix: Fix::empty(), parent: None, }); @@ -75,8 +74,7 @@ pub fn check_logical_lines( if settings.rules.enabled(kind.rule()) { diagnostics.push(Diagnostic { kind, - location, - end_location: location, + range: TextRange::empty(location), fix: Fix::empty(), parent: None, }); @@ -86,8 +84,7 @@ pub fn check_logical_lines( if settings.rules.enabled(kind.rule()) { diagnostics.push(Diagnostic { kind, - location, - end_location: location, + range: TextRange::empty(location), fix: Fix::empty(), parent: None, }); @@ -108,8 +105,7 @@ pub fn check_logical_lines( if settings.rules.enabled(kind.rule()) { diagnostics.push(Diagnostic { kind, - location, - end_location: location, + range: TextRange::empty(location), fix: Fix::empty(), parent: None, }); @@ -121,8 +117,7 @@ pub fn check_logical_lines( if settings.rules.enabled(kind.rule()) { diagnostics.push(Diagnostic { kind, - location, - end_location: location, + range: TextRange::empty(location), fix: Fix::empty(), parent: None, }); @@ -133,8 +128,7 @@ pub fn check_logical_lines( if settings.rules.enabled(kind.rule()) { diagnostics.push(Diagnostic { kind, - location, - end_location: location, + range: TextRange::empty(location), fix: Fix::empty(), parent: None, }); @@ -142,12 +136,13 @@ 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, - location: range.location, - end_location: range.end_location, + range, fix: Fix::empty(), parent: None, }); @@ -167,12 +162,21 @@ pub fn check_logical_lines( } // Extract the indentation level. - let Some(start_loc) = line.first_token_location() else { continue; }; - let start_line = locator.slice(Range::new(Location::new(start_loc.row(), 0), start_loc)); - let indent_level = expand_indent(start_line); + let Some(first_token) = line.first_token() else { + continue; + }; + + let range = if first_token.kind() == TokenKind::Indent { + first_token.range() + } else { + TextRange::new(locator.line_start(first_token.start()), first_token.start()) + }; + + let indent_level = expand_indent(locator.slice(range)); + let indent_size = 4; - for (location, kind) in indentation( + for kind in indentation( &line, prev_line.as_ref(), indent_char, @@ -183,8 +187,7 @@ pub fn check_logical_lines( if settings.rules.enabled(kind.rule()) { diagnostics.push(Diagnostic { kind, - location: Location::new(start_loc.row(), 0), - end_location: location, + range, fix: Fix::empty(), parent: None, }); diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index 714cfd0675..25ad27a999 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -1,15 +1,13 @@ //! `NoQA` enforcement and validation. -use nohash_hasher::IntMap; -use rustpython_parser::ast::Location; +use itertools::Itertools; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Edit}; -use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; +use ruff_python_ast::source_code::Locator; -use crate::codes::NoqaCode; use crate::noqa; -use crate::noqa::{Directive, FileExemption}; +use crate::noqa::{Directive, FileExemption, NoqaDirectives, NoqaMapping}; use crate::registry::{AsRule, Rule}; use crate::rule_redirects::get_redirect_target; use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA}; @@ -17,37 +15,25 @@ use crate::settings::{flags, Settings}; pub fn check_noqa( diagnostics: &mut Vec, - contents: &str, - commented_lines: &[usize], - noqa_line_for: &IntMap, + locator: &Locator, + comment_ranges: &[TextRange], + noqa_line_for: &NoqaMapping, settings: &Settings, autofix: flags::Autofix, ) -> Vec { let enforce_noqa = settings.rules.enabled(Rule::UnusedNOQA); - let lines: Vec<&str> = contents.universal_newlines().collect(); - // Identify any codes that are globally exempted (within the current file). - let exemption = noqa::file_exemption(&lines, commented_lines); - - // Map from line number to `noqa` directive on that line, along with any codes - // that were matched by the directive. - let mut noqa_directives: IntMap)> = IntMap::default(); + let exemption = noqa::file_exemption(locator.contents(), comment_ranges); // Extract all `noqa` directives. - if enforce_noqa { - for lineno in commented_lines { - noqa_directives - .entry(lineno - 1) - .or_insert_with(|| (noqa::extract_noqa_directive(lines[lineno - 1]), vec![])); - } - } + let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator); // Indices of diagnostics that were ignored by a `noqa` directive. let mut ignored_diagnostics = vec![]; // Remove any ignored diagnostics. - for (index, diagnostic) in diagnostics.iter().enumerate() { + 'outer: for (index, diagnostic) in diagnostics.iter().enumerate() { if matches!(diagnostic.kind.rule(), Rule::BlanketNOQA) { continue; } @@ -68,92 +54,65 @@ pub fn check_noqa( FileExemption::None => {} } - let diagnostic_lineno = diagnostic.location.row(); + let noqa_offsets = diagnostic + .parent + .into_iter() + .chain(std::iter::once(diagnostic.start())) + .map(|position| noqa_line_for.resolve(position)) + .unique(); - // Is the violation ignored by a `noqa` directive on the parent line? - if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) { - if parent_lineno != diagnostic_lineno { - let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno); - if commented_lines.contains(noqa_lineno) { - let noqa = noqa_directives.entry(noqa_lineno - 1).or_insert_with(|| { - (noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![]) - }); - match noqa { - (Directive::All(..), matches) => { - matches.push(diagnostic.kind.rule().noqa_code()); - ignored_diagnostics.push(index); - continue; - } - (Directive::Codes(.., codes, _), matches) => { - if noqa::includes(diagnostic.kind.rule(), codes) { - matches.push(diagnostic.kind.rule().noqa_code()); - ignored_diagnostics.push(index); - continue; - } - } - (Directive::None, ..) => {} - } - } - } - } - - // Is the diagnostic ignored by a `noqa` directive on the same line? - let noqa_lineno = noqa_line_for - .get(&diagnostic_lineno) - .unwrap_or(&diagnostic_lineno); - if commented_lines.contains(noqa_lineno) { - let noqa = noqa_directives - .entry(noqa_lineno - 1) - .or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![])); - match noqa { - (Directive::All(..), matches) => { - matches.push(diagnostic.kind.rule().noqa_code()); - ignored_diagnostics.push(index); - continue; - } - (Directive::Codes(.., codes, _), matches) => { - if noqa::includes(diagnostic.kind.rule(), codes) { - matches.push(diagnostic.kind.rule().noqa_code()); + for noqa_offset in noqa_offsets { + if let Some(directive_line) = noqa_directives.find_line_with_directive_mut(noqa_offset) + { + let suppressed = match &directive_line.directive { + Directive::All(..) => { + directive_line + .matches + .push(diagnostic.kind.rule().noqa_code()); ignored_diagnostics.push(index); - continue; + true } + Directive::Codes(.., codes, _) => { + if noqa::includes(diagnostic.kind.rule(), codes) { + directive_line + .matches + .push(diagnostic.kind.rule().noqa_code()); + ignored_diagnostics.push(index); + true + } else { + false + } + } + Directive::None => unreachable!(), + }; + + if suppressed { + continue 'outer; } - (Directive::None, ..) => {} } } } // Enforce that the noqa directive was actually used (RUF100). if enforce_noqa { - for (row, (directive, matches)) in noqa_directives { - match directive { - Directive::All(leading_spaces, start_byte, end_byte, trailing_spaces) => { - if matches.is_empty() { - let start_char = lines[row][..start_byte].chars().count(); - let end_char = - start_char + lines[row][start_byte..end_byte].chars().count(); - - let mut diagnostic = Diagnostic::new( - UnusedNOQA { codes: None }, - Range::new( - Location::new(row + 1, start_char), - Location::new(row + 1, end_char), - ), - ); + for line in noqa_directives.lines() { + match &line.directive { + Directive::All(leading_spaces, noqa_range, trailing_spaces) => { + if line.matches.is_empty() { + let mut diagnostic = + Diagnostic::new(UnusedNOQA { codes: None }, *noqa_range); if autofix.into() && settings.rules.should_fix(diagnostic.kind.rule()) { diagnostic.set_fix(delete_noqa( - row, - lines[row], - leading_spaces, - start_byte, - end_byte, - trailing_spaces, + *leading_spaces, + *noqa_range, + *trailing_spaces, + locator, )); } diagnostics.push(diagnostic); } } - Directive::Codes(leading_spaces, start_byte, end_byte, codes, trailing_spaces) => { + Directive::Codes(leading_spaces, range, codes, trailing_spaces) => { let mut disabled_codes = vec![]; let mut unknown_codes = vec![]; let mut unmatched_codes = vec![]; @@ -166,7 +125,9 @@ pub fn check_noqa( break; } - if matches.iter().any(|m| *m == code) || settings.external.contains(code) { + if line.matches.iter().any(|m| *m == code) + || settings.external.contains(code) + { valid_codes.push(code); } else { if let Ok(rule) = Rule::from_code(code) { @@ -189,10 +150,6 @@ pub fn check_noqa( && unknown_codes.is_empty() && unmatched_codes.is_empty()) { - let start_char = lines[row][..start_byte].chars().count(); - let end_char = - start_char + lines[row][start_byte..end_byte].chars().count(); - let mut diagnostic = Diagnostic::new( UnusedNOQA { codes: Some(UnusedCodes { @@ -210,26 +167,20 @@ pub fn check_noqa( .collect(), }), }, - Range::new( - Location::new(row + 1, start_char), - Location::new(row + 1, end_char), - ), + *range, ); if autofix.into() && settings.rules.should_fix(diagnostic.kind.rule()) { if valid_codes.is_empty() { diagnostic.set_fix(delete_noqa( - row, - lines[row], - leading_spaces, - start_byte, - end_byte, - trailing_spaces, + *leading_spaces, + *range, + *trailing_spaces, + locator, )); } else { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( format!("# noqa: {}", valid_codes.join(", ")), - Location::new(row + 1, start_char), - Location::new(row + 1, end_char), + *range, )); } } @@ -247,39 +198,37 @@ pub fn check_noqa( /// Generate a [`Edit`] to delete a `noqa` directive. fn delete_noqa( - row: usize, - line: &str, - leading_spaces: usize, - start_byte: usize, - end_byte: usize, - trailing_spaces: usize, + leading_spaces: TextSize, + noqa_range: TextRange, + trailing_spaces: TextSize, + locator: &Locator, ) -> Edit { - if start_byte - leading_spaces == 0 && end_byte == line.len() { - // Ex) `# noqa` - Edit::deletion(Location::new(row + 1, 0), Location::new(row + 2, 0)) - } else if end_byte == line.len() { - // Ex) `x = 1 # noqa` - let start_char = line[..start_byte].chars().count(); - let end_char = start_char + line[start_byte..end_byte].chars().count(); - Edit::deletion( - Location::new(row + 1, start_char - leading_spaces), - Location::new(row + 1, end_char + trailing_spaces), + let line_range = locator.line_range(noqa_range.start()); + + // Ex) `# noqa` + if line_range + == TextRange::new( + noqa_range.start() - leading_spaces, + noqa_range.end() + trailing_spaces, ) - } else if line[end_byte..].trim_start().starts_with('#') { - // Ex) `x = 1 # noqa # type: ignore` - let start_char = line[..start_byte].chars().count(); - let end_char = start_char + line[start_byte..end_byte].chars().count(); + { + let full_line_end = locator.full_line_end(line_range.end()); + Edit::deletion(line_range.start(), full_line_end) + } + // Ex) `x = 1 # noqa` + else if noqa_range.end() + trailing_spaces == line_range.end() { + Edit::deletion(noqa_range.start() - leading_spaces, line_range.end()) + } + // Ex) `x = 1 # noqa # type: ignore` + else if locator.contents()[usize::from(noqa_range.end() + trailing_spaces)..].starts_with('#') + { + Edit::deletion(noqa_range.start(), noqa_range.end() + trailing_spaces) + } + // Ex) `x = 1 # noqa here` + else { Edit::deletion( - Location::new(row + 1, start_char), - Location::new(row + 1, end_char + trailing_spaces), - ) - } else { - // Ex) `x = 1 # noqa here` - let start_char = line[..start_byte].chars().count(); - let end_char = start_char + line[start_byte..end_byte].chars().count(); - Edit::deletion( - Location::new(row + 1, start_char + 1 + 1), - Location::new(row + 1, end_char + trailing_spaces), + noqa_range.start() + "# ".text_len(), + noqa_range.end() + trailing_spaces, ) } } diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 692e0bf8a1..b4437cad52 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -1,5 +1,6 @@ //! Lint rules based on checking physical lines. +use ruff_text_size::TextSize; use std::path::Path; use ruff_diagnostics::Diagnostic; @@ -25,7 +26,7 @@ pub fn check_physical_lines( locator: &Locator, stylist: &Stylist, indexer: &Indexer, - doc_lines: &[usize], + doc_lines: &[TextSize], settings: &Settings, autofix: flags::Autofix, ) -> Vec { @@ -55,20 +56,19 @@ pub fn check_physical_lines( let fix_shebang_whitespace = autofix.into() && settings.rules.should_fix(Rule::ShebangLeadingWhitespace); - let mut commented_lines_iter = indexer.commented_lines().iter().peekable(); + let mut commented_lines_iter = indexer.comment_ranges().iter().peekable(); let mut doc_lines_iter = doc_lines.iter().peekable(); - - let string_lines = indexer.string_ranges(); + let string_lines = indexer.triple_quoted_string_ranges(); for (index, line) in locator.contents().universal_newlines().enumerate() { while commented_lines_iter - .next_if(|lineno| &(index + 1) == *lineno) + .next_if(|comment_range| line.range().contains_range(**comment_range)) .is_some() { if enforce_unnecessary_coding_comment { if index < 2 { if let Some(diagnostic) = - unnecessary_coding_comment(index, line, fix_unnecessary_coding_comment) + unnecessary_coding_comment(&line, fix_unnecessary_coding_comment) { diagnostics.push(diagnostic); } @@ -76,11 +76,11 @@ pub fn check_physical_lines( } if enforce_blanket_type_ignore { - blanket_type_ignore(&mut diagnostics, index, line); + blanket_type_ignore(&mut diagnostics, &line); } if enforce_blanket_noqa { - blanket_noqa(&mut diagnostics, index, line); + blanket_noqa(&mut diagnostics, &line); } if enforce_shebang_missing @@ -89,31 +89,31 @@ pub fn check_physical_lines( || enforce_shebang_newline || enforce_shebang_python { - let shebang = extract_shebang(line); + let shebang = extract_shebang(&line); if enforce_shebang_not_executable { - if let Some(diagnostic) = shebang_not_executable(path, index, &shebang) { + if let Some(diagnostic) = shebang_not_executable(path, line.range(), &shebang) { diagnostics.push(diagnostic); } } if enforce_shebang_missing { - if !has_any_shebang && matches!(shebang, ShebangDirective::Match(_, _, _, _)) { + if !has_any_shebang && matches!(shebang, ShebangDirective::Match(..)) { has_any_shebang = true; } } if enforce_shebang_whitespace { if let Some(diagnostic) = - shebang_whitespace(index, &shebang, fix_shebang_whitespace) + shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace) { diagnostics.push(diagnostic); } } if enforce_shebang_newline { - if let Some(diagnostic) = shebang_newline(index, &shebang) { + if let Some(diagnostic) = shebang_newline(line.range(), &shebang, index == 0) { diagnostics.push(diagnostic); } } if enforce_shebang_python { - if let Some(diagnostic) = shebang_python(index, &shebang) { + if let Some(diagnostic) = shebang_python(line.range(), &shebang) { diagnostics.push(diagnostic); } } @@ -121,40 +121,40 @@ pub fn check_physical_lines( } while doc_lines_iter - .next_if(|lineno| &(index + 1) == *lineno) + .next_if(|doc_line_start| line.range().contains(**doc_line_start)) .is_some() { if enforce_doc_line_too_long { - if let Some(diagnostic) = doc_line_too_long(index, line, settings) { + if let Some(diagnostic) = doc_line_too_long(&line, settings) { diagnostics.push(diagnostic); } } } if enforce_mixed_spaces_and_tabs { - if let Some(diagnostic) = mixed_spaces_and_tabs(index, line) { + if let Some(diagnostic) = mixed_spaces_and_tabs(&line) { diagnostics.push(diagnostic); } } if enforce_line_too_long { - if let Some(diagnostic) = line_too_long(index, line, settings) { + if let Some(diagnostic) = line_too_long(&line, settings) { diagnostics.push(diagnostic); } } if enforce_bidirectional_unicode { - diagnostics.extend(pylint::rules::bidirectional_unicode(index, line)); + diagnostics.extend(pylint::rules::bidirectional_unicode(&line)); } if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace { - if let Some(diagnostic) = trailing_whitespace(index, line, settings, autofix) { + if let Some(diagnostic) = trailing_whitespace(&line, settings, autofix) { diagnostics.push(diagnostic); } } if enforce_tab_indentation { - if let Some(diagnostic) = tab_indentation(index + 1, line, string_lines) { + if let Some(diagnostic) = tab_indentation(&line, string_lines) { diagnostics.push(diagnostic); } } @@ -197,7 +197,7 @@ mod tests { let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8. let locator = Locator::new(line); let tokens: Vec<_> = lex(line, Mode::Module).collect(); - let indexer: Indexer = tokens.as_slice().into(); + let indexer = Indexer::from_tokens(&tokens, &locator); let stylist = Stylist::from_tokens(&tokens, &locator); let check_with_max_line_length = |line_length: usize| { diff --git a/crates/ruff/src/checkers/tokens.rs b/crates/ruff/src/checkers/tokens.rs index 8f337b471f..0b9f76e619 100644 --- a/crates/ruff/src/checkers/tokens.rs +++ b/crates/ruff/src/checkers/tokens.rs @@ -64,7 +64,7 @@ pub fn check_tokens( // RUF001, RUF002, RUF003 if enforce_ambiguous_unicode_character { let mut state_machine = StateMachine::default(); - for &(start, ref tok, end) in tokens.iter().flatten() { + for &(ref tok, range) in tokens.iter().flatten() { let is_docstring = if enforce_ambiguous_unicode_character { state_machine.consume(tok) } else { @@ -74,8 +74,7 @@ pub fn check_tokens( if matches!(tok, Tok::String { .. } | Tok::Comment(_)) { diagnostics.extend(ruff::rules::ambiguous_unicode_character( locator, - start, - end, + range, if matches!(tok, Tok::String { .. }) { if is_docstring { Context::Docstring @@ -94,10 +93,10 @@ pub fn check_tokens( // ERA001 if enforce_commented_out_code { - for (start, tok, end) in tokens.iter().flatten() { + for (tok, range) in tokens.iter().flatten() { if matches!(tok, Tok::Comment(_)) { if let Some(diagnostic) = - eradicate::rules::commented_out_code(locator, *start, *end, settings, autofix) + eradicate::rules::commented_out_code(locator, *range, settings, autofix) { diagnostics.push(diagnostic); } @@ -107,12 +106,11 @@ pub fn check_tokens( // W605 if enforce_invalid_escape_sequence { - for (start, tok, end) in tokens.iter().flatten() { + for (tok, range) in tokens.iter().flatten() { if matches!(tok, Tok::String { .. }) { diagnostics.extend(pycodestyle::rules::invalid_escape_sequence( locator, - *start, - *end, + *range, autofix.into() && settings.rules.should_fix(Rule::InvalidEscapeSequence), )); } @@ -120,10 +118,10 @@ pub fn check_tokens( } // PLE2510, PLE2512, PLE2513 if enforce_invalid_string_character { - for (start, tok, end) in tokens.iter().flatten() { + for (tok, range) in tokens.iter().flatten() { if matches!(tok, Tok::String { .. }) { diagnostics.extend( - pylint::rules::invalid_string_characters(locator, *start, *end, autofix.into()) + pylint::rules::invalid_string_characters(locator, *range, autofix.into()) .into_iter() .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), ); @@ -155,6 +153,7 @@ pub fn check_tokens( flake8_implicit_str_concat::rules::implicit( tokens, &settings.flake8_implicit_str_concat, + locator, ) .into_iter() .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 5a1f864cd3..bbb33c41d4 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -1,8 +1,15 @@ use crate::registry::{Linter, Rule}; +use std::fmt::Formatter; #[derive(PartialEq, Eq, PartialOrd, Ord)] pub struct NoqaCode(&'static str, &'static str); +impl std::fmt::Debug for NoqaCode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + impl std::fmt::Display for NoqaCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, "{}{}", self.0, self.1) diff --git a/crates/ruff/src/directives.rs b/crates/ruff/src/directives.rs index 094670d6a2..77e94177b4 100644 --- a/crates/ruff/src/directives.rs +++ b/crates/ruff/src/directives.rs @@ -1,8 +1,9 @@ //! Extract `# noqa` and `# isort: skip` directives from tokenized source. +use crate::noqa::NoqaMapping; use bitflags::bitflags; -use nohash_hasher::{IntMap, IntSet}; -use rustpython_parser::ast::Location; +use ruff_python_ast::source_code::{Indexer, Locator}; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; @@ -11,7 +12,7 @@ use crate::settings::Settings; bitflags! { #[derive(Debug, Copy, Clone)] pub struct Flags: u8 { - const NOQA = 0b0000_0001; + const NOQA = 0b0000_0001; const ISORT = 0b0000_0010; } } @@ -30,27 +31,50 @@ impl Flags { } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct IsortDirectives { - pub exclusions: IntSet, - pub splits: Vec, + /// Ranges for which sorting is disabled + pub exclusions: Vec, + /// Text positions at which splits should be inserted + pub splits: Vec, pub skip_file: bool, } +impl IsortDirectives { + pub fn is_excluded(&self, offset: TextSize) -> bool { + for range in &self.exclusions { + if range.contains(offset) { + return true; + } + + if range.start() > offset { + break; + } + } + + false + } +} + pub struct Directives { - pub noqa_line_for: IntMap, + pub noqa_line_for: NoqaMapping, pub isort: IsortDirectives, } -pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives { +pub fn extract_directives( + lxr: &[LexResult], + flags: Flags, + locator: &Locator, + indexer: &Indexer, +) -> Directives { Directives { noqa_line_for: if flags.contains(Flags::NOQA) { - extract_noqa_line_for(lxr) + extract_noqa_line_for(lxr, locator, indexer) } else { - IntMap::default() + NoqaMapping::default() }, isort: if flags.contains(Flags::ISORT) { - extract_isort_directives(lxr) + extract_isort_directives(lxr, locator) } else { IsortDirectives::default() }, @@ -58,48 +82,92 @@ pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives { } /// Extract a mapping from logical line to noqa line. -pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap { - let mut noqa_line_for: IntMap = IntMap::default(); - let mut prev_non_newline: Option<(&Location, &Tok, &Location)> = None; - for (start, tok, end) in lxr.iter().flatten() { - if matches!(tok, Tok::EndOfFile) { - break; - } - // For multi-line strings, we expect `noqa` directives on the last line of the - // string. - if matches!(tok, Tok::String { .. }) && end.row() > start.row() { - for i in start.row()..end.row() { - noqa_line_for.insert(i, end.row()); +pub fn extract_noqa_line_for( + lxr: &[LexResult], + locator: &Locator, + indexer: &Indexer, +) -> NoqaMapping { + let mut string_mappings = Vec::new(); + + for (tok, range) in lxr.iter().flatten() { + match tok { + Tok::EndOfFile => { + break; } - } - // For continuations, we expect `noqa` directives on the last line of the - // continuation. - if matches!( - tok, - Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..) - ) { - if let Some((.., end)) = prev_non_newline { - for i in end.row()..start.row() { - noqa_line_for.insert(i, start.row()); + + // For multi-line strings, we expect `noqa` directives on the last line of the + // string. + Tok::String { + triple_quoted: true, + .. + } => { + if locator.contains_line_break(*range) { + string_mappings.push(*range); } } - prev_non_newline = None; - } else if prev_non_newline.is_none() { - prev_non_newline = Some((start, tok, end)); + + _ => {} } } - noqa_line_for + + let mut continuation_mappings = Vec::new(); + + // For continuations, we expect `noqa` directives on the last line of the + // continuation. + let mut last: Option = None; + for continuation_line in indexer.continuation_line_starts() { + let line_end = locator.full_line_end(*continuation_line); + if let Some(last_range) = last.take() { + if last_range.end() == *continuation_line { + last = Some(TextRange::new(last_range.start(), line_end)); + continue; + } + // new continuation + continuation_mappings.push(last_range); + } + + last = Some(TextRange::new(*continuation_line, line_end)); + } + + if let Some(last_range) = last.take() { + continuation_mappings.push(last_range); + } + + // Merge the mappings in sorted order + let mut mappings = + NoqaMapping::with_capacity(continuation_mappings.len() + string_mappings.len()); + + let mut continuation_mappings = continuation_mappings.into_iter().peekable(); + let mut string_mappings = string_mappings.into_iter().peekable(); + + while let (Some(continuation), Some(string)) = + (continuation_mappings.peek(), string_mappings.peek()) + { + if continuation.start() <= string.start() { + mappings.push_mapping(continuation_mappings.next().unwrap()); + } else { + mappings.push_mapping(string_mappings.next().unwrap()); + } + } + + for mapping in continuation_mappings { + mappings.push_mapping(mapping); + } + + for mapping in string_mappings { + mappings.push_mapping(mapping); + } + + mappings } -/// Extract a set of lines over which to disable isort. -pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives { - let mut exclusions: IntSet = IntSet::default(); - let mut splits: Vec = Vec::default(); - let mut off: Option = None; - let mut last: Option = None; - for &(start, ref tok, end) in lxr.iter().flatten() { - last = Some(end); +/// Extract a set of ranges over which to disable isort. +pub fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives { + let mut exclusions: Vec = Vec::default(); + let mut splits: Vec = Vec::default(); + let mut off: Option = None; + for &(ref tok, range) in lxr.iter().flatten() { let Tok::Comment(comment_text) = tok else { continue; }; @@ -109,7 +177,7 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives { // required to include the space, and must appear on their own lines. let comment_text = comment_text.trim_end(); if matches!(comment_text, "# isort: split" | "# ruff: isort: split") { - splits.push(start.row()); + splits.push(range.start()); } else if matches!( comment_text, "# isort: skip_file" @@ -123,30 +191,25 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives { }; } else if off.is_some() { if comment_text == "# isort: on" || comment_text == "# ruff: isort: on" { - if let Some(start) = off { - for row in start.row() + 1..=end.row() { - exclusions.insert(row); - } + if let Some(exclusion_start) = off { + exclusions.push(TextRange::new(exclusion_start, range.start())); } off = None; } } else { if comment_text.contains("isort: skip") || comment_text.contains("isort:skip") { - exclusions.insert(start.row()); + exclusions.push(locator.line_range(range.start())); } else if comment_text == "# isort: off" || comment_text == "# ruff: isort: off" { - off = Some(start); + off = Some(range.start()); } } } if let Some(start) = off { // Enforce unterminated `isort: off`. - if let Some(end) = last { - for row in start.row() + 1..=end.row() { - exclusions.insert(row); - } - } + exclusions.push(TextRange::new(start, locator.contents().text_len())); } + IsortDirectives { exclusions, splits, @@ -156,120 +219,98 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives { #[cfg(test)] mod tests { - use nohash_hasher::{IntMap, IntSet}; + use ruff_python_ast::source_code::{Indexer, Locator}; + use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::lexer::LexResult; use rustpython_parser::{lexer, Mode}; use crate::directives::{extract_isort_directives, extract_noqa_line_for}; + use crate::noqa::NoqaMapping; + + fn noqa_mappings(contents: &str) -> NoqaMapping { + let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); + let locator = Locator::new(contents); + let indexer = Indexer::from_tokens(&lxr, &locator); + + extract_noqa_line_for(&lxr, &locator, &indexer) + } #[test] fn noqa_extraction() { - let lxr: Vec = lexer::lex( - "x = 1 -y = 2 -z = x + 1", - Mode::Module, - ) - .collect(); - assert_eq!(extract_noqa_line_for(&lxr), IntMap::default()); + let contents = "x = 1 +y = 2 \ + + 1 +z = x + 1"; - let lxr: Vec = lexer::lex( - " + assert_eq!(noqa_mappings(contents), NoqaMapping::default()); + + let contents = " x = 1 y = 2 -z = x + 1", - Mode::Module, - ) - .collect(); - assert_eq!(extract_noqa_line_for(&lxr), IntMap::default()); +z = x + 1"; + assert_eq!(noqa_mappings(contents), NoqaMapping::default()); - let lxr: Vec = lexer::lex( - "x = 1 + let contents = "x = 1 y = 2 z = x + 1 - ", - Mode::Module, - ) - .collect(); - assert_eq!(extract_noqa_line_for(&lxr), IntMap::default()); + "; + assert_eq!(noqa_mappings(contents), NoqaMapping::default()); - let lxr: Vec = lexer::lex( - "x = 1 + let contents = "x = 1 y = 2 z = x + 1 - ", - Mode::Module, - ) - .collect(); - assert_eq!(extract_noqa_line_for(&lxr), IntMap::default()); + "; + assert_eq!(noqa_mappings(contents), NoqaMapping::default()); - let lxr: Vec = lexer::lex( - "x = '''abc + let contents = "x = '''abc def ghi ''' y = 2 -z = x + 1", - Mode::Module, - ) - .collect(); +z = x + 1"; assert_eq!( - extract_noqa_line_for(&lxr), - IntMap::from_iter([(1, 4), (2, 4), (3, 4)]) + noqa_mappings(contents), + NoqaMapping::from_iter([TextRange::new(TextSize::from(4), TextSize::from(22)),]) ); - let lxr: Vec = lexer::lex( - "x = 1 + let contents = "x = 1 y = '''abc def ghi ''' -z = 2", - Mode::Module, - ) - .collect(); +z = 2"; assert_eq!( - extract_noqa_line_for(&lxr), - IntMap::from_iter([(2, 5), (3, 5), (4, 5)]) + noqa_mappings(contents), + NoqaMapping::from_iter([TextRange::new(TextSize::from(10), TextSize::from(28))]) ); - let lxr: Vec = lexer::lex( - "x = 1 + let contents = "x = 1 y = '''abc def ghi -'''", - Mode::Module, - ) - .collect(); +'''"; assert_eq!( - extract_noqa_line_for(&lxr), - IntMap::from_iter([(2, 5), (3, 5), (4, 5)]) + noqa_mappings(contents), + NoqaMapping::from_iter([TextRange::new(TextSize::from(10), TextSize::from(28))]) ); - let lxr: Vec = lexer::lex( - r#"x = \ - 1"#, - Mode::Module, - ) - .collect(); - assert_eq!(extract_noqa_line_for(&lxr), IntMap::from_iter([(1, 2)])); + let contents = r#"x = \ + 1"#; + assert_eq!( + noqa_mappings(contents), + NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(6))]) + ); - let lxr: Vec = lexer::lex( - r#"from foo import \ + let contents = r#"from foo import \ bar as baz, \ - qux as quux"#, - Mode::Module, - ) - .collect(); + qux as quux"#; assert_eq!( - extract_noqa_line_for(&lxr), - IntMap::from_iter([(1, 3), (2, 3)]) + noqa_mappings(contents), + NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(36))]) ); - let lxr: Vec = lexer::lex( - r#" + let contents = r#" # Foo from foo import \ bar as baz, \ @@ -277,13 +318,14 @@ from foo import \ x = \ 1 y = \ - 2"#, - Mode::Module, - ) - .collect(); + 2"#; assert_eq!( - extract_noqa_line_for(&lxr), - IntMap::from_iter([(3, 5), (4, 5), (6, 7), (8, 9)]) + noqa_mappings(contents), + NoqaMapping::from_iter([ + TextRange::new(TextSize::from(7), TextSize::from(43)), + TextRange::new(TextSize::from(65), TextSize::from(71)), + TextRange::new(TextSize::from(77), TextSize::from(83)), + ]) ); } @@ -293,7 +335,10 @@ y = \ y = 2 z = x + 1"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default()); + assert_eq!( + extract_isort_directives(&lxr, &Locator::new(contents)).exclusions, + Vec::default() + ); let contents = "# isort: off x = 1 @@ -302,8 +347,8 @@ y = 2 z = x + 1"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); assert_eq!( - extract_isort_directives(&lxr).exclusions, - IntSet::from_iter([2, 3, 4]) + extract_isort_directives(&lxr, &Locator::new(contents)).exclusions, + Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(25))]) ); let contents = "# isort: off @@ -315,8 +360,8 @@ z = x + 1 # isort: on"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); assert_eq!( - extract_isort_directives(&lxr).exclusions, - IntSet::from_iter([2, 3, 4, 5]) + extract_isort_directives(&lxr, &Locator::new(contents)).exclusions, + Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(38))]) ); let contents = "# isort: off @@ -325,8 +370,8 @@ y = 2 z = x + 1"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); assert_eq!( - extract_isort_directives(&lxr).exclusions, - IntSet::from_iter([2, 3, 4]) + extract_isort_directives(&lxr, &Locator::new(contents)).exclusions, + Vec::from_iter([TextRange::at(TextSize::from(0), contents.text_len())]) ); let contents = "# isort: skip_file @@ -334,7 +379,10 @@ x = 1 y = 2 z = x + 1"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default()); + assert_eq!( + extract_isort_directives(&lxr, &Locator::new(contents)).exclusions, + Vec::default() + ); let contents = "# isort: off x = 1 @@ -343,7 +391,10 @@ y = 2 # isort: skip_file z = x + 1"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default()); + assert_eq!( + extract_isort_directives(&lxr, &Locator::new(contents)).exclusions, + Vec::default() + ); } #[test] @@ -352,19 +403,28 @@ z = x + 1"; y = 2 z = x + 1"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - assert_eq!(extract_isort_directives(&lxr).splits, Vec::::new()); + assert_eq!( + extract_isort_directives(&lxr, &Locator::new(contents)).splits, + Vec::new() + ); let contents = "x = 1 y = 2 # isort: split z = x + 1"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - assert_eq!(extract_isort_directives(&lxr).splits, vec![3]); + assert_eq!( + extract_isort_directives(&lxr, &Locator::new(contents)).splits, + vec![TextSize::from(12)] + ); let contents = "x = 1 y = 2 # isort: split z = x + 1"; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - assert_eq!(extract_isort_directives(&lxr).splits, vec![2]); + assert_eq!( + extract_isort_directives(&lxr, &Locator::new(contents)).splits, + vec![TextSize::from(13)] + ); } } diff --git a/crates/ruff/src/doc_lines.rs b/crates/ruff/src/doc_lines.rs index c06ec1704b..7cb8de1ff6 100644 --- a/crates/ruff/src/doc_lines.rs +++ b/crates/ruff/src/doc_lines.rs @@ -1,8 +1,10 @@ //! Doc line extraction. In this context, a doc line is a line consisting of a //! standalone comment or a constant string statement. +use ruff_text_size::{TextRange, TextSize}; use std::iter::FusedIterator; +use ruff_python_ast::source_code::Locator; use rustpython_parser::ast::{Constant, ExprKind, Stmt, StmtKind, Suite}; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; @@ -11,46 +13,56 @@ use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; /// Extract doc lines (standalone comments) from a token sequence. -pub fn doc_lines_from_tokens(lxr: &[LexResult]) -> DocLines { - DocLines::new(lxr) +pub fn doc_lines_from_tokens<'a>(lxr: &'a [LexResult], locator: &'a Locator<'a>) -> DocLines<'a> { + DocLines::new(lxr, locator) } pub struct DocLines<'a> { inner: std::iter::Flatten>, - prev: Option, + locator: &'a Locator<'a>, + prev: TextSize, } impl<'a> DocLines<'a> { - fn new(lxr: &'a [LexResult]) -> Self { + fn new(lxr: &'a [LexResult], locator: &'a Locator) -> Self { Self { inner: lxr.iter().flatten(), - prev: None, + locator, + prev: TextSize::default(), } } } impl Iterator for DocLines<'_> { - type Item = usize; + type Item = TextSize; fn next(&mut self) -> Option { + let mut at_start_of_line = true; loop { - let (start, tok, end) = self.inner.next()?; + let (tok, range) = self.inner.next()?; match tok { - Tok::Indent | Tok::Dedent | Tok::Newline => continue, Tok::Comment(..) => { - if let Some(prev) = self.prev { - if start.row() > prev { - break Some(start.row()); - } - } else { - break Some(start.row()); + if at_start_of_line + || self + .locator + .contains_line_break(TextRange::new(self.prev, range.start())) + { + break Some(range.start()); } } - _ => {} + Tok::Newline => { + at_start_of_line = true; + } + Tok::Indent | Tok::Dedent => { + // ignore + } + _ => { + at_start_of_line = false; + } } - self.prev = Some(end.row()); + self.prev = range.end(); } } } @@ -59,7 +71,7 @@ impl FusedIterator for DocLines<'_> {} #[derive(Default)] struct StringLinesVisitor { - string_lines: Vec, + string_lines: Vec, } impl Visitor<'_> for StringLinesVisitor { @@ -70,16 +82,15 @@ impl Visitor<'_> for StringLinesVisitor { .. } = &value.node { - self.string_lines - .extend(value.location.row()..=value.end_location.unwrap().row()); + self.string_lines.push(value.start()); } } visitor::walk_stmt(self, stmt); } } -/// Extract doc lines (standalone strings) from an AST. -pub fn doc_lines_from_ast(python_ast: &Suite) -> Vec { +/// Extract doc lines (standalone strings) start positions from an AST. +pub fn doc_lines_from_ast(python_ast: &Suite) -> Vec { let mut visitor = StringLinesVisitor::default(); visitor.visit_body(python_ast); visitor.string_lines diff --git a/crates/ruff/src/docstrings/definition.rs b/crates/ruff/src/docstrings/definition.rs index 9a6e38d443..cf338a364a 100644 --- a/crates/ruff/src/docstrings/definition.rs +++ b/crates/ruff/src/docstrings/definition.rs @@ -1,4 +1,7 @@ +use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{Expr, Stmt}; +use std::fmt::{Debug, Formatter}; +use std::ops::Deref; use ruff_python_semantic::analyze::visibility::{ class_visibility, function_visibility, method_visibility, Modifier, Visibility, VisibleScope, @@ -25,11 +28,78 @@ pub struct Definition<'a> { pub struct Docstring<'a> { pub kind: DefinitionKind<'a>, pub expr: &'a Expr, + /// The content of the docstring, including the leading and trailing quotes. pub contents: &'a str, - pub body: &'a str, + + /// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`]. + pub body_range: TextRange, pub indentation: &'a str, } +impl<'a> Docstring<'a> { + pub fn body(&self) -> DocstringBody { + DocstringBody { docstring: self } + } + + pub const fn start(&self) -> TextSize { + self.expr.start() + } + + pub const fn end(&self) -> TextSize { + self.expr.end() + } + + pub const fn range(&self) -> TextRange { + self.expr.range() + } + + pub fn leading_quote(&self) -> &'a str { + &self.contents[TextRange::up_to(self.body_range.start())] + } +} + +#[derive(Copy, Clone)] +pub struct DocstringBody<'a> { + docstring: &'a Docstring<'a>, +} + +impl<'a> DocstringBody<'a> { + #[inline] + pub fn start(self) -> TextSize { + self.range().start() + } + + #[inline] + pub fn end(self) -> TextSize { + self.range().end() + } + + pub fn range(self) -> TextRange { + self.docstring.body_range + self.docstring.start() + } + + pub fn as_str(self) -> &'a str { + &self.docstring.contents[self.docstring.body_range] + } +} + +impl Deref for DocstringBody<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl Debug for DocstringBody<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DocstringBody") + .field("text", &self.as_str()) + .field("range", &self.range()) + .finish() + } +} + #[derive(Copy, Clone)] pub enum Documentable { Class, diff --git a/crates/ruff/src/docstrings/sections.rs b/crates/ruff/src/docstrings/sections.rs index b8169aa53b..1685c7e761 100644 --- a/crates/ruff/src/docstrings/sections.rs +++ b/crates/ruff/src/docstrings/sections.rs @@ -1,5 +1,10 @@ +use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator}; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use std::fmt::{Debug, Formatter}; +use std::iter::FusedIterator; use strum_macros::EnumIter; +use crate::docstrings::definition::{Docstring, DocstringBody}; use crate::docstrings::styles::SectionStyle; use ruff_python_ast::whitespace; @@ -125,17 +130,259 @@ impl SectionKind { } } +pub(crate) struct SectionContexts<'a> { + contexts: Vec, + docstring: &'a Docstring<'a>, +} + +impl<'a> SectionContexts<'a> { + /// Extract all `SectionContext` values from a docstring. + pub fn from_docstring(docstring: &'a Docstring<'a>, style: SectionStyle) -> Self { + let contents = docstring.body(); + + let mut contexts = Vec::new(); + let mut last: Option = None; + let mut previous_line = None; + + for line in contents.universal_newlines() { + if previous_line.is_none() { + // skip the first line + previous_line = Some(line.as_str()); + continue; + } + + if let Some(section_kind) = suspected_as_section(&line, style) { + let indent = whitespace::leading_space(&line); + let section_name = whitespace::leading_words(&line); + + let section_name_range = TextRange::at(indent.text_len(), section_name.text_len()); + + if is_docstring_section( + &line, + section_name_range, + previous_line.unwrap_or_default(), + ) { + if let Some(mut last) = last.take() { + last.range = TextRange::new(last.range.start(), line.start()); + contexts.push(last); + } + + last = Some(SectionContextData { + kind: section_kind, + name_range: section_name_range + line.start(), + range: TextRange::empty(line.start()), + summary_full_end: line.full_end(), + }); + } + } + + previous_line = Some(line.as_str()); + } + + if let Some(mut last) = last.take() { + last.range = TextRange::new(last.range.start(), contents.text_len()); + contexts.push(last); + } + + Self { + contexts, + docstring, + } + } + + pub fn len(&self) -> usize { + self.contexts.len() + } + + pub fn iter(&self) -> SectionContextsIter { + SectionContextsIter { + docstring_body: self.docstring.body(), + inner: self.contexts.iter(), + } + } +} + +impl<'a> IntoIterator for &'a SectionContexts<'a> { + type Item = SectionContext<'a>; + type IntoIter = SectionContextsIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl Debug for SectionContexts<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.iter()).finish() + } +} + +pub struct SectionContextsIter<'a> { + docstring_body: DocstringBody<'a>, + inner: std::slice::Iter<'a, SectionContextData>, +} + +impl<'a> Iterator for SectionContextsIter<'a> { + type Item = SectionContext<'a>; + + fn next(&mut self) -> Option { + let next = self.inner.next()?; + + Some(SectionContext { + data: next, + docstring_body: self.docstring_body, + }) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl<'a> DoubleEndedIterator for SectionContextsIter<'a> { + fn next_back(&mut self) -> Option { + let back = self.inner.next_back()?; + Some(SectionContext { + data: back, + docstring_body: self.docstring_body, + }) + } +} + +impl FusedIterator for SectionContextsIter<'_> {} +impl ExactSizeIterator for SectionContextsIter<'_> {} + #[derive(Debug)] -pub(crate) struct SectionContext<'a> { - /// The "kind" of the section, e.g. "SectionKind::Args" or "SectionKind::Returns". - pub(crate) kind: SectionKind, +struct SectionContextData { + kind: SectionKind, + + /// Range of the section name, relative to the [`Docstring::body`] + name_range: TextRange, + + /// Range from the start to the end of the section, relative to the [`Docstring::body`] + range: TextRange, + + /// End of the summary, relative to the [`Docstring::body`] + summary_full_end: TextSize, +} + +pub struct SectionContext<'a> { + data: &'a SectionContextData, + docstring_body: DocstringBody<'a>, +} + +impl<'a> SectionContext<'a> { + pub fn is_last(&self) -> bool { + self.range().end() == self.docstring_body.end() + } + + /// The `kind` of the section, e.g. [`SectionKind::Args`] or [`SectionKind::Returns`]. + pub const fn kind(&self) -> SectionKind { + self.data.kind + } + /// The name of the section as it appears in the docstring, e.g. "Args" or "Returns". - pub(crate) section_name: &'a str, - pub(crate) previous_line: &'a str, - pub(crate) line: &'a str, - pub(crate) following_lines: &'a [&'a str], - pub(crate) is_last_section: bool, - pub(crate) original_index: usize, + pub fn section_name(&self) -> &'a str { + &self.docstring_body.as_str()[self.data.name_range] + } + + /// Returns the rest of the summary line after the section name. + pub fn summary_after_section_name(&self) -> &'a str { + &self.summary_line()[usize::from(self.data.name_range.end() - self.data.range.start())..] + } + + fn offset(&self) -> TextSize { + self.docstring_body.start() + } + + /// The absolute range of the section name + pub fn section_name_range(&self) -> TextRange { + self.data.name_range + self.offset() + } + + /// Summary range relative to the start of the document. Includes the trailing newline. + pub fn summary_full_range(&self) -> TextRange { + self.summary_full_range_relative() + self.offset() + } + + /// The absolute range of the summary line, excluding any trailing newline character. + pub fn summary_range(&self) -> TextRange { + TextRange::at(self.range().start(), self.summary_line().text_len()) + } + + /// Range of the summary line relative to [`Docstring::body`], including the trailing newline character. + fn summary_full_range_relative(&self) -> TextRange { + TextRange::new(self.range_relative().start(), self.data.summary_full_end) + } + + /// Returns the range of this section relative to [`Docstring::body`] + const fn range_relative(&self) -> TextRange { + self.data.range + } + + /// The absolute range of the full-section. + pub fn range(&self) -> TextRange { + self.range_relative() + self.offset() + } + + /// Summary line without the trailing newline characters + pub fn summary_line(&self) -> &'a str { + let full_summary = &self.docstring_body.as_str()[self.summary_full_range_relative()]; + + let mut bytes = full_summary.bytes().rev(); + + let newline_width = match bytes.next() { + Some(b'\n') => { + if bytes.next() == Some(b'\r') { + 2 + } else { + 1 + } + } + Some(b'\r') => 1, + _ => 0, + }; + + &full_summary[..full_summary.len() - newline_width] + } + + /// Returns the text of the last line of the previous section or an empty string if it is the first section. + pub fn previous_line(&self) -> Option<&'a str> { + let previous = + &self.docstring_body.as_str()[TextRange::up_to(self.range_relative().start())]; + previous.universal_newlines().last().map(|l| l.as_str()) + } + + /// Returns the lines belonging to this section after the summary line. + pub fn following_lines(&self) -> UniversalNewlineIterator<'a> { + let lines = self.following_lines_str(); + UniversalNewlineIterator::with_offset(lines, self.offset() + self.data.summary_full_end) + } + + fn following_lines_str(&self) -> &'a str { + &self.docstring_body.as_str()[self.following_range_relative()] + } + + /// Returns the range to the following lines relative to [`Docstring::body`]. + const fn following_range_relative(&self) -> TextRange { + TextRange::new(self.data.summary_full_end, self.range_relative().end()) + } + + /// Returns the absolute range of the following lines. + pub fn following_range(&self) -> TextRange { + self.following_range_relative() + self.offset() + } +} + +impl Debug for SectionContext<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SectionContext") + .field("kind", &self.kind()) + .field("section_name", &self.section_name()) + .field("summary_line", &self.summary_line()) + .field("following_lines", &&self.following_lines_str()) + .finish() + } } fn suspected_as_section(line: &str, style: SectionStyle) -> Option { @@ -148,20 +395,15 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option } /// Check if the suspected context is really a section header. -fn is_docstring_section(context: &SectionContext) -> bool { - let section_name_suffix = context - .line - .trim() - .strip_prefix(context.section_name) - .unwrap() - .trim(); +fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool { + let section_name_suffix = line[usize::from(section_name_range.end())..].trim(); let this_looks_like_a_section_name = section_name_suffix == ":" || section_name_suffix.is_empty(); if !this_looks_like_a_section_name { return false; } - let prev_line = context.previous_line.trim(); + let prev_line = previous_lines.trim(); let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')'] .into_iter() .any(|char| prev_line.ends_with(char)); @@ -173,50 +415,3 @@ fn is_docstring_section(context: &SectionContext) -> bool { true } - -/// Extract all `SectionContext` values from a docstring. -pub(crate) fn section_contexts<'a>( - lines: &'a [&'a str], - style: SectionStyle, -) -> Vec> { - let mut contexts = vec![]; - for (kind, lineno) in lines - .iter() - .enumerate() - .skip(1) - .filter_map(|(lineno, line)| suspected_as_section(line, style).map(|kind| (kind, lineno))) - { - let context = SectionContext { - kind, - section_name: whitespace::leading_words(lines[lineno]), - previous_line: lines[lineno - 1], - line: lines[lineno], - following_lines: &lines[lineno + 1..], - original_index: lineno, - is_last_section: false, - }; - if is_docstring_section(&context) { - contexts.push(context); - } - } - - let mut truncated_contexts = Vec::with_capacity(contexts.len()); - let mut end: Option = None; - for context in contexts.into_iter().rev() { - let next_end = context.original_index; - truncated_contexts.push(SectionContext { - kind: context.kind, - section_name: context.section_name, - previous_line: context.previous_line, - line: context.line, - following_lines: end.map_or(context.following_lines, |end| { - &lines[context.original_index + 1..end] - }), - original_index: context.original_index, - is_last_section: end.is_none(), - }); - end = Some(next_end); - } - truncated_contexts.reverse(); - truncated_contexts -} diff --git a/crates/ruff/src/importer.rs b/crates/ruff/src/importer.rs index 1a271edf98..3af7d1bbe9 100644 --- a/crates/ruff/src/importer.rs +++ b/crates/ruff/src/importer.rs @@ -2,8 +2,9 @@ use anyhow::Result; use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute}; +use ruff_text_size::TextSize; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{Location, Stmt, StmtKind, Suite}; +use rustpython_parser::ast::{Stmt, StmtKind, Suite}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::Edit; @@ -95,7 +96,7 @@ impl<'a> Importer<'a> { /// Add the given member to an existing `StmtKind::ImportFrom` statement. pub fn add_member(&self, stmt: &Stmt, member: &str) -> Result { - let mut tree = match_module(self.locator.slice(stmt))?; + let mut tree = match_module(self.locator.slice(stmt.range()))?; let import_from = match_import_from(&mut tree)?; let aliases = match_aliases(import_from)?; aliases.push(ImportAlias { @@ -113,11 +114,7 @@ impl<'a> Importer<'a> { ..CodegenState::default() }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - stmt.location, - stmt.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), stmt.range())) } } @@ -126,13 +123,13 @@ struct Insertion { /// The content to add before the insertion. prefix: &'static str, /// The location at which to insert. - location: Location, + location: TextSize, /// The content to add after the insertion. suffix: &'static str, } impl Insertion { - fn new(prefix: &'static str, location: Location, suffix: &'static str) -> Self { + fn new(prefix: &'static str, location: TextSize, suffix: &'static str) -> Self { Self { prefix, location, @@ -142,7 +139,7 @@ impl Insertion { } /// Find the end of the last docstring. -fn match_docstring_end(body: &[Stmt]) -> Option { +fn match_docstring_end(body: &[Stmt]) -> Option { let mut iter = body.iter(); let Some(mut stmt) = iter.next() else { return None; @@ -156,7 +153,7 @@ fn match_docstring_end(body: &[Stmt]) -> Option { } stmt = next; } - Some(stmt.end_location.unwrap()) + Some(stmt.end()) } /// Find the location at which a "top-of-file" import should be inserted, @@ -173,17 +170,17 @@ fn match_docstring_end(body: &[Stmt]) -> Option { /// The location returned will be the start of the `import os` statement, /// along with a trailing newline suffix. fn end_of_statement_insertion(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Insertion { - let location = stmt.end_location.unwrap(); + let location = stmt.end(); let mut tokens = lexer::lex_located(locator.after(location), Mode::Module, location).flatten(); - if let Some((.., Tok::Semi, end)) = tokens.next() { + if let Some((Tok::Semi, range)) = tokens.next() { // If the first token after the docstring is a semicolon, insert after the semicolon as an // inline statement; - Insertion::new(" ", end, ";") + Insertion::new(" ", range.end(), ";") } else { // Otherwise, insert on the next line. Insertion::new( "", - Location::new(location.row() + 1, 0), + locator.line_end(location), stylist.line_ending().as_str(), ) } @@ -210,22 +207,22 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) -> let first_token = lexer::lex_located(locator.after(location), Mode::Module, location) .flatten() .next(); - if let Some((.., Tok::Semi, end)) = first_token { - return Insertion::new(" ", end, ";"); + if let Some((Tok::Semi, range)) = first_token { + return Insertion::new(" ", range.end(), ";"); } // Otherwise, advance to the next row. - Location::new(location.row() + 1, 0) + locator.full_line_end(location) } else { - Location::default() + TextSize::default() }; // Skip over any comments and empty lines. - for (.., tok, end) in + for (tok, range) in lexer::lex_located(locator.after(location), Mode::Module, location).flatten() { if matches!(tok, Tok::Comment(..) | Tok::Newline) { - location = Location::new(end.row() + 1, 0); + location = locator.full_line_end(range.end()); } else { break; } @@ -237,8 +234,8 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) -> #[cfg(test)] mod tests { use anyhow::Result; + use ruff_text_size::TextSize; use rustpython_parser as parser; - use rustpython_parser::ast::Location; use rustpython_parser::lexer::LexResult; use ruff_python_ast::source_code::{LineEnding, Locator, Stylist}; @@ -258,7 +255,7 @@ mod tests { let contents = ""; assert_eq!( insert(contents)?, - Insertion::new("", Location::new(1, 0), LineEnding::default().as_str()) + Insertion::new("", TextSize::from(0), LineEnding::default().as_str()) ); let contents = r#" @@ -266,7 +263,7 @@ mod tests { .trim_start(); assert_eq!( insert(contents)?, - Insertion::new("", Location::new(2, 0), LineEnding::default().as_str()) + Insertion::new("", TextSize::from(19), LineEnding::default().as_str()) ); let contents = r#" @@ -275,7 +272,7 @@ mod tests { .trim_start(); assert_eq!( insert(contents)?, - Insertion::new("", Location::new(2, 0), "\n") + Insertion::new("", TextSize::from(20), "\n") ); let contents = r#" @@ -285,7 +282,7 @@ mod tests { .trim_start(); assert_eq!( insert(contents)?, - Insertion::new("", Location::new(3, 0), "\n") + Insertion::new("", TextSize::from(40), "\n") ); let contents = r#" @@ -294,7 +291,7 @@ x = 1 .trim_start(); assert_eq!( insert(contents)?, - Insertion::new("", Location::new(1, 0), "\n") + Insertion::new("", TextSize::from(0), "\n") ); let contents = r#" @@ -303,7 +300,7 @@ x = 1 .trim_start(); assert_eq!( insert(contents)?, - Insertion::new("", Location::new(2, 0), "\n") + Insertion::new("", TextSize::from(23), "\n") ); let contents = r#" @@ -313,7 +310,7 @@ x = 1 .trim_start(); assert_eq!( insert(contents)?, - Insertion::new("", Location::new(3, 0), "\n") + Insertion::new("", TextSize::from(43), "\n") ); let contents = r#" @@ -323,7 +320,7 @@ x = 1 .trim_start(); assert_eq!( insert(contents)?, - Insertion::new("", Location::new(3, 0), "\n") + Insertion::new("", TextSize::from(43), "\n") ); let contents = r#" @@ -332,7 +329,7 @@ x = 1 .trim_start(); assert_eq!( insert(contents)?, - Insertion::new("", Location::new(1, 0), "\n") + Insertion::new("", TextSize::from(0), "\n") ); let contents = r#" @@ -341,7 +338,7 @@ x = 1 .trim_start(); assert_eq!( insert(contents)?, - Insertion::new(" ", Location::new(1, 20), ";") + Insertion::new(" ", TextSize::from(20), ";") ); let contents = r#" @@ -351,7 +348,7 @@ x = 1 .trim_start(); assert_eq!( insert(contents)?, - Insertion::new(" ", Location::new(1, 20), ";") + Insertion::new(" ", TextSize::from(20), ";") ); Ok(()) diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 9453e1b197..85a97dd6e6 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -1,3 +1,4 @@ +use ruff_text_size::TextRange; use std::fs::File; use std::io::{BufReader, BufWriter}; use std::iter; @@ -7,7 +8,6 @@ use serde::Serialize; use serde_json::error::Category; use ruff_diagnostics::Diagnostic; -use ruff_python_ast::types::Range; use crate::jupyter::{CellType, JupyterNotebook, SourceValue}; use crate::rules::pycodestyle::rules::SyntaxError; @@ -18,7 +18,7 @@ pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb"; /// Jupyter Notebook indexing table /// /// When we lint a jupyter notebook, we have to translate the row/column based on -/// [`crate::message::Location`] +/// [`ruff_text_size::TextSize`] /// to jupyter notebook cell/row/column. #[derive(Debug, Eq, PartialEq)] pub struct JupyterIndex { @@ -46,7 +46,7 @@ impl JupyterNotebook { IOError { message: format!("{err}"), }, - Range::default(), + TextRange::default(), ) })?); let notebook: JupyterNotebook = match serde_json::from_reader(reader) { @@ -59,7 +59,7 @@ impl JupyterNotebook { IOError { message: format!("{err}"), }, - Range::default(), + TextRange::default(), ), Category::Syntax | Category::Eof => { // Maybe someone saved the python sources (those with the `# %%` separator) @@ -69,7 +69,7 @@ impl JupyterNotebook { IOError { message: format!("{err}"), }, - Range::default(), + TextRange::default(), ) })?; // Check if tokenizing was successful and the file is non-empty @@ -84,7 +84,7 @@ impl JupyterNotebook { but this file isn't valid JSON: {err}" ), }, - Range::default(), + TextRange::default(), ) } else { Diagnostic::new( @@ -95,7 +95,7 @@ impl JupyterNotebook { but found a Python source file: {err}" ), }, - Range::default(), + TextRange::default(), ) } } @@ -108,7 +108,7 @@ impl JupyterNotebook { "This file does not match the schema expected of Jupyter Notebooks: {err}" ), }, - Range::default(), + TextRange::default(), ) } } @@ -126,7 +126,7 @@ impl JupyterNotebook { notebook.nbformat ), }, - Range::default(), + TextRange::default(), ))); } diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index df12559b1b..e359ffdbd0 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -6,7 +6,6 @@ //! [Ruff]: https://github.com/charliermarsh/ruff pub use ruff_python_ast::source_code::round_trip; -pub use ruff_python_ast::types::Range; pub use rule_selector::RuleSelector; pub use rules::pycodestyle::rules::IOError; diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index db42254372..8271bb74ea 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -23,6 +23,7 @@ use crate::checkers::physical_lines::check_physical_lines; use crate::checkers::tokens::check_tokens; use crate::directives::Directives; use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens}; +use crate::logging::DisplayParseError; use crate::message::Message; use crate::noqa::add_noqa; use crate::registry::{AsRule, Rule}; @@ -68,7 +69,6 @@ pub struct FixerResult<'a> { pub fn check_path( path: &Path, package: Option<&Path>, - contents: &str, tokens: Vec, locator: &Locator, stylist: &Stylist, @@ -88,7 +88,7 @@ pub fn check_path( let use_doc_lines = settings.rules.enabled(Rule::DocLineTooLong); let mut doc_lines = vec![]; if use_doc_lines { - doc_lines.extend(doc_lines_from_tokens(&tokens)); + doc_lines.extend(doc_lines_from_tokens(&tokens, locator)); } // Run the token-based rules. @@ -178,7 +178,7 @@ pub fn check_path( // if it's disabled via any of the usual mechanisms (e.g., `noqa`, // `per-file-ignores`), and the easiest way to detect that suppression is // to see if the diagnostic persists to the end of the function. - pycodestyle::rules::syntax_error(&mut diagnostics, &parse_error); + pycodestyle::rules::syntax_error(&mut diagnostics, &parse_error, locator); error = Some(parse_error); } } @@ -218,8 +218,8 @@ pub fn check_path( { let ignored = check_noqa( &mut diagnostics, - contents, - indexer.commented_lines(), + locator, + indexer.comment_ranges(), &directives.noqa_line_for, settings, error.as_ref().map_or(autofix, |_| flags::Autofix::Disabled), @@ -268,11 +268,15 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings let stylist = Stylist::from_tokens(&tokens, &locator); // Extra indices from the code. - let indexer: Indexer = tokens.as_slice().into(); + let indexer = Indexer::from_tokens(&tokens, &locator); // Extract the `# noqa` and `# isort: skip` directives from the source. - let directives = - directives::extract_directives(&tokens, directives::Flags::from_settings(settings)); + let directives = directives::extract_directives( + &tokens, + directives::Flags::from_settings(settings), + &locator, + &indexer, + ); // Generate diagnostics, ignoring any existing `noqa` directives. let LinterResult { @@ -281,7 +285,6 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings } = check_path( path, package, - &contents, tokens, &locator, &stylist, @@ -294,20 +297,15 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings // Log any parse errors. if let Some(err) = error { - error!( - "{}{}{} {err:?}", - "Failed to parse ".bold(), - fs::relativize_path(path).bold(), - ":".bold() - ); + error!("{}", DisplayParseError::new(err, locator.to_source_code())); } // Add any missing `# noqa` pragmas. add_noqa( path, &diagnostics.0, - &contents, - indexer.commented_lines(), + &locator, + indexer.comment_ranges(), &directives.noqa_line_for, stylist.line_ending(), ) @@ -333,17 +331,20 @@ pub fn lint_only( let stylist = Stylist::from_tokens(&tokens, &locator); // Extra indices from the code. - let indexer: Indexer = tokens.as_slice().into(); + let indexer = Indexer::from_tokens(&tokens, &locator); // Extract the `# noqa` and `# isort: skip` directives from the source. - let directives = - directives::extract_directives(&tokens, directives::Flags::from_settings(settings)); + let directives = directives::extract_directives( + &tokens, + directives::Flags::from_settings(settings), + &locator, + &indexer, + ); // Generate diagnostics. let result = check_path( path, package, - contents, tokens, &locator, &stylist, @@ -356,7 +357,7 @@ pub fn lint_only( result.map(|(diagnostics, imports)| { ( - diagnostics_to_messages(diagnostics, path, settings, &locator, &directives), + diagnostics_to_messages(diagnostics, path, &locator, &directives), imports, ) }) @@ -366,14 +367,15 @@ pub fn lint_only( fn diagnostics_to_messages( diagnostics: Vec, path: &Path, - settings: &Settings, locator: &Locator, directives: &Directives, ) -> Vec { let file = once_cell::unsync::Lazy::new(|| { - let mut builder = SourceFileBuilder::new(&path.to_string_lossy()); - if settings.show_source { - builder.set_source_code(&locator.to_source_code()); + let mut builder = + SourceFileBuilder::new(path.to_string_lossy().as_ref(), locator.contents()); + + if let Some(line_index) = locator.line_index() { + builder.set_line_index(line_index.clone()); } builder.finish() @@ -382,9 +384,8 @@ fn diagnostics_to_messages( diagnostics .into_iter() .map(|diagnostic| { - let lineno = diagnostic.location.row(); - let noqa_row = *directives.noqa_line_for.get(&lineno).unwrap_or(&lineno); - Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_row) + let noqa_offset = directives.noqa_line_for.resolve(diagnostic.start()); + Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_offset) }) .collect() } @@ -421,17 +422,20 @@ pub fn lint_fix<'a>( let stylist = Stylist::from_tokens(&tokens, &locator); // Extra indices from the code. - let indexer: Indexer = tokens.as_slice().into(); + let indexer = Indexer::from_tokens(&tokens, &locator); // Extract the `# noqa` and `# isort: skip` directives from the source. - let directives = - directives::extract_directives(&tokens, directives::Flags::from_settings(settings)); + let directives = directives::extract_directives( + &tokens, + directives::Flags::from_settings(settings), + &locator, + &indexer, + ); // Generate diagnostics. let result = check_path( path, package, - &transformed, tokens, &locator, &stylist, @@ -513,7 +517,7 @@ This indicates a bug in `{}`. If you could open an issue at: return Ok(FixerResult { result: result.map(|(diagnostics, imports)| { ( - diagnostics_to_messages(diagnostics, path, settings, &locator, &directives), + diagnostics_to_messages(diagnostics, path, &locator, &directives), imports, ) }), diff --git a/crates/ruff/src/logging.rs b/crates/ruff/src/logging.rs index 9a797e3c2f..1607508ead 100644 --- a/crates/ruff/src/logging.rs +++ b/crates/ruff/src/logging.rs @@ -1,10 +1,15 @@ +use std::fmt::{Display, Formatter}; +use std::path::Path; use std::sync::Mutex; +use crate::fs; use anyhow::Result; use colored::Colorize; use fern; use log::Level; use once_cell::sync::Lazy; +use ruff_python_ast::source_code::SourceCode; +use rustpython_parser::ParseError; pub(crate) static WARNINGS: Lazy>> = Lazy::new(Mutex::default); @@ -42,13 +47,13 @@ macro_rules! warn_user_once { #[macro_export] macro_rules! warn_user { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ use colored::Colorize; use log::warn; let message = format!("{}", format_args!($($arg)*)); warn!("{}", message.bold()); - }; + }}; } #[macro_export] @@ -127,6 +132,34 @@ pub fn set_up_logging(level: &LogLevel) -> Result<()> { Ok(()) } +pub struct DisplayParseError<'a> { + error: ParseError, + source_code: SourceCode<'a, 'a>, +} + +impl<'a> DisplayParseError<'a> { + pub fn new(error: ParseError, source_code: SourceCode<'a, 'a>) -> Self { + Self { error, source_code } + } +} + +impl Display for DisplayParseError<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let source_location = self.source_code.source_location(self.error.location); + + write!( + f, + "{header} {path}{colon}{row}{colon}{column}{colon} {inner}", + header = "Failed to parse ".bold(), + path = fs::relativize_path(Path::new(&self.error.source_path)).bold(), + row = source_location.row, + column = source_location.column, + colon = ":".cyan(), + inner = &self.error.error + ) + } +} + #[cfg(test)] mod tests { use crate::logging::LogLevel; diff --git a/crates/ruff/src/message/azure.rs b/crates/ruff/src/message/azure.rs index 8396dd4e35..f29a89ce9c 100644 --- a/crates/ruff/src/message/azure.rs +++ b/crates/ruff/src/message/azure.rs @@ -1,5 +1,6 @@ use crate::message::{Emitter, EmitterContext, Message}; use crate::registry::AsRule; +use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; use std::io::Write; /// Generate error logging commands for Azure Pipelines format. @@ -15,12 +16,15 @@ impl Emitter for AzureEmitter { context: &EmitterContext, ) -> anyhow::Result<()> { for message in messages { - let (line, col) = if context.is_jupyter_notebook(message.filename()) { + let location = if context.is_jupyter_notebook(message.filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - (1, 0) + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(0), + } } else { - (message.location.row(), message.location.column()) + message.compute_start_location() }; writeln!( @@ -28,6 +32,8 @@ impl Emitter for AzureEmitter { "##vso[task.logissue type=error\ ;sourcepath={filename};linenumber={line};columnnumber={col};code={code};]{body}", filename = message.filename(), + line = location.row, + col = location.column, code = message.kind.rule().noqa_code(), body = message.kind.body, )?; diff --git a/crates/ruff/src/message/diff.rs b/crates/ruff/src/message/diff.rs index 173973e837..76b9ef2901 100644 --- a/crates/ruff/src/message/diff.rs +++ b/crates/ruff/src/message/diff.rs @@ -1,8 +1,7 @@ use crate::message::Message; use colored::{Color, ColoredString, Colorize, Styles}; use ruff_diagnostics::Fix; -use ruff_python_ast::source_code::{OneIndexed, SourceCode}; -use ruff_python_ast::types::Range; +use ruff_python_ast::source_code::{OneIndexed, SourceFile}; use ruff_text_size::{TextRange, TextSize}; use similar::{ChangeTag, TextDiff}; use std::fmt::{Display, Formatter}; @@ -18,38 +17,39 @@ use std::num::NonZeroUsize; /// * Compute the diff from the [`Edit`] because diff calculation is expensive. pub(super) struct Diff<'a> { fix: &'a Fix, - source_code: SourceCode<'a, 'a>, + source_code: &'a SourceFile, } impl<'a> Diff<'a> { pub fn from_message(message: &'a Message) -> Option { - match message.file.source_code() { - Some(source_code) if !message.fix.is_empty() => Some(Diff { - source_code, + if message.fix.is_empty() { + None + } else { + Some(Diff { + source_code: &message.file, fix: &message.fix, - }), - _ => None, + }) } } } impl Display for Diff<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut output = String::with_capacity(self.source_code.text().len()); + let mut output = String::with_capacity(self.source_code.source_text().len()); let mut last_end = TextSize::default(); for edit in self.fix.edits() { - let edit_range = self - .source_code - .text_range(Range::new(edit.location(), edit.end_location())); - output.push_str(&self.source_code.text()[TextRange::new(last_end, edit_range.start())]); + output.push_str( + self.source_code + .slice(TextRange::new(last_end, edit.start())), + ); output.push_str(edit.content().unwrap_or_default()); - last_end = edit_range.end(); + last_end = edit.end(); } - output.push_str(&self.source_code.text()[usize::from(last_end)..]); + output.push_str(&self.source_code.source_text()[usize::from(last_end)..]); - let diff = TextDiff::from_lines(self.source_code.text(), &output); + let diff = TextDiff::from_lines(self.source_code.source_text(), &output); writeln!(f, "{}", "ℹ Suggested fix".blue())?; diff --git a/crates/ruff/src/message/github.rs b/crates/ruff/src/message/github.rs index 013bf5426d..5b3552473d 100644 --- a/crates/ruff/src/message/github.rs +++ b/crates/ruff/src/message/github.rs @@ -1,6 +1,7 @@ use crate::fs::relativize_path; use crate::message::{Emitter, EmitterContext, Message}; use crate::registry::AsRule; +use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; use std::io::Write; /// Generate error workflow command in GitHub Actions format. @@ -16,30 +17,38 @@ impl Emitter for GithubEmitter { context: &EmitterContext, ) -> anyhow::Result<()> { for message in messages { - let (row, column) = if context.is_jupyter_notebook(message.filename()) { + let source_location = message.compute_start_location(); + let location = if context.is_jupyter_notebook(message.filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - (1, 0) + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(0), + } } else { - (message.location.row(), message.location.column()) + source_location.clone() }; + let end_location = message.compute_end_location(); + write!( writer, "::error title=Ruff \ ({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::", code = message.kind.rule().noqa_code(), file = message.filename(), - row = message.location.row(), - column = message.location.column(), - end_row = message.end_location.row(), - end_column = message.end_location.column(), + row = source_location.row, + column = source_location.column, + end_row = end_location.row, + end_column = end_location.column, )?; writeln!( writer, "{path}:{row}:{column}: {code} {body}", path = relativize_path(message.filename()), + row = location.row, + column = location.column, code = message.kind.rule().noqa_code(), body = message.kind.body, )?; diff --git a/crates/ruff/src/message/gitlab.rs b/crates/ruff/src/message/gitlab.rs index 7d70610ee9..b699884b4b 100644 --- a/crates/ruff/src/message/gitlab.rs +++ b/crates/ruff/src/message/gitlab.rs @@ -64,9 +64,11 @@ impl Serialize for SerializedMessages<'_> { "end": 1 }) } else { + let start_location = message.compute_start_location(); + let end_location = message.compute_end_location(); json!({ - "begin": message.location.row(), - "end": message.end_location.row() + "begin": start_location.row, + "end": end_location.row }) }; @@ -96,20 +98,16 @@ impl Serialize for SerializedMessages<'_> { fn fingerprint(message: &Message) -> String { let Message { kind, - location, - end_location, + range, fix: _fix, file, - noqa_row: _noqa_row, + noqa_offset: _, } = message; let mut hasher = DefaultHasher::new(); kind.rule().hash(&mut hasher); - location.row().hash(&mut hasher); - location.column().hash(&mut hasher); - end_location.row().hash(&mut hasher); - end_location.column().hash(&mut hasher); + range.hash(&mut hasher); file.name().hash(&mut hasher); format!("{:x}", hasher.finish()) diff --git a/crates/ruff/src/message/grouped.rs b/crates/ruff/src/message/grouped.rs index cec82a5915..4502f2e20c 100644 --- a/crates/ruff/src/message/grouped.rs +++ b/crates/ruff/src/message/grouped.rs @@ -1,14 +1,20 @@ use crate::fs::relativize_path; use crate::jupyter::JupyterIndex; +use crate::message::diff::calculate_print_width; use crate::message::text::{MessageCodeFrame, RuleCodeAndBody}; -use crate::message::{group_messages_by_filename, Emitter, EmitterContext, Message}; +use crate::message::{ + group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation, +}; use colored::Colorize; +use ruff_python_ast::source_code::OneIndexed; use std::fmt::{Display, Formatter}; use std::io::Write; +use std::num::NonZeroUsize; #[derive(Default)] pub struct GroupedEmitter { show_fix_status: bool, + show_source: bool, } impl GroupedEmitter { @@ -17,6 +23,12 @@ impl GroupedEmitter { self.show_fix_status = show_fix_status; self } + + #[must_use] + pub fn with_show_source(mut self, show_source: bool) -> Self { + self.show_source = show_source; + self + } } impl Emitter for GroupedEmitter { @@ -29,20 +41,17 @@ impl Emitter for GroupedEmitter { for (filename, messages) in group_messages_by_filename(messages) { // Compute the maximum number of digits in the row and column, for messages in // this file. - let row_length = num_digits( - messages - .iter() - .map(|message| message.location.row()) - .max() - .unwrap(), - ); - let column_length = num_digits( - messages - .iter() - .map(|message| message.location.column()) - .max() - .unwrap(), - ); + + let mut max_row_length = OneIndexed::MIN; + let mut max_column_length = OneIndexed::MIN; + + for message in &messages { + max_row_length = max_row_length.max(message.start_location.row); + max_column_length = max_column_length.max(message.start_location.column); + } + + let row_length = calculate_print_width(max_row_length); + let column_length = calculate_print_width(max_column_length); // Print the filename. writeln!(writer, "{}:", relativize_path(filename).underline())?; @@ -53,11 +62,12 @@ impl Emitter for GroupedEmitter { writer, "{}", DisplayGroupedMessage { + jupyter_index: context.jupyter_index(message.filename()), message, show_fix_status: self.show_fix_status, + show_source: self.show_source, row_length, column_length, - jupyter_index: context.jupyter_index(message.filename()), } )?; } @@ -69,21 +79,26 @@ impl Emitter for GroupedEmitter { } struct DisplayGroupedMessage<'a> { - message: &'a Message, + message: MessageWithLocation<'a>, show_fix_status: bool, - row_length: usize, - column_length: usize, + show_source: bool, + row_length: NonZeroUsize, + column_length: NonZeroUsize, jupyter_index: Option<&'a JupyterIndex>, } impl Display for DisplayGroupedMessage<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let message = self.message; + let MessageWithLocation { + message, + start_location, + } = &self.message; write!( f, " {row_padding}", - row_padding = " ".repeat(self.row_length - num_digits(message.location.row())) + row_padding = + " ".repeat(self.row_length.get() - calculate_print_width(start_location.row).get()) )?; // Check if we're working on a jupyter notebook and translate positions with cell accordingly @@ -91,29 +106,31 @@ impl Display for DisplayGroupedMessage<'_> { write!( f, "cell {cell}{sep}", - cell = jupyter_index.row_to_cell[message.location.row()], + cell = jupyter_index.row_to_cell[start_location.row.get()], sep = ":".cyan() )?; ( - jupyter_index.row_to_row_in_cell[message.location.row()] as usize, - message.location.column(), + jupyter_index.row_to_row_in_cell[start_location.row.get()] as usize, + start_location.column.get(), ) } else { - (message.location.row(), message.location.column()) + (start_location.row.get(), start_location.column.get()) }; writeln!( f, "{row}{sep}{col}{col_padding} {code_and_body}", sep = ":".cyan(), - col_padding = " ".repeat(self.column_length - num_digits(message.location.column())), + col_padding = " ".repeat( + self.column_length.get() - calculate_print_width(start_location.column).get() + ), code_and_body = RuleCodeAndBody { message_kind: &message.kind, show_fix_status: self.show_fix_status }, )?; - { + if self.show_source { use std::fmt::Write; let mut padded = PadAdapter::new(f); write!(padded, "{}", MessageCodeFrame { message })?; @@ -125,16 +142,6 @@ impl Display for DisplayGroupedMessage<'_> { } } -fn num_digits(n: usize) -> usize { - std::iter::successors(Some(n), |n| { - let next = n / 10; - - (next > 0).then_some(next) - }) - .count() - .max(1) -} - /// Adapter that adds a ' ' at the start of every line without the need to copy the string. /// Inspired by Rust's `debug_struct()` internal implementation that also uses a `PadAdapter`. struct PadAdapter<'buf> { @@ -174,7 +181,7 @@ mod tests { #[test] fn default() { - let mut emitter = GroupedEmitter::default(); + let mut emitter = GroupedEmitter::default().with_show_source(true); let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); @@ -182,7 +189,9 @@ mod tests { #[test] fn fix_status() { - let mut emitter = GroupedEmitter::default().with_show_fix_status(true); + let mut emitter = GroupedEmitter::default() + .with_show_fix_status(true) + .with_show_source(true); let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/json.rs b/crates/ruff/src/message/json.rs index 8eaf8d7de9..1e980f78b5 100644 --- a/crates/ruff/src/message/json.rs +++ b/crates/ruff/src/message/json.rs @@ -1,9 +1,10 @@ use crate::message::{Emitter, EmitterContext, Message}; use crate::registry::AsRule; use ruff_diagnostics::Edit; +use ruff_python_ast::source_code::{SourceCode, SourceLocation}; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -use serde_json::json; +use serde_json::{json, Value}; use std::io::Write; #[derive(Default)] @@ -34,23 +35,29 @@ impl Serialize for ExpandedMessages<'_> { let mut s = serializer.serialize_seq(Some(self.messages.len()))?; for message in self.messages { + let source_code = message.file.to_source_code(); + let fix = if message.fix.is_empty() { None } else { Some(json!({ "message": message.kind.suggestion.as_deref(), - "edits": &ExpandedEdits { edits: message.fix.edits() }, + "edits": &ExpandedEdits { edits: message.fix.edits(), source_code: &source_code }, })) }; + let start_location = source_code.source_location(message.start()); + let end_location = source_code.source_location(message.end()); + let noqa_location = source_code.source_location(message.noqa_offset); + let value = json!({ "code": message.kind.rule().noqa_code().to_string(), "message": message.kind.body, "fix": fix, - "location": message.location, - "end_location": message.end_location, + "location": start_location, + "end_location": end_location, "filename": message.filename(), - "noqa_row": message.noqa_row + "noqa_row": noqa_location.row }); s.serialize_element(&value)?; @@ -62,6 +69,7 @@ impl Serialize for ExpandedMessages<'_> { struct ExpandedEdits<'a> { edits: &'a [Edit], + source_code: &'a SourceCode<'a, 'a>, } impl Serialize for ExpandedEdits<'_> { @@ -72,10 +80,12 @@ impl Serialize for ExpandedEdits<'_> { let mut s = serializer.serialize_seq(Some(self.edits.len()))?; for edit in self.edits { + let start_location = self.source_code.source_location(edit.start()); + let end_location = self.source_code.source_location(edit.end()); let value = json!({ "content": edit.content().unwrap_or_default(), - "location": edit.location(), - "end_location": edit.end_location() + "location": to_zero_indexed_column(&start_location), + "end_location": to_zero_indexed_column(&end_location) }); s.serialize_element(&value)?; @@ -85,6 +95,13 @@ impl Serialize for ExpandedEdits<'_> { } } +fn to_zero_indexed_column(location: &SourceLocation) -> Value { + json!({ + "row": location.row, + "column": location.column.to_zero_indexed() + }) +} + #[cfg(test)] mod tests { use crate::message::tests::{capture_emitter_output, create_messages}; diff --git a/crates/ruff/src/message/junit.rs b/crates/ruff/src/message/junit.rs index fbfa7c6467..2ed8e6dd7f 100644 --- a/crates/ruff/src/message/junit.rs +++ b/crates/ruff/src/message/junit.rs @@ -1,6 +1,9 @@ -use crate::message::{group_messages_by_filename, Emitter, EmitterContext, Message}; +use crate::message::{ + group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation, +}; use crate::registry::AsRule; use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite}; +use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; use std::io::Write; use std::path::Path; @@ -23,17 +26,29 @@ impl Emitter for JunitEmitter { .insert("package".to_string(), "org.ruff".to_string()); for message in messages { + let MessageWithLocation { + message, + start_location, + } = message; let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure); status.set_message(message.kind.body.clone()); - let (row, col) = if context.is_jupyter_notebook(message.filename()) { + let location = if context.is_jupyter_notebook(message.filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - (1, 0) + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(0), + } } else { - (message.location.row(), message.location.column()) + start_location }; - status.set_description(format!("line {row}, col {col}, {}", message.kind.body)); + status.set_description(format!( + "line {row}, col {col}, {body}", + row = location.row, + col = location.column, + body = message.kind.body + )); let mut case = TestCase::new( format!("org.ruff.{}", message.kind.rule().noqa_code()), status, @@ -43,9 +58,9 @@ impl Emitter for JunitEmitter { let classname = file_path.parent().unwrap().join(file_stem); case.set_classname(classname.to_str().unwrap()); case.extra - .insert("line".to_string(), message.location.row().to_string()); + .insert("line".to_string(), location.row.to_string()); case.extra - .insert("column".to_string(), message.location.column().to_string()); + .insert("column".to_string(), location.column.to_string()); test_suite.add_test_case(case); } diff --git a/crates/ruff/src/message/mod.rs b/crates/ruff/src/message/mod.rs index e07c2d8f17..41c1776a88 100644 --- a/crates/ruff/src/message/mod.rs +++ b/crates/ruff/src/message/mod.rs @@ -8,10 +8,12 @@ mod junit; mod pylint; mod text; +use ruff_text_size::{TextRange, TextSize}; use rustc_hash::FxHashMap; use std::cmp::Ordering; use std::collections::BTreeMap; use std::io::Write; +use std::ops::Deref; pub use azure::AzureEmitter; pub use github::GithubEmitter; @@ -20,49 +22,64 @@ pub use grouped::GroupedEmitter; pub use json::JsonEmitter; pub use junit::JunitEmitter; pub use pylint::PylintEmitter; -pub use rustpython_parser::ast::Location; pub use text::TextEmitter; use crate::jupyter::JupyterIndex; +use crate::registry::AsRule; use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; -use ruff_python_ast::source_code::SourceFile; +use ruff_python_ast::source_code::{SourceFile, SourceLocation}; #[derive(Debug, PartialEq, Eq)] pub struct Message { pub kind: DiagnosticKind, - pub location: Location, - pub end_location: Location, + pub range: TextRange, pub fix: Fix, pub file: SourceFile, - pub noqa_row: usize, + pub noqa_offset: TextSize, } impl Message { - pub fn from_diagnostic(diagnostic: Diagnostic, file: SourceFile, noqa_row: usize) -> Self { + pub fn from_diagnostic( + diagnostic: Diagnostic, + file: SourceFile, + noqa_offset: TextSize, + ) -> Self { Self { + range: diagnostic.range(), kind: diagnostic.kind, - location: Location::new(diagnostic.location.row(), diagnostic.location.column() + 1), - end_location: Location::new( - diagnostic.end_location.row(), - diagnostic.end_location.column() + 1, - ), fix: diagnostic.fix, file, - noqa_row, + noqa_offset, } } pub fn filename(&self) -> &str { self.file.name() } + + pub fn compute_start_location(&self) -> SourceLocation { + self.file.to_source_code().source_location(self.start()) + } + + pub fn compute_end_location(&self) -> SourceLocation { + self.file.to_source_code().source_location(self.end()) + } + + pub const fn start(&self) -> TextSize { + self.range.start() + } + + pub const fn end(&self) -> TextSize { + self.range.end() + } } impl Ord for Message { fn cmp(&self, other: &Self) -> Ordering { - (self.filename(), self.location.row(), self.location.column()).cmp(&( + (self.filename(), self.start(), self.kind.rule()).cmp(&( other.filename(), - other.location.row(), - other.location.column(), + other.start(), + other.kind.rule(), )) } } @@ -73,13 +90,28 @@ impl PartialOrd for Message { } } -fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<&Message>> { +struct MessageWithLocation<'a> { + message: &'a Message, + start_location: SourceLocation, +} + +impl Deref for MessageWithLocation<'_> { + type Target = Message; + fn deref(&self) -> &Self::Target { + self.message + } +} + +fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec> { let mut grouped_messages = BTreeMap::default(); for message in messages { grouped_messages .entry(message.filename()) .or_insert_with(Vec::new) - .push(message); + .push(MessageWithLocation { + message, + start_location: message.compute_start_location(), + }); } grouped_messages } @@ -120,11 +152,11 @@ impl<'a> EmitterContext<'a> { #[cfg(test)] mod tests { - use crate::message::{Emitter, EmitterContext, Location, Message}; + use crate::message::{Emitter, EmitterContext, Message}; use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::source_code::SourceFileBuilder; - use ruff_python_ast::types::Range; + use ruff_text_size::{TextRange, TextSize}; use rustc_hash::FxHashMap; pub(super) fn create_messages() -> Vec { @@ -148,20 +180,20 @@ def fibonacci(n): context: None, multiple: false, }, - Range::new(Location::new(1, 7), Location::new(1, 9)), + TextRange::new(TextSize::from(7), TextSize::from(9)), ); - let fib_source = SourceFileBuilder::new("fib.py").source_text(fib).finish(); + let fib_source = SourceFileBuilder::new("fib.py", fib).finish(); let unused_variable = Diagnostic::new( UnusedVariable { name: "x".to_string(), }, - Range::new(Location::new(6, 4), Location::new(6, 5)), + TextRange::new(TextSize::from(94), TextSize::from(95)), ) .with_fix(Fix::new(vec![Edit::deletion( - Location::new(6, 4), - Location::new(6, 9), + TextSize::from(94), + TextSize::from(99), )])); let file_2 = r#"if a == 1: pass"#; @@ -170,17 +202,18 @@ def fibonacci(n): UndefinedName { name: "a".to_string(), }, - Range::new(Location::new(1, 3), Location::new(1, 4)), + TextRange::new(TextSize::from(3), TextSize::from(4)), ); - let file_2_source = SourceFileBuilder::new("undef.py") - .source_text(file_2) - .finish(); + let file_2_source = SourceFileBuilder::new("undef.py", file_2).finish(); + let unused_import_start = unused_import.start(); + let unused_variable_start = unused_variable.start(); + let undefined_name_start = undefined_name.start(); vec![ - Message::from_diagnostic(unused_import, fib_source.clone(), 1), - Message::from_diagnostic(unused_variable, fib_source, 1), - Message::from_diagnostic(undefined_name, file_2_source, 1), + Message::from_diagnostic(unused_import, fib_source.clone(), unused_import_start), + Message::from_diagnostic(unused_variable, fib_source, unused_variable_start), + Message::from_diagnostic(undefined_name, file_2_source, undefined_name_start), ] } diff --git a/crates/ruff/src/message/pylint.rs b/crates/ruff/src/message/pylint.rs index 0b0e682b40..d456fa6e10 100644 --- a/crates/ruff/src/message/pylint.rs +++ b/crates/ruff/src/message/pylint.rs @@ -1,6 +1,7 @@ use crate::fs::relativize_path; use crate::message::{Emitter, EmitterContext, Message}; use crate::registry::AsRule; +use ruff_python_ast::source_code::OneIndexed; use std::io::Write; /// Generate violations in Pylint format. @@ -19,9 +20,9 @@ impl Emitter for PylintEmitter { let row = if context.is_jupyter_notebook(message.filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - 1 + OneIndexed::from_zero_indexed(0) } else { - message.location.row() + message.compute_start_location().row }; writeln!( diff --git a/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap index f7063d882a..2aa4807b10 100644 --- a/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap +++ b/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap @@ -46,7 +46,7 @@ expression: content "column": 6 }, "filename": "fib.py", - "noqa_row": 1 + "noqa_row": 6 }, { "code": "F821", diff --git a/crates/ruff/src/message/text.rs b/crates/ruff/src/message/text.rs index 3981da9815..c0801a3ce6 100644 --- a/crates/ruff/src/message/text.rs +++ b/crates/ruff/src/message/text.rs @@ -4,30 +4,45 @@ use crate::message::{Emitter, EmitterContext, Message}; use crate::registry::AsRule; use annotate_snippets::display_list::{DisplayList, FormatOptions}; use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}; +use bitflags::bitflags; use colored::Colorize; use ruff_diagnostics::DiagnosticKind; -use ruff_python_ast::source_code::OneIndexed; +use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; use ruff_text_size::TextRange; -use std::cmp; use std::fmt::{Display, Formatter}; use std::io::Write; +bitflags! { + #[derive(Default)] + struct EmitterFlags: u8 { + const SHOW_FIX_STATUS = 0b0000_0001; + const SHOW_FIX = 0b0000_0010; + const SHOW_SOURCE = 0b0000_0100; + } +} + #[derive(Default)] pub struct TextEmitter { - show_fix_status: bool, - show_fix: bool, + flags: EmitterFlags, } impl TextEmitter { #[must_use] pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self { - self.show_fix_status = show_fix_status; + self.flags + .set(EmitterFlags::SHOW_FIX_STATUS, show_fix_status); self } #[must_use] pub fn with_show_fix(mut self, show_fix: bool) -> Self { - self.show_fix = show_fix; + self.flags.set(EmitterFlags::SHOW_FIX, show_fix); + self + } + + #[must_use] + pub fn with_show_source(mut self, show_source: bool) -> Self { + self.flags.set(EmitterFlags::SHOW_SOURCE, show_source); self } } @@ -47,41 +62,48 @@ impl Emitter for TextEmitter { sep = ":".cyan(), )?; - // Check if we're working on a jupyter notebook and translate positions with cell accordingly - let (row, col) = if let Some(jupyter_index) = context.jupyter_index(message.filename()) - { - write!( - writer, - "cell {cell}{sep}", - cell = jupyter_index.row_to_cell[message.location.row()], - sep = ":".cyan(), - )?; + let start_location = message.compute_start_location(); - ( - jupyter_index.row_to_row_in_cell[message.location.row()] as usize, - message.location.column(), - ) - } else { - (message.location.row(), message.location.column()) - }; + // Check if we're working on a jupyter notebook and translate positions with cell accordingly + let diagnostic_location = + if let Some(jupyter_index) = context.jupyter_index(message.filename()) { + write!( + writer, + "cell {cell}{sep}", + cell = jupyter_index.row_to_cell[start_location.row.get()], + sep = ":".cyan(), + )?; + + SourceLocation { + row: OneIndexed::new( + jupyter_index.row_to_row_in_cell[start_location.row.get()] as usize, + ) + .unwrap(), + column: start_location.column, + } + } else { + start_location + }; writeln!( writer, "{row}{sep}{col}{sep} {code_and_body}", + row = diagnostic_location.row, + col = diagnostic_location.column, sep = ":".cyan(), code_and_body = RuleCodeAndBody { message_kind: &message.kind, - show_fix_status: self.show_fix_status + show_fix_status: self.flags.contains(EmitterFlags::SHOW_FIX_STATUS) } )?; - if message.file.source_code().is_some() { + if self.flags.contains(EmitterFlags::SHOW_SOURCE) { writeln!(writer, "{}", MessageCodeFrame { message })?; + } - if self.show_fix { - if let Some(diff) = Diff::from_message(message) { - writeln!(writer, "{diff}")?; - } + if self.flags.contains(EmitterFlags::SHOW_FIX) { + if let Some(diff) = Diff::from_message(message) { + writeln!(writer, "{diff}")?; } } } @@ -135,105 +157,91 @@ pub(super) struct MessageCodeFrame<'a> { impl Display for MessageCodeFrame<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let Message { - kind, - file, - location, - end_location, - .. + kind, file, range, .. } = self.message; - if let Some(source_code) = file.source_code() { - let suggestion = kind.suggestion.as_deref(); - let footer = if suggestion.is_some() { - vec![Annotation { - id: None, - label: suggestion, - annotation_type: AnnotationType::Help, - }] - } else { - Vec::new() - }; + let suggestion = kind.suggestion.as_deref(); + let footer = if suggestion.is_some() { + vec![Annotation { + id: None, + label: suggestion, + annotation_type: AnnotationType::Help, + }] + } else { + Vec::new() + }; - let mut start_index = - OneIndexed::new(cmp::max(1, location.row().saturating_sub(2))).unwrap(); - let content_start_index = OneIndexed::new(location.row()).unwrap(); + let source_code = file.to_source_code(); + let content_start_index = source_code.line_index(range.start()); + let mut start_index = content_start_index.saturating_sub(2); - // Trim leading empty lines. - while start_index < content_start_index { - if !source_code.line_text(start_index).trim().is_empty() { - break; - } - start_index = start_index.saturating_add(1); + // Trim leading empty lines. + while start_index < content_start_index { + if !source_code.line_text(start_index).trim().is_empty() { + break; + } + start_index = start_index.saturating_add(1); + } + + let content_end_index = source_code.line_index(range.end()); + let mut end_index = content_end_index + .saturating_add(2) + .min(OneIndexed::from_zero_indexed(source_code.line_count())); + + // Trim trailing empty lines + while end_index > content_end_index { + if !source_code.line_text(end_index).trim().is_empty() { + break; } - let mut end_index = OneIndexed::new(cmp::min( - end_location.row().saturating_add(2), - source_code.line_count() + 1, - )) - .unwrap(); + end_index = end_index.saturating_sub(1); + } - let content_end_index = OneIndexed::new(end_location.row()).unwrap(); + let start_offset = source_code.line_start(start_index); + let end_offset = source_code.line_end(end_index); - // Trim trailing empty lines - while end_index > content_end_index { - if !source_code.line_text(end_index).trim().is_empty() { - break; - } + let source_text = source_code.slice(TextRange::new(start_offset, end_offset)); - end_index = end_index.saturating_sub(1); - } + let annotation_start_offset = range.start() - start_offset; + let annotation_end_offset = range.end() - start_offset; - let start_offset = source_code.line_start(start_index); - let end_offset = source_code.line_end(end_index); - - let source_text = &source_code.text()[TextRange::new(start_offset, end_offset)]; - - let annotation_start_offset = - // Message columns are one indexed - source_code.offset(location.with_col_offset(-1)) - start_offset; - let annotation_end_offset = - source_code.offset(end_location.with_col_offset(-1)) - start_offset; - - let start_char = source_text[TextRange::up_to(annotation_start_offset)] - .chars() - .count(); - - let char_length = source_text - [TextRange::new(annotation_start_offset, annotation_end_offset)] + let start_char = source_text[TextRange::up_to(annotation_start_offset)] .chars() .count(); - let label = kind.rule().noqa_code().to_string(); + let char_length = source_text + [TextRange::new(annotation_start_offset, annotation_end_offset)] + .chars() + .count(); - let snippet = Snippet { - title: None, - slices: vec![Slice { - source: source_text, - line_start: location.row(), - annotations: vec![SourceAnnotation { - label: &label, - annotation_type: AnnotationType::Error, - range: (start_char, start_char + char_length), - }], - // The origin (file name, line number, and column number) is already encoded - // in the `label`. - origin: None, - fold: false, + let label = kind.rule().noqa_code().to_string(); + + let snippet = Snippet { + title: None, + slices: vec![Slice { + source: source_text, + line_start: content_start_index.get(), + annotations: vec![SourceAnnotation { + label: &label, + annotation_type: AnnotationType::Error, + range: (start_char, start_char + char_length), }], - footer, - opt: FormatOptions { - #[cfg(test)] - color: false, - #[cfg(not(test))] - color: colored::control::SHOULD_COLORIZE.should_colorize(), - ..FormatOptions::default() - }, - }; + // The origin (file name, line number, and column number) is already encoded + // in the `label`. + origin: None, + fold: false, + }], + footer, + opt: FormatOptions { + #[cfg(test)] + color: false, + #[cfg(not(test))] + color: colored::control::SHOULD_COLORIZE.should_colorize(), + ..FormatOptions::default() + }, + }; - writeln!(f, "{message}", message = DisplayList::from(snippet))?; - } - - Ok(()) + writeln!(f, "{message}", message = DisplayList::from(snippet)) } } @@ -245,7 +253,7 @@ mod tests { #[test] fn default() { - let mut emitter = TextEmitter::default(); + let mut emitter = TextEmitter::default().with_show_source(true); let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); @@ -253,7 +261,9 @@ mod tests { #[test] fn fix_status() { - let mut emitter = TextEmitter::default().with_show_fix_status(true); + let mut emitter = TextEmitter::default() + .with_show_fix_status(true) + .with_show_source(true); let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 7752c55b1f..9bab41bf9d 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::fmt::{Display, Write}; use std::fs; use std::path::Path; @@ -5,16 +6,12 @@ use std::path::Path; use anyhow::Result; use itertools::Itertools; use log::warn; -use nohash_hasher::IntMap; use once_cell::sync::Lazy; use regex::Regex; -use rustc_hash::FxHashMap; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::Diagnostic; -use ruff_python_ast::newlines::StrExt; use ruff_python_ast::source_code::{LineEnding, Locator}; -use ruff_python_ast::types::Range; use crate::codes::NoqaCode; use crate::registry::{AsRule, Rule, RuleSet}; @@ -31,46 +28,52 @@ static SPLIT_COMMA_REGEX: Lazy = Lazy::new(|| Regex::new(r"[,\s]").unwrap #[derive(Debug)] pub enum Directive<'a> { None, - All(usize, usize, usize, usize), - Codes(usize, usize, usize, Vec<&'a str>, usize), + // (leading spaces, noqa_range, trailing_spaces) + All(TextSize, TextRange, TextSize), + // (leading spaces, start_offset, end_offset, codes, trailing_spaces) + Codes(TextSize, TextRange, Vec<&'a str>, TextSize), } /// Extract the noqa `Directive` from a line of Python source code. -pub fn extract_noqa_directive(line: &str) -> Directive { - match NOQA_LINE_REGEX.captures(line) { - Some(caps) => match caps.name("leading_spaces") { - Some(leading_spaces) => match caps.name("trailing_spaces") { - Some(trailing_spaces) => match caps.name("noqa") { - Some(noqa) => match caps.name("codes") { - Some(codes) => { - let codes: Vec<&str> = SPLIT_COMMA_REGEX - .split(codes.as_str().trim()) - .map(str::trim) - .filter(|code| !code.is_empty()) - .collect(); - if codes.is_empty() { - warn!("Expected rule codes on `noqa` directive: \"{line}\""); - } - Directive::Codes( - leading_spaces.as_str().chars().count(), - noqa.start(), - noqa.end(), - codes, - trailing_spaces.as_str().chars().count(), - ) - } - None => Directive::All( - leading_spaces.as_str().chars().count(), - noqa.start(), - noqa.end(), - trailing_spaces.as_str().chars().count(), - ), - }, - None => Directive::None, - }, - None => Directive::None, - }, - None => Directive::None, +pub fn extract_noqa_directive<'a>(range: TextRange, locator: &'a Locator) -> Directive<'a> { + let text = &locator.contents()[range]; + match NOQA_LINE_REGEX.captures(text) { + Some(caps) => match ( + caps.name("leading_spaces"), + caps.name("noqa"), + caps.name("codes"), + caps.name("trailing_spaces"), + ) { + (Some(leading_spaces), Some(noqa), Some(codes), Some(trailing_spaces)) => { + let codes: Vec<&str> = SPLIT_COMMA_REGEX + .split(codes.as_str().trim()) + .map(str::trim) + .filter(|code| !code.is_empty()) + .collect(); + + let start = range.start() + TextSize::try_from(noqa.start()).unwrap(); + if codes.is_empty() { + #[allow(deprecated)] + let line = locator.compute_line_index(start); + warn!("Expected rule codes on `noqa` directive: \"{line}\""); + } + Directive::Codes( + leading_spaces.as_str().text_len(), + TextRange::at(start, noqa.as_str().text_len()), + codes, + trailing_spaces.as_str().text_len(), + ) + } + + (Some(leading_spaces), Some(noqa), None, Some(trailing_spaces)) => Directive::All( + leading_spaces.as_str().text_len(), + TextRange::at( + range.start() + TextSize::try_from(noqa.start()).unwrap(), + noqa.as_str().text_len(), + ), + trailing_spaces.as_str().text_len(), + ), + _ => Directive::None, }, None => Directive::None, } @@ -129,16 +132,13 @@ pub fn includes(needle: Rule, haystack: &[&str]) -> bool { /// Returns `true` if the given [`Rule`] is ignored at the specified `lineno`. pub fn rule_is_ignored( code: Rule, - lineno: usize, - noqa_line_for: &IntMap, + offset: TextSize, + noqa_line_for: &NoqaMapping, locator: &Locator, ) -> bool { - let noqa_lineno = noqa_line_for.get(&lineno).unwrap_or(&lineno); - let line = locator.slice(Range::new( - Location::new(*noqa_lineno, 0), - Location::new(noqa_lineno + 1, 0), - )); - match extract_noqa_directive(line) { + let offset = noqa_line_for.resolve(offset); + let line_range = locator.line_range(offset); + match extract_noqa_directive(line_range, locator) { Directive::None => false, Directive::All(..) => true, Directive::Codes(.., codes, _) => includes(code, &codes), @@ -153,11 +153,11 @@ pub enum FileExemption { /// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are /// globally ignored within the file. -pub fn file_exemption(lines: &[&str], commented_lines: &[usize]) -> FileExemption { +pub fn file_exemption(contents: &str, comment_ranges: &[TextRange]) -> FileExemption { let mut exempt_codes: Vec = vec![]; - for lineno in commented_lines { - match parse_file_exemption(lines[lineno - 1]) { + for range in comment_ranges { + match parse_file_exemption(&contents[*range]) { ParsedExemption::All => { return FileExemption::All; } @@ -182,17 +182,18 @@ pub fn file_exemption(lines: &[&str], commented_lines: &[usize]) -> FileExemptio } } +/// Adds noqa comments to suppress all diagnostics of a file. pub fn add_noqa( path: &Path, diagnostics: &[Diagnostic], - contents: &str, - commented_lines: &[usize], - noqa_line_for: &IntMap, + locator: &Locator, + commented_lines: &[TextRange], + noqa_line_for: &NoqaMapping, line_ending: LineEnding, ) -> Result { let (count, output) = add_noqa_inner( diagnostics, - contents, + locator, commented_lines, noqa_line_for, line_ending, @@ -203,19 +204,19 @@ pub fn add_noqa( fn add_noqa_inner( diagnostics: &[Diagnostic], - contents: &str, - commented_lines: &[usize], - noqa_line_for: &IntMap, + locator: &Locator, + commented_ranges: &[TextRange], + noqa_line_for: &NoqaMapping, line_ending: LineEnding, ) -> (usize, String) { - // Map of line number to set of (non-ignored) diagnostic codes that are triggered on that line. - let mut matches_by_line: FxHashMap = FxHashMap::default(); - - let lines: Vec<&str> = contents.universal_newlines().collect(); + // Map of line start offset to set of (non-ignored) diagnostic codes that are triggered on that line. + let mut matches_by_line: BTreeMap)> = + BTreeMap::default(); // Whether the file is exempted from all checks. // Codes that are globally exempted (within the current file). - let exemption = file_exemption(&lines, commented_lines); + let exemption = file_exemption(locator.contents(), commented_ranges); + let directives = NoqaDirectives::from_commented_ranges(commented_ranges, locator); // Mark any non-ignored diagnostics. for diagnostic in diagnostics { @@ -233,116 +234,122 @@ fn add_noqa_inner( FileExemption::None => {} } - let diagnostic_lineno = diagnostic.location.row(); - // Is the violation ignored by a `noqa` directive on the parent line? - if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) { - if parent_lineno != diagnostic_lineno { - let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno); - if commented_lines.contains(noqa_lineno) { - match extract_noqa_directive(lines[noqa_lineno - 1]) { - Directive::All(..) => { + if let Some(parent) = diagnostic.parent { + if let Some(directive_line) = + directives.find_line_with_directive(noqa_line_for.resolve(parent)) + { + match &directive_line.directive { + Directive::All(..) => { + continue; + } + Directive::Codes(.., codes, _) => { + if includes(diagnostic.kind.rule(), codes) { continue; } - Directive::Codes(.., codes, _) => { - if includes(diagnostic.kind.rule(), &codes) { - continue; - } - } - Directive::None => {} } + Directive::None => {} } } } - // Is the diagnostic ignored by a `noqa` directive on the same line? - let noqa_lineno = noqa_line_for - .get(&diagnostic_lineno) - .unwrap_or(&diagnostic_lineno); - if commented_lines.contains(noqa_lineno) { - match extract_noqa_directive(lines[noqa_lineno - 1]) { + let noqa_offset = noqa_line_for.resolve(diagnostic.start()); + + // Or ignored by the directive itself + if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) { + match &directive_line.directive { Directive::All(..) => { continue; } Directive::Codes(.., codes, _) => { - if includes(diagnostic.kind.rule(), &codes) { - continue; + let rule = diagnostic.kind.rule(); + if !includes(rule, codes) { + matches_by_line + .entry(directive_line.range.start()) + .or_insert_with(|| { + (RuleSet::default(), Some(&directive_line.directive)) + }) + .0 + .insert(rule); } + continue; } Directive::None => {} } } - // The diagnostic is not ignored by any `noqa` directive; add it to the list. - let lineno = diagnostic.location.row() - 1; - let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1; + // There's no existing noqa directive that suppresses the diagnostic. matches_by_line - .entry(noqa_lineno) - .or_default() + .entry(locator.line_start(noqa_offset)) + .or_insert_with(|| (RuleSet::default(), None)) + .0 .insert(diagnostic.kind.rule()); } - let mut count: usize = 0; - let mut output = String::new(); - for (lineno, line) in lines.into_iter().enumerate() { - match matches_by_line.get(&lineno) { - None => { - output.push_str(line); + let mut count = 0; + let mut output = String::with_capacity(locator.len()); + let mut prev_end = TextSize::default(); + + for (offset, (rules, directive)) in matches_by_line { + output.push_str(&locator.contents()[TextRange::new(prev_end, offset)]); + + let line = locator.full_line(offset); + + match directive { + None | Some(Directive::None) => { + // Add existing content. + output.push_str(line.trim_end()); + + // Add `noqa` directive. + output.push_str(" # noqa: "); + + // Add codes. + push_codes(&mut output, rules.iter().map(|rule| rule.noqa_code())); + output.push_str(&line_ending); + count += 1; + } + Some(Directive::All(..)) => { + // Does not get inserted into the map. + } + Some(Directive::Codes(_, noqa_range, existing, _)) => { + // Reconstruct the line based on the preserved rule codes. + // This enables us to tally the number of edits. + let output_start = output.len(); + + // Add existing content. + output.push_str( + locator + .slice(TextRange::new(offset, noqa_range.start())) + .trim_end(), + ); + + // Add `noqa` directive. + output.push_str(" # noqa: "); + + // Add codes. + push_codes( + &mut output, + rules + .iter() + .map(|r| r.noqa_code().to_string()) + .chain(existing.iter().map(ToString::to_string)) + .sorted_unstable(), + ); + + // Only count if the new line is an actual edit. + if &output[output_start..] != line.trim_end() { + count += 1; + } + output.push_str(&line_ending); } - Some(rules) => { - match extract_noqa_directive(line) { - Directive::None => { - // Add existing content. - output.push_str(line.trim_end()); - - // Add `noqa` directive. - output.push_str(" # noqa: "); - - // Add codes. - push_codes(&mut output, rules.iter().map(|rule| rule.noqa_code())); - output.push_str(&line_ending); - count += 1; - } - Directive::All(..) => { - // Leave the line as-is. - output.push_str(line); - output.push_str(&line_ending); - } - Directive::Codes(_, start_byte, _, existing, _) => { - // Reconstruct the line based on the preserved rule codes. - // This enables us to tally the number of edits. - let mut formatted = String::with_capacity(line.len()); - - // Add existing content. - formatted.push_str(line[..start_byte].trim_end()); - - // Add `noqa` directive. - formatted.push_str(" # noqa: "); - - // Add codes. - push_codes( - &mut formatted, - rules - .iter() - .map(|r| r.noqa_code().to_string()) - .chain(existing.into_iter().map(ToString::to_string)) - .sorted_unstable(), - ); - - output.push_str(&formatted); - output.push_str(&line_ending); - - // Only count if the new line is an actual edit. - if formatted != line { - count += 1; - } - } - }; - } } + + prev_end = offset + line.text_len(); } + output.push_str(&locator.contents()[usize::from(prev_end)..]); + (count, output) } @@ -352,21 +359,161 @@ fn push_codes(str: &mut String, codes: impl Iterator) { if !first { str.push_str(", "); } - _ = write!(str, "{code}"); + write!(str, "{code}").unwrap(); first = false; } } +#[derive(Debug)] +pub(crate) struct NoqaDirectiveLine<'a> { + // The range of the text line for which the noqa directive applies. + pub range: TextRange, + pub directive: Directive<'a>, + pub matches: Vec, +} + +#[derive(Debug, Default)] +pub(crate) struct NoqaDirectives<'a> { + inner: Vec>, +} + +impl<'a> NoqaDirectives<'a> { + pub fn from_commented_ranges(comment_ranges: &[TextRange], locator: &'a Locator<'a>) -> Self { + let mut directives = Vec::new(); + + for comment_range in comment_ranges { + let line_range = locator.line_range(comment_range.start()); + let directive = match extract_noqa_directive(line_range, locator) { + Directive::None => { + continue; + } + directive @ (Directive::All(..) | Directive::Codes(..)) => directive, + }; + + // noqa comments are guaranteed to be single line. + directives.push(NoqaDirectiveLine { + range: line_range, + directive, + matches: Vec::new(), + }); + } + + // Extend a mapping at the end of the file to also include the EOF token. + if let Some(last) = directives.last_mut() { + if last.range.end() == locator.contents().text_len() { + last.range = last.range.add_end(TextSize::from(1)); + } + } + + Self { inner: directives } + } + + pub fn find_line_with_directive(&self, offset: TextSize) -> Option<&NoqaDirectiveLine> { + self.find_line_index(offset).map(|index| &self.inner[index]) + } + + pub fn find_line_with_directive_mut( + &mut self, + offset: TextSize, + ) -> Option<&mut NoqaDirectiveLine<'a>> { + if let Some(index) = self.find_line_index(offset) { + Some(&mut self.inner[index]) + } else { + None + } + } + + fn find_line_index(&self, offset: TextSize) -> Option { + self.inner + .binary_search_by(|directive| { + if directive.range.end() < offset { + std::cmp::Ordering::Less + } else if directive.range.contains(offset) { + std::cmp::Ordering::Equal + } else { + std::cmp::Ordering::Greater + } + }) + .ok() + } + + pub fn lines(&self) -> &[NoqaDirectiveLine] { + &self.inner + } +} + +/// 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, +} + +impl NoqaMapping { + pub(crate) fn with_capacity(capacity: usize) -> Self { + Self { + ranges: Vec::with_capacity(capacity), + } + } + + /// Returns the re-mapped position or `position` if no mapping exists. + pub fn resolve(&self, offset: TextSize) -> TextSize { + let index = self.ranges.binary_search_by(|range| { + if range.end() < offset { + std::cmp::Ordering::Less + } else if range.contains(offset) { + std::cmp::Ordering::Equal + } else { + std::cmp::Ordering::Greater + } + }); + + if let Ok(index) = index { + self.ranges[index].end() + } else { + offset + } + } + + pub fn push_mapping(&mut self, range: TextRange) { + if let Some(last_range) = self.ranges.last_mut() { + // Strictly sorted insertion + if last_range.end() <= range.start() { + // OK + } + // Try merging with the last inserted range + else if let Some(intersected) = last_range.intersect(range) { + *last_range = intersected; + return; + } else { + panic!("Ranges must be inserted in sorted order") + } + } + + self.ranges.push(range); + } +} + +impl FromIterator for NoqaMapping { + fn from_iter>(iter: T) -> Self { + let mut mappings = NoqaMapping::default(); + + for range in iter { + mappings.push_mapping(range); + } + + mappings + } +} + #[cfg(test)] mod tests { - use nohash_hasher::IntMap; - use rustpython_parser::ast::Location; + use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::Diagnostic; - use ruff_python_ast::source_code::LineEnding; - use ruff_python_ast::types::Range; + use ruff_python_ast::source_code::{LineEnding, Locator}; - use crate::noqa::{add_noqa_inner, NOQA_LINE_REGEX}; + use crate::noqa::{add_noqa_inner, NoqaMapping, NOQA_LINE_REGEX}; use crate::rules::pycodestyle::rules::AmbiguousVariableName; use crate::rules::pyflakes; @@ -386,87 +533,83 @@ mod tests { #[test] fn modification() { - let diagnostics = vec![]; let contents = "x = 1"; - let commented_lines = vec![]; - let noqa_line_for = IntMap::default(); + let noqa_line_for = NoqaMapping::default(); let (count, output) = add_noqa_inner( - &diagnostics, - contents, - &commented_lines, + &[], + &Locator::new(contents), + &[], &noqa_line_for, LineEnding::Lf, ); assert_eq!(count, 0); - assert_eq!(output, format!("{contents}\n")); + assert_eq!(output, format!("{contents}")); - let diagnostics = vec![Diagnostic::new( + let diagnostics = [Diagnostic::new( pyflakes::rules::UnusedVariable { name: "x".to_string(), }, - Range::new(Location::new(1, 0), Location::new(1, 0)), + TextRange::new(TextSize::from(0), TextSize::from(0)), )]; + let contents = "x = 1"; - let commented_lines = vec![1]; - let noqa_line_for = IntMap::default(); + let noqa_line_for = NoqaMapping::default(); let (count, output) = add_noqa_inner( &diagnostics, - contents, - &commented_lines, + &Locator::new(contents), + &[], &noqa_line_for, LineEnding::Lf, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: F841\n"); - let diagnostics = vec![ + let diagnostics = [ Diagnostic::new( AmbiguousVariableName("x".to_string()), - Range::new(Location::new(1, 0), Location::new(1, 0)), + TextRange::new(TextSize::from(0), TextSize::from(0)), ), Diagnostic::new( pyflakes::rules::UnusedVariable { name: "x".to_string(), }, - Range::new(Location::new(1, 0), Location::new(1, 0)), + TextRange::new(TextSize::from(0), TextSize::from(0)), ), ]; let contents = "x = 1 # noqa: E741\n"; - let commented_lines = vec![1]; - let noqa_line_for = IntMap::default(); + let noqa_line_for = NoqaMapping::default(); let (count, output) = add_noqa_inner( &diagnostics, - contents, - &commented_lines, + &Locator::new(contents), + &[TextRange::new(TextSize::from(7), TextSize::from(19))], &noqa_line_for, LineEnding::Lf, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: E741, F841\n"); - let diagnostics = vec![ + let diagnostics = [ Diagnostic::new( AmbiguousVariableName("x".to_string()), - Range::new(Location::new(1, 0), Location::new(1, 0)), + TextRange::new(TextSize::from(0), TextSize::from(0)), ), Diagnostic::new( pyflakes::rules::UnusedVariable { name: "x".to_string(), }, - Range::new(Location::new(1, 0), Location::new(1, 0)), + TextRange::new(TextSize::from(0), TextSize::from(0)), ), ]; let contents = "x = 1 # noqa"; - let commented_lines = vec![1]; - let noqa_line_for = IntMap::default(); + let noqa_line_for = NoqaMapping::default(); let (count, output) = add_noqa_inner( &diagnostics, - contents, - &commented_lines, + &Locator::new(contents), + &[TextRange::new(TextSize::from(7), TextSize::from(13))], &noqa_line_for, LineEnding::Lf, ); assert_eq!(count, 0); - assert_eq!(output, "x = 1 # noqa\n"); + assert_eq!(output, "x = 1 # noqa"); } } diff --git a/crates/ruff/src/rules/eradicate/rules.rs b/crates/ruff/src/rules/eradicate/rules.rs index 7808cff4a9..ff0380165b 100644 --- a/crates/ruff/src/rules/eradicate/rules.rs +++ b/crates/ruff/src/rules/eradicate/rules.rs @@ -1,9 +1,8 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::registry::Rule; use crate::settings::{flags, Settings}; @@ -49,20 +48,20 @@ fn is_standalone_comment(line: &str) -> bool { /// ERA001 pub fn commented_out_code( locator: &Locator, - start: Location, - end: Location, + range: TextRange, settings: &Settings, autofix: flags::Autofix, ) -> Option { - let location = Location::new(start.row(), 0); - let end_location = Location::new(end.row() + 1, 0); - let line = locator.slice(Range::new(location, end_location)); + let line = locator.full_lines(range); // Verify that the comment is on its own line, and that it contains code. if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) { - let mut diagnostic = Diagnostic::new(CommentedOutCode, Range::new(start, end)); + let mut diagnostic = Diagnostic::new(CommentedOutCode, range); if autofix.into() && settings.rules.should_fix(Rule::CommentedOutCode) { - diagnostic.set_fix(Edit::deletion(location, end_location)); + diagnostic.set_fix(Edit::range_deletion(TextRange::at( + range.start(), + line.text_len(), + ))); } Some(diagnostic) } else { diff --git a/crates/ruff/src/rules/flake8_2020/rules.rs b/crates/ruff/src/rules/flake8_2020/rules.rs index d6f36a801d..a4d7bd3898 100644 --- a/crates/ruff/src/rules/flake8_2020/rules.rs +++ b/crates/ruff/src/rules/flake8_2020/rules.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Located}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -141,13 +140,13 @@ pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) { { checker .diagnostics - .push(Diagnostic::new(SysVersionSlice1, Range::from(value))); + .push(Diagnostic::new(SysVersionSlice1, value.range())); } else if *i == BigInt::from(3) && checker.settings.rules.enabled(Rule::SysVersionSlice3) { checker .diagnostics - .push(Diagnostic::new(SysVersionSlice3, Range::from(value))); + .push(Diagnostic::new(SysVersionSlice3, value.range())); } } } @@ -159,12 +158,12 @@ pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) { if *i == BigInt::from(2) && checker.settings.rules.enabled(Rule::SysVersion2) { checker .diagnostics - .push(Diagnostic::new(SysVersion2, Range::from(value))); + .push(Diagnostic::new(SysVersion2, value.range())); } else if *i == BigInt::from(0) && checker.settings.rules.enabled(Rule::SysVersion0) { checker .diagnostics - .push(Diagnostic::new(SysVersion0, Range::from(value))); + .push(Diagnostic::new(SysVersion0, value.range())); } } @@ -200,7 +199,7 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: & { checker .diagnostics - .push(Diagnostic::new(SysVersionInfo0Eq3, Range::from(left))); + .push(Diagnostic::new(SysVersionInfo0Eq3, left.range())); } } } else if *i == BigInt::from(1) { @@ -219,7 +218,7 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: & if checker.settings.rules.enabled(Rule::SysVersionInfo1CmpInt) { checker .diagnostics - .push(Diagnostic::new(SysVersionInfo1CmpInt, Range::from(left))); + .push(Diagnostic::new(SysVersionInfo1CmpInt, left.range())); } } } @@ -246,10 +245,9 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: & .rules .enabled(Rule::SysVersionInfoMinorCmpInt) { - checker.diagnostics.push(Diagnostic::new( - SysVersionInfoMinorCmpInt, - Range::from(left), - )); + checker + .diagnostics + .push(Diagnostic::new(SysVersionInfoMinorCmpInt, left.range())); } } } @@ -274,12 +272,12 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: & if checker.settings.rules.enabled(Rule::SysVersionCmpStr10) { checker .diagnostics - .push(Diagnostic::new(SysVersionCmpStr10, Range::from(left))); + .push(Diagnostic::new(SysVersionCmpStr10, left.range())); } } else if checker.settings.rules.enabled(Rule::SysVersionCmpStr3) { checker .diagnostics - .push(Diagnostic::new(SysVersionCmpStr3, Range::from(left))); + .push(Diagnostic::new(SysVersionCmpStr3, left.range())); } } } @@ -294,6 +292,6 @@ pub fn name_or_attribute(checker: &mut Checker, expr: &Expr) { { checker .diagnostics - .push(Diagnostic::new(SixPY3, Range::from(expr))); + .push(Diagnostic::new(SixPY3, expr.range())); } } diff --git a/crates/ruff/src/rules/flake8_annotations/fixes.rs b/crates/ruff/src/rules/flake8_annotations/fixes.rs index 5b499b4da9..5a6fea6f4f 100644 --- a/crates/ruff/src/rules/flake8_annotations/fixes.rs +++ b/crates/ruff/src/rules/flake8_annotations/fixes.rs @@ -4,21 +4,19 @@ use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; /// ANN204 pub fn add_return_annotation(locator: &Locator, stmt: &Stmt, annotation: &str) -> Result { - let range = Range::from(stmt); - let contents = locator.slice(range); + let contents = &locator.contents()[stmt.range()]; // Find the colon (following the `def` keyword). let mut seen_lpar = false; let mut seen_rpar = false; let mut count: usize = 0; - for (start, tok, ..) in lexer::lex_located(contents, Mode::Module, range.location).flatten() { + for (tok, range) in lexer::lex_located(contents, Mode::Module, stmt.start()).flatten() { if seen_lpar && seen_rpar { if matches!(tok, Tok::Colon) { - return Ok(Edit::insertion(format!(" -> {annotation}"), start)); + return Ok(Edit::insertion(format!(" -> {annotation}"), range.start())); } } diff --git a/crates/ruff/src/rules/flake8_annotations/rules.rs b/crates/ruff/src/rules/flake8_annotations/rules.rs index 0da004ae36..f3f479ab3a 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::ReturnStatementVisitor; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{cast, helpers}; use ruff_python_semantic::analyze::visibility; @@ -446,7 +445,7 @@ fn check_dynamically_typed( if checker.ctx.match_typing_expr(annotation, "Any") { diagnostics.push(Diagnostic::new( AnyType { name: func() }, - Range::from(annotation), + annotation.range(), )); }; } @@ -513,7 +512,7 @@ pub fn definition( MissingTypeFunctionArgument { name: arg.node.arg.to_string(), }, - Range::from(arg), + arg.range(), )); } } @@ -544,7 +543,7 @@ pub fn definition( MissingTypeArgs { name: arg.node.arg.to_string(), }, - Range::from(arg), + arg.range(), )); } } @@ -575,7 +574,7 @@ pub fn definition( MissingTypeKwargs { name: arg.node.arg.to_string(), }, - Range::from(arg), + arg.range(), )); } } @@ -592,7 +591,7 @@ pub fn definition( MissingTypeCls { name: arg.node.arg.to_string(), }, - Range::from(arg), + arg.range(), )); } } else { @@ -601,7 +600,7 @@ pub fn definition( MissingTypeSelf { name: arg.node.arg.to_string(), }, - Range::from(arg), + arg.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/assert_used.rs b/crates/ruff/src/rules/flake8_bandit/rules/assert_used.rs index bf9b2627ad..5678602b24 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/assert_used.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/assert_used.rs @@ -1,8 +1,8 @@ +use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for uses of the `assert` keyword. @@ -37,8 +37,5 @@ impl Violation for Assert { /// S101 pub fn assert_used(stmt: &Stmt) -> Diagnostic { - Diagnostic::new( - Assert, - Range::new(stmt.location, stmt.location.with_col_offset("assert".len())), - ) + Diagnostic::new(Assert, TextRange::at(stmt.start(), "assert".text_len())) } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs index 38a5b5d29e..65daba361e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -7,7 +7,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::compose_call_path; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -114,7 +113,7 @@ pub fn bad_file_permissions( if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) { checker.diagnostics.push(Diagnostic::new( BadFilePermissions { mask: int_value }, - Range::from(mode_arg), + mode_arg.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs index 71801d6c43..d54dee0850 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct ExecBuiltin; @@ -22,5 +21,5 @@ pub fn exec_used(expr: &Expr, func: &Expr) -> Option { if id != "exec" { return None; } - Some(Diagnostic::new(ExecBuiltin, Range::from(expr))) + Some(Diagnostic::new(ExecBuiltin, expr.range())) } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs index aee713585f..a74288a841 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_text_size::TextRange; #[violation] pub struct HardcodedBindAllInterfaces; @@ -13,9 +13,9 @@ impl Violation for HardcodedBindAllInterfaces { } /// S104 -pub fn hardcoded_bind_all_interfaces(value: &str, range: &Range) -> Option { +pub fn hardcoded_bind_all_interfaces(value: &str, range: TextRange) -> Option { if value == "0.0.0.0" { - Some(Diagnostic::new(HardcodedBindAllInterfaces, *range)) + Some(Diagnostic::new(HardcodedBindAllInterfaces, range)) } else { None } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs index c5ecf9da6a..76669b993c 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Arg, Arguments, Expr}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use super::super::helpers::{matches_password_name, string_literal}; @@ -29,7 +28,7 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option { HardcodedPasswordDefault { string: string.to_string(), }, - Range::from(default), + default.range(), )) } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs index d04dd5d082..f64e6abd74 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Keyword; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use super::super::helpers::{matches_password_name, string_literal}; @@ -33,7 +32,7 @@ pub fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec { HardcodedPasswordFuncArg { string: string.to_string(), }, - Range::from(keyword), + keyword.range(), )) }) .collect() diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_string.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_string.rs index b12f995d36..633c7c338e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_string.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_string.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use super::super::helpers::{matches_password_name, string_literal}; @@ -52,7 +51,7 @@ pub fn compare_to_hardcoded_password_string(left: &Expr, comparators: &[Expr]) - HardcodedPasswordString { string: string.to_string(), }, - Range::from(comp), + comp.range(), )) }) .collect() @@ -67,7 +66,7 @@ pub fn assign_hardcoded_password_string(value: &Expr, targets: &[Expr]) -> Optio HardcodedPasswordString { string: string.to_string(), }, - Range::from(value), + value.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs index 56a53ff609..72216ff9d4 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs @@ -5,7 +5,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Operator}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{any_over_expr, unparse_expr}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -98,7 +97,7 @@ pub fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) { Some(string) if matches_sql_statement(&string) => { checker .diagnostics - .push(Diagnostic::new(HardcodedSQLExpression, Range::from(expr))); + .push(Diagnostic::new(HardcodedSQLExpression, expr.range())); } _ => (), } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs index eec211e302..840243c0a2 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct HardcodedTempFile { @@ -31,7 +30,7 @@ pub fn hardcoded_tmp_directory( HardcodedTempFile { string: value.to_string(), }, - Range::from(expr), + expr.range(), )) } else { None diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index 63ae4a8e18..9ade65368c 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -74,7 +73,7 @@ pub fn hashlib_insecure_hash_functions( HashlibInsecureHashFunction { string: hash_func_name.to_string(), }, - Range::from(name_arg), + name_arg.range(), )); } } @@ -91,7 +90,7 @@ pub fn hashlib_insecure_hash_functions( HashlibInsecureHashFunction { string: (*func_name).to_string(), }, - Range::from(func), + func.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs b/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs index 3670759b81..519bfafd77 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -57,20 +56,20 @@ pub fn jinja2_autoescape_false( if id.as_str() != "select_autoescape" { checker.diagnostics.push(Diagnostic::new( Jinja2AutoescapeFalse { value: true }, - Range::from(autoescape_arg), + autoescape_arg.range(), )); } } } _ => checker.diagnostics.push(Diagnostic::new( Jinja2AutoescapeFalse { value: true }, - Range::from(autoescape_arg), + autoescape_arg.range(), )), } } else { checker.diagnostics.push(Diagnostic::new( Jinja2AutoescapeFalse { value: false }, - Range::from(func), + func.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs b/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs index e216732c60..d838fe9dd1 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -34,10 +33,9 @@ pub fn logging_config_insecure_listen( let call_args = SimpleCallArgs::new(args, keywords); if call_args.keyword_argument("verify").is_none() { - checker.diagnostics.push(Diagnostic::new( - LoggingConfigInsecureListen, - Range::from(func), - )); + checker + .diagnostics + .push(Diagnostic::new(LoggingConfigInsecureListen, func.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs b/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs index bf2f2138e1..3974eee8fd 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -66,7 +65,7 @@ pub fn request_with_no_cert_validation( RequestWithNoCertValidation { string: target.to_string(), }, - Range::from(verify_arg), + verify_arg.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs b/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs index 18d4c6cee8..0ac9bf93b4 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{unparse_constant, SimpleCallArgs}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -56,13 +55,13 @@ pub fn request_without_timeout( RequestWithoutTimeout { timeout: Some(timeout), }, - Range::from(timeout_arg), + timeout_arg.range(), )); } } else { checker.diagnostics.push(Diagnostic::new( RequestWithoutTimeout { timeout: None }, - Range::from(func), + func.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs index 85a2c54eef..dec37b05ff 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs @@ -7,7 +7,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::Truthiness; -use ruff_python_ast::types::Range; use ruff_python_semantic::context::Context; use crate::{ @@ -202,7 +201,7 @@ pub fn shell_injection(checker: &mut Checker, func: &Expr, args: &[Expr], keywor SubprocessPopenWithShellEqualsTrue { seems_safe: shell_call_seems_safe(arg), }, - Range::from(keyword), + keyword.range(), )); } } @@ -218,7 +217,7 @@ pub fn shell_injection(checker: &mut Checker, func: &Expr, args: &[Expr], keywor { checker.diagnostics.push(Diagnostic::new( SubprocessWithoutShellEqualsTrue, - Range::from(keyword), + keyword.range(), )); } } @@ -231,7 +230,7 @@ pub fn shell_injection(checker: &mut Checker, func: &Expr, args: &[Expr], keywor { checker.diagnostics.push(Diagnostic::new( SubprocessWithoutShellEqualsTrue, - Range::from(arg), + arg.range(), )); } } @@ -248,10 +247,9 @@ pub fn shell_injection(checker: &mut Checker, func: &Expr, args: &[Expr], keywor .rules .enabled(Rule::CallWithShellEqualsTrue) { - checker.diagnostics.push(Diagnostic::new( - CallWithShellEqualsTrue, - Range::from(keyword), - )); + checker + .diagnostics + .push(Diagnostic::new(CallWithShellEqualsTrue, keyword.range())); } } @@ -263,7 +261,7 @@ pub fn shell_injection(checker: &mut Checker, func: &Expr, args: &[Expr], keywor StartProcessWithAShell { seems_safe: shell_call_seems_safe(arg), }, - Range::from(arg), + arg.range(), )); } } @@ -278,7 +276,7 @@ pub fn shell_injection(checker: &mut Checker, func: &Expr, args: &[Expr], keywor { checker .diagnostics - .push(Diagnostic::new(StartProcessWithNoShell, Range::from(func))); + .push(Diagnostic::new(StartProcessWithNoShell, func.range())); } } @@ -292,10 +290,9 @@ pub fn shell_injection(checker: &mut Checker, func: &Expr, args: &[Expr], keywor { if let Some(value) = try_string_literal(arg) { if FULL_PATH_REGEX.find(value).is_none() { - checker.diagnostics.push(Diagnostic::new( - StartProcessWithPartialPath, - Range::from(arg), - )); + checker + .diagnostics + .push(Diagnostic::new(StartProcessWithPartialPath, arg.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs b/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs index 9525c78d0e..1a32c9620b 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -40,10 +39,9 @@ pub fn snmp_insecure_version( } = &mp_model_arg.node { if value.is_zero() || value.is_one() { - checker.diagnostics.push(Diagnostic::new( - SnmpInsecureVersion, - Range::from(mp_model_arg), - )); + checker + .diagnostics + .push(Diagnostic::new(SnmpInsecureVersion, mp_model_arg.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs b/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs index 2c34b3c364..72a586a8fb 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -38,7 +37,7 @@ pub fn snmp_weak_cryptography( if call_args.len() < 3 { checker .diagnostics - .push(Diagnostic::new(SnmpWeakCryptography, Range::from(func))); + .push(Diagnostic::new(SnmpWeakCryptography, func.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs index f6a5c35df5..2921c9569b 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -5,7 +5,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -512,7 +511,7 @@ pub fn suspicious_function_call(checker: &mut Checker, expr: &Expr) { Reason::Telnet => SuspiciousTelnetUsage.into(), Reason::FTPLib => SuspiciousFTPLibUsage.into(), }; - let diagnostic = Diagnostic::new::(diagnostic_kind, Range::from(expr)); + let diagnostic = Diagnostic::new::(diagnostic_kind, expr.range()); if checker.settings.rules.enabled(diagnostic.kind.rule()) { checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs index b71eb67093..9a48016b75 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Excepthandler, Expr, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::rules::flake8_bandit::helpers::is_untyped_exception; @@ -30,9 +29,8 @@ pub fn try_except_continue( && body[0].node == StmtKind::Continue && (check_typed_exception || is_untyped_exception(type_, checker)) { - checker.diagnostics.push(Diagnostic::new( - TryExceptContinue, - Range::from(excepthandler), - )); + checker + .diagnostics + .push(Diagnostic::new(TryExceptContinue, excepthandler.range())); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs index f888b8fb46..3700fe79d7 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Excepthandler, Expr, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::rules::flake8_bandit::helpers::is_untyped_exception; @@ -32,6 +31,6 @@ pub fn try_except_pass( { checker .diagnostics - .push(Diagnostic::new(TryExceptPass, Range::from(excepthandler))); + .push(Diagnostic::new(TryExceptPass, excepthandler.range())); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs b/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs index 9ec7019c8c..cd05f639b6 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -55,13 +54,13 @@ pub fn unsafe_yaml_load(checker: &mut Checker, func: &Expr, args: &[Expr], keywo }; checker.diagnostics.push(Diagnostic::new( UnsafeYAMLLoad { loader }, - Range::from(loader_arg), + loader_arg.range(), )); } } else { checker.diagnostics.push(Diagnostic::new( UnsafeYAMLLoad { loader: None }, - Range::from(func), + func.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_blind_except/rules.rs b/crates/ruff/src/rules/flake8_blind_except/rules.rs index 556260f424..97772b3e8a 100644 --- a/crates/ruff/src/rules/flake8_blind_except/rules.rs +++ b/crates/ruff/src/rules/flake8_blind_except/rules.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{find_keyword, is_const_true}; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::logging; use crate::checkers::ast::Checker; @@ -84,7 +83,7 @@ pub fn blind_except( BlindExcept { name: id.to_string(), }, - Range::from(type_), + type_.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules.rs index bafebf2142..bf6a788223 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules.rs @@ -4,7 +4,6 @@ use ruff_diagnostics::Violation; use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -90,9 +89,7 @@ const fn is_boolean_arg(arg: &Expr) -> bool { fn add_if_boolean(checker: &mut Checker, arg: &Expr, kind: DiagnosticKind) { if is_boolean_arg(arg) { - checker - .diagnostics - .push(Diagnostic::new(kind, Range::from(arg))); + checker.diagnostics.push(Diagnostic::new(kind, arg.range())); } } @@ -134,7 +131,7 @@ pub fn check_positional_boolean_in_def( } checker.diagnostics.push(Diagnostic::new( BooleanPositionalArgInFunctionDefinition, - Range::from(arg), + arg.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs b/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs index eed77aa051..967afa1d86 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use crate::checkers::ast::Checker; @@ -128,7 +127,7 @@ pub fn abstract_base_class( EmptyMethodWithoutAbstractDecorator { name: format!("{name}.{method_name}"), }, - Range::from(stmt), + stmt.range(), )); } } @@ -142,7 +141,7 @@ pub fn abstract_base_class( AbstractBaseClassWithoutAbstractMethod { name: name.to_string(), }, - Range::from(stmt), + stmt.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs index 849dd0773c..801a1fae98 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs @@ -1,9 +1,9 @@ -use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind}; +use ruff_text_size::TextSize; +use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::unparse_stmt; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -24,16 +24,16 @@ impl AlwaysAutofixableViolation for AssertFalse { fn assertion_error(msg: Option<&Expr>) -> Stmt { Stmt::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), StmtKind::Raise { exc: Some(Box::new(Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::Call { func: Box::new(Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::Name { id: "AssertionError".to_string(), ctx: ExprContext::Load, @@ -61,12 +61,11 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option return; }; - let mut diagnostic = Diagnostic::new(AssertFalse, Range::from(test)); + let mut diagnostic = Diagnostic::new(AssertFalse, test.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_stmt(&assertion_error(msg), checker.stylist), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index 8d6853f6ca..74ecfb14ad 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{ExprKind, Stmt, Withitem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -99,6 +98,6 @@ pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[With checker.diagnostics.push(Diagnostic::new( AssertRaisesException { kind }, - Range::from(stmt), + stmt.range(), )); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs index 93e6376621..c5aea9ca3d 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -35,5 +34,5 @@ pub fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr]) { } checker .diagnostics - .push(Diagnostic::new(AssignmentToOsEnviron, Range::from(target))); + .push(Diagnostic::new(AssignmentToOsEnviron, target.range())); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs index 0aa7451e14..12cca25355 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::scope::ScopeKind; use crate::checkers::ast::Checker; @@ -51,10 +50,9 @@ pub fn cached_instance_method(checker: &mut Checker, decorator_list: &[Expr]) { _ => decorator, }, ) { - checker.diagnostics.push(Diagnostic::new( - CachedInstanceMethod, - Range::from(decorator), - )); + checker + .diagnostics + .push(Diagnostic::new(CachedInstanceMethod, decorator.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs b/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs index e7f03a8b49..9709649cb0 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -23,5 +22,5 @@ pub fn cannot_raise_literal(checker: &mut Checker, expr: &Expr) { }; checker .diagnostics - .push(Diagnostic::new(CannotRaiseLiteral, Range::from(expr))); + .push(Diagnostic::new(CannotRaiseLiteral, expr.range())); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs index 55c054a688..3c6def1b6b 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -1,8 +1,7 @@ use itertools::Itertools; +use ruff_text_size::TextSize; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_parser::ast::{ - Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Location, -}; +use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit}; @@ -10,7 +9,6 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path; use ruff_python_ast::call_path::CallPath; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; @@ -52,8 +50,8 @@ impl AlwaysAutofixableViolation for DuplicateHandlerException { fn type_pattern(elts: Vec<&Expr>) -> Expr { Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::Tuple { elts: elts.into_iter().cloned().collect(), ctx: ExprContext::Load, @@ -95,17 +93,16 @@ fn duplicate_handler_exceptions<'a>( .sorted() .collect::>(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( if unique_elts.len() == 1 { unparse_expr(unique_elts[0], checker.stylist) } else { unparse_expr(&type_pattern(unique_elts), checker.stylist) }, - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -156,7 +153,7 @@ pub fn duplicate_exceptions(checker: &mut Checker, handlers: &[Excepthandler]) { DuplicateTryBlockException { name: name.join("."), }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs index ae4738743f..cf183851ec 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{ExcepthandlerKind, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -27,9 +26,8 @@ pub fn except_with_empty_tuple(checker: &mut Checker, excepthandler: &Excepthand return; }; if elts.is_empty() { - checker.diagnostics.push(Diagnostic::new( - ExceptWithEmptyTuple, - Range::from(excepthandler), - )); + checker + .diagnostics + .push(Diagnostic::new(ExceptWithEmptyTuple, excepthandler.range())); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs index 52c28f9b64..dbdce82222 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -55,10 +54,9 @@ pub fn except_with_non_exception_classes(checker: &mut Checker, excepthandler: & | ExprKind::Name { .. } | ExprKind::Call { .. }, ) { - checker.diagnostics.push(Diagnostic::new( - ExceptWithNonExceptionClasses, - Range::from(expr), - )); + checker + .diagnostics + .push(Diagnostic::new(ExceptWithNonExceptionClasses, expr.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index f9a39150cf..d255bcf9d0 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -1,3 +1,4 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Arguments, Constant, Expr, ExprKind}; use ruff_diagnostics::Violation; @@ -5,7 +6,6 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::from_qualified_name; use ruff_python_ast::call_path::{compose_call_path, CallPath}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -61,7 +61,7 @@ fn is_immutable_func(checker: &Checker, func: &Expr, extend_immutable_calls: &[C struct ArgumentDefaultVisitor<'a> { checker: &'a Checker<'a>, - diagnostics: Vec<(DiagnosticKind, Range)>, + diagnostics: Vec<(DiagnosticKind, TextRange)>, extend_immutable_calls: Vec>, } @@ -81,7 +81,7 @@ where name: compose_call_path(func), } .into(), - Range::from(expr), + expr.range(), )); } visitor::walk_expr(self, expr); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index d111909e0a..485bef03dc 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -1,10 +1,11 @@ +use ruff_text_size::TextRange; use rustc_hash::FxHashSet; use rustpython_parser::ast::{Comprehension, Expr, ExprContext, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::collect_arg_names; -use ruff_python_ast::types::{Node, Range}; +use ruff_python_ast::types::Node; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -26,9 +27,9 @@ impl Violation for FunctionUsesLoopVariable { #[derive(Default)] struct LoadedNamesVisitor<'a> { // Tuple of: name, defining expression, and defining range. - loaded: Vec<(&'a str, &'a Expr, Range)>, + loaded: Vec<(&'a str, &'a Expr, TextRange)>, // Tuple of: name, defining expression, and defining range. - stored: Vec<(&'a str, &'a Expr, Range)>, + stored: Vec<(&'a str, &'a Expr, TextRange)>, } /// `Visitor` to collect all used identifiers in a statement. @@ -39,8 +40,8 @@ where fn visit_expr(&mut self, expr: &'b Expr) { match &expr.node { ExprKind::Name { id, ctx } => match ctx { - ExprContext::Load => self.loaded.push((id, expr, Range::from(expr))), - ExprContext::Store => self.stored.push((id, expr, Range::from(expr))), + ExprContext::Load => self.loaded.push((id, expr, expr.range())), + ExprContext::Store => self.stored.push((id, expr, expr.range())), ExprContext::Del => {} }, _ => visitor::walk_expr(self, expr), @@ -50,7 +51,7 @@ where #[derive(Default)] struct SuspiciousVariablesVisitor<'a> { - names: Vec<(&'a str, &'a Expr, Range)>, + names: Vec<(&'a str, &'a Expr, TextRange)>, safe_functions: Vec<&'a Expr>, } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs b/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs index a4fc2f1221..22019ae69f 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs @@ -1,9 +1,9 @@ -use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Location}; +use ruff_text_size::TextSize; +use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use crate::checkers::ast::Checker; @@ -27,8 +27,8 @@ impl AlwaysAutofixableViolation for GetAttrWithConstant { } fn attribute(value: &Expr, attr: &str) -> Expr { Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::Attribute { value: Box::new(value.clone()), attr: attr.to_string(), @@ -61,13 +61,12 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar return; } - let mut diagnostic = Diagnostic::new(GetAttrWithConstant, Range::from(expr)); + let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&attribute(obj, value), checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs b/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs index 836563599a..1ce2d401cd 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -33,7 +32,7 @@ fn walk_stmt(checker: &mut Checker, body: &[Stmt], f: fn(&Stmt) -> bool) { ), }, }, - Range::from(stmt), + stmt.range(), )); } match &stmt.node { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs index 784fe1a488..493b1afb83 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -74,7 +73,7 @@ pub fn loop_variable_overrides_iterator(checker: &mut Checker, target: &Expr, it LoopVariableOverridesIterator { name: name.to_string(), }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index 0f6fa75fac..600468c804 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Arguments, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::typing::is_immutable_annotation; use crate::checkers::ast::Checker; @@ -74,10 +73,9 @@ pub fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) { .as_ref() .map_or(false, |expr| is_immutable_annotation(&checker.ctx, expr)) { - checker.diagnostics.push(Diagnostic::new( - MutableArgumentDefault, - Range::from(default), - )); + checker + .diagnostics + .push(Diagnostic::new(MutableArgumentDefault, default.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index b93961fed8..266bb7ef45 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -64,5 +63,5 @@ pub fn no_explicit_stacklevel( checker .diagnostics - .push(Diagnostic::new(NoExplicitStacklevel, Range::from(func))); + .push(Diagnostic::new(NoExplicitStacklevel, func.range())); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs b/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs index 2ae2556e75..21412249a5 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -45,13 +44,12 @@ pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[E RedundantTupleInExceptionHandler { name: unparse_expr(elt, checker.stylist), }, - Range::from(type_), + type_.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(elt, checker.stylist), - type_.location, - type_.end_location.unwrap(), + type_.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index 2b3c888d88..3e53f7d205 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Comprehension, Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor::{self, Visitor}; use crate::checkers::ast::Checker; @@ -339,6 +338,6 @@ pub fn reuse_of_groupby_generator( for expr in finder.exprs { checker .diagnostics - .push(Diagnostic::new(ReuseOfGroupbyGenerator, Range::from(expr))); + .push(Diagnostic::new(ReuseOfGroupbyGenerator, expr.range())); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs b/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs index 76a0f6fef3..705c1e03a3 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs @@ -1,10 +1,10 @@ -use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind}; +use ruff_text_size::TextSize; +use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::unparse_stmt; use ruff_python_ast::source_code::Stylist; -use ruff_python_ast::types::Range; use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use crate::checkers::ast::Checker; @@ -29,12 +29,12 @@ impl AlwaysAutofixableViolation for SetAttrWithConstant { fn assignment(obj: &Expr, name: &str, value: &Expr, stylist: &Stylist) -> String { let stmt = Stmt::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), StmtKind::Assign { targets: vec![Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::Attribute { value: Box::new(obj.clone()), attr: name.to_string(), @@ -76,13 +76,12 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar // (i.e., it's directly within an `StmtKind::Expr`). if let StmtKind::Expr { value: child } = &checker.ctx.current_stmt().node { if expr == child.as_ref() { - let mut diagnostic = Diagnostic::new(SetAttrWithConstant, Range::from(expr)); + let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( assignment(obj, name, value, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs b/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs index 94f0768cca..e4cc0733b7 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs @@ -11,7 +11,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -38,12 +37,12 @@ pub fn star_arg_unpacking_after_keyword_arg( let ExprKind::Starred { .. } = arg.node else { continue; }; - if arg.location <= keyword.location { + if arg.start() <= keyword.start() { continue; } checker.diagnostics.push(Diagnostic::new( StarArgUnpackingAfterKeywordArg, - Range::from(arg), + arg.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs b/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs index 24cc14d2d9..551ca2eea2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -40,6 +39,6 @@ pub fn strip_with_multi_characters(checker: &mut Checker, expr: &Expr, func: &Ex if num_chars > 1 && num_chars != value.chars().unique().count() { checker .diagnostics - .push(Diagnostic::new(StripWithMultiCharacters, Range::from(expr))); + .push(Diagnostic::new(StripWithMultiCharacters, expr.range())); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs index 999f4893c1..066b7cf677 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs @@ -21,7 +21,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Unaryop}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -48,5 +47,5 @@ pub fn unary_prefix_increment(checker: &mut Checker, expr: &Expr, op: &Unaryop, } checker .diagnostics - .push(Diagnostic::new(UnaryPrefixIncrement, Range::from(expr))); + .push(Diagnostic::new(UnaryPrefixIncrement, expr.range())); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs index 4e6475b5c0..d8b5b8793c 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -47,19 +46,17 @@ pub fn unintentional_type_annotation( match &target.node { ExprKind::Subscript { value, .. } => { if matches!(&value.node, ExprKind::Name { .. }) { - checker.diagnostics.push(Diagnostic::new( - UnintentionalTypeAnnotation, - Range::from(stmt), - )); + checker + .diagnostics + .push(Diagnostic::new(UnintentionalTypeAnnotation, stmt.range())); } } ExprKind::Attribute { value, .. } => { if let ExprKind::Name { id, .. } = &value.node { if id != "self" { - checker.diagnostics.push(Diagnostic::new( - UnintentionalTypeAnnotation, - Range::from(stmt), - )); + checker + .diagnostics + .push(Diagnostic::new(UnintentionalTypeAnnotation, stmt.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs index daeb582ff9..570b2f1a5a 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -42,5 +41,5 @@ pub fn unreliable_callable_check(checker: &mut Checker, expr: &Expr, func: &Expr } checker .diagnostics - .push(Diagnostic::new(UnreliableCallableCheck, Range::from(expr))); + .push(Diagnostic::new(UnreliableCallableCheck, expr.range())); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs index bf74cc096c..7e53161e5d 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs @@ -24,7 +24,7 @@ use serde::{Deserialize, Serialize}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::{Range, RefEquality}; +use ruff_python_ast::types::RefEquality; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{helpers, visitor}; @@ -160,7 +160,7 @@ pub fn unused_loop_control_variable( rename: rename.clone(), certainty, }, - Range::from(expr), + expr.range(), ); if let Some(rename) = rename { if certainty.into() && checker.patch(diagnostic.kind.rule()) { @@ -176,11 +176,7 @@ pub fn unused_loop_control_variable( if let Some(binding) = binding { if binding.kind.is_loop_var() { if !binding.used() { - diagnostic.set_fix(Edit::replacement( - rename, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(rename, expr.range())); } } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs index 18ca6cbd4c..4d4dc2fe63 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -24,6 +23,6 @@ pub fn useless_comparison(checker: &mut Checker, expr: &Expr) { if matches!(expr.node, ExprKind::Compare { .. }) { checker .diagnostics - .push(Diagnostic::new(UselessComparison, Range::from(expr))); + .push(Diagnostic::new(UselessComparison, expr.range())); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index 20731a61bd..7b618caa9a 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -1,10 +1,8 @@ use rustpython_parser::ast::Expr; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - -use crate::checkers::ast::Checker; #[violation] pub struct UselessContextlibSuppress; @@ -29,9 +27,8 @@ pub fn useless_contextlib_suppress(checker: &mut Checker, expr: &Expr, func: &Ex call_path.as_slice() == ["contextlib", "suppress"] }) { - checker.diagnostics.push(Diagnostic::new( - UselessContextlibSuppress, - Range::from(expr), - )); + checker + .diagnostics + .push(Diagnostic::new(UselessContextlibSuppress, expr.range())); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs index 45ce25a4ec..3ff2acaec7 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::contains_effect; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -62,7 +61,7 @@ pub fn useless_expression(checker: &mut Checker, value: &Expr) { UselessExpression { kind: Kind::Attribute, }, - Range::from(value), + value.range(), )); } return; @@ -72,6 +71,6 @@ pub fn useless_expression(checker: &mut Checker, value: &Expr) { UselessExpression { kind: Kind::Expression, }, - Range::from(value), + value.range(), )); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index d2e565ee65..ec3c3db340 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -1,10 +1,8 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - -use crate::checkers::ast::Checker; #[violation] pub struct ZipWithoutExplicitStrict; @@ -36,7 +34,7 @@ pub fn zip_without_explicit_strict( { checker .diagnostics - .push(Diagnostic::new(ZipWithoutExplicitStrict, Range::from(expr))); + .push(Diagnostic::new(ZipWithoutExplicitStrict, expr.range())); } } } diff --git a/crates/ruff/src/rules/flake8_builtins/rules.rs b/crates/ruff/src/rules/flake8_builtins/rules.rs index 0899166b96..9c87506e80 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::Located; use ruff_diagnostics::Violation; use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_stdlib::builtins::BUILTINS; use super::types::ShadowingType; @@ -191,7 +190,7 @@ pub fn builtin_shadowing( } .into(), }, - Range::from(located), + located.range(), )) } else { None diff --git a/crates/ruff/src/rules/flake8_commas/rules.rs b/crates/ruff/src/rules/flake8_commas/rules.rs index d541768a33..3f0ea6246d 100644 --- a/crates/ruff/src/rules/flake8_commas/rules.rs +++ b/crates/ruff/src/rules/flake8_commas/rules.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use ruff_text_size::TextRange; use rustpython_parser::lexer::{LexResult, Spanned}; use rustpython_parser::Tok; @@ -6,7 +7,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::registry::Rule; use crate::settings::{flags, Settings}; @@ -46,7 +46,7 @@ impl<'tok> Token<'tok> { } const fn from_spanned(spanned: &'tok Spanned) -> Token<'tok> { - let type_ = match &spanned.1 { + let type_ = match &spanned.0 { Tok::NonLogicalNewline => TokenType::NonLogicalNewline, Tok::Newline => TokenType::Newline, Tok::For => TokenType::For, @@ -161,7 +161,7 @@ pub fn trailing_commas( .iter() .flatten() // Completely ignore comments -- they just interfere with the logic. - .filter(|&r| !matches!(r, (_, Tok::Comment(_), _))) + .filter(|&r| !matches!(r, (Tok::Comment(_), _))) .map(Token::from_spanned); let tokens = [Token::irrelevant(), Token::irrelevant()] .into_iter() @@ -253,15 +253,9 @@ pub fn trailing_commas( }; if comma_prohibited { let comma = prev.spanned.unwrap(); - let mut diagnostic = Diagnostic::new( - ProhibitedTrailingComma, - Range { - location: comma.0, - end_location: comma.2, - }, - ); + let mut diagnostic = Diagnostic::new(ProhibitedTrailingComma, comma.1); if autofix.into() && settings.rules.should_fix(Rule::ProhibitedTrailingComma) { - diagnostic.set_fix(Edit::deletion(comma.0, comma.2)); + diagnostic.set_fix(Edit::range_deletion(diagnostic.range())); } diagnostics.push(diagnostic); } @@ -272,13 +266,7 @@ pub fn trailing_commas( prev.type_ == TokenType::Comma && token.type_ == TokenType::Newline; if bare_comma_prohibited { let comma = prev.spanned.unwrap(); - diagnostics.push(Diagnostic::new( - TrailingCommaOnBareTuple, - Range { - location: comma.0, - end_location: comma.2, - }, - )); + diagnostics.push(Diagnostic::new(TrailingCommaOnBareTuple, comma.1)); } // Comma is required if: @@ -299,21 +287,17 @@ pub fn trailing_commas( let missing_comma = prev_prev.spanned.unwrap(); let mut diagnostic = Diagnostic::new( MissingTrailingComma, - Range { - location: missing_comma.2, - end_location: missing_comma.2, - }, + TextRange::empty(missing_comma.1.end()), ); if autofix.into() && settings.rules.should_fix(Rule::MissingTrailingComma) { // Create a replacement that includes the final bracket (or other token), // rather than just inserting a comma at the end. This prevents the UP034 autofix // removing any brackets in the same linter pass - doing both at the same time could // lead to a syntax error. - let contents = locator.slice(Range::new(missing_comma.0, missing_comma.2)); - diagnostic.set_fix(Edit::replacement( + let contents = locator.slice(missing_comma.1); + diagnostic.set_fix(Edit::range_replacement( format!("{contents},"), - missing_comma.0, - missing_comma.2, + missing_comma.1, )); } diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index ad99e3484b..57a0a78ee9 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -35,7 +35,7 @@ pub fn fix_unnecessary_generator_list( expr: &rustpython_parser::ast::Expr, ) -> Result { // Expr(Call(GeneratorExp)))) -> Expr(ListComp))) - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -67,11 +67,7 @@ pub fn fix_unnecessary_generator_list( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C401) Convert `set(x for x in y)` to `{x for x in y}`. @@ -82,7 +78,7 @@ pub fn fix_unnecessary_generator_set( parent: Option<&rustpython_parser::ast::Expr>, ) -> Result { // Expr(Call(GeneratorExp)))) -> Expr(SetComp))) - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -124,11 +120,7 @@ pub fn fix_unnecessary_generator_set( } } - Ok(Edit::replacement( - content, - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(content, expr.range())) } /// (C402) Convert `dict((x, x) for x in range(3))` to `{x: x for x in @@ -139,7 +131,7 @@ pub fn fix_unnecessary_generator_dict( expr: &rustpython_parser::ast::Expr, parent: Option<&rustpython_parser::ast::Expr>, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -198,11 +190,7 @@ pub fn fix_unnecessary_generator_dict( } } - Ok(Edit::replacement( - content, - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(content, expr.range())) } /// (C403) Convert `set([x for x in y])` to `{x for x in y}`. @@ -213,7 +201,7 @@ pub fn fix_unnecessary_list_comprehension_set( ) -> Result { // Expr(Call(ListComp)))) -> // Expr(SetComp))) - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -243,11 +231,7 @@ pub fn fix_unnecessary_list_comprehension_set( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C404) Convert `dict([(i, i) for i in range(3)])` to `{i: i for i in @@ -257,7 +241,7 @@ pub fn fix_unnecessary_list_comprehension_dict( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -299,11 +283,7 @@ pub fn fix_unnecessary_list_comprehension_dict( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// Drop a trailing comma from a list of tuple elements. @@ -356,7 +336,7 @@ pub fn fix_unnecessary_literal_set( expr: &rustpython_parser::ast::Expr, ) -> Result { // Expr(Call(List|Tuple)))) -> Expr(Set))) - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let mut call = match_call(body)?; @@ -393,11 +373,7 @@ pub fn fix_unnecessary_literal_set( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C406) Convert `dict([(1, 2)])` to `{1: 2}`. @@ -407,7 +383,7 @@ pub fn fix_unnecessary_literal_dict( expr: &rustpython_parser::ast::Expr, ) -> Result { // Expr(Call(List|Tuple)))) -> Expr(Dict))) - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -466,11 +442,7 @@ pub fn fix_unnecessary_literal_dict( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C408) @@ -480,7 +452,7 @@ pub fn fix_unnecessary_collection_call( expr: &rustpython_parser::ast::Expr, ) -> Result { // Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict) - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -582,11 +554,7 @@ pub fn fix_unnecessary_collection_call( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C409) Convert `tuple([1, 2])` to `tuple(1, 2)` @@ -595,7 +563,7 @@ pub fn fix_unnecessary_literal_within_tuple_call( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -641,11 +609,7 @@ pub fn fix_unnecessary_literal_within_tuple_call( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C410) Convert `list([1, 2])` to `[1, 2]` @@ -654,7 +618,7 @@ pub fn fix_unnecessary_literal_within_list_call( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -702,11 +666,7 @@ pub fn fix_unnecessary_literal_within_list_call( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C411) Convert `list([i * i for i in x])` to `[i * i for i in x]`. @@ -716,7 +676,7 @@ pub fn fix_unnecessary_list_call( expr: &rustpython_parser::ast::Expr, ) -> Result { // Expr(Call(List|Tuple)))) -> Expr(List|Tuple))) - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -731,11 +691,7 @@ pub fn fix_unnecessary_list_call( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C413) Convert `list(sorted([2, 3, 1]))` to `sorted([2, 3, 1])`. @@ -746,7 +702,7 @@ pub fn fix_unnecessary_call_around_sorted( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let outer_call = match_call(body)?; @@ -860,11 +816,7 @@ pub fn fix_unnecessary_call_around_sorted( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C414) Convert `sorted(list(foo))` to `sorted(foo)` @@ -873,7 +825,7 @@ pub fn fix_unnecessary_double_cast_or_process( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let body = match_expr(&mut tree)?; let mut outer_call = match_call(body)?; @@ -901,11 +853,7 @@ pub fn fix_unnecessary_double_cast_or_process( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C416) Convert `[i for i in x]` to `list(x)`. @@ -914,7 +862,7 @@ pub fn fix_unnecessary_comprehension( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; @@ -997,11 +945,7 @@ pub fn fix_unnecessary_comprehension( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C417) Convert `map(lambda x: x * 2, bar)` to `(x * 2 for x in bar)`. @@ -1012,7 +956,7 @@ pub fn fix_unnecessary_map( parent: Option<&rustpython_parser::ast::Expr>, kind: &str, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -1164,11 +1108,7 @@ pub fn fix_unnecessary_map( } } - Ok(Edit::replacement( - content, - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(content, expr.range())) } else { bail!("Should have two arguments"); } @@ -1180,7 +1120,7 @@ pub fn fix_unnecessary_literal_within_dict_call( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let mut body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -1195,11 +1135,7 @@ pub fn fix_unnecessary_literal_within_dict_call( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } /// (C419) Convert `[i for i in a]` into `i for i in a` @@ -1209,7 +1145,7 @@ pub fn fix_unnecessary_comprehension_any_all( expr: &rustpython_parser::ast::Expr, ) -> Result { // Expr(ListComp) -> Expr(GeneratorExp) - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut tree = match_module(module_text)?; let body = match_expr(&mut tree)?; let call = match_call(body)?; @@ -1239,9 +1175,5 @@ pub fn fix_unnecessary_comprehension_any_all( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - expr.location, - expr.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), expr.range())) } diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs index 67aed7883a..784d149587 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs @@ -1,12 +1,10 @@ use rustpython_parser::ast::{Expr, ExprKind}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_comprehensions::fixes; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_macros::{derive_message_formats, violation}; use super::helpers; @@ -83,7 +81,7 @@ pub fn unnecessary_call_around_sorted( UnnecessaryCallAroundSorted { func: outer.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs index ee97babdd3..b1fe2feffd 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, Keyword}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -87,7 +86,7 @@ pub fn unnecessary_collection_call( UnnecessaryCollectionCall { obj_type: id.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs index 96171fafea..07b78e548c 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Comprehension, Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -64,7 +63,7 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) { UnnecessaryComprehension { obj_type: id.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs index 92856ca2d6..643e409205 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs @@ -4,7 +4,6 @@ use ruff_diagnostics::AlwaysAutofixableViolation; use ruff_diagnostics::Diagnostic; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::any_over_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -78,7 +77,7 @@ pub fn unnecessary_comprehension_any_all( if !checker.ctx.is_builtin(id) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, Range::from(&args[0])); + let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, args[0].range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_comprehension_any_all(checker.locator, checker.stylist, expr) 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 36f8a1b6be..5ff56baa22 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 @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -108,7 +107,7 @@ pub fn unnecessary_double_cast_or_process( inner: inner.to_string(), outer: outer.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs index a3e5cf3810..9cc603a25c 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -57,7 +56,7 @@ pub fn unnecessary_generator_dict( if let ExprKind::GeneratorExp { elt, .. } = argument { match &elt.node { ExprKind::Tuple { elts, .. } if elts.len() == 2 => { - let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, Range::from(expr)); + let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_generator_dict( diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs index 63b1aa6b20..d6842674c0 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -57,7 +56,7 @@ pub fn unnecessary_generator_list( return; } if let ExprKind::GeneratorExp { .. } = argument { - let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, Range::from(expr)); + let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_generator_list(checker.locator, checker.stylist, expr) diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs index 0978042f4a..8d821a7368 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -58,7 +57,7 @@ pub fn unnecessary_generator_set( return; } if let ExprKind::GeneratorExp { .. } = argument { - let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, Range::from(expr)); + let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_generator_set(checker.locator, checker.stylist, expr, parent) diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs index d0b2fac8f0..cf1730d240 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs @@ -1,12 +1,10 @@ use rustpython_parser::ast::{Expr, ExprKind}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_comprehensions::fixes; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_macros::{derive_message_formats, violation}; use super::helpers; @@ -50,7 +48,7 @@ pub fn unnecessary_list_call(checker: &mut Checker, expr: &Expr, func: &Expr, ar if !matches!(argument, ExprKind::ListComp { .. }) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryListCall, Range::from(expr)); + let mut diagnostic = Diagnostic::new(UnnecessaryListCall, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_list_call(checker.locator, checker.stylist, expr) diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs index 2e334fcb09..888e99c327 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs @@ -1,12 +1,10 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_comprehensions::fixes; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_macros::{derive_message_formats, violation}; use super::helpers; @@ -63,7 +61,7 @@ pub fn unnecessary_list_comprehension_dict( if elts.len() != 2 { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionDict, Range::from(expr)); + let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionDict, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_list_comprehension_dict(checker.locator, checker.stylist, expr) diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs index 9e362915de..61d6739b0d 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -55,7 +54,7 @@ pub fn unnecessary_list_comprehension_set( return; } if let ExprKind::ListComp { .. } = &argument { - let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, Range::from(expr)); + let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_list_comprehension_set( diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs index c3f3fb4497..45d6854efd 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs @@ -1,12 +1,10 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_comprehensions::fixes; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_macros::{derive_message_formats, violation}; use super::helpers; @@ -77,7 +75,7 @@ pub fn unnecessary_literal_dict( UnnecessaryLiteralDict { obj_type: kind.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs index a94483d26c..129c26a94e 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs @@ -1,12 +1,10 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_comprehensions::fixes; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_macros::{derive_message_formats, violation}; use super::helpers; @@ -71,7 +69,7 @@ pub fn unnecessary_literal_set( UnnecessaryLiteralSet { obj_type: kind.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs index be047d5d27..e4f5520243 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs @@ -3,7 +3,6 @@ use std::fmt; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -88,7 +87,7 @@ pub fn unnecessary_literal_within_dict_call( UnnecessaryLiteralWithinDictCall { kind: argument_kind, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs index 9cb6e2601a..02f8a51052 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs @@ -1,12 +1,10 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_comprehensions::fixes; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_macros::{derive_message_formats, violation}; use super::helpers; @@ -92,7 +90,7 @@ pub fn unnecessary_literal_within_list_call( UnnecessaryLiteralWithinListCall { literal: argument_kind.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index ff39c76eb1..66fe07888e 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -1,12 +1,10 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_comprehensions::fixes; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_macros::{derive_message_formats, violation}; use super::helpers; @@ -93,7 +91,7 @@ pub fn unnecessary_literal_within_tuple_call( UnnecessaryLiteralWithinTupleCall { literal: argument_kind.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs index db5e56450e..c81adc085e 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -1,9 +1,9 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::Diagnostic; use ruff_diagnostics::{AutofixKind, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -75,7 +75,7 @@ pub fn unnecessary_map( func: &Expr, args: &[Expr], ) { - fn create_diagnostic(kind: &str, location: Range) -> Diagnostic { + fn create_diagnostic(kind: &str, location: TextRange) -> Diagnostic { Diagnostic::new( UnnecessaryMap { obj_type: kind.to_string(), @@ -105,7 +105,7 @@ pub fn unnecessary_map( }; if args.len() == 2 && matches!(&args[0].node, ExprKind::Lambda { .. }) { - let mut diagnostic = create_diagnostic("generator", Range::from(expr)); + let mut diagnostic = create_diagnostic("generator", expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_map( @@ -134,7 +134,7 @@ pub fn unnecessary_map( return; }; if let ExprKind::Lambda { .. } = argument { - let mut diagnostic = create_diagnostic(id, Range::from(expr)); + let mut diagnostic = create_diagnostic(id, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_map( @@ -164,7 +164,7 @@ pub fn unnecessary_map( if let ExprKind::Lambda { body, .. } = &argument { if matches!(&body.node, ExprKind::Tuple { elts, .. } | ExprKind::List { elts, .. } if elts.len() == 2) { - let mut diagnostic = create_diagnostic(id, Range::from(expr)); + let mut diagnostic = create_diagnostic(id, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { fixes::fix_unnecessary_map( diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs index 10a7c43b86..26a66120dd 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs @@ -1,11 +1,9 @@ use num_bigint::BigInt; use rustpython_parser::ast::{Constant, Expr, ExprKind, Unaryop}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - -use crate::checkers::ast::Checker; use super::helpers; @@ -93,6 +91,6 @@ pub fn unnecessary_subscript_reversal( UnnecessarySubscriptReversal { func: id.to_string(), }, - Range::from(expr), + expr.range(), )); } diff --git a/crates/ruff/src/rules/flake8_datetimez/rules.rs b/crates/ruff/src/rules/flake8_datetimez/rules.rs index 0da562732d..7cdca81647 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules.rs @@ -1,9 +1,9 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -122,7 +122,7 @@ pub fn call_datetime_without_tzinfo( func: &Expr, args: &[Expr], keywords: &[Keyword], - location: Range, + location: TextRange, ) { if !checker .ctx @@ -156,7 +156,7 @@ pub fn call_datetime_without_tzinfo( /// /// It uses the system local timezone. /// Use `datetime.datetime.now(tz=)` instead. -pub fn call_datetime_today(checker: &mut Checker, func: &Expr, location: Range) { +pub fn call_datetime_today(checker: &mut Checker, func: &Expr, location: TextRange) { if checker .ctx .resolve_call_path(func) @@ -178,7 +178,7 @@ pub fn call_datetime_today(checker: &mut Checker, func: &Expr, location: Range) /// local times, it is preferred to use aware datetimes to represent times in /// UTC. As such, the recommended way to create an object representing the /// current time in UTC is by calling `datetime.now(timezone.utc)`. -pub fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: Range) { +pub fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) { if checker .ctx .resolve_call_path(func) @@ -201,7 +201,7 @@ pub fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: Range) /// UTC. As such, the recommended way to create an object representing a /// specific timestamp in UTC is by calling `datetime.fromtimestamp(timestamp, /// tz=timezone.utc)`. -pub fn call_datetime_utcfromtimestamp(checker: &mut Checker, func: &Expr, location: Range) { +pub fn call_datetime_utcfromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) { if checker .ctx .resolve_call_path(func) @@ -221,7 +221,7 @@ pub fn call_datetime_now_without_tzinfo( func: &Expr, args: &[Expr], keywords: &[Keyword], - location: Range, + location: TextRange, ) { if !checker .ctx @@ -263,7 +263,7 @@ pub fn call_datetime_fromtimestamp( func: &Expr, args: &[Expr], keywords: &[Keyword], - location: Range, + location: TextRange, ) { if !checker .ctx @@ -304,7 +304,7 @@ pub fn call_datetime_strptime_without_zone( checker: &mut Checker, func: &Expr, args: &[Expr], - location: Range, + location: TextRange, ) { if !checker .ctx @@ -362,7 +362,7 @@ pub fn call_datetime_strptime_without_zone( /// /// It uses the system local timezone. /// Use `datetime.datetime.now(tz=).date()` instead. -pub fn call_date_today(checker: &mut Checker, func: &Expr, location: Range) { +pub fn call_date_today(checker: &mut Checker, func: &Expr, location: TextRange) { if checker .ctx .resolve_call_path(func) @@ -382,7 +382,7 @@ pub fn call_date_today(checker: &mut Checker, func: &Expr, location: Range) { /// /// It uses the system local timezone. /// Use `datetime.datetime.fromtimestamp(, tz=).date()` instead. -pub fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: Range) { +pub fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) { if checker .ctx .resolve_call_path(func) diff --git a/crates/ruff/src/rules/flake8_debugger/rules.rs b/crates/ruff/src/rules/flake8_debugger/rules.rs index 00abd923cb..806c8a5d16 100644 --- a/crates/ruff/src/rules/flake8_debugger/rules.rs +++ b/crates/ruff/src/rules/flake8_debugger/rules.rs @@ -1,12 +1,10 @@ use rustpython_parser::ast::{Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::{format_call_path, from_unqualified_name, CallPath}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::rules::flake8_debugger::types::DebuggerUsingType; +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::{format_call_path, from_unqualified_name, CallPath}; #[violation] pub struct Debugger { @@ -53,7 +51,7 @@ pub fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) { Debugger { using_type: DebuggerUsingType::Call(format_call_path(target)), }, - Range::from(expr), + expr.range(), )); } } @@ -77,7 +75,7 @@ pub fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option< Debugger { using_type: DebuggerUsingType::Import(format_call_path(&call_path)), }, - Range::from(stmt), + stmt.range(), )); } } else { @@ -90,7 +88,7 @@ pub fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option< Debugger { using_type: DebuggerUsingType::Import(name.to_string()), }, - Range::from(stmt), + stmt.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs b/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs index 11d0b87422..8fbff6667a 100644 --- a/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs +++ b/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::rules::flake8_django::rules::helpers::is_model_form; @@ -76,18 +75,12 @@ pub fn all_with_model_form(checker: &Checker, bases: &[Expr], body: &[Stmt]) -> match &value { Constant::Str(s) => { if s == "__all__" { - return Some(Diagnostic::new( - DjangoAllWithModelForm, - Range::from(element), - )); + return Some(Diagnostic::new(DjangoAllWithModelForm, element.range())); } } Constant::Bytes(b) => { if b == "__all__".as_bytes() { - return Some(Diagnostic::new( - DjangoAllWithModelForm, - Range::from(element), - )); + return Some(Diagnostic::new(DjangoAllWithModelForm, element.range())); } } _ => (), diff --git a/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs b/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs index 731cbcc698..f80e3d1dfd 100644 --- a/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs +++ b/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::rules::flake8_django::rules::helpers::is_model_form; @@ -68,10 +67,7 @@ pub fn exclude_with_model_form( continue; }; if id == "exclude" { - return Some(Diagnostic::new( - DjangoExcludeWithModelForm, - Range::from(target), - )); + return Some(Diagnostic::new(DjangoExcludeWithModelForm, target.range())); } } } diff --git a/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs b/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs index ea44e83220..cbbf2745c2 100644 --- a/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs +++ b/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -80,7 +79,7 @@ pub fn locals_in_render_function( checker.diagnostics.push(Diagnostic::new( DjangoLocalsInRenderFunction, - Range::from(locals), + locals.range(), )); } diff --git a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs index 770347a28f..6a321ef50a 100644 --- a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs +++ b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{ExprKind, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -63,7 +62,7 @@ pub fn model_without_dunder_str( if !has_dunder_method(body) { return Some(Diagnostic::new( DjangoModelWithoutDunderStr, - Range::from(class_location), + class_location.range(), )); } None diff --git a/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index 813b9ac4b5..189a5ccabc 100644 --- a/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs +++ b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::CallPath; -use ruff_python_ast::types::Range; /// ## What it does /// Checks that Django's `@receiver` decorator is listed first, prior to @@ -68,7 +67,7 @@ where if i > 0 && is_receiver && !seen_receiver { diagnostics.push(Diagnostic::new( DjangoNonLeadingReceiverDecorator, - Range::from(decorator), + decorator.range(), )); } if !is_receiver && seen_receiver { diff --git a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs index fe90e0bbdc..5a218dacd9 100644 --- a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -73,7 +72,7 @@ pub fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> Vec checker.settings.flake8_errmsg.max_string_length { checker .diagnostics - .push(Diagnostic::new(RawStringInException, Range::from(first))); + .push(Diagnostic::new(RawStringInException, first.range())); } } } @@ -175,7 +174,7 @@ pub fn string_in_exception(checker: &mut Checker, exc: &Expr) { if checker.settings.rules.enabled(Rule::FStringInException) { checker .diagnostics - .push(Diagnostic::new(FStringInException, Range::from(first))); + .push(Diagnostic::new(FStringInException, first.range())); } } // Check for .format() calls @@ -183,10 +182,9 @@ pub fn string_in_exception(checker: &mut Checker, exc: &Expr) { if checker.settings.rules.enabled(Rule::DotFormatInException) { if let ExprKind::Attribute { value, attr, .. } = &func.node { if attr == "format" && matches!(value.node, ExprKind::Constant { .. }) { - checker.diagnostics.push(Diagnostic::new( - DotFormatInException, - Range::from(first), - )); + checker + .diagnostics + .push(Diagnostic::new(DotFormatInException, first.range())); } } } diff --git a/crates/ruff/src/rules/flake8_executable/helpers.rs b/crates/ruff/src/rules/flake8_executable/helpers.rs index 9a54795da9..6dcf5f1a79 100644 --- a/crates/ruff/src/rules/flake8_executable/helpers.rs +++ b/crates/ruff/src/rules/flake8_executable/helpers.rs @@ -7,15 +7,16 @@ use std::path::Path; use anyhow::Result; use once_cell::sync::Lazy; use regex::Regex; +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 - Match(usize, usize, usize, &'a str), + // whitespace length, start of the shebang, contents + Match(TextSize, TextSize, &'a str), } pub fn extract_shebang(line: &str) -> ShebangDirective { @@ -27,9 +28,8 @@ pub fn extract_shebang(line: &str) -> ShebangDirective { Some(caps) => match caps.name("spaces") { Some(spaces) => match caps.name("directive") { Some(matches) => ShebangDirective::Match( - spaces.as_str().chars().count(), - matches.start(), - matches.end(), + spaces.as_str().text_len(), + TextSize::try_from(matches.start()).unwrap(), matches.as_str(), ), None => ShebangDirective::None, @@ -55,6 +55,8 @@ mod tests { extract_shebang, ShebangDirective, SHEBANG_REGEX, }; + use ruff_text_size::TextSize; + #[test] fn shebang_regex() { // Positive cases @@ -69,21 +71,18 @@ 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(0, 2, 21, "/usr/bin/env python") - )); - assert!(matches!( + ShebangDirective::Match(TextSize::from(0), TextSize::from(2), "/usr/bin/env python") + ); + assert_eq!( extract_shebang(" #!/usr/bin/env python"), - ShebangDirective::Match(2, 4, 23, "/usr/bin/env python") - )); - assert!(matches!( + ShebangDirective::Match(TextSize::from(2), TextSize::from(4), "/usr/bin/env python") + ); + assert_eq!( extract_shebang("print('test') #!/usr/bin/python"), ShebangDirective::None - )); + ); } } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs index 40df587276..dc3c02d2e4 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs @@ -1,10 +1,10 @@ #![allow(unused_imports)] +use ruff_text_size::TextRange; use std::path::Path; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::registry::AsRule; #[cfg(target_family = "unix")] @@ -24,7 +24,7 @@ impl Violation for ShebangMissingExecutableFile { #[cfg(target_family = "unix")] pub fn shebang_missing(filepath: &Path) -> Option { if let Ok(true) = is_executable(filepath) { - let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, Range::default()); + let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, TextRange::default()); return Some(diagnostic); } None diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs index 10b502d509..ad5e2cc1d3 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs @@ -1,8 +1,7 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::rules::flake8_executable::helpers::ShebangDirective; @@ -17,19 +16,20 @@ impl Violation for ShebangNotFirstLine { } /// EXE005 -pub fn shebang_newline(lineno: usize, shebang: &ShebangDirective) -> Option { - if let ShebangDirective::Match(_, start, end, _) = shebang { - if lineno > 1 { +pub fn shebang_newline( + range: TextRange, + shebang: &ShebangDirective, + first_line: bool, +) -> Option { + if let ShebangDirective::Match(_, start, content) = shebang { + if first_line { + None + } else { let diagnostic = Diagnostic::new( ShebangNotFirstLine, - Range::new( - Location::new(lineno + 1, *start), - Location::new(lineno + 1, *end), - ), + TextRange::at(range.start() + start, content.text_len()), ); Some(diagnostic) - } else { - None } } else { None diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs index 574b7f914b..72e284f895 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -1,12 +1,10 @@ #![allow(unused_imports)] +use ruff_text_size::{TextLen, TextRange, TextSize}; use std::path::Path; -use rustpython_parser::ast::Location; - use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::registry::AsRule; #[cfg(target_family = "unix")] @@ -27,17 +25,14 @@ impl Violation for ShebangNotExecutable { #[cfg(target_family = "unix")] pub fn shebang_not_executable( filepath: &Path, - lineno: usize, + range: TextRange, shebang: &ShebangDirective, ) -> Option { - if let ShebangDirective::Match(_, start, end, _) = shebang { + if let ShebangDirective::Match(_, start, content) = shebang { if let Ok(false) = is_executable(filepath) { let diagnostic = Diagnostic::new( ShebangNotExecutable, - Range::new( - Location::new(lineno + 1, *start), - Location::new(lineno + 1, *end), - ), + TextRange::at(range.start() + start, content.text_len()), ); return Some(diagnostic); } @@ -48,7 +43,7 @@ pub fn shebang_not_executable( #[cfg(not(target_family = "unix"))] pub fn shebang_not_executable( _filepath: &Path, - _lineno: usize, + _range: TextRange, _shebang: &ShebangDirective, ) -> Option { None diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs index be768bd967..ded384a05b 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs @@ -1,8 +1,7 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::rules::flake8_executable::helpers::ShebangDirective; @@ -17,17 +16,15 @@ impl Violation for ShebangMissingPython { } /// EXE003 -pub fn shebang_python(lineno: usize, shebang: &ShebangDirective) -> Option { - if let ShebangDirective::Match(_, start, end, content) = shebang { +pub fn shebang_python(range: TextRange, shebang: &ShebangDirective) -> Option { + if let ShebangDirective::Match(_, start, content) = shebang { if content.contains("python") || content.contains("pytest") { None } else { let diagnostic = Diagnostic::new( ShebangMissingPython, - Range::new( - Location::new(lineno + 1, start - 2), - Location::new(lineno + 1, *end), - ), + TextRange::at(range.start() + start, content.text_len()) + .sub_start(TextSize::from(2)), ); Some(diagnostic) diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs index 15f4d79d2b..a50105cadc 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs @@ -1,8 +1,7 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::rules::flake8_executable::helpers::ShebangDirective; @@ -22,24 +21,21 @@ impl AlwaysAutofixableViolation for ShebangLeadingWhitespace { /// EXE004 pub fn shebang_whitespace( - lineno: usize, + range: TextRange, shebang: &ShebangDirective, autofix: bool, ) -> Option { if let ShebangDirective::Match(n_spaces, start, ..) = shebang { - if *n_spaces > 0 && *start == n_spaces + 2 { + if *n_spaces > TextSize::from(0) && *start == n_spaces + TextSize::from(2) { let mut diagnostic = Diagnostic::new( ShebangLeadingWhitespace, - Range::new( - Location::new(lineno + 1, 0), - Location::new(lineno + 1, *n_spaces), - ), + TextRange::at(range.start(), *n_spaces), ); if autofix { - diagnostic.set_fix(Edit::deletion( - Location::new(lineno + 1, 0), - Location::new(lineno + 1, *n_spaces), - )); + diagnostic.set_fix(Edit::range_deletion(TextRange::at( + range.start(), + *n_spaces, + ))); } Some(diagnostic) } else { diff --git a/crates/ruff/src/rules/flake8_gettext/rules.rs b/crates/ruff/src/rules/flake8_gettext/rules.rs index a370c58e0b..36f9cfca09 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Operator}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct FStringInGetTextFuncCall; @@ -46,10 +45,7 @@ pub fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool { pub fn f_string_in_gettext_func_call(args: &[Expr]) -> Option { if let Some(first) = args.first() { if matches!(first.node, ExprKind::JoinedStr { .. }) { - return Some(Diagnostic::new( - FStringInGetTextFuncCall {}, - Range::from(first), - )); + return Some(Diagnostic::new(FStringInGetTextFuncCall {}, first.range())); } } None @@ -61,10 +57,7 @@ pub fn format_in_gettext_func_call(args: &[Expr]) -> Option { if let ExprKind::Call { func, .. } = &first.node { if let ExprKind::Attribute { attr, .. } = &func.node { if attr == "format" { - return Some(Diagnostic::new( - FormatInGetTextFuncCall {}, - Range::from(first), - )); + return Some(Diagnostic::new(FormatInGetTextFuncCall {}, first.range())); } } } @@ -86,10 +79,7 @@ pub fn printf_in_gettext_func_call(args: &[Expr]) -> Option { .. } = left.node { - return Some(Diagnostic::new( - PrintfInGetTextFuncCall {}, - Range::from(first), - )); + return Some(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range())); } } } diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules.rs index 664ab8255b..6ce93c3121 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules.rs @@ -1,11 +1,12 @@ use itertools::Itertools; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Constant, Expr, ExprKind, Operator}; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::source_code::Locator; use crate::rules::flake8_implicit_str_concat::settings::Settings; @@ -118,33 +119,27 @@ impl Violation for ExplicitStringConcatenation { } /// ISC001, ISC002 -pub fn implicit(tokens: &[LexResult], settings: &Settings) -> Vec { +pub fn implicit(tokens: &[LexResult], settings: &Settings, locator: &Locator) -> Vec { let mut diagnostics = vec![]; - for ((a_start, a_tok, a_end), (b_start, b_tok, b_end)) in tokens + for ((a_tok, a_range), (b_tok, b_range)) in tokens .iter() .flatten() - .filter(|(_, tok, _)| { + .filter(|(tok, _)| { !matches!(tok, Tok::Comment(..)) && (settings.allow_multiline || !matches!(tok, Tok::NonLogicalNewline)) }) .tuple_windows() { if matches!(a_tok, Tok::String { .. }) && matches!(b_tok, Tok::String { .. }) { - if a_end.row() == b_start.row() { + if locator.contains_line_break(TextRange::new(a_range.end(), b_range.start())) { diagnostics.push(Diagnostic::new( - SingleLineImplicitStringConcatenation, - Range { - location: *a_start, - end_location: *b_end, - }, + MultiLineImplicitStringConcatenation, + TextRange::new(a_range.start(), b_range.end()), )); } else { diagnostics.push(Diagnostic::new( - MultiLineImplicitStringConcatenation, - Range { - location: *a_start, - end_location: *b_end, - }, + SingleLineImplicitStringConcatenation, + TextRange::new(a_range.start(), b_range.end()), )); } } @@ -171,10 +166,7 @@ pub fn explicit(expr: &Expr) -> Option { .. } ) { - return Some(Diagnostic::new( - ExplicitStringConcatenation, - Range::from(expr), - )); + return Some(Diagnostic::new(ExplicitStringConcatenation, expr.range())); } } } diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/banned_import_alias.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/banned_import_alias.rs index 1aecd175c5..7c0619565e 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/banned_import_alias.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/banned_import_alias.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for imports that use non-standard naming conventions, like @@ -58,7 +57,7 @@ pub fn banned_import_alias( name: name.to_string(), asname: asname.to_string(), }, - Range::from(stmt), + stmt.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/banned_import_from.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/banned_import_from.rs index 2ab72be687..93268497b5 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/banned_import_from.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/banned_import_from.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for member imports that should instead be accessed by importing the @@ -52,7 +51,7 @@ pub fn banned_import_from( BannedImportFrom { name: name.to_string(), }, - Range::from(stmt), + stmt.range(), )); } None diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs index d5febbbd63..184c76e351 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for imports that are typically imported using a common convention, @@ -54,7 +53,7 @@ pub fn conventional_import_alias( name: name.to_string(), asname: expected_alias.to_string(), }, - Range::from(stmt), + stmt.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_logging_format/rules.rs b/crates/ruff/src/rules/flake8_logging_format/rules.rs index df60592b70..0f09459327 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules.rs @@ -1,8 +1,9 @@ -use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Location, Operator}; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Operator}; +use std::ops::Add; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_python_ast::helpers::{find_keyword, SimpleCallArgs}; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::logging; use ruff_python_stdlib::logging::LoggingLevel; @@ -47,14 +48,14 @@ fn check_msg(checker: &mut Checker, msg: &Expr) { if checker.settings.rules.enabled(Rule::LoggingStringConcat) { checker .diagnostics - .push(Diagnostic::new(LoggingStringConcat, Range::from(msg))); + .push(Diagnostic::new(LoggingStringConcat, msg.range())); } } Operator::Mod => { if checker.settings.rules.enabled(Rule::LoggingPercentFormat) { checker .diagnostics - .push(Diagnostic::new(LoggingPercentFormat, Range::from(msg))); + .push(Diagnostic::new(LoggingPercentFormat, msg.range())); } } _ => {} @@ -64,7 +65,7 @@ fn check_msg(checker: &mut Checker, msg: &Expr) { if checker.settings.rules.enabled(Rule::LoggingFString) { checker .diagnostics - .push(Diagnostic::new(LoggingFString, Range::from(msg))); + .push(Diagnostic::new(LoggingFString, msg.range())); } } // Check for .format() calls. @@ -74,7 +75,7 @@ fn check_msg(checker: &mut Checker, msg: &Expr) { if attr == "format" && matches!(value.node, ExprKind::Constant { .. }) { checker .diagnostics - .push(Diagnostic::new(LoggingStringFormat, Range::from(msg))); + .push(Diagnostic::new(LoggingStringFormat, msg.range())); } } } @@ -97,7 +98,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { if RESERVED_ATTRS.contains(&string.as_str()) { checker.diagnostics.push(Diagnostic::new( LoggingExtraAttrClash(string.to_string()), - Range::from(key), + key.range(), )); } } @@ -115,7 +116,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { if RESERVED_ATTRS.contains(&key.as_str()) { checker.diagnostics.push(Diagnostic::new( LoggingExtraAttrClash(key.to_string()), - Range::from(keyword), + keyword.range(), )); } } @@ -153,16 +154,7 @@ pub fn logging_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: if let ExprKind::Attribute { value, attr, .. } = &func.node { if let Some(logging_call_type) = LoggingCallType::from_attribute(attr.as_str()) { let call_args = SimpleCallArgs::new(args, keywords); - let level_call_range = Range::new( - Location::new( - func.location.row(), - value.end_location.unwrap().column() + 1, - ), - Location::new( - func.end_location.unwrap().row(), - func.end_location.unwrap().column(), - ), - ); + let level_call_range = TextRange::new(value.end().add(TextSize::from(1)), func.end()); // G001 - G004 let msg_pos = usize::from(matches!(logging_call_type, LoggingCallType::LogCall)); @@ -179,10 +171,9 @@ pub fn logging_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: { let mut diagnostic = Diagnostic::new(LoggingWarn, level_call_range); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "warning".to_string(), - level_call_range.location, - level_call_range.end_location, + level_call_range, )); } checker.diagnostics.push(diagnostic); @@ -244,7 +235,7 @@ pub fn logging_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: { checker.diagnostics.push(Diagnostic::new( LoggingRedundantExcInfo, - Range::from(exc_info), + exc_info.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_no_pep420/rules.rs b/crates/ruff/src/rules/flake8_no_pep420/rules.rs index cb6a329605..6973ce7e41 100644 --- a/crates/ruff/src/rules/flake8_no_pep420/rules.rs +++ b/crates/ruff/src/rules/flake8_no_pep420/rules.rs @@ -1,8 +1,8 @@ +use ruff_text_size::TextRange; use std::path::{Path, PathBuf}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::fs; @@ -64,7 +64,7 @@ pub fn implicit_namespace_package( ImplicitNamespacePackage { filename: fs::relativize_path(path), }, - Range::default(), + TextRange::default(), )) } else { None diff --git a/crates/ruff/src/rules/flake8_pie/rules.rs b/crates/ruff/src/rules/flake8_pie/rules.rs index 1eb603ab92..48f4acb4d4 100644 --- a/crates/ruff/src/rules/flake8_pie/rules.rs +++ b/crates/ruff/src/rules/flake8_pie/rules.rs @@ -12,13 +12,12 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::helpers::{create_expr, match_trailing_comment, unparse_expr}; -use ruff_python_ast::types::{Range, RefEquality}; +use ruff_python_ast::helpers::{create_expr, trailing_comment_start_offset, unparse_expr}; +use ruff_python_ast::types::RefEquality; use ruff_python_stdlib::identifiers::is_identifier; use crate::autofix::actions::delete_stmt; use crate::checkers::ast::Checker; -use crate::message::Location; use crate::registry::AsRule; #[violation] @@ -134,16 +133,10 @@ pub fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) { } ) { if matches!(pass_stmt.node, StmtKind::Pass) { - let mut diagnostic = Diagnostic::new(UnnecessaryPass, Range::from(pass_stmt)); + let mut diagnostic = Diagnostic::new(UnnecessaryPass, pass_stmt.range()); if checker.patch(diagnostic.kind.rule()) { - if let Some(index) = match_trailing_comment(pass_stmt, checker.locator) { - diagnostic.set_fix(Edit::deletion( - pass_stmt.location, - Location::new( - pass_stmt.end_location.unwrap().row(), - pass_stmt.end_location.unwrap().column() + index, - ), - )); + if let Some(index) = trailing_comment_start_offset(pass_stmt, checker.locator) { + diagnostic.set_fix(Edit::range_deletion(pass_stmt.range().add_end(index))); } else { diagnostic.try_set_fix(|| { delete_stmt( @@ -198,7 +191,7 @@ pub fn duplicate_class_field_definition<'a, 'b>( if !seen_targets.insert(target) { let mut diagnostic = Diagnostic::new( DuplicateClassFieldDefinition(target.to_string()), - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect(); @@ -264,7 +257,7 @@ where NonUniqueEnums { value: unparse_expr(value, checker.stylist), }, - Range::from(stmt), + stmt.range(), ); checker.diagnostics.push(diagnostic); } @@ -278,7 +271,7 @@ pub fn unnecessary_spread(checker: &mut Checker, keys: &[Option], values: // We only care about when the key is None which indicates a spread `**` // inside a dict. if let ExprKind::Dict { .. } = value.node { - let diagnostic = Diagnostic::new(UnnecessarySpread, Range::from(value)); + let diagnostic = Diagnostic::new(UnnecessarySpread, value.range()); checker.diagnostics.push(diagnostic); } } @@ -309,7 +302,7 @@ pub fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[Key // handle case of foo(**{**bar}) (keys.len() == 1 && keys[0].is_none()) { - let diagnostic = Diagnostic::new(UnnecessaryDictKwargs, Range::from(expr)); + let diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range()); checker.diagnostics.push(diagnostic); } } @@ -363,7 +356,7 @@ pub fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { MultipleStartsEndsWith { attr: attr_name.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { let words: Vec<&Expr> = indices @@ -421,10 +414,9 @@ pub fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { .collect(), }); - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&bool_op, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -445,13 +437,9 @@ pub fn reimplemented_list_builtin(checker: &mut Checker, expr: &Expr) { { if let ExprKind::List { elts, .. } = &body.node { if elts.is_empty() { - let mut diagnostic = Diagnostic::new(ReimplementedListBuiltin, Range::from(expr)); + let mut diagnostic = Diagnostic::new(ReimplementedListBuiltin, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "list".to_string(), - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("list".to_string(), expr.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_print/rules/print_call.rs b/crates/ruff/src/rules/flake8_print/rules/print_call.rs index aebee8abb0..150609beb6 100644 --- a/crates/ruff/src/rules/flake8_print/rules/print_call.rs +++ b/crates/ruff/src/rules/flake8_print/rules/print_call.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_const_none; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -54,11 +53,11 @@ pub fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) { } } } - Diagnostic::new(Print, Range::from(func)) + Diagnostic::new(Print, func.range()) } else if call_path.as_ref().map_or(false, |call_path| { *call_path.as_slice() == ["pprint", "pprint"] }) { - Diagnostic::new(PPrint, Range::from(func)) + Diagnostic::new(PPrint, func.range()) } else { return; } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 0b7bd09144..94bbc61f33 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -1,10 +1,8 @@ use rustpython_parser::ast::{Cmpop, Expr}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - -use crate::checkers::ast::Checker; /// ## What it does /// Checks for usages of comparators other than `<` and `>=` for @@ -80,7 +78,7 @@ pub fn bad_version_info_comparison( } if !matches!(op, Cmpop::Lt | Cmpop::GtE) { - let diagnostic = Diagnostic::new(BadVersionInfoComparison, Range::from(expr)); + let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range()); checker.diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/docstring_in_stubs.rs b/crates/ruff/src/rules/flake8_pyi/rules/docstring_in_stubs.rs index 500621ed50..10aa9bee40 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/docstring_in_stubs.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/docstring_in_stubs.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -21,6 +20,6 @@ pub fn docstring_in_stubs(checker: &mut Checker, docstring: Option<&Expr>) { if let Some(docstr) = &docstring { checker .diagnostics - .push(Diagnostic::new(DocstringInStub, Range::from(*docstr))); + .push(Diagnostic::new(DocstringInStub, docstr.range())); } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs index 47e3fafe98..8af4c51005 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -5,7 +5,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -63,7 +62,7 @@ fn traverse_union<'a>( DuplicateUnionMember { duplicate_name: unparse_expr(expr, checker.stylist), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { // Delete the "|" character as well as the duplicate value by reconstructing the @@ -78,13 +77,12 @@ fn traverse_union<'a>( }; // Replace the parent with its non-duplicate child. - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr( if expr.node == left.node { right } else { left }, checker.stylist, ), - parent.location, - parent.end_location.unwrap(), + parent.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_empty_stub_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_empty_stub_body.rs index 055c92c081..5467df5780 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/non_empty_stub_body.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_empty_stub_body.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -30,5 +29,5 @@ pub fn non_empty_stub_body(checker: &mut Checker, body: &[Stmt]) { } checker .diagnostics - .push(Diagnostic::new(NonEmptyStubBody, Range::from(&body[0]))); + .push(Diagnostic::new(NonEmptyStubBody, body[0].range())); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs index b7b898a107..8e6355e3ad 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs @@ -2,7 +2,7 @@ use crate::autofix::actions::delete_stmt; use log::error; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::{Range, RefEquality}; +use ruff_python_ast::types::RefEquality; use crate::checkers::ast::Checker; @@ -32,7 +32,7 @@ pub fn pass_in_class_body<'a>(checker: &mut Checker<'a>, parent: &'a Stmt, body: for stmt in body { if matches!(stmt.node, StmtKind::Pass) { - let mut diagnostic = Diagnostic::new(PassInClassBody, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(PassInClassBody, stmt.range()); if checker.patch(diagnostic.kind.rule()) { let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect(); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs index fd81adb2ad..4223262548 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -22,9 +21,8 @@ pub fn pass_statement_stub_body(checker: &mut Checker, body: &[Stmt]) { return; } if matches!(body[0].node, StmtKind::Pass) { - checker.diagnostics.push(Diagnostic::new( - PassStatementStubBody, - Range::from(&body[0]), - )); + checker + .diagnostics + .push(Diagnostic::new(PassStatementStubBody, body[0].range())); } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs b/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs index 7bd2f50c76..272f312017 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -84,9 +83,8 @@ pub fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: &[Expr]) }) else { return; }; - checker.diagnostics.push(Diagnostic::new( - UnprefixedTypeParam { kind }, - Range::from(value), - )); + checker + .diagnostics + .push(Diagnostic::new(UnprefixedTypeParam { kind }, value.range())); } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs index 08c1fb261c..5313b68af4 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Arguments, Constant, Expr, ExprKind, Operator, Unar use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::context::Context; use crate::checkers::ast::Checker; @@ -106,17 +105,17 @@ fn is_valid_default_value_with_annotation( ExprKind::Constant { value: Constant::Str(..), .. - } => return checker.locator.slice(default).len() <= 50, + } => return checker.locator.slice(default.range()).len() <= 50, ExprKind::Constant { value: Constant::Bytes(..), .. - } => return checker.locator.slice(default).len() <= 50, + } => return checker.locator.slice(default.range()).len() <= 50, // Ex) `123`, `True`, `False`, `3.14` ExprKind::Constant { value: Constant::Int(..) | Constant::Bool(..) | Constant::Float(..), .. } => { - return checker.locator.slice(default).len() <= 10; + return checker.locator.slice(default.range()).len() <= 10; } // Ex) `2j` ExprKind::Constant { @@ -124,7 +123,7 @@ fn is_valid_default_value_with_annotation( .. } => { if *real == 0.0 { - return checker.locator.slice(default).len() <= 10; + return checker.locator.slice(default.range()).len() <= 10; } } ExprKind::UnaryOp { @@ -137,7 +136,7 @@ fn is_valid_default_value_with_annotation( .. } = &operand.node { - return checker.locator.slice(operand).len() <= 10; + return checker.locator.slice(operand.range()).len() <= 10; } // Ex) `-2j` if let ExprKind::Constant { @@ -146,7 +145,7 @@ fn is_valid_default_value_with_annotation( } = &operand.node { if *real == 0.0 { - return checker.locator.slice(operand).len() <= 10; + return checker.locator.slice(operand.range()).len() <= 10; } } // Ex) `-math.inf`, `-math.pi`, etc. @@ -182,7 +181,7 @@ fn is_valid_default_value_with_annotation( .. } = &left.node { - return checker.locator.slice(left).len() <= 10; + return checker.locator.slice(left.range()).len() <= 10; } else if let ExprKind::UnaryOp { op: Unaryop::USub, operand, @@ -194,7 +193,7 @@ fn is_valid_default_value_with_annotation( .. } = &operand.node { - return checker.locator.slice(operand).len() <= 10; + return checker.locator.slice(operand.range()).len() <= 10; } } } @@ -296,13 +295,12 @@ pub fn typed_argument_simple_defaults(checker: &mut Checker, args: &Arguments) { if arg.node.annotation.is_some() { if !is_valid_default_value_with_annotation(default, checker, true) { let mut diagnostic = - Diagnostic::new(TypedArgumentDefaultInStub, Range::from(default)); + Diagnostic::new(TypedArgumentDefaultInStub, default.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "...".to_string(), - default.location, - default.end_location.unwrap(), + default.range(), )); } @@ -323,13 +321,12 @@ pub fn typed_argument_simple_defaults(checker: &mut Checker, args: &Arguments) { if kwarg.node.annotation.is_some() { if !is_valid_default_value_with_annotation(default, checker, true) { let mut diagnostic = - Diagnostic::new(TypedArgumentDefaultInStub, Range::from(default)); + Diagnostic::new(TypedArgumentDefaultInStub, default.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "...".to_string(), - default.location, - default.end_location.unwrap(), + default.range(), )); } @@ -353,13 +350,12 @@ pub fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) { if arg.node.annotation.is_none() { if !is_valid_default_value_with_annotation(default, checker, true) { let mut diagnostic = - Diagnostic::new(ArgumentDefaultInStub, Range::from(default)); + Diagnostic::new(ArgumentDefaultInStub, default.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "...".to_string(), - default.location, - default.end_location.unwrap(), + default.range(), )); } @@ -380,13 +376,12 @@ pub fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) { if kwarg.node.annotation.is_none() { if !is_valid_default_value_with_annotation(default, checker, true) { let mut diagnostic = - Diagnostic::new(ArgumentDefaultInStub, Range::from(default)); + Diagnostic::new(ArgumentDefaultInStub, default.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "...".to_string(), - default.location, - default.end_location.unwrap(), + default.range(), )); } @@ -413,13 +408,9 @@ pub fn assignment_default_in_stub(checker: &mut Checker, targets: &[Expr], value return; } - let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, Range::from(value)); + let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, value.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "...".to_string(), - value.location, - value.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("...".to_string(), value.range())); } checker.diagnostics.push(diagnostic); } @@ -444,13 +435,9 @@ pub fn annotated_assignment_default_in_stub( return; } - let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, Range::from(value)); + let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, value.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "...".to_string(), - value.location, - value.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("...".to_string(), value.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs index 48426acea3..c699875e22 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -5,7 +5,6 @@ use rustpython_parser::Tok; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for the use of type comments (e.g., `x = 1 # type: int`) in stub @@ -40,15 +39,9 @@ pub fn type_comment_in_stub(tokens: &[LexResult]) -> Vec { let mut diagnostics = vec![]; for token in tokens.iter().flatten() { - if let (location, Tok::Comment(comment), end_location) = token { + if let (Tok::Comment(comment), range) = token { if TYPE_COMMENT_REGEX.is_match(comment) && !TYPE_IGNORE_REGEX.is_match(comment) { - diagnostics.push(Diagnostic::new( - TypeCommentInStub, - Range { - location: *location, - end_location: *end_location, - }, - )); + diagnostics.push(Diagnostic::new(TypeCommentInStub, *range)); } } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs index 7492c0eb68..d1e43ea404 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -102,7 +101,7 @@ pub fn unrecognized_platform( }; let diagnostic_unrecognized_platform_check = - Diagnostic::new(UnrecognizedPlatformCheck, Range::from(expr)); + Diagnostic::new(UnrecognizedPlatformCheck, expr.range()); if !checker .ctx .resolve_call_path(left) @@ -143,7 +142,7 @@ pub fn unrecognized_platform( UnrecognizedPlatformName { platform: value.clone(), }, - Range::from(right), + right.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index 2fe2ca84e3..df6ef9c688 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -6,15 +6,13 @@ use libcst_native::{ SmallStatement, Statement, Suite, TrailingWhitespace, UnaryOp, UnaryOperation, }; use rustpython_parser::ast::{ - Boolop, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, Location, Stmt, StmtKind, - Unaryop, + Boolop, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, Stmt, StmtKind, Unaryop, }; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{has_comments_in, unparse_stmt, Truthiness}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{visitor, whitespace}; @@ -161,7 +159,7 @@ where PytestAssertInExcept { name: id.to_string(), }, - Range::from(current_assert), + current_assert.range(), )); } } @@ -195,20 +193,19 @@ pub fn unittest_assertion( // the assertion is part of a larger expression. let fixable = checker.ctx.current_expr_parent().is_none() && matches!(checker.ctx.current_stmt().node, StmtKind::Expr { .. }) - && !has_comments_in(Range::from(expr), checker.locator); + && !has_comments_in(expr.range(), checker.locator); let mut diagnostic = Diagnostic::new( PytestUnittestAssertion { assertion: unittest_assert.to_string(), fixable, }, - Range::from(func), + func.range(), ); if fixable && checker.patch(diagnostic.kind.rule()) { if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_stmt(&stmt, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } } @@ -226,7 +223,7 @@ pub fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { if Truthiness::from_expr(test, |id| checker.ctx.is_builtin(id)).is_falsey() { checker .diagnostics - .push(Diagnostic::new(PytestAssertAlwaysFalse, Range::from(stmt))); + .push(Diagnostic::new(PytestAssertAlwaysFalse, stmt.range())); } } @@ -324,10 +321,7 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> }; // Extract the module text. - let contents = locator.slice(Range::new( - Location::new(stmt.location.row(), 0), - Location::new(stmt.end_location.unwrap().row() + 1, 0), - )); + let contents = locator.lines(stmt.range()); // "Embed" it in a function definition, to preserve indentation while retaining valid source // code. (We'll strip the prefix later on.) @@ -418,11 +412,9 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> .unwrap() .to_string(); - Ok(Edit::replacement( - contents, - Location::new(stmt.location.row(), 0), - Location::new(stmt.end_location.unwrap().row() + 1, 0), - )) + let range = locator.full_lines_range(stmt.range()); + + Ok(Edit::range_replacement(contents, range)) } /// PT018 @@ -431,9 +423,8 @@ pub fn composite_condition(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: if matches!(composite, CompositionKind::Simple | CompositionKind::Mixed) { let fixable = matches!(composite, CompositionKind::Simple) && msg.is_none() - && !has_comments_in(Range::from(stmt), checker.locator); - let mut diagnostic = - Diagnostic::new(PytestCompositeAssertion { fixable }, Range::from(stmt)); + && !has_comments_in(stmt.range(), checker.locator); + let mut diagnostic = Diagnostic::new(PytestCompositeAssertion { fixable }, stmt.range()); if fixable && checker.patch(diagnostic.kind.rule()) { diagnostic .try_set_fix(|| fix_composite_condition(stmt, checker.locator, checker.stylist)); diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs index 722102d1eb..842092ee13 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -28,12 +27,12 @@ pub fn fail_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: &[ if is_empty_or_null_string(msg) { checker .diagnostics - .push(Diagnostic::new(PytestFailWithoutMessage, Range::from(func))); + .push(Diagnostic::new(PytestFailWithoutMessage, func.range())); } } else { checker .diagnostics - .push(Diagnostic::new(PytestFailWithoutMessage, Range::from(func))); + .push(Diagnostic::new(PytestFailWithoutMessage, func.range())); } } } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs index b5c3221af9..f98ace22b2 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -1,5 +1,6 @@ use anyhow::Result; -use rustpython_parser::ast::{Arguments, Expr, ExprKind, Keyword, Location, Stmt, StmtKind}; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::{Arguments, Expr, ExprKind, Keyword, Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit}; @@ -7,7 +8,6 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; use ruff_python_ast::helpers::collect_arg_names; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{helpers, visitor}; @@ -248,7 +248,7 @@ fn pytest_fixture_parentheses( expected_parens: preferred.to_string(), actual_parens: actual.to_string(), }, - Range::from(decorator), + decorator.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(fix); @@ -258,13 +258,12 @@ fn pytest_fixture_parentheses( pub fn fix_extraneous_scope_function( locator: &Locator, - stmt_at: Location, - expr_at: Location, - expr_end: Location, + stmt_at: TextSize, + expr_range: TextRange, args: &[Expr], keywords: &[Keyword], ) -> Result { - remove_argument(locator, stmt_at, expr_at, expr_end, args, keywords, false) + remove_argument(locator, stmt_at, expr_range, args, keywords, false) } /// PT001, PT002, PT003 @@ -284,8 +283,7 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &E && args.is_empty() && keywords.is_empty() { - let fix = - Edit::deletion(func.end_location.unwrap(), decorator.end_location.unwrap()); + let fix = Edit::deletion(func.end(), decorator.end()); pytest_fixture_parentheses(checker, decorator, fix, "", "()"); } @@ -299,7 +297,7 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &E PytestFixturePositionalArgs { function: func_name.to_string(), }, - Range::from(decorator), + decorator.range(), )); } @@ -314,19 +312,15 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &E if let Some(scope_keyword) = scope_keyword { if keyword_is_literal(scope_keyword, "function") { - let mut diagnostic = Diagnostic::new( - PytestExtraneousScopeFunction, - Range::from(scope_keyword), - ); + let mut diagnostic = + Diagnostic::new(PytestExtraneousScopeFunction, scope_keyword.range()); if checker.patch(diagnostic.kind.rule()) { - let location = diagnostic.location; - let end_location = diagnostic.end_location; + let expr_range = diagnostic.range(); diagnostic.try_set_fix(|| { fix_extraneous_scope_function( checker.locator, - decorator.location, - location, - end_location, + decorator.start(), + expr_range, args, keywords, ) @@ -344,7 +338,7 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &E .enabled(Rule::PytestFixtureIncorrectParenthesesStyle) && checker.settings.flake8_pytest_style.fixture_parentheses { - let fix = Edit::insertion("()".to_string(), decorator.end_location.unwrap()); + let fix = Edit::insertion("()".to_string(), decorator.end()); pytest_fixture_parentheses(checker, decorator, fix, "()", ""); } } @@ -401,16 +395,12 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestUselessYieldFixture { name: name.to_string(), }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "return".to_string(), - stmt.location, - Location::new( - stmt.location.row(), - stmt.location.column() + "yield".len(), - ), + TextRange::at(stmt.start(), "yield".text_len()), )); } checker.diagnostics.push(diagnostic); @@ -430,7 +420,7 @@ fn check_test_function_args(checker: &mut Checker, args: &Arguments) { PytestFixtureParamWithoutValue { name: name.to_string(), }, - Range::from(arg), + arg.range(), )); } }); @@ -441,7 +431,7 @@ fn check_fixture_decorator_name(checker: &mut Checker, decorator: &Expr) { if is_pytest_yield_fixture(decorator, checker) { checker.diagnostics.push(Diagnostic::new( PytestDeprecatedYieldFixture, - Range::from(decorator), + decorator.range(), )); } } @@ -461,7 +451,7 @@ fn check_fixture_addfinalizer(checker: &mut Checker, args: &Arguments, body: &[S if let Some(addfinalizer) = visitor.addfinalizer_call { checker.diagnostics.push(Diagnostic::new( PytestFixtureFinalizerCallback, - Range::from(addfinalizer), + addfinalizer.range(), )); } } @@ -477,11 +467,10 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Expr]) { { if *name == "asyncio" { let mut diagnostic = - Diagnostic::new(PytestUnnecessaryAsyncioMarkOnFixture, Range::from(expr)); + Diagnostic::new(PytestUnnecessaryAsyncioMarkOnFixture, expr.range()); if checker.patch(diagnostic.kind.rule()) { - let start = Location::new(expr.location.row(), 0); - let end = Location::new(expr.end_location.unwrap().row() + 1, 0); - diagnostic.set_fix(Edit::deletion(start, end)); + let range = checker.locator.full_lines_range(expr.range()); + diagnostic.set_fix(Edit::range_deletion(range)); } checker.diagnostics.push(diagnostic); } @@ -494,11 +483,10 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Expr]) { { if *name == "usefixtures" { let mut diagnostic = - Diagnostic::new(PytestErroneousUseFixturesOnFixture, Range::from(expr)); + Diagnostic::new(PytestErroneousUseFixturesOnFixture, expr.range()); if checker.patch(diagnostic.kind.rule()) { - let start = Location::new(expr.location.row(), 0); - let end = Location::new(expr.end_location.unwrap().row() + 1, 0); - diagnostic.set_fix(Edit::deletion(start, end)); + let line_range = checker.locator.full_lines_range(expr.range()); + diagnostic.set_fix(Edit::range_deletion(line_range)); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/imports.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/imports.rs index 75de55b8ab..50154ed0e3 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/imports.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/imports.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct PytestIncorrectPytestImport; @@ -25,7 +24,7 @@ pub fn import(import_from: &Stmt, name: &str, asname: Option<&str>) -> Option { if checker.settings.flake8_pytest_style.mark_parentheses { - let fix = Edit::insertion("()".to_string(), decorator.end_location.unwrap()); + let fix = Edit::insertion("()".to_string(), decorator.end()); pytest_mark_parentheses(checker, decorator, call_path, fix, "()", ""); } } @@ -112,11 +111,11 @@ fn check_useless_usefixtures(checker: &mut Checker, decorator: &Expr, call_path: } if !has_parameters { - let mut diagnostic = - Diagnostic::new(PytestUseFixturesWithoutParameters, Range::from(decorator)); + let mut diagnostic = Diagnostic::new(PytestUseFixturesWithoutParameters, decorator.range()); if checker.patch(diagnostic.kind.rule()) { - let at_start = Location::new(decorator.location.row(), decorator.location.column() - 1); - diagnostic.set_fix(Edit::deletion(at_start, decorator.end_location.unwrap())); + diagnostic.set_fix(Edit::range_deletion( + decorator.range().sub_start(TextSize::from(1)), + )); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs index ca5c446b39..e72b3b11cb 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -1,3 +1,4 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind}; use rustpython_parser::{lexer, Mode, Tok}; @@ -5,7 +6,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_expr, unparse_expr}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; @@ -94,24 +94,24 @@ fn elts_to_csv(elts: &[Expr], checker: &Checker) -> Option { /// ``` /// /// This method assumes that the first argument is a string. -fn get_parametrize_name_range(checker: &Checker, decorator: &Expr, expr: &Expr) -> Range { +fn get_parametrize_name_range(checker: &Checker, decorator: &Expr, expr: &Expr) -> TextRange { let mut locations = Vec::new(); let mut implicit_concat = None; // The parenthesis are not part of the AST, so we need to tokenize the // decorator to find them. - for (start, tok, end) in lexer::lex_located( - checker.locator.slice(decorator), + for (tok, range) in lexer::lex_located( + checker.locator.slice(decorator.range()), Mode::Module, - decorator.location, + decorator.start(), ) .flatten() { match tok { - Tok::Lpar => locations.push(start), + Tok::Lpar => locations.push(range.start()), Tok::Rpar => { if let Some(start) = locations.pop() { - implicit_concat = Some(Range::new(start, end)); + implicit_concat = Some(TextRange::new(start, range.end())); } } // Stop after the first argument. @@ -123,7 +123,7 @@ fn get_parametrize_name_range(checker: &Checker, decorator: &Expr, expr: &Expr) if let Some(range) = implicit_concat { range } else { - Range::from(expr) + expr.range() } } @@ -148,7 +148,7 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { name_range, ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( format!( "({})", unparse_expr( @@ -167,8 +167,7 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { checker.stylist, ) ), - name_range.location, - name_range.end_location, + name_range, )); } checker.diagnostics.push(diagnostic); @@ -182,7 +181,7 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { name_range, ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr( &create_expr(ExprKind::List { elts: names @@ -198,8 +197,7 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { }), checker.stylist, ), - name_range.location, - name_range.end_location, + name_range, )); } checker.diagnostics.push(diagnostic); @@ -221,10 +219,10 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { PytestParametrizeNamesWrongType { expected: names_type, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr( &create_expr(ExprKind::List { elts: elts.clone(), @@ -232,8 +230,7 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { }), checker.stylist, ), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -243,15 +240,11 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { PytestParametrizeNamesWrongType { expected: names_type, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { if let Some(content) = elts_to_csv(elts, checker) { - diagnostic.set_fix(Edit::replacement( - content, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(content, expr.range())); } } checker.diagnostics.push(diagnostic); @@ -272,10 +265,10 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { PytestParametrizeNamesWrongType { expected: names_type, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( format!( "({})", unparse_expr( @@ -286,8 +279,7 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { checker.stylist, ) ), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -297,15 +289,11 @@ fn check_names(checker: &mut Checker, decorator: &Expr, expr: &Expr) { PytestParametrizeNamesWrongType { expected: names_type, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { if let Some(content) = elts_to_csv(elts, checker) { - diagnostic.set_fix(Edit::replacement( - content, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(content, expr.range())); } } checker.diagnostics.push(diagnostic); @@ -344,7 +332,7 @@ fn check_values(checker: &mut Checker, names: &Expr, values: &Expr) { values: values_type, row: values_row_type, }, - Range::from(values), + values.range(), )); } if is_multi_named { @@ -358,7 +346,7 @@ fn check_values(checker: &mut Checker, names: &Expr, values: &Expr) { values: values_type, row: values_row_type, }, - Range::from(values), + values.range(), )); } if is_multi_named { @@ -374,14 +362,13 @@ fn handle_single_name(checker: &mut Checker, expr: &Expr, value: &Expr) { PytestParametrizeNamesWrongType { expected: types::ParametrizeNameType::Csv, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&create_expr(value.node.clone()), checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -402,7 +389,7 @@ fn handle_value_rows( values: values_type, row: values_row_type, }, - Range::from(elt), + elt.range(), )); } } @@ -413,7 +400,7 @@ fn handle_value_rows( values: values_type, row: values_row_type, }, - Range::from(elt), + elt.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs index d992df5728..4363a8fb80 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs @@ -5,7 +5,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::compose_call_path; use ruff_python_ast::helpers::{collect_arg_names, SimpleCallArgs}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -83,7 +82,7 @@ fn check_patch_call( visitor.visit_expr(body); if !visitor.uses_args { - return Some(Diagnostic::new(PytestPatchWithLambda, Range::from(call))); + return Some(Diagnostic::new(PytestPatchWithLambda, call.range())); } } } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs index 732795fe5a..d8b5b03784 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs @@ -4,7 +4,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::format_call_path; use ruff_python_ast::call_path::from_qualified_name; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -74,10 +73,9 @@ pub fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: .enabled(Rule::PytestRaisesWithoutException) { if args.is_empty() && keywords.is_empty() { - checker.diagnostics.push(Diagnostic::new( - PytestRaisesWithoutException, - Range::from(func), - )); + checker + .diagnostics + .push(Diagnostic::new(PytestRaisesWithoutException, func.range())); } } @@ -134,7 +132,7 @@ pub fn complex_raises(checker: &mut Checker, stmt: &Stmt, items: &[Withitem], bo if is_too_complex { checker.diagnostics.push(Diagnostic::new( PytestRaisesWithMultipleStatements, - Range::from(stmt), + stmt.range(), )); } } @@ -169,7 +167,7 @@ fn exception_needs_match(checker: &mut Checker, exception: &Expr) { PytestRaisesTooBroad { exception: call_path, }, - Range::from(exception), + exception.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_quotes/rules.rs b/crates/ruff/src/rules/flake8_quotes/rules.rs index 859a488788..b706126755 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules.rs @@ -1,11 +1,10 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::TextRange; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::lex::docstring_detection::StateMachine; use crate::registry::Rule; @@ -258,14 +257,13 @@ impl<'a> From<&'a str> for Trivia<'a> { /// Q003 fn docstring( locator: &Locator, - start: Location, - end: Location, + range: TextRange, settings: &Settings, autofix: flags::Autofix, ) -> Option { let quotes_settings = &settings.flake8_quotes; - let text = locator.slice(Range::new(start, end)); + let text = locator.slice(range); let trivia: Trivia = text.into(); if trivia @@ -279,7 +277,7 @@ fn docstring( BadQuotesDocstring { quote: quotes_settings.docstring_quotes, }, - Range::new(start, end), + range, ); if autofix.into() && settings.rules.should_fix(Rule::BadQuotesDocstring) { let quote_count = if trivia.is_multiline { 3 } else { 1 }; @@ -291,7 +289,7 @@ fn docstring( fixed_contents.push_str("e); fixed_contents.push_str(string_contents); fixed_contents.push_str("e); - diagnostic.set_fix(Edit::replacement(fixed_contents, start, end)); + diagnostic.set_fix(Edit::range_replacement(fixed_contents, range)); } Some(diagnostic) } @@ -299,7 +297,7 @@ fn docstring( /// Q001, Q002 fn strings( locator: &Locator, - sequence: &[(Location, Location)], + sequence: &[TextRange], settings: &Settings, autofix: flags::Autofix, ) -> Vec { @@ -309,8 +307,8 @@ fn strings( let trivia = sequence .iter() - .map(|(start, end)| { - let text = locator.slice(Range::new(*start, *end)); + .map(|range| { + let text = locator.slice(*range); let trivia: Trivia = text.into(); trivia }) @@ -331,7 +329,7 @@ fn strings( string_contents.contains(good_single(quotes_settings.inline_quotes)) }); - for ((start, end), trivia) in sequence.iter().zip(trivia.into_iter()) { + for (range, trivia) in sequence.iter().zip(trivia.into_iter()) { if trivia.is_multiline { // If our string is or contains a known good string, ignore it. if trivia @@ -353,7 +351,7 @@ fn strings( BadQuotesMultilineString { quote: quotes_settings.multiline_quotes, }, - Range::new(*start, *end), + *range, ); if autofix.into() && settings.rules.should_fix(Rule::BadQuotesMultilineString) { @@ -366,7 +364,7 @@ fn strings( fixed_contents.push_str(quote); fixed_contents.push_str(string_contents); fixed_contents.push_str(quote); - diagnostic.set_fix(Edit::replacement(fixed_contents, *start, *end)); + diagnostic.set_fix(Edit::range_replacement(fixed_contents, *range)); } diagnostics.push(diagnostic); } else { @@ -384,8 +382,7 @@ fn strings( if string_contents.contains(good_single(quotes_settings.inline_quotes)) && !string_contents.contains(bad_single(quotes_settings.inline_quotes)) { - let mut diagnostic = - Diagnostic::new(AvoidableEscapedQuote, Range::new(*start, *end)); + let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, *range); if autofix.into() && settings.rules.should_fix(Rule::AvoidableEscapedQuote) { let quote = bad_single(quotes_settings.inline_quotes); @@ -430,7 +427,7 @@ fn strings( fixed_contents.push(quote); - diagnostic.set_fix(Edit::replacement(fixed_contents, *start, *end)); + diagnostic.set_fix(Edit::range_replacement(fixed_contents, *range)); } diagnostics.push(diagnostic); } @@ -443,7 +440,7 @@ fn strings( BadQuotesInlineString { quote: quotes_settings.inline_quotes, }, - Range::new(*start, *end), + *range, ); if autofix.into() && settings.rules.should_fix(Rule::BadQuotesInlineString) { let quote = good_single(quotes_settings.inline_quotes); @@ -453,7 +450,7 @@ fn strings( fixed_contents.push(quote); fixed_contents.push_str(string_contents); fixed_contents.push(quote); - diagnostic.set_fix(Edit::replacement(fixed_contents, *start, *end)); + diagnostic.set_fix(Edit::range_replacement(fixed_contents, *range)); } diagnostics.push(diagnostic); } @@ -476,7 +473,7 @@ pub fn from_tokens( // concatenation, and should thus be handled as a single unit. let mut sequence = vec![]; let mut state_machine = StateMachine::default(); - for &(start, ref tok, end) in lxr.iter().flatten() { + for &(ref tok, range) in lxr.iter().flatten() { let is_docstring = state_machine.consume(tok); // If this is a docstring, consume the existing sequence, then consume the @@ -486,13 +483,13 @@ pub fn from_tokens( diagnostics.extend(strings(locator, &sequence, settings, autofix)); sequence.clear(); } - if let Some(diagnostic) = docstring(locator, start, end, settings, autofix) { + if let Some(diagnostic) = docstring(locator, range, settings, autofix) { diagnostics.push(diagnostic); } } else { if matches!(tok, Tok::String { .. }) { // If this is a string, add it to the sequence. - sequence.push((start, end)); + sequence.push(range); } else if !matches!(tok, Tok::Comment(..) | Tok::NonLogicalNewline) { // Otherwise, consume the sequence. if !sequence.is_empty() { diff --git a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index 836516d890..6d6564af70 100644 --- a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -30,14 +30,11 @@ pub fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr: &Expr) } = &expr.node { if args.is_empty() && keywords.is_empty() { - let range = match_parens(func.end_location.unwrap(), checker.locator) + let range = match_parens(func.end(), checker.locator) .expect("Expected call to include parentheses"); let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, range); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::deletion( - func.end_location.unwrap(), - range.end_location, - )); + diagnostic.set_fix(Edit::deletion(func.end(), range.end())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_return/helpers.rs b/crates/ruff/src/rules/flake8_return/helpers.rs index baf29f4f30..5352fe9587 100644 --- a/crates/ruff/src/rules/flake8_return/helpers.rs +++ b/crates/ruff/src/rules/flake8_return/helpers.rs @@ -1,6 +1,6 @@ -use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt}; +use ruff_text_size::TextSize; +use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt}; -use ruff_python_ast::helpers::to_absolute; use ruff_python_ast::newlines::StrExt; use ruff_python_ast::source_code::Locator; @@ -28,24 +28,21 @@ pub fn result_exists(returns: &[(&Stmt, Option<&Expr>)]) -> bool { /// /// This method assumes that the statement is the last statement in its body; specifically, that /// the statement isn't followed by a semicolon, followed by a multi-line statement. -pub fn end_of_last_statement(stmt: &Stmt, locator: &Locator) -> Location { - let contents = locator.after(stmt.end_location.unwrap()); - +pub fn end_of_last_statement(stmt: &Stmt, locator: &Locator) -> TextSize { // End-of-file, so just return the end of the statement. - if contents.is_empty() { - return stmt.end_location.unwrap(); + if stmt.end() == locator.text_len() { + stmt.end() } - // Otherwise, find the end of the last line that's "part of" the statement. - for (lineno, line) in contents.universal_newlines().enumerate() { - if line.ends_with('\\') { - continue; - } - return to_absolute( - Location::new(lineno + 1, line.chars().count()), - stmt.end_location.unwrap(), - ); - } + else { + let contents = locator.after(stmt.end()); - unreachable!("Expected to find end-of-statement") + for line in contents.universal_newlines() { + if !line.ends_with('\\') { + return stmt.end() + line.end(); + } + } + + unreachable!("Expected to find end-of-statement") + } } diff --git a/crates/ruff/src/rules/flake8_return/rules.rs b/crates/ruff/src/rules/flake8_return/rules.rs index fda0985d85..6737a98ef1 100644 --- a/crates/ruff/src/rules/flake8_return/rules.rs +++ b/crates/ruff/src/rules/flake8_return/rules.rs @@ -1,12 +1,12 @@ use itertools::Itertools; -use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind}; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::elif_else_range; use ruff_python_ast::helpers::is_const_none; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::whitespace::indentation; use ruff_python_semantic::context::Context; @@ -140,13 +140,9 @@ fn unnecessary_return_none(checker: &mut Checker, stack: &Stack) { ) { continue; } - let mut diagnostic = Diagnostic::new(UnnecessaryReturnNone, Range::from(*stmt)); + let mut diagnostic = Diagnostic::new(UnnecessaryReturnNone, stmt.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "return".to_string(), - stmt.location, - stmt.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("return".to_string(), stmt.range())); } checker.diagnostics.push(diagnostic); } @@ -158,12 +154,11 @@ fn implicit_return_value(checker: &mut Checker, stack: &Stack) { if expr.is_some() { continue; } - let mut diagnostic = Diagnostic::new(ImplicitReturnValue, Range::from(*stmt)); + let mut diagnostic = Diagnostic::new(ImplicitReturnValue, stmt.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "return None".to_string(), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), )); } checker.diagnostics.push(diagnostic); @@ -211,7 +206,7 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { if let Some(last_stmt) = orelse.last() { implicit_return(checker, last_stmt); } else { - let mut diagnostic = Diagnostic::new(ImplicitReturn, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(ImplicitReturn, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(indent) = indentation(checker.locator, stmt) { let mut content = String::new(); @@ -249,7 +244,7 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { if let Some(last_stmt) = orelse.last() { implicit_return(checker, last_stmt); } else { - let mut diagnostic = Diagnostic::new(ImplicitReturn, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(ImplicitReturn, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(indent) = indentation(checker.locator, stmt) { let mut content = String::new(); @@ -288,7 +283,7 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { if is_noreturn_func(&checker.ctx, func) ) => {} _ => { - let mut diagnostic = Diagnostic::new(ImplicitReturn, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(ImplicitReturn, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(indent) = indentation(checker.locator, stmt) { let mut content = String::new(); @@ -318,18 +313,16 @@ fn has_multiple_assigns(id: &str, stack: &Stack) -> bool { /// Return `true` if the `id` has a (read) reference between the `return_location` and its /// preceding assignment. -fn has_refs_before_next_assign(id: &str, return_location: Location, stack: &Stack) -> bool { - let mut assignment_before_return: Option<&Location> = None; - let mut assignment_after_return: Option<&Location> = None; +fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack) -> bool { + let mut assignment_before_return: Option = None; + let mut assignment_after_return: Option = None; if let Some(assignments) = stack.assignments.get(&id) { for location in assignments.iter().sorted() { - if location.row() > return_location.row() { - assignment_after_return = Some(location); + if *location > return_range.start() { + assignment_after_return = Some(*location); break; } - if location.row() <= return_location.row() { - assignment_before_return = Some(location); - } + assignment_before_return = Some(*location); } } @@ -341,17 +334,18 @@ fn has_refs_before_next_assign(id: &str, return_location: Location, stack: &Stac if let Some(references) = stack.references.get(&id) { for location in references { - if location.row() == return_location.row() { + if return_range.contains(*location) { continue; } - if let Some(assignment_after_return) = assignment_after_return { - if assignment_before_return.row() < location.row() - && location.row() <= assignment_after_return.row() - { + + if assignment_before_return < *location { + if let Some(assignment_after_return) = assignment_after_return { + if *location <= assignment_after_return { + return true; + } + } else { return true; } - } else if assignment_before_return.row() < location.row() { - return true; } } } @@ -363,29 +357,27 @@ fn has_refs_before_next_assign(id: &str, return_location: Location, stack: &Stac fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool { if let Some(references) = stack.references.get(&id) { for location in references { - for (try_location, try_end_location) in &stack.tries { - if try_location.row() < location.row() && location.row() <= try_end_location.row() { + for try_range in &stack.tries { + if try_range.contains(*location) { return true; } } - for (loop_location, loop_end_location) in &stack.loops { - if loop_location.row() < location.row() && location.row() <= loop_end_location.row() - { + for loop_range in &stack.loops { + if loop_range.contains(*location) { return true; } } } } - if let Some(assignments) = stack.assignments.get(&id) { - for location in assignments { - for (try_location, try_end_location) in &stack.tries { - if try_location.row() < location.row() && location.row() <= try_end_location.row() { + if let Some(references) = stack.assignments.get(&id) { + for location in references { + for try_range in &stack.tries { + if try_range.contains(*location) { return true; } } - for (loop_location, loop_end_location) in &stack.loops { - if loop_location.row() < location.row() && location.row() <= loop_end_location.row() - { + for loop_range in &stack.loops { + if loop_range.contains(*location) { return true; } } @@ -404,12 +396,12 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) { if !stack.references.contains_key(id.as_str()) { checker .diagnostics - .push(Diagnostic::new(UnnecessaryAssign, Range::from(expr))); + .push(Diagnostic::new(UnnecessaryAssign, expr.range())); return; } if has_multiple_assigns(id, stack) - || has_refs_before_next_assign(id, expr.location, stack) + || has_refs_before_next_assign(id, expr.range(), stack) || has_refs_or_assigns_within_try_or_loop(id, stack) { return; @@ -421,7 +413,7 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) { checker .diagnostics - .push(Diagnostic::new(UnnecessaryAssign, Range::from(expr))); + .push(Diagnostic::new(UnnecessaryAssign, expr.range())); } } @@ -434,7 +426,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &Stmt, branch: Branch) -> if matches!(child.node, StmtKind::Return { .. }) { let diagnostic = Diagnostic::new( SuperfluousElseReturn { branch }, - elif_else_range(stmt, checker.locator).unwrap_or_else(|| Range::from(stmt)), + elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()), ); if checker.settings.rules.enabled(diagnostic.kind.rule()) { checker.diagnostics.push(diagnostic); @@ -444,7 +436,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &Stmt, branch: Branch) -> if matches!(child.node, StmtKind::Break) { let diagnostic = Diagnostic::new( SuperfluousElseBreak { branch }, - elif_else_range(stmt, checker.locator).unwrap_or_else(|| Range::from(stmt)), + elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()), ); if checker.settings.rules.enabled(diagnostic.kind.rule()) { checker.diagnostics.push(diagnostic); @@ -454,7 +446,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &Stmt, branch: Branch) -> if matches!(child.node, StmtKind::Raise { .. }) { let diagnostic = Diagnostic::new( SuperfluousElseRaise { branch }, - elif_else_range(stmt, checker.locator).unwrap_or_else(|| Range::from(stmt)), + elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()), ); if checker.settings.rules.enabled(diagnostic.kind.rule()) { checker.diagnostics.push(diagnostic); @@ -464,7 +456,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &Stmt, branch: Branch) -> if matches!(child.node, StmtKind::Continue) { let diagnostic = Diagnostic::new( SuperfluousElseContinue { branch }, - elif_else_range(stmt, checker.locator).unwrap_or_else(|| Range::from(stmt)), + elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()), ); if checker.settings.rules.enabled(diagnostic.kind.rule()) { checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_return/visitor.rs b/crates/ruff/src/rules/flake8_return/visitor.rs index f44b76124b..a1b0d018e9 100644 --- a/crates/ruff/src/rules/flake8_return/visitor.rs +++ b/crates/ruff/src/rules/flake8_return/visitor.rs @@ -1,5 +1,6 @@ +use ruff_text_size::{TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_parser::ast::{Expr, ExprKind, Location, Stmt, StmtKind}; +use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -10,11 +11,11 @@ pub struct Stack<'a> { pub yields: Vec<&'a Expr>, pub elses: Vec<&'a Stmt>, pub elifs: Vec<&'a Stmt>, - pub references: FxHashMap<&'a str, Vec>, + pub references: FxHashMap<&'a str, Vec>, pub non_locals: FxHashSet<&'a str>, - pub assignments: FxHashMap<&'a str, Vec>, - pub loops: Vec<(Location, Location)>, - pub tries: Vec<(Location, Location)>, + pub assignments: FxHashMap<&'a str, Vec>, + pub loops: Vec, + pub tries: Vec, } #[derive(Default)] @@ -37,7 +38,7 @@ impl<'a> ReturnVisitor<'a> { .assignments .entry(id) .or_insert_with(Vec::new) - .push(expr.location); + .push(expr.start()); return; } ExprKind::Attribute { .. } => { @@ -49,7 +50,7 @@ impl<'a> ReturnVisitor<'a> { .references .entry(name) .or_insert_with(Vec::new) - .push(expr.location); + .push(expr.start()); } } _ => {} @@ -129,7 +130,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { .references .entry(id) .or_insert_with(Vec::new) - .push(value.location); + .push(value.start()); } visitor::walk_expr(self, value); @@ -146,18 +147,14 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { } } StmtKind::For { .. } | StmtKind::AsyncFor { .. } | StmtKind::While { .. } => { - self.stack - .loops - .push((stmt.location, stmt.end_location.unwrap())); + self.stack.loops.push(stmt.range()); self.parents.push(stmt); visitor::walk_stmt(self, stmt); self.parents.pop(); } StmtKind::Try { .. } | StmtKind::TryStar { .. } => { - self.stack - .tries - .push((stmt.location, stmt.end_location.unwrap())); + self.stack.tries.push(stmt.range()); self.parents.push(stmt); visitor::walk_stmt(self, stmt); @@ -181,7 +178,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { .references .entry(name) .or_insert_with(Vec::new) - .push(expr.location); + .push(expr.start()); } } ExprKind::Name { id, .. } => { @@ -189,7 +186,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { .references .entry(id) .or_insert_with(Vec::new) - .push(expr.location); + .push(expr.start()); } ExprKind::YieldFrom { .. } | ExprKind::Yield { .. } => { self.stack.yields.push(expr); diff --git a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs index 119c29c6b9..be25d11ca3 100644 --- a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_ast::types::Range; use ruff_python_semantic::scope::ScopeKind; use crate::checkers::ast::Checker; @@ -122,7 +121,7 @@ pub fn private_member_access(checker: &mut Checker, expr: &Expr) { PrivateMemberAccess { access: attr.to_string(), }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs index f343fed399..5c2d571d6c 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -3,8 +3,9 @@ use std::iter; use itertools::Either::{Left, Right}; use itertools::Itertools; +use ruff_text_size::TextRange; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{Boolop, Cmpop, Expr, ExprContext, ExprKind, Location, Unaryop}; +use rustpython_parser::ast::{Boolop, Cmpop, Expr, ExprContext, ExprKind, Unaryop}; use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -13,7 +14,6 @@ use ruff_python_ast::helpers::{ contains_effect, create_expr, has_comments, unparse_expr, Truthiness, }; use ruff_python_ast::source_code::Stylist; -use ruff_python_ast::types::Range; use ruff_python_semantic::context::Context; use crate::checkers::ast::Checker; @@ -307,7 +307,7 @@ pub fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { }, fixable, }, - Range::from(expr), + expr.range(), ); if fixable && checker.patch(diagnostic.kind.rule()) { // Grab the types used in each duplicate `isinstance` call (e.g., `int` and `str` @@ -366,10 +366,9 @@ pub fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { // Populate the `Fix`. Replace the _entire_ `BoolOp`. Note that if we have // multiple duplicates, the fixes will conflict. - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&bool_op, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -451,7 +450,7 @@ pub fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { CompareWithTuple { replacement: unparse_expr(&in_expr, checker.stylist), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { let unmatched: Vec = values @@ -469,10 +468,9 @@ pub fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { values: iter::once(in_expr).chain(unmatched).collect(), }) }; - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&in_expr, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -518,14 +516,10 @@ pub fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { ExprAndNotExpr { name: id.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "False".to_string(), - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("False".to_string(), expr.range())); } checker.diagnostics.push(diagnostic); } @@ -572,14 +566,10 @@ pub fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { ExprOrNotExpr { name: id.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "True".to_string(), - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("True".to_string(), expr.range())); } checker.diagnostics.push(diagnostic); } @@ -589,8 +579,7 @@ pub fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { pub fn get_short_circuit_edit( expr: &Expr, - location: Location, - end_location: Location, + range: TextRange, truthiness: Truthiness, in_boolean_test: bool, stylist: &Stylist, @@ -606,7 +595,7 @@ pub fn get_short_circuit_edit( } else { unparse_expr(expr, stylist) }; - Edit::replacement(content, location, end_location) + Edit::range_replacement(content, range) } fn is_short_circuit( @@ -626,7 +615,7 @@ fn is_short_circuit( Boolop::Or => Truthiness::Truthy, }; - let mut location = expr.location; + let mut location = expr.start(); let mut edit = None; let mut remove = None; @@ -639,7 +628,7 @@ fn is_short_circuit( if value_truthiness.is_unknown() && (!context.in_boolean_test || contains_effect(value, |id| context.is_builtin(id))) { - location = next_value.location; + location = next_value.start(); continue; } @@ -648,15 +637,14 @@ fn is_short_circuit( // short-circuit expression is the first expression in the list; otherwise, we'll see it // as `next_value` before we see it as `value`. if value_truthiness == short_circuit_truthiness { - remove = Some(if location == value.location { + remove = Some(if location == value.start() { ContentAround::After } else { ContentAround::Both }); edit = Some(get_short_circuit_edit( value, - location, - expr.end_location.unwrap(), + TextRange::new(location, expr.end()), short_circuit_truthiness, context.in_boolean_test, stylist, @@ -674,8 +662,7 @@ fn is_short_circuit( }); edit = Some(get_short_circuit_edit( next_value, - location, - expr.end_location.unwrap(), + TextRange::new(location, expr.end()), short_circuit_truthiness, context.in_boolean_test, stylist, @@ -699,7 +686,7 @@ pub fn expr_or_true(checker: &mut Checker, expr: &Expr) { expr: edit.content().unwrap_or_default().to_string(), remove, }, - Range::new(edit.location(), edit.end_location()), + edit.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(edit); @@ -718,7 +705,7 @@ pub fn expr_and_false(checker: &mut Checker, expr: &Expr) { expr: edit.content().unwrap_or_default().to_string(), remove, }, - Range::new(edit.location(), edit.end_location()), + edit.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(edit); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs index cd21aa465f..d010dd109c 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_expr, unparse_expr}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -80,7 +79,7 @@ pub fn use_capital_environment_variables(checker: &mut Checker, expr: &Expr) { expected: capital_env_var, original: env_var.clone(), }, - Range::from(arg), + arg.range(), )); } @@ -110,17 +109,16 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { expected: capital_env_var.clone(), original: env_var.clone(), }, - Range::from(slice), + slice.range(), ); if checker.patch(diagnostic.kind.rule()) { let new_env_var = create_expr(ExprKind::Constant { value: capital_env_var.into(), kind: kind.clone(), }); - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&new_env_var, checker.stylist), - slice.location, - slice.end_location.unwrap(), + slice.range(), )); } checker.diagnostics.push(diagnostic); @@ -164,25 +162,21 @@ pub fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) { let expected = format!( "{}({})", - checker.locator.slice(func), - checker.locator.slice(key) + checker.locator.slice(func.range()), + checker.locator.slice(key.range()) ); - let original = checker.locator.slice(expr).to_string(); + let original = checker.locator.slice(expr.range()).to_string(); let mut diagnostic = Diagnostic::new( DictGetWithNoneDefault { expected: expected.clone(), original, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - expected, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(expected, expr.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs index 18450e83aa..1a33073175 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -1,4 +1,5 @@ use log::error; +use ruff_text_size::TextRange; use rustc_hash::FxHashSet; use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind}; use unicode_width::UnicodeWidthStr; @@ -11,7 +12,6 @@ use ruff_python_ast::helpers::{ has_comments_in, unparse_expr, unparse_stmt, }; use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; use ruff_python_semantic::context::Context; use crate::checkers::ast::Checker; @@ -276,7 +276,7 @@ pub fn nested_if_statements( }; let colon = first_colon_range( - Range::new(test.end_location.unwrap(), first_stmt.location), + TextRange::new(test.end(), first_stmt.start()), checker.locator, ); @@ -284,15 +284,15 @@ pub fn nested_if_statements( // the outer and inner if statements. let nested_if = &body[0]; let fixable = !has_comments_in( - Range::new(stmt.location, nested_if.location), + TextRange::new(stmt.start(), nested_if.start()), checker.locator, ); let mut diagnostic = Diagnostic::new( CollapsibleIf { fixable }, colon.map_or_else( - || Range::from(stmt), - |colon| Range::new(stmt.location, colon.end_location), + || stmt.range(), + |colon| TextRange::new(stmt.start(), colon.end()), ), ); if fixable && checker.patch(diagnostic.kind.rule()) { @@ -366,24 +366,23 @@ pub fn needless_bool(checker: &mut Checker, stmt: &Stmt) { && !has_comments(stmt, checker.locator) && (matches!(test.node, ExprKind::Compare { .. }) || checker.ctx.is_builtin("bool")); - let mut diagnostic = Diagnostic::new(NeedlessBool { condition, fixable }, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(NeedlessBool { condition, fixable }, stmt.range()); if fixable && checker.patch(diagnostic.kind.rule()) { if matches!(test.node, ExprKind::Compare { .. }) { // If the condition is a comparison, we can replace it with the condition. - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_stmt( &create_stmt(StmtKind::Return { value: Some(test.clone()), }), checker.stylist, ), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), )); } else { // Otherwise, we need to wrap the condition in a call to `bool`. (We've already // verified, above, that `bool` is a builtin.) - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_stmt( &create_stmt(StmtKind::Return { value: Some(Box::new(create_expr(ExprKind::Call { @@ -397,8 +396,7 @@ pub fn needless_bool(checker: &mut Checker, stmt: &Stmt) { }), checker.stylist, ), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), )); }; } @@ -513,7 +511,11 @@ pub fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<& let contents = unparse_stmt(&ternary, checker.stylist); // Don't flag if the resulting expression would exceed the maximum line length. - if stmt.location.column() + contents.width() > checker.settings.line_length { + let line_start = checker.locator.line_start(stmt.start()); + if checker.locator.contents()[TextRange::new(line_start, stmt.start())].width() + + contents.width() + > checker.settings.line_length + { return; } @@ -523,14 +525,10 @@ pub fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<& contents: contents.clone(), fixable, }, - Range::from(stmt), + stmt.range(), ); if fixable && checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - contents, - stmt.location, - stmt.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(contents, stmt.range())); } checker.diagnostics.push(diagnostic); } @@ -598,9 +596,9 @@ pub fn if_with_same_arms(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stm if compare_body(body, next_body) { checker.diagnostics.push(Diagnostic::new( IfWithSameArms, - Range::new( - if i == 0 { stmt.location } else { test.location }, - next_body.last().unwrap().end_location.unwrap(), + TextRange::new( + if i == 0 { stmt.start() } else { test.start() }, + next_body.last().unwrap().end(), ), )); } @@ -749,7 +747,7 @@ pub fn manual_dict_lookup( checker.diagnostics.push(Diagnostic::new( IfElseBlockInsteadOfDictLookup, - Range::from(stmt), + stmt.range(), )); } @@ -860,7 +858,11 @@ pub fn use_dict_get_with_default( ); // Don't flag if the resulting expression would exceed the maximum line length. - if stmt.location.column() + contents.width() > checker.settings.line_length { + let line_start = checker.locator.line_start(stmt.start()); + if checker.locator.contents()[TextRange::new(line_start, stmt.start())].width() + + contents.width() + > checker.settings.line_length + { return; } @@ -870,14 +872,10 @@ pub fn use_dict_get_with_default( contents: contents.clone(), fixable, }, - Range::from(stmt), + stmt.range(), ); if fixable && checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - contents, - stmt.location, - stmt.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(contents, stmt.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs index aedd8c9766..0d9ccc6f7d 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Unaryop}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_expr, unparse_expr}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -97,17 +96,16 @@ pub fn explicit_true_false_in_ifexpr( IfExprWithTrueFalse { expr: unparse_expr(test, checker.stylist), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { if matches!(test.node, ExprKind::Compare { .. }) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&test.clone(), checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } else if checker.ctx.is_builtin("bool") { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr( &create_expr(ExprKind::Call { func: Box::new(create_expr(ExprKind::Name { @@ -119,8 +117,7 @@ pub fn explicit_true_false_in_ifexpr( }), checker.stylist, ), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); }; } @@ -152,10 +149,10 @@ pub fn explicit_false_true_in_ifexpr( IfExprWithFalseTrue { expr: unparse_expr(test, checker.stylist), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr( &create_expr(ExprKind::UnaryOp { op: Unaryop::Not, @@ -163,8 +160,7 @@ pub fn explicit_false_true_in_ifexpr( }), checker.stylist, ), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -201,10 +197,10 @@ pub fn twisted_arms_in_ifexpr( expr_body: unparse_expr(body, checker.stylist), expr_else: unparse_expr(orelse, checker.stylist), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr( &create_expr(ExprKind::IfExp { test: Box::new(create_expr(orelse.node.clone())), @@ -213,8 +209,7 @@ pub fn twisted_arms_in_ifexpr( }), checker.stylist, ), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs index c187dc4afe..1b978989cf 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Cmpop, Expr, ExprKind, Stmt, StmtKind, Unaryop}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_expr, unparse_expr}; -use ruff_python_ast::types::Range; use ruff_python_semantic::scope::ScopeKind; use crate::checkers::ast::Checker; @@ -105,10 +104,10 @@ pub fn negation_with_equal_op(checker: &mut Checker, expr: &Expr, op: &Unaryop, left: unparse_expr(left, checker.stylist), right: unparse_expr(&comparators[0], checker.stylist), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr( &create_expr(ExprKind::Compare { left: left.clone(), @@ -117,8 +116,7 @@ pub fn negation_with_equal_op(checker: &mut Checker, expr: &Expr, op: &Unaryop, }), checker.stylist, ), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -156,10 +154,10 @@ pub fn negation_with_not_equal_op( left: unparse_expr(left, checker.stylist), right: unparse_expr(&comparators[0], checker.stylist), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr( &create_expr(ExprKind::Compare { left: left.clone(), @@ -168,8 +166,7 @@ pub fn negation_with_not_equal_op( }), checker.stylist, ), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -191,13 +188,12 @@ pub fn double_negation(checker: &mut Checker, expr: &Expr, op: &Unaryop, operand DoubleNegation { expr: unparse_expr(operand, checker.stylist), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(operand, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs index 457d12cb99..00fb633d2f 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs @@ -1,4 +1,5 @@ use log::error; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Located, Stmt, StmtKind, Withitem}; use unicode_width::UnicodeWidthStr; @@ -7,7 +8,6 @@ use ruff_diagnostics::{AutofixKind, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{first_colon_range, has_comments_in}; use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -85,27 +85,24 @@ pub fn multiple_with_statements( if let Some((items, body)) = find_last_with(with_body) { let last_item = items.last().expect("Expected items to be non-empty"); let colon = first_colon_range( - Range::new( + TextRange::new( last_item .optional_vars .as_ref() - .map_or(last_item.context_expr.end_location, |v| v.end_location) - .unwrap(), - body.first() - .expect("Expected body to be non-empty") - .location, + .map_or(last_item.context_expr.end(), |v| v.end()), + body.first().expect("Expected body to be non-empty").start(), ), checker.locator, ); let fixable = !has_comments_in( - Range::new(with_stmt.location, with_body[0].location), + TextRange::new(with_stmt.start(), with_body[0].start()), checker.locator, ); let mut diagnostic = Diagnostic::new( MultipleWithStatements { fixable }, colon.map_or_else( - || Range::from(with_stmt), - |colon| Range::new(with_stmt.location, colon.end_location), + || with_stmt.range(), + |colon| TextRange::new(with_stmt.start(), colon.end()), ), ); if fixable && checker.patch(diagnostic.kind.rule()) { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs index 252de4f18a..14a808c8ea 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs @@ -4,11 +4,10 @@ use libcst_native::{ LeftParen, ParenthesizableWhitespace, ParenthesizedNode, RightParen, SimpleWhitespace, Statement, Suite, }; -use rustpython_parser::ast::Location; +use std::borrow::Cow; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace; use crate::cst::matchers::match_module; @@ -40,10 +39,7 @@ pub(crate) fn fix_nested_if_statements( }; // Extract the module text. - let contents = locator.slice(Range::new( - Location::new(stmt.location.row(), 0), - Location::new(stmt.end_location.unwrap().row() + 1, 0), - )); + let contents = locator.lines(stmt.range()); // Handle `elif` blocks differently; detect them upfront. let is_elif = contents.trim_start().starts_with("elif"); @@ -51,9 +47,9 @@ pub(crate) fn fix_nested_if_statements( // If this is an `elif`, we have to remove the `elif` keyword for now. (We'll // restore the `el` later on.) let module_text = if is_elif { - contents.replacen("elif", "if", 1) + Cow::Owned(contents.replacen("elif", "if", 1)) } else { - contents.to_string() + Cow::Borrowed(contents) }; // If the block is indented, "embed" it in a function definition, to preserve @@ -62,7 +58,10 @@ pub(crate) fn fix_nested_if_statements( let module_text = if outer_indent.is_empty() { module_text } else { - format!("def f():{}{module_text}", stylist.line_ending().as_str()) + Cow::Owned(format!( + "def f():{}{module_text}", + stylist.line_ending().as_str() + )) }; // Parse the CST. @@ -130,14 +129,11 @@ pub(crate) fn fix_nested_if_statements( .unwrap() }; let contents = if is_elif { - module_text.replacen("if", "elif", 1) + Cow::Owned(module_text.replacen("if", "elif", 1)) } else { - module_text.to_string() + Cow::Borrowed(module_text) }; - Ok(Edit::replacement( - contents, - Location::new(stmt.location.row(), 0), - Location::new(stmt.end_location.unwrap().row() + 1, 0), - )) + let range = locator.lines_range(stmt.range()); + Ok(Edit::range_replacement(contents.to_string(), range)) } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs index 952b59c01e..b81232881d 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs @@ -1,10 +1,8 @@ use anyhow::{bail, Result}; use libcst_native::{Codegen, CodegenState, CompoundStatement, Statement, Suite, With}; -use rustpython_parser::ast::Location; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace; use crate::cst::matchers::match_module; @@ -21,10 +19,7 @@ pub(crate) fn fix_multiple_with_statements( }; // Extract the module text. - let contents = locator.slice(Range::new( - Location::new(stmt.location.row(), 0), - Location::new(stmt.end_location.unwrap().row() + 1, 0), - )); + let contents = locator.lines(stmt.range()); // If the block is indented, "embed" it in a function definition, to preserve // indentation while retaining valid source code. (We'll strip the prefix later @@ -95,9 +90,7 @@ pub(crate) fn fix_multiple_with_statements( .to_string() }; - Ok(Edit::replacement( - contents, - Location::new(stmt.location.row(), 0), - Location::new(stmt.end_location.unwrap().row() + 1, 0), - )) + let range = locator.lines_range(stmt.range()); + + Ok(Edit::range_replacement(contents, range)) } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs index 9dea6d6f9b..eebd1131f0 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -1,13 +1,13 @@ use anyhow::Result; use libcst_native::{Codegen, CodegenState}; use log::error; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Cmpop, Expr, ExprKind}; use ruff_diagnostics::Edit; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::cst::matchers::{match_attribute, match_call, match_expression}; @@ -37,7 +37,7 @@ fn get_value_content_for_key_in_dict( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, ) -> Result { - let content = locator.slice(expr); + let content = locator.slice(expr.range()); let mut expression = match_expression(content)?; let call = match_call(&mut expression)?; let attribute = match_attribute(&mut call.func)?; @@ -53,7 +53,7 @@ fn get_value_content_for_key_in_dict( } /// SIM118 -fn key_in_dict(checker: &mut Checker, left: &Expr, right: &Expr, range: Range) { +fn key_in_dict(checker: &mut Checker, left: &Expr, right: &Expr, range: TextRange) { let ExprKind::Call { func, args, @@ -73,7 +73,7 @@ fn key_in_dict(checker: &mut Checker, left: &Expr, right: &Expr, range: Range) { } // Slice exact content to preserve formatting. - let left_content = checker.locator.slice(left); + let left_content = checker.locator.slice(left.range()); let value_content = match get_value_content_for_key_in_dict(checker.locator, checker.stylist, right) { Ok(value_content) => value_content, @@ -91,11 +91,7 @@ fn key_in_dict(checker: &mut Checker, left: &Expr, right: &Expr, range: Range) { range, ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - value_content, - right.location, - right.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(value_content, right.range())); } checker.diagnostics.push(diagnostic); } @@ -106,7 +102,7 @@ pub fn key_in_dict_for(checker: &mut Checker, target: &Expr, iter: &Expr) { checker, target, iter, - Range::new(target.location, iter.end_location.unwrap()), + TextRange::new(target.start(), iter.end()), ); } @@ -127,5 +123,5 @@ pub fn key_in_dict_compare( } let right = comparators.first().unwrap(); - key_in_dict(checker, left, right, Range::from(expr)); + key_in_dict(checker, left, right, expr.range()); } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs b/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs index 32aa270c91..cc315cffe4 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -112,10 +111,9 @@ pub fn open_file_with_context_handler(checker: &mut Checker, func: &Expr) { return; } - checker.diagnostics.push(Diagnostic::new( - OpenFileWithContextHandler, - Range::from(func), - )); + checker + .diagnostics + .push(Diagnostic::new(OpenFileWithContextHandler, func.range())); } } } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index 4ceb5c49b5..ab31839c26 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -1,5 +1,6 @@ +use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{ - Cmpop, Comprehension, Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind, Unaryop, + Cmpop, Comprehension, Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind, Unaryop, }; use unicode_width::UnicodeWidthStr; @@ -7,7 +8,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_expr, create_stmt, unparse_stmt}; use ruff_python_ast::source_code::Stylist; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; @@ -36,7 +36,7 @@ struct Loop<'a> { test: &'a Expr, target: &'a Expr, iter: &'a Expr, - terminal: Location, + terminal: TextSize, } /// Extract the returned boolean values a `StmtKind::For` with an `else` body. @@ -99,7 +99,7 @@ fn return_values_for_else(stmt: &Stmt) -> Option { test: nested_test, target, iter, - terminal: stmt.end_location.unwrap(), + terminal: stmt.end(), }) } @@ -164,7 +164,7 @@ fn return_values_for_siblings<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option checker.settings.line_length { + let line_start = checker.locator.line_start(stmt.start()); + if checker.locator.contents()[TextRange::new(line_start, stmt.start())].width() + + contents.width() + > checker.settings.line_length + { return; } @@ -220,12 +224,12 @@ pub fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt, sibling: ReimplementedBuiltin { repl: contents.clone(), }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) && checker.ctx.is_builtin("any") { diagnostic.set_fix(Edit::replacement( contents, - stmt.location, + stmt.start(), loop_info.terminal, )); } @@ -289,7 +293,11 @@ pub fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt, sibling: ); // Don't flag if the resulting expression would exceed the maximum line length. - if stmt.location.column() + contents.width() > checker.settings.line_length { + let line_start = checker.locator.line_start(stmt.start()); + if checker.locator.contents()[TextRange::new(line_start, stmt.start())].width() + + contents.width() + > checker.settings.line_length + { return; } @@ -297,12 +305,12 @@ pub fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt, sibling: ReimplementedBuiltin { repl: contents.clone(), }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) && checker.ctx.is_builtin("all") { diagnostic.set_fix(Edit::replacement( contents, - stmt.location, + stmt.start(), loop_info.terminal, )); } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs b/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs index 38cbc9e50c..159c104cd3 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -39,7 +38,7 @@ pub fn return_in_try_except_finally( if try_has_return || except_has_return { checker.diagnostics.push(Diagnostic::new( ReturnInTryExceptFinally, - Range::from(finally_return), + finally_return.range(), )); } } 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 aceb8874e6..faea06b192 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -1,12 +1,12 @@ +use ruff_text_size::{TextLen, TextRange}; 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}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::compose_call_path; use ruff_python_ast::helpers; -use ruff_python_ast::types::Range; use crate::autofix::actions::get_or_import_symbol; use crate::checkers::ast::Checker; @@ -86,7 +86,7 @@ pub fn suppressible_exception( SuppressibleException { exception: exception.clone(), }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { @@ -98,15 +98,12 @@ pub fn suppressible_exception( &checker.importer, checker.locator, )?; - let try_ending = stmt.location.with_col_offset(3); // size of "try" - let replace_try = Edit::replacement( + let replace_try = Edit::range_replacement( format!("with {binding}({exception})"), - stmt.location, - try_ending, + TextRange::at(stmt.start(), "try".text_len()), ); - let handler_line_begin = Location::new(handler.location.row(), 0); - let remove_handler = - Edit::deletion(handler_line_begin, handler.end_location.unwrap()); + let handler_line_begin = checker.locator.line_start(handler.start()); + let remove_handler = Edit::deletion(handler_line_begin, handler.end()); Ok(Fix::from_iter([import_edit, replace_try, remove_handler])) }); } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs index e06806856a..f475588905 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -5,7 +5,6 @@ use rustpython_parser::ast::{Cmpop, Expr, ExprKind, Unaryop}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use ruff_python_stdlib::str::{self}; use crate::checkers::ast::Checker; @@ -60,7 +59,7 @@ fn is_constant_like(expr: &Expr) -> bool { /// Generate a fix to reverse a comparison. fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Result { - let range = Range::from(expr); + let range = expr.range(); let contents = locator.slice(range); let mut expression = match_expression(contents)?; @@ -159,20 +158,16 @@ pub fn yoda_conditions( YodaConditions { suggestion: Some(suggestion.to_string()), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - suggestion, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(suggestion, expr.range())); } checker.diagnostics.push(diagnostic); } else { checker.diagnostics.push(Diagnostic::new( YodaConditions { suggestion: None }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_tidy_imports/banned_api.rs b/crates/ruff/src/rules/flake8_tidy_imports/banned_api.rs index 56bc0ea952..0e01b03932 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/banned_api.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/banned_api.rs @@ -3,12 +3,10 @@ use rustpython_parser::ast::{Expr, Located}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation, CacheKey}; use ruff_python_ast::call_path::from_qualified_name; -use ruff_python_ast::types::Range; - -use crate::checkers::ast::Checker; pub type Settings = FxHashMap; @@ -59,7 +57,7 @@ pub fn name_is_banned(checker: &mut Checker, name: String, located: &Located< name, message: ban.msg.to_string(), }, - Range::from(located), + located.range(), )); } } @@ -75,7 +73,7 @@ pub fn name_or_parent_is_banned(checker: &mut Checker, name: &str, located: & name: name.to_string(), message: ban.msg.to_string(), }, - Range::from(located), + located.range(), )); return; } @@ -101,7 +99,7 @@ pub fn banned_attribute_access(checker: &mut Checker, expr: &Expr) { name: banned_path.to_string(), message: ban.msg.to_string(), }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs b/crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs index 04aaa7b049..bdc706e777 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs @@ -6,7 +6,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation, CacheKey}; use ruff_python_ast::helpers::{create_stmt, resolve_imported_module_path, unparse_stmt}; use ruff_python_ast::source_code::Stylist; -use ruff_python_ast::types::Range; use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; @@ -116,11 +115,7 @@ fn fix_banned_relative_import( stylist, ); - Some(Edit::replacement( - content, - stmt.location, - stmt.end_location.unwrap(), - )) + Some(Edit::range_replacement(content, stmt.range())) } /// TID252 @@ -141,7 +136,7 @@ pub fn banned_relative_import( RelativeImports { strictness: *strictness, }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { if let Some(fix) = diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs index c310074ce2..35d1d0aa45 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::{Range, RefEquality}; +use ruff_python_ast::types::RefEquality; use crate::autofix::actions::delete_stmt; use crate::checkers::ast::Checker; @@ -56,7 +56,7 @@ pub fn empty_type_checking_block<'a, 'b>( 'b: 'a, { if body.len() == 1 && matches!(body[0].node, StmtKind::Pass) { - let mut diagnostic = Diagnostic::new(EmptyTypeCheckingBlock, Range::from(&body[0])); + let mut diagnostic = Diagnostic::new(EmptyTypeCheckingBlock, body[0].range()); // Delete the entire type-checking block. if checker.patch(diagnostic.kind.rule()) { diff --git a/crates/ruff/src/rules/flake8_use_pathlib/helpers.rs b/crates/ruff/src/rules/flake8_use_pathlib/helpers.rs index 6d71a4c0a5..4a9378db09 100644 --- a/crates/ruff/src/rules/flake8_use_pathlib/helpers.rs +++ b/crates/ruff/src/rules/flake8_use_pathlib/helpers.rs @@ -1,8 +1,5 @@ use rustpython_parser::ast::Expr; -use ruff_diagnostics::{Diagnostic, DiagnosticKind}; -use ruff_python_ast::types::Range; - use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_use_pathlib::violations::{ @@ -12,6 +9,7 @@ use crate::rules::flake8_use_pathlib::violations::{ OsRmdir, OsStat, OsUnlink, PathlibReplace, PyPath, }; use crate::settings::types::PythonVersion; +use ruff_diagnostics::{Diagnostic, DiagnosticKind}; pub fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) { if let Some(diagnostic_kind) = @@ -51,7 +49,7 @@ pub fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) { _ => None, }) { - let diagnostic = Diagnostic::new::(diagnostic_kind, Range::from(expr)); + let diagnostic = Diagnostic::new::(diagnostic_kind, expr.range()); if checker.settings.rules.enabled(diagnostic.kind.rule()) { checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/isort/annotate.rs b/crates/ruff/src/rules/isort/annotate.rs index 45083db651..a2d0866f13 100644 --- a/crates/ruff/src/rules/isort/annotate.rs +++ b/crates/ruff/src/rules/isort/annotate.rs @@ -1,3 +1,4 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Stmt, StmtKind}; use ruff_python_ast::source_code::Locator; @@ -13,111 +14,120 @@ pub fn annotate_imports<'a>( locator: &Locator, split_on_trailing_comma: bool, ) -> Vec> { - let mut annotated = vec![]; let mut comments_iter = comments.into_iter().peekable(); - for import in imports { - match &import.node { - StmtKind::Import { names } => { - // Find comments above. - let mut atop = vec![]; - while let Some(comment) = - comments_iter.next_if(|comment| comment.location.row() < import.location.row()) - { - atop.push(comment); - } - // Find comments inline. - let mut inline = vec![]; - while let Some(comment) = comments_iter.next_if(|comment| { - comment.end_location.row() == import.end_location.unwrap().row() - }) { - inline.push(comment); - } - - annotated.push(AnnotatedImport::Import { - names: names - .iter() - .map(|alias| AliasData { - name: &alias.node.name, - asname: alias.node.asname.as_deref(), - }) - .collect(), - atop, - inline, - }); - } - StmtKind::ImportFrom { - module, - names, - level, - } => { - // Find comments above. - let mut atop = vec![]; - while let Some(comment) = - comments_iter.next_if(|comment| comment.location.row() < import.location.row()) - { - atop.push(comment); - } - - // Find comments inline. - // We associate inline comments with the import statement unless there's a - // single member, and it's a single-line import (like `from foo - // import bar # noqa`). - let mut inline = vec![]; - if names.len() > 1 - || names - .first() - .map_or(false, |alias| alias.location.row() > import.location.row()) - { - while let Some(comment) = comments_iter - .next_if(|comment| comment.location.row() == import.location.row()) - { - inline.push(comment); - } - } - - // Capture names. - let mut aliases = vec![]; - for alias in names { + imports + .iter() + .map(|import| { + match &import.node { + StmtKind::Import { names } => { // Find comments above. - let mut alias_atop = vec![]; - while let Some(comment) = comments_iter - .next_if(|comment| comment.location.row() < alias.location.row()) + let mut atop = vec![]; + while let Some(comment) = + comments_iter.next_if(|comment| comment.start() < import.start()) { - alias_atop.push(comment); + atop.push(comment); } // Find comments inline. - let mut alias_inline = vec![]; - while let Some(comment) = comments_iter.next_if(|comment| { - comment.end_location.row() == alias.end_location.unwrap().row() - }) { - alias_inline.push(comment); + let mut inline = vec![]; + let import_line_end = locator.line_end(import.end()); + + while let Some(comment) = + comments_iter.next_if(|comment| comment.end() <= import_line_end) + { + inline.push(comment); } - aliases.push(AnnotatedAliasData { - name: &alias.node.name, - asname: alias.node.asname.as_deref(), - atop: alias_atop, - inline: alias_inline, - }); + AnnotatedImport::Import { + names: names + .iter() + .map(|alias| AliasData { + name: &alias.node.name, + asname: alias.node.asname.as_deref(), + }) + .collect(), + atop, + inline, + } } + StmtKind::ImportFrom { + module, + names, + level, + } => { + // Find comments above. + let mut atop = vec![]; + while let Some(comment) = + comments_iter.next_if(|comment| comment.start() < import.start()) + { + atop.push(comment); + } - annotated.push(AnnotatedImport::ImportFrom { - module: module.as_deref(), - names: aliases, - level: *level, - trailing_comma: if split_on_trailing_comma { - trailing_comma(import, locator) - } else { - TrailingComma::default() - }, - atop, - inline, - }); + // Find comments inline. + // We associate inline comments with the import statement unless there's a + // single member, and it's a single-line import (like `from foo + // import bar # noqa`). + let mut inline = vec![]; + if names.len() > 1 + || names.first().map_or(false, |alias| { + locator + .contains_line_break(TextRange::new(import.start(), alias.start())) + }) + { + let import_start_line_end = locator.line_end(import.start()); + while let Some(comment) = + comments_iter.next_if(|comment| comment.end() <= import_start_line_end) + { + inline.push(comment); + } + } + + // Capture names. + let aliases = names + .iter() + .map(|alias| { + // Find comments above. + let mut alias_atop = vec![]; + while let Some(comment) = + comments_iter.next_if(|comment| comment.start() < alias.start()) + { + alias_atop.push(comment); + } + + // Find comments inline. + let mut alias_inline = vec![]; + let alias_line_end = locator.line_end(alias.end()); + while let Some(comment) = + comments_iter.next_if(|comment| comment.end() <= alias_line_end) + { + alias_inline.push(comment); + } + + AnnotatedAliasData { + name: &alias.node.name, + asname: alias.node.asname.as_deref(), + atop: alias_atop, + inline: alias_inline, + } + }) + .collect(); + + AnnotatedImport::ImportFrom { + module: module.as_deref(), + names: aliases, + level: *level, + trailing_comma: if split_on_trailing_comma { + trailing_comma(import, locator) + } else { + TrailingComma::default() + }, + atop, + inline, + } + } + _ => panic!("Expected StmtKind::Import | StmtKind::ImportFrom"), } - _ => panic!("Expected StmtKind::Import | StmtKind::ImportFrom"), - } - } - annotated + }) + .collect() } diff --git a/crates/ruff/src/rules/isort/comments.rs b/crates/ruff/src/rules/isort/comments.rs index 3a44763635..3bc02d2f42 100644 --- a/crates/ruff/src/rules/isort/comments.rs +++ b/crates/ruff/src/rules/isort/comments.rs @@ -1,29 +1,40 @@ +use ruff_text_size::{TextRange, TextSize}; use std::borrow::Cow; -use rustpython_parser::ast::Location; use rustpython_parser::{lexer, Mode, Tok}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; #[derive(Debug)] pub struct Comment<'a> { pub value: Cow<'a, str>, - pub location: Location, - pub end_location: Location, + pub range: TextRange, +} + +impl Comment<'_> { + pub const fn start(&self) -> TextSize { + self.range.start() + } + + pub const fn end(&self) -> TextSize { + self.range.end() + } + + pub const fn range(&self) -> TextRange { + self.range + } } /// Collect all comments in an import block. -pub fn collect_comments<'a>(range: Range, locator: &'a Locator) -> Vec> { +pub fn collect_comments<'a>(range: TextRange, locator: &'a Locator) -> Vec> { let contents = locator.slice(range); - lexer::lex_located(contents, Mode::Module, range.location) + lexer::lex_located(contents, Mode::Module, range.start()) .flatten() - .filter_map(|(start, tok, end)| { + .filter_map(|(tok, range)| { if let Tok::Comment(value) = tok { Some(Comment { value: value.into(), - location: start, - end_location: end, + range, }) } else { None diff --git a/crates/ruff/src/rules/isort/helpers.rs b/crates/ruff/src/rules/isort/helpers.rs index e8ec76e893..ef25643a00 100644 --- a/crates/ruff/src/rules/isort/helpers.rs +++ b/crates/ruff/src/rules/isort/helpers.rs @@ -9,10 +9,10 @@ use crate::rules::isort::types::TrailingComma; /// Return `true` if a `StmtKind::ImportFrom` statement ends with a magic /// trailing comma. pub fn trailing_comma(stmt: &Stmt, locator: &Locator) -> TrailingComma { - let contents = locator.slice(stmt); + let contents = locator.slice(stmt.range()); let mut count: usize = 0; let mut trailing_comma = TrailingComma::Absent; - for (_, tok, _) in lexer::lex_located(contents, Mode::Module, stmt.location).flatten() { + for (tok, _) in lexer::lex_located(contents, Mode::Module, stmt.start()).flatten() { if matches!(tok, Tok::Lpar) { count += 1; } @@ -62,7 +62,7 @@ pub fn has_comment_break(stmt: &Stmt, locator: &Locator) -> bool { // # Direct comment. // def f(): pass let mut seen_blank = false; - for line in locator.up_to(stmt.location).universal_newlines().rev() { + for line in locator.up_to(stmt.start()).universal_newlines().rev() { let line = line.trim(); if seen_blank { if line.starts_with('#') { diff --git a/crates/ruff/src/rules/isort/mod.rs b/crates/ruff/src/rules/isort/mod.rs index da3c80a94e..5fa1c2ce12 100644 --- a/crates/ruff/src/rules/isort/mod.rs +++ b/crates/ruff/src/rules/isort/mod.rs @@ -282,6 +282,8 @@ mod tests { use std::path::Path; use anyhow::Result; + + use crate::message::Message; use rustc_hash::FxHashMap; use test_case::test_case; @@ -574,7 +576,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } @@ -602,7 +604,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } @@ -632,7 +634,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } @@ -660,7 +662,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } @@ -680,7 +682,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } @@ -815,7 +817,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } @@ -840,7 +842,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } @@ -861,7 +863,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } @@ -880,7 +882,7 @@ mod tests { ..Settings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Message::start); assert_messages!(snapshot, diagnostics); Ok(()) } diff --git a/crates/ruff/src/rules/isort/rules/add_required_imports.rs b/crates/ruff/src/rules/isort/rules/add_required_imports.rs index a02d5b509d..2696e05f65 100644 --- a/crates/ruff/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff/src/rules/isort/rules/add_required_imports.rs @@ -1,13 +1,13 @@ use log::error; +use ruff_text_size::TextRange; use rustpython_parser as parser; -use rustpython_parser::ast::{Location, StmtKind, Suite}; +use rustpython_parser::ast::{StmtKind, Suite}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::imports::{Alias, AnyImport, FutureImport, Import, ImportFrom}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use crate::importer::Importer; use crate::registry::Rule; @@ -117,7 +117,7 @@ fn add_required_import( // Always insert the diagnostic at top-of-file. let mut diagnostic = Diagnostic::new( MissingRequiredImport(required_import.to_string()), - Range::new(Location::default(), Location::default()), + TextRange::default(), ); if autofix.into() && settings.rules.should_fix(Rule::MissingRequiredImport) { diagnostic.set_fix(Importer::new(python_ast, locator, stylist).add_import(required_import)); diff --git a/crates/ruff/src/rules/isort/rules/organize_imports.rs b/crates/ruff/src/rules/isort/rules/organize_imports.rs index b5e1e53ede..46bcba3d8e 100644 --- a/crates/ruff/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff/src/rules/isort/rules/organize_imports.rs @@ -1,16 +1,16 @@ use std::path::Path; use itertools::{EitherOrBoth, Itertools}; -use rustpython_parser::ast::{Location, Stmt}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::Stmt; use textwrap::indent; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{ - count_trailing_lines, followed_by_multi_statement_line, preceded_by_multi_statement_line, + followed_by_multi_statement_line, preceded_by_multi_statement_line, trailing_lines_end, }; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace::leading_space; use crate::registry::AsRule; @@ -51,15 +51,16 @@ impl AlwaysAutofixableViolation for UnsortedImports { } } -fn extract_range(body: &[&Stmt]) -> Range { - let location = body.first().unwrap().location; - let end_location = body.last().unwrap().end_location.unwrap(); - Range::new(location, end_location) +fn extract_range(body: &[&Stmt]) -> TextRange { + let location = body.first().unwrap().start(); + let end_location = body.last().unwrap().end(); + TextRange::new(location, end_location) } -fn extract_indentation_range(body: &[&Stmt]) -> Range { - let location = body.first().unwrap().location; - Range::new(Location::new(location.row(), 0), location) +fn extract_indentation_range(body: &[&Stmt], locator: &Locator) -> TextRange { + let location = body.first().unwrap().start(); + + TextRange::new(locator.line_start(location), location) } /// Compares two strings, returning true if they are equal modulo whitespace @@ -84,7 +85,7 @@ pub fn organize_imports( autofix: flags::Autofix, package: Option<&Path>, ) -> Option { - let indentation = locator.slice(extract_indentation_range(&block.imports)); + let indentation = locator.slice(extract_indentation_range(&block.imports, locator)); let indentation = leading_space(indentation); let range = extract_range(&block.imports); @@ -99,17 +100,14 @@ pub fn organize_imports( // Extract comments. Take care to grab any inline comments from the last line. let comments = comments::collect_comments( - Range::new( - range.location, - Location::new(range.end_location.row() + 1, 0), - ), + TextRange::new(range.start(), locator.full_line_end(range.end())), locator, ); - let num_trailing_lines = if block.trailer.is_none() { - 0 + let trailing_line_end = if block.trailer.is_none() { + locator.full_line_end(range.end()) } else { - count_trailing_lines(block.imports.last().unwrap(), locator) + trailing_lines_end(block.imports.last().unwrap(), locator) }; // Generate the sorted import block. @@ -143,20 +141,16 @@ pub fn organize_imports( ); // Expand the span the entire range, including leading and trailing space. - let range = Range::new( - Location::new(range.location.row(), 0), - Location::new(range.end_location.row() + 1 + num_trailing_lines, 0), - ); + let range = TextRange::new(locator.line_start(range.start()), trailing_line_end); let actual = locator.slice(range); if matches_ignoring_indentation(actual, &expected) { None } else { let mut diagnostic = Diagnostic::new(UnsortedImports, range); if autofix.into() && settings.rules.should_fix(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( indent(&expected, indentation), - range.location, - range.end_location, + range, )); } Some(diagnostic) diff --git a/crates/ruff/src/rules/isort/track.rs b/crates/ruff/src/rules/isort/track.rs index 06405bffdb..cb590ef5c6 100644 --- a/crates/ruff/src/rules/isort/track.rs +++ b/crates/ruff/src/rules/isort/track.rs @@ -1,3 +1,4 @@ +use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{ Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, StmtKind, @@ -26,10 +27,10 @@ pub struct Block<'a> { pub struct ImportTracker<'a> { locator: &'a Locator<'a>, - directives: &'a IsortDirectives, is_stub: bool, blocks: Vec>, - split_index: usize, + splits: &'a [TextSize], + exclusions: &'a [TextRange], nested: bool, } @@ -37,10 +38,10 @@ impl<'a> ImportTracker<'a> { pub fn new(locator: &'a Locator<'a>, directives: &'a IsortDirectives, is_stub: bool) -> Self { Self { locator, - directives, is_stub, blocks: vec![Block::default()], - split_index: 0, + splits: &directives.splits, + exclusions: &directives.exclusions, nested: false, } } @@ -116,20 +117,31 @@ where { fn visit_stmt(&mut self, stmt: &'b Stmt) { // Track manual splits. - while self.split_index < self.directives.splits.len() { - if stmt.location.row() >= self.directives.splits[self.split_index] { + for (index, split) in self.splits.iter().enumerate() { + if stmt.end() >= *split { self.finalize(self.trailer_for(stmt)); - self.split_index += 1; + self.splits = &self.splits[index + 1..]; } else { break; } } + // Test if the statement is in an excluded range + let mut is_excluded = false; + for (index, exclusion) in self.exclusions.iter().enumerate() { + if exclusion.end() < stmt.start() { + self.exclusions = &self.exclusions[index + 1..]; + } else { + is_excluded = exclusion.contains(stmt.start()); + break; + } + } + // Track imports. if matches!( stmt.node, StmtKind::Import { .. } | StmtKind::ImportFrom { .. } - ) && !self.directives.exclusions.contains(&stmt.location.row()) + ) && !is_excluded { self.track_import(stmt); } else { diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs index f94e894e77..b4b59df98a 100644 --- a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -68,18 +67,17 @@ pub fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) { NumpyDeprecatedTypeAlias { type_name: type_name.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( match type_name { "unicode" => "str", "long" => "int", _ => type_name, } .to_string(), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs b/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs index 3e8a340abf..f08405aea8 100644 --- a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs +++ b/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs @@ -1,10 +1,8 @@ use rustpython_parser::ast::Expr; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - -use crate::checkers::ast::Checker; /// ## What it does /// Checks for the use of legacy `np.random` function calls. @@ -122,7 +120,7 @@ pub fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) { NumpyLegacyRandom { method_name: method_name.to_string(), }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/pandas_vet/fixes.rs b/crates/ruff/src/rules/pandas_vet/fixes.rs index 4b610093fd..360d9ef7a6 100644 --- a/crates/ruff/src/rules/pandas_vet/fixes.rs +++ b/crates/ruff/src/rules/pandas_vet/fixes.rs @@ -1,4 +1,5 @@ -use rustpython_parser::ast::{Expr, ExprKind, Keyword, Location}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{Expr, ExprKind, Keyword}; use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::source_code::Locator; @@ -21,21 +22,19 @@ fn match_name(expr: &Expr) -> Option<&str> { pub(super) fn convert_inplace_argument_to_assignment( locator: &Locator, expr: &Expr, - violation_at: Location, - violation_end: Location, + violation_range: TextRange, args: &[Expr], keywords: &[Keyword], ) -> Option { // Add the assignment. let name = match_name(expr)?; - let insert_assignment = Edit::insertion(format!("{name} = "), expr.location); + let insert_assignment = Edit::insertion(format!("{name} = "), expr.start()); // Remove the `inplace` argument. let remove_argument = remove_argument( locator, - expr.location, - violation_at, - violation_end, + expr.start(), + violation_range, args, keywords, false, diff --git a/crates/ruff/src/rules/pandas_vet/mod.rs b/crates/ruff/src/rules/pandas_vet/mod.rs index c83e590206..f3fc615ada 100644 --- a/crates/ruff/src/rules/pandas_vet/mod.rs +++ b/crates/ruff/src/rules/pandas_vet/mod.rs @@ -27,16 +27,19 @@ mod tests { let tokens: Vec = ruff_rustpython::tokenize(&contents); let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); - let indexer: Indexer = tokens.as_slice().into(); - let directives = - directives::extract_directives(&tokens, directives::Flags::from_settings(&settings)); + let indexer = Indexer::from_tokens(&tokens, &locator); + let directives = directives::extract_directives( + &tokens, + directives::Flags::from_settings(&settings), + &locator, + &indexer, + ); let LinterResult { data: (diagnostics, _imports), .. } = check_path( Path::new(""), None, - &contents, tokens, &locator, &stylist, diff --git a/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs b/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs index 9db64e7949..3aca25eb52 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct PandasDfVariableName; @@ -26,5 +25,5 @@ pub fn assignment_to_df(targets: &[Expr]) -> Option { if id != "df" { return None; } - Some(Diagnostic::new(PandasDfVariableName, Range::from(target))) + Some(Diagnostic::new(PandasDfVariableName, target.range())) } diff --git a/crates/ruff/src/rules/pandas_vet/rules/check_attr.rs b/crates/ruff/src/rules/pandas_vet/rules/check_attr.rs index 1af4748471..0f4f11e47b 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/check_attr.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/check_attr.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::Violation; use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::binding::BindingKind; use crate::checkers::ast::Checker; @@ -93,5 +92,5 @@ pub fn check_attr(checker: &mut Checker, attr: &str, value: &Expr, attr_expr: &E checker .diagnostics - .push(Diagnostic::new(violation, Range::from(attr_expr))); + .push(Diagnostic::new(violation, attr_expr.range())); } diff --git a/crates/ruff/src/rules/pandas_vet/rules/check_call.rs b/crates/ruff/src/rules/pandas_vet/rules/check_call.rs index 58729ddf31..494e33909e 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/check_call.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/check_call.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::Violation; use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::binding::{BindingKind, Importation}; use crate::checkers::ast::Checker; @@ -111,5 +110,5 @@ pub fn check_call(checker: &mut Checker, func: &Expr) { checker .diagnostics - .push(Diagnostic::new(violation, Range::from(func))); + .push(Diagnostic::new(violation, func.range())); } diff --git a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs index 83b4b4b5b2..bac28367e7 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, StmtKind}; use ruff_diagnostics::{AutofixKind, Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -82,13 +81,12 @@ pub fn inplace_argument( && matches!(checker.ctx.current_stmt().node, StmtKind::Expr { .. }) && checker.ctx.current_expr_parent().is_none(); let mut diagnostic = - Diagnostic::new(PandasUseOfInplaceArgument { fixable }, Range::from(keyword)); + Diagnostic::new(PandasUseOfInplaceArgument { fixable }, keyword.range()); if fixable && checker.patch(diagnostic.kind.rule()) { if let Some(fix) = convert_inplace_argument_to_assignment( checker.locator, expr, - diagnostic.location, - diagnostic.end_location, + diagnostic.range(), args, keywords, ) { diff --git a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs index 7e09c4aac2..acd6cee0a6 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct PandasUseOfPdMerge; @@ -22,7 +21,7 @@ pub fn use_of_pd_merge(func: &Expr) -> Option { if let ExprKind::Attribute { attr, value, .. } = &func.node { if let ExprKind::Name { id, .. } = &value.node { if id == "pd" && attr == "merge" { - return Some(Diagnostic::new(PandasUseOfPdMerge, Range::from(func))); + return Some(Diagnostic::new(PandasUseOfPdMerge, func.range())); } } } diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs index d37d38dfdb..a86fe30e99 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Alias, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_stdlib::str::{self}; use crate::rules::pep8_naming::helpers; @@ -64,9 +63,9 @@ pub fn camelcase_imported_as_acronym( name: name.to_string(), asname: asname.to_string(), }, - Range::from(alias), + alias.range(), ); - diagnostic.set_parent(stmt.location); + diagnostic.set_parent(stmt.start()); return Some(diagnostic); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs index e050b20e9a..e8aa0cf5bd 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Alias, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_stdlib::str::{self}; use crate::rules::pep8_naming::helpers; @@ -61,9 +60,9 @@ pub fn camelcase_imported_as_constant( name: name.to_string(), asname: asname.to_string(), }, - Range::from(alias), + alias.range(), ); - diagnostic.set_parent(stmt.location); + diagnostic.set_parent(stmt.start()); return Some(diagnostic); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs index 5f1c2e1567..4e6100728a 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs @@ -2,8 +2,6 @@ use rustpython_parser::ast::{Alias, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; -use ruff_python_stdlib::str; use crate::rules::pep8_naming::helpers; @@ -51,15 +49,15 @@ pub fn camelcase_imported_as_lowercase( alias: &Alias, stmt: &Stmt, ) -> Option { - if helpers::is_camelcase(name) && str::is_lower(asname) { + if helpers::is_camelcase(name) && ruff_python_stdlib::str::is_lower(asname) { let mut diagnostic = Diagnostic::new( CamelcaseImportedAsLowercase { name: name.to_string(), asname: asname.to_string(), }, - Range::from(alias), + alias.range(), ); - diagnostic.set_parent(stmt.location); + diagnostic.set_parent(stmt.start()); return Some(diagnostic); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs b/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs index 8954383104..f6a7cadfc7 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Alias, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_stdlib::str; /// ## What it does @@ -56,9 +55,9 @@ pub fn constant_imported_as_non_constant( name: name.to_string(), asname: asname.to_string(), }, - Range::from(alias), + alias.range(), ); - diagnostic.set_parent(stmt.location); + diagnostic.set_parent(stmt.start()); return Some(diagnostic); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs index 6e7521787d..94d04aefe2 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Arg; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct InvalidArgumentName { @@ -27,7 +26,7 @@ pub fn invalid_argument_name(name: &str, arg: &Arg, ignore_names: &[String]) -> InvalidArgumentName { name: name.to_string(), }, - Range::from(arg), + arg.range(), )); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs index dea5da31af..76296104a1 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Arguments, Expr}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::scope::Scope; @@ -89,7 +88,7 @@ pub fn invalid_first_argument_name_for_class_method( } return Some(Diagnostic::new( InvalidFirstArgumentNameForClassMethod, - Range::from(arg), + arg.range(), )); } } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs index 8819edb483..148b565712 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Arguments, Expr}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::scope::Scope; @@ -88,6 +87,6 @@ pub fn invalid_first_argument_name_for_method( } Some(Diagnostic::new( InvalidFirstArgumentNameForMethod, - Range::from(arg), + arg.range(), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs index 0d16c3e5e7..1fc44be2a6 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs @@ -1,9 +1,9 @@ +use ruff_text_size::TextRange; use std::ffi::OsStr; use std::path::Path; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_stdlib::identifiers::{is_migration_name, is_module_name}; /// ## What it does @@ -73,7 +73,7 @@ pub fn invalid_module_name(path: &Path, package: Option<&Path>) -> Option bool { @@ -20,22 +21,20 @@ pub fn compare(left: &Expr, ops: &[Cmpop], comparators: &[Expr], stylist: &Styli } pub(super) fn is_overlong( - line: &str, + line: &Line, limit: usize, ignore_overlong_task_comments: bool, task_tags: &[String], ) -> Option { - let mut start_column = 0; + let mut start_offset = line.start(); let mut width = 0; - let mut end = 0; for c in line.chars() { if width < limit { - start_column += 1; + start_offset += c.text_len(); } width += c.width().unwrap_or(0); - end += 1; } if width <= limit { @@ -67,24 +66,19 @@ pub(super) fn is_overlong( } Some(Overlong { - column: start_column, - end_column: end, + range: TextRange::new(start_offset, line.end()), width, }) } pub(super) struct Overlong { - column: usize, - end_column: usize, + range: TextRange, width: usize, } impl Overlong { - pub(super) fn range(&self, line_no: usize) -> Range { - Range::new( - Location::new(line_no + 1, self.column), - Location::new(line_no + 1, self.end_column), - ) + pub(super) const fn range(&self) -> TextRange { + self.range } pub(super) const fn width(&self) -> usize { diff --git a/crates/ruff/src/rules/pycodestyle/rules/ambiguous_class_name.rs b/crates/ruff/src/rules/pycodestyle/rules/ambiguous_class_name.rs index 3a1d75f13a..0967b96930 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/ambiguous_class_name.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/ambiguous_class_name.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_text_size::TextRange; use crate::rules::pycodestyle::helpers::is_ambiguous_name; @@ -36,7 +36,7 @@ impl Violation for AmbiguousClassName { /// E742 pub fn ambiguous_class_name(name: &str, locate: F) -> Option where - F: FnOnce() -> Range, + F: FnOnce() -> TextRange, { if is_ambiguous_name(name) { Some(Diagnostic::new( diff --git a/crates/ruff/src/rules/pycodestyle/rules/ambiguous_function_name.rs b/crates/ruff/src/rules/pycodestyle/rules/ambiguous_function_name.rs index b3656bdffc..df1f5e052e 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/ambiguous_function_name.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/ambiguous_function_name.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_text_size::TextRange; use crate::rules::pycodestyle::helpers::is_ambiguous_name; @@ -36,7 +36,7 @@ impl Violation for AmbiguousFunctionName { /// E743 pub fn ambiguous_function_name(name: &str, locate: F) -> Option where - F: FnOnce() -> Range, + F: FnOnce() -> TextRange, { if is_ambiguous_name(name) { Some(Diagnostic::new( diff --git a/crates/ruff/src/rules/pycodestyle/rules/ambiguous_variable_name.rs b/crates/ruff/src/rules/pycodestyle/rules/ambiguous_variable_name.rs index bdfe813106..fb80afbbce 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/ambiguous_variable_name.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/ambiguous_variable_name.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_text_size::TextRange; use crate::rules::pycodestyle::helpers::is_ambiguous_name; @@ -37,7 +37,7 @@ impl Violation for AmbiguousVariableName { } /// E741 -pub fn ambiguous_variable_name(name: &str, range: Range) -> Option { +pub fn ambiguous_variable_name(name: &str, range: TextRange) -> Option { if is_ambiguous_name(name) { Some(Diagnostic::new( AmbiguousVariableName(name.to_string()), diff --git a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs index 5a09245566..2446984993 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs @@ -1,10 +1,10 @@ +use ruff_text_size::TextRange; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::registry::Rule; use crate::settings::{flags, Settings}; @@ -131,7 +131,7 @@ pub fn compound_statements( let mut sqb_count = 0; let mut brace_count = 0; - for &(start, ref tok, end) in lxr.iter().flatten() { + for &(ref tok, range) in lxr.iter().flatten() { match tok { Tok::Lpar => { par_count += 1; @@ -161,7 +161,8 @@ pub fn compound_statements( match tok { Tok::Newline => { if let Some((start, end)) = semi { - let mut diagnostic = Diagnostic::new(UselessSemicolon, Range::new(start, end)); + let mut diagnostic = + Diagnostic::new(UselessSemicolon, TextRange::new(start, end)); if autofix.into() && settings.rules.should_fix(Rule::UselessSemicolon) { diagnostic.set_fix(Edit::deletion(start, end)); }; @@ -198,12 +199,12 @@ pub fn compound_statements( || while_.is_some() || with.is_some() { - colon = Some((start, end)); + colon = Some((range.start(), range.end())); allow_ellipsis = true; } } Tok::Semi => { - semi = Some((start, end)); + semi = Some((range.start(), range.end())); } Tok::Comment(..) | Tok::Indent | Tok::Dedent | Tok::NonLogicalNewline => {} Tok::Ellipsis if allow_ellipsis => { @@ -214,7 +215,7 @@ pub fn compound_statements( if let Some((start, end)) = semi { diagnostics.push(Diagnostic::new( MultipleStatementsOnOneLineSemicolon, - Range::new(start, end), + TextRange::new(start, end), )); // Reset. @@ -224,7 +225,7 @@ pub fn compound_statements( if let Some((start, end)) = colon { diagnostics.push(Diagnostic::new( MultipleStatementsOnOneLineColon, - Range::new(start, end), + TextRange::new(start, end), )); // Reset. @@ -263,40 +264,40 @@ pub fn compound_statements( with = None; } Tok::Case => { - case = Some((start, end)); + case = Some((range.start(), range.end())); } Tok::If => { - if_ = Some((start, end)); + if_ = Some((range.start(), range.end())); } Tok::While => { - while_ = Some((start, end)); + while_ = Some((range.start(), range.end())); } Tok::For => { - for_ = Some((start, end)); + for_ = Some((range.start(), range.end())); } Tok::Try => { - try_ = Some((start, end)); + try_ = Some((range.start(), range.end())); } Tok::Except => { - except = Some((start, end)); + except = Some((range.start(), range.end())); } Tok::Finally => { - finally = Some((start, end)); + finally = Some((range.start(), range.end())); } Tok::Elif => { - elif = Some((start, end)); + elif = Some((range.start(), range.end())); } Tok::Else => { - else_ = Some((start, end)); + else_ = Some((range.start(), range.end())); } Tok::Class => { - class = Some((start, end)); + class = Some((range.start(), range.end())); } Tok::With => { - with = Some((start, end)); + with = Some((range.start(), range.end())); } Tok::Match => { - match_ = Some((start, end)); + match_ = Some((range.start(), range.end())); } _ => {} }; diff --git a/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs b/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs index c3526c063c..99b663a733 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs @@ -1,5 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::newlines::Line; use crate::rules::pycodestyle::helpers::is_overlong; use crate::settings::Settings; @@ -37,7 +38,7 @@ impl Violation for DocLineTooLong { } /// W505 -pub fn doc_line_too_long(lineno: usize, line: &str, settings: &Settings) -> Option { +pub(crate) fn doc_line_too_long(line: &Line, settings: &Settings) -> Option { let Some(limit) = settings.pycodestyle.max_doc_length else { return None; }; @@ -48,10 +49,5 @@ pub fn doc_line_too_long(lineno: usize, line: &str, settings: &Settings) -> Opti settings.pycodestyle.ignore_overlong_task_comments, &settings.task_tags, ) - .map(|overlong| { - Diagnostic::new( - DocLineTooLong(overlong.width(), limit), - overlong.range(lineno), - ) - }) + .map(|overlong| Diagnostic::new(DocLineTooLong(overlong.width(), limit), overlong.range())) } diff --git a/crates/ruff/src/rules/pycodestyle/rules/errors.rs b/crates/ruff/src/rules/pycodestyle/rules/errors.rs index 291fd5fa63..7816752162 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/errors.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/errors.rs @@ -1,8 +1,9 @@ +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ParseError; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::source_code::Locator; #[violation] pub struct IOError { @@ -32,11 +33,25 @@ impl Violation for SyntaxError { } /// E901 -pub fn syntax_error(diagnostics: &mut Vec, parse_error: &ParseError) { +pub fn syntax_error( + diagnostics: &mut Vec, + parse_error: &ParseError, + locator: &Locator, +) { + let rest = locator.after(parse_error.location); + + // Try to create a non-empty range so that the diagnostic can print a caret at the + // right position. This requires that we retrieve the next character, if any, and take its length + // to maintain char-boundaries. + let len = rest + .chars() + .next() + .map_or(TextSize::new(0), TextLen::text_len); + diagnostics.push(Diagnostic::new( SyntaxError { message: parse_error.error.to_string(), }, - Range::new(parse_error.location, parse_error.location), + TextRange::at(parse_error.location, len), )); } diff --git a/crates/ruff/src/rules/pycodestyle/rules/imports.rs b/crates/ruff/src/rules/pycodestyle/rules/imports.rs index e6c34d4caf..5a368a3fed 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/imports.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/imports.rs @@ -1,10 +1,9 @@ use rustpython_parser::ast::{Alias, Stmt}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; - -use crate::checkers::ast::Checker; +use ruff_python_ast::source_code::Locator; /// ## What it does /// Check for multiple imports on one line. @@ -77,16 +76,15 @@ pub fn multiple_imports_on_one_line(checker: &mut Checker, stmt: &Stmt, names: & if names.len() > 1 { checker .diagnostics - .push(Diagnostic::new(MultipleImportsOnOneLine, Range::from(stmt))); + .push(Diagnostic::new(MultipleImportsOnOneLine, stmt.range())); } } /// E402 -pub fn module_import_not_at_top_of_file(checker: &mut Checker, stmt: &Stmt) { - if checker.ctx.seen_import_boundary && stmt.location.column() == 0 { - checker.diagnostics.push(Diagnostic::new( - ModuleImportNotAtTopOfFile, - Range::from(stmt), - )); +pub fn module_import_not_at_top_of_file(checker: &mut Checker, stmt: &Stmt, locator: &Locator) { + if checker.ctx.seen_import_boundary && locator.is_at_start_of_line(stmt.start()) { + checker + .diagnostics + .push(Diagnostic::new(ModuleImportNotAtTopOfFile, stmt.range())); } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index d46ae48c1f..2a1bd7620f 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -1,12 +1,10 @@ use anyhow::{bail, Result}; use log::error; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::StrExt; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for invalid escape sequences. @@ -59,13 +57,12 @@ fn extract_quote(text: &str) -> Result<&str> { /// W605 pub fn invalid_escape_sequence( locator: &Locator, - start: Location, - end: Location, + range: TextRange, autofix: bool, ) -> Vec { let mut diagnostics = vec![]; - let text = locator.slice(Range::new(start, end)); + let text = locator.slice(range); // Determine whether the string is single- or triple-quoted. let Ok(quote) = extract_quote(text) else { @@ -74,53 +71,49 @@ pub fn invalid_escape_sequence( }; let quote_pos = text.find(quote).unwrap(); let prefix = text[..quote_pos].to_lowercase(); - let body = &text[(quote_pos + quote.len())..(text.len() - quote.len())]; + let body = &text[quote_pos + quote.len()..text.len() - quote.len()]; if !prefix.contains('r') { - for (row_offset, line) in body.universal_newlines().enumerate() { - let chars: Vec = line.chars().collect(); - for col_offset in 0..chars.len() { - if chars[col_offset] != '\\' { - continue; - } + let start_offset = + range.start() + TextSize::try_from(quote_pos).unwrap() + quote.text_len(); - // If the previous character was also a backslash, skip. - if col_offset > 0 && chars[col_offset - 1] == '\\' { - continue; - } + let mut chars_iter = body.char_indices().peekable(); - // If we're at the end of the line, skip. - if col_offset == chars.len() - 1 { - continue; - } - - // If the next character is a valid escape sequence, skip. - let next_char = chars[col_offset + 1]; - if VALID_ESCAPE_SEQUENCES.contains(&next_char) { - continue; - } - - // Compute the location of the escape sequence by offsetting the location of the - // string token by the characters we've seen thus far. - let col = if row_offset == 0 { - start.column() + prefix.len() + quote.len() + col_offset - } else { - col_offset - }; - let location = Location::new(start.row() + row_offset, col); - let end_location = Location::new(location.row(), location.column() + 2); - let mut diagnostic = Diagnostic::new( - InvalidEscapeSequence(next_char), - Range::new(location, end_location), - ); - if autofix { - diagnostic.set_fix(Edit::insertion( - r"\".to_string(), - Location::new(location.row(), location.column() + 1), - )); - } - diagnostics.push(diagnostic); + while let Some((i, c)) = chars_iter.next() { + if c != '\\' { + continue; } + + // If the previous character was also a backslash, skip. + if i > 0 && body.as_bytes()[i - 1] == b'\\' { + continue; + } + + // If we're at the end of the file, skip. + let Some((_, next_char)) = chars_iter.peek() else { + continue; + }; + + // If we're at the end of the line, skip + if matches!(next_char, '\n' | '\r') { + continue; + } + + // If the next character is a valid escape sequence, skip. + if VALID_ESCAPE_SEQUENCES.contains(next_char) { + continue; + } + + let location = start_offset + TextSize::try_from(i).unwrap(); + let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); + let mut diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range); + if autofix { + diagnostic.set_fix(Edit::insertion( + r"\".to_string(), + range.start() + TextSize::from(1), + )); + } + diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index c166c7099e..0ecd28b187 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -1,16 +1,13 @@ -use ruff_python_semantic::context::Context; -use rustpython_parser::ast::{ - Arg, ArgData, Arguments, Constant, Expr, ExprKind, Location, Stmt, StmtKind, -}; - use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{match_leading_content, match_trailing_content, unparse_stmt}; +use ruff_python_ast::helpers::{has_leading_content, has_trailing_content, unparse_stmt}; use ruff_python_ast::newlines::StrExt; use ruff_python_ast::source_code::Stylist; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace::leading_space; +use ruff_python_semantic::context::Context; use ruff_python_semantic::scope::ScopeKind; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{Arg, ArgData, Arguments, Constant, Expr, ExprKind, Stmt, StmtKind}; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -82,18 +79,15 @@ pub fn lambda_assignment( name: id.to_string(), fixable, }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) && fixable - && !match_leading_content(stmt, checker.locator) - && !match_trailing_content(stmt, checker.locator) + && !has_leading_content(stmt, checker.locator) + && !has_trailing_content(stmt, checker.locator) { - let first_line = checker.locator.slice(Range::new( - Location::new(stmt.location.row(), 0), - Location::new(stmt.location.row() + 1, 0), - )); + let first_line = checker.locator.line(stmt.start()); let indentation = &leading_space(first_line); let mut indented = String::new(); for (idx, line) in @@ -102,18 +96,14 @@ pub fn lambda_assignment( .enumerate() { if idx == 0 { - indented.push_str(line); + indented.push_str(&line); } else { indented.push_str(checker.stylist.line_ending().as_str()); indented.push_str(indentation); - indented.push_str(line); + indented.push_str(&line); } } - diagnostic.set_fix(Edit::replacement( - indented, - stmt.location, - stmt.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(indented, stmt.range())); } checker.diagnostics.push(diagnostic); @@ -169,8 +159,8 @@ fn function( stylist: &Stylist, ) -> String { let body = Stmt::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), StmtKind::Return { value: Some(Box::new(body.clone())), }, @@ -184,15 +174,14 @@ fn function( .iter() .enumerate() .map(|(idx, arg)| { - Arg::new( - Location::default(), - Location::default(), + Arg::with_range( ArgData { annotation: arg_types .get(idx) .map(|arg_type| Box::new(arg_type.clone())), ..arg.node.clone() }, + TextRange::default(), ) }) .collect::>(); @@ -201,21 +190,18 @@ fn function( .iter() .enumerate() .map(|(idx, arg)| { - Arg::new( - Location::default(), - Location::default(), + Arg::with_range( ArgData { annotation: arg_types .get(idx + new_posonlyargs.len()) .map(|arg_type| Box::new(arg_type.clone())), ..arg.node.clone() }, + TextRange::default(), ) }) .collect::>(); - let func = Stmt::new( - Location::default(), - Location::default(), + let func = Stmt::with_range( StmtKind::FunctionDef { name: name.to_string(), args: Box::new(Arguments { @@ -228,13 +214,14 @@ fn function( returns: Some(Box::new(return_type)), type_comment: None, }, + TextRange::default(), ); return unparse_stmt(&func, stylist); } } let func = Stmt::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), StmtKind::FunctionDef { name: name.to_string(), args: Box::new(args.clone()), diff --git a/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs b/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs index c5eafbb423..4e34d25059 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs @@ -1,5 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::newlines::Line; use crate::rules::pycodestyle::helpers::is_overlong; use crate::settings::Settings; @@ -34,7 +35,7 @@ impl Violation for LineTooLong { } /// E501 -pub fn line_too_long(lineno: usize, line: &str, settings: &Settings) -> Option { +pub(crate) fn line_too_long(line: &Line, settings: &Settings) -> Option { let limit = settings.line_length; is_overlong( @@ -43,5 +44,5 @@ pub fn line_too_long(lineno: usize, line: &str, settings: &Settings) -> Option>(); let content = compare(left, &ops, comparators, checker.stylist); for diagnostic in &mut diagnostics { - diagnostic.set_fix(Edit::replacement( - content.to_string(), - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(content.to_string(), expr.range())); } } 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 6f6816ecb3..5d668d672d 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,4 +1,4 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::TextSize; use super::{LogicalLine, Whitespace}; use ruff_diagnostics::DiagnosticKind; @@ -101,7 +101,7 @@ impl Violation for WhitespaceBeforePunctuation { } /// E201, E202, E203 -pub(crate) fn extraneous_whitespace(line: &LogicalLine) -> Vec<(Location, DiagnosticKind)> { +pub(crate) fn extraneous_whitespace(line: &LogicalLine) -> Vec<(TextSize, DiagnosticKind)> { let mut diagnostics = vec![]; let mut last_token: Option = None; @@ -111,10 +111,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 +132,7 @@ 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 - offset, diagnostic_kind)); } } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs index 5978c97927..df4b8a4540 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs @@ -2,7 +2,6 @@ use ruff_diagnostics::DiagnosticKind; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use rustpython_parser::ast::Location; use super::LogicalLine; @@ -237,51 +236,40 @@ pub(crate) fn indentation( indent_level: usize, prev_indent_level: Option, indent_size: usize, -) -> Vec<(Location, DiagnosticKind)> { +) -> Vec { let mut diagnostics = vec![]; - let location = logical_line.first_token_location().unwrap(); - if indent_level % indent_size != 0 { - diagnostics.push(( - location, - if logical_line.is_comment_only() { - IndentationWithInvalidMultipleComment { indent_size }.into() - } else { - IndentationWithInvalidMultiple { indent_size }.into() - }, - )); + diagnostics.push(if logical_line.is_comment_only() { + DiagnosticKind::from(IndentationWithInvalidMultipleComment { indent_size }) + } else { + DiagnosticKind::from(IndentationWithInvalidMultiple { indent_size }) + }); } let indent_expect = prev_logical_line .and_then(|prev_logical_line| prev_logical_line.tokens_trimmed().last()) .map_or(false, |t| t.kind() == TokenKind::Colon); if indent_expect && indent_level <= prev_indent_level.unwrap_or(0) { - diagnostics.push(( - location, - if logical_line.is_comment_only() { - NoIndentedBlockComment.into() - } else { - NoIndentedBlock.into() - }, - )); + diagnostics.push(if logical_line.is_comment_only() { + DiagnosticKind::from(NoIndentedBlockComment) + } else { + DiagnosticKind::from(NoIndentedBlock) + }); } else if !indent_expect && prev_indent_level.map_or(false, |prev_indent_level| indent_level > prev_indent_level) { - diagnostics.push(( - location, - if logical_line.is_comment_only() { - UnexpectedIndentationComment.into() - } else { - UnexpectedIndentation.into() - }, - )); + diagnostics.push(if logical_line.is_comment_only() { + DiagnosticKind::from(UnexpectedIndentationComment) + } else { + DiagnosticKind::from(UnexpectedIndentation) + }); } if indent_expect { let expected_indent_amount = if indent_char == '\t' { 8 } else { 4 }; let expected_indent_level = prev_indent_level.unwrap_or(0) + expected_indent_amount; if indent_level > expected_indent_level { - diagnostics.push((location, OverIndented.into())); + diagnostics.push(OverIndented.into()); } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs index e3f5d21aee..8ef55caa22 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs @@ -3,7 +3,7 @@ use ruff_diagnostics::Edit; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use ruff_python_ast::types::Range; +use ruff_text_size::TextRange; #[violation] pub struct MissingWhitespace { @@ -80,11 +80,10 @@ pub(crate) fn missing_whitespace(line: &LogicalLine, autofix: bool) -> Vec Vec<(Location, DiagnosticKind)> { +) -> Vec<(TextSize, DiagnosticKind)> { let mut diagnostics = vec![]; for (tok0, tok1) in tokens.iter().tuple_windows() { diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs index a76ee581cf..8fd3f08d45 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs @@ -1,9 +1,8 @@ -use rustpython_parser::ast::Location; - use ruff_diagnostics::DiagnosticKind; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; +use ruff_text_size::TextSize; use crate::rules::pycodestyle::rules::logical_lines::LogicalLineTokens; @@ -55,15 +54,15 @@ impl Violation for MissingWhitespaceAroundModuloOperator { #[allow(clippy::if_same_then_else)] pub(crate) fn missing_whitespace_around_operator( tokens: &LogicalLineTokens, -) -> Vec<(Location, DiagnosticKind)> { +) -> Vec<(TextSize, DiagnosticKind)> { let mut diagnostics = vec![]; let mut needs_space_main: Option = Some(false); let mut needs_space_aux: Option = None; - let mut prev_end_aux: Option = None; + let mut prev_end_aux: Option = None; let mut parens = 0u32; let mut prev_type: Option = None; - let mut prev_end: Option = None; + let mut prev_end: Option = None; for token in tokens { let kind = token.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 6c73fae101..2c814a0160 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -1,12 +1,11 @@ use bitflags::bitflags; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::lexer::LexResult; use std::fmt::{Debug, Formatter}; use std::iter::FusedIterator; use ruff_python_ast::source_code::Locator; use ruff_python_ast::token_kind::TokenKind; -use ruff_python_ast::types::Range; pub(crate) use extraneous_whitespace::{ extraneous_whitespace, WhitespaceAfterOpenBracket, WhitespaceBeforeCloseBracket, @@ -90,9 +89,9 @@ impl<'a> LogicalLines<'a> { let mut builder = LogicalLinesBuilder::with_capacity(tokens.len()); let mut parens: u32 = 0; - for (start, token, end) in tokens.iter().flatten() { + for (token, range) in tokens.iter().flatten() { let token_kind = TokenKind::from_token(token); - builder.push_token(*start, token_kind, *end); + builder.push_token(token_kind, *range); match token_kind { TokenKind::Lbrace | TokenKind::Lpar | TokenKind::Lsqb => { @@ -224,7 +223,7 @@ impl<'a> LogicalLine<'a> { let last_token = self.tokens().last().unwrap(); self.lines .locator - .slice(Range::new(token.end(), last_token.end())) + .slice(TextRange::new(token.end(), last_token.end())) } /// Returns the text before `token` @@ -240,7 +239,7 @@ impl<'a> LogicalLine<'a> { let first_token = self.tokens().first().unwrap(); self.lines .locator - .slice(Range::new(first_token.start(), token.start())) + .slice(TextRange::new(first_token.start(), token.start())) } /// Returns the whitespace *after* the `token` @@ -248,8 +247,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)) } @@ -262,9 +261,8 @@ impl<'a> LogicalLine<'a> { } } - /// Returns the [`Location`] of the first token on the line or [`None`]. - pub fn first_token_location(&self) -> Option { - self.tokens().first().map(|t| t.start()) + pub fn first_token(&self) -> Option { + self.tokens().first() } /// Returns the line's flags @@ -344,7 +342,7 @@ impl<'a> LogicalLineTokens<'a> { match (self.first(), self.last()) { (Some(first), Some(last)) => { let locator = self.lines.locator; - locator.slice(Range::new(first.start(), last.end())) + locator.slice(TextRange::new(first.start(), last.end())) } _ => "", } @@ -452,26 +450,23 @@ impl<'a> LogicalLineToken<'a> { /// Returns the token's start location #[inline] - pub fn start(&self) -> Location { - #[allow(unsafe_code)] - unsafe { - *self.tokens.starts.get_unchecked(self.position) - } + pub fn start(&self) -> TextSize { + self.range().start() } /// Returns the token's end location #[inline] - pub fn end(&self) -> Location { - #[allow(unsafe_code)] - unsafe { - *self.tokens.ends.get_unchecked(self.position) - } + pub fn end(&self) -> TextSize { + self.range().end() } /// Returns a tuple with the token's `(start, end)` locations #[inline] - pub fn range(&self) -> (Location, Location) { - (self.start(), self.end()) + pub fn range(&self) -> TextRange { + #[allow(unsafe_code)] + unsafe { + *self.tokens.ranges.get_unchecked(self.position) + } } } @@ -515,26 +510,35 @@ 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), + _ => { + if len == content.text_len() { + // All whitespace up to the start of the line -> Indent + (Self::None, TextSize::default()) + } else { + (Self::Many, len) + } + } } } } @@ -563,7 +567,7 @@ impl LogicalLinesBuilder { // SAFETY: `LogicalLines::from_tokens` asserts that the file has less than `u32::MAX` tokens and each tokens is at least one character long #[allow(clippy::cast_possible_truncation)] - fn push_token(&mut self, start: Location, kind: TokenKind, end: Location) { + fn push_token(&mut self, kind: TokenKind, range: TextRange) { let tokens_start = self.tokens.len(); let line = self.current_line.get_or_insert_with(|| CurrentLine { @@ -608,7 +612,7 @@ impl LogicalLinesBuilder { ), ); - self.tokens.push(kind, start, end); + self.tokens.push(kind, range); } // SAFETY: `LogicalLines::from_tokens` asserts that the file has less than `u32::MAX` tokens and each tokens is at least one character long @@ -646,11 +650,8 @@ struct Tokens { /// The token kinds kinds: Vec, - /// The start locations - starts: Vec, - - /// The end locations - ends: Vec, + /// The ranges + ranges: Vec, } impl Tokens { @@ -658,8 +659,7 @@ impl Tokens { fn with_capacity(capacity: usize) -> Self { Self { kinds: Vec::with_capacity(capacity), - starts: Vec::with_capacity(capacity), - ends: Vec::with_capacity(capacity), + ranges: Vec::with_capacity(capacity), } } @@ -668,10 +668,9 @@ impl Tokens { self.kinds.len() } - /// Adds a new token with the given `kind` and `start`, `end` location. - fn push(&mut self, kind: TokenKind, start: Location, end: Location) { + /// Adds a new token with the given `kind` and `range` + fn push(&mut self, kind: TokenKind, range: TextRange) { self.kinds.push(kind); - self.starts.push(start); - self.ends.push(end); + self.ranges.push(range); } } 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 b0a61519cd..3ab63d0d51 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 efaae2343b..82f3718016 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 @@ -1,9 +1,8 @@ -use rustpython_parser::ast::Location; - use super::{LogicalLine, Whitespace}; use ruff_diagnostics::DiagnosticKind; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; +use ruff_text_size::TextSize; /// ## What it does /// Checks for extraneous whitespace after keywords. @@ -109,7 +108,7 @@ impl Violation for TabBeforeKeyword { } /// E271, E272, E273, E274 -pub(crate) fn whitespace_around_keywords(line: &LogicalLine) -> Vec<(Location, DiagnosticKind)> { +pub(crate) fn whitespace_around_keywords(line: &LogicalLine) -> Vec<(TextSize, DiagnosticKind)> { let mut diagnostics = vec![]; let mut after_keyword = false; @@ -120,17 +119,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_around_named_parameter_equals.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs index 1aa3619cc6..c6ce5620f2 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs @@ -2,7 +2,7 @@ use ruff_diagnostics::DiagnosticKind; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use rustpython_parser::ast::Location; +use ruff_text_size::TextSize; use super::LogicalLineTokens; @@ -41,11 +41,11 @@ fn is_in_def(tokens: &LogicalLineTokens) -> bool { /// E251, E252 pub(crate) fn whitespace_around_named_parameter_equals( tokens: &LogicalLineTokens, -) -> Vec<(Location, DiagnosticKind)> { +) -> Vec<(TextSize, DiagnosticKind)> { let mut diagnostics = vec![]; let mut parens = 0u32; let mut annotated_func_arg = false; - let mut prev_end: Option = None; + let mut prev_end: Option = None; let in_def = is_in_def(tokens); let mut iter = tokens.iter().peekable(); 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 ec45cfd689..edb6617311 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_python_ast::types::Range; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; /// ## What it does /// Checks if inline comments are separated by at least two spaces. @@ -140,26 +139,27 @@ impl Violation for MultipleLeadingHashesForBlockComment { pub(crate) fn whitespace_before_comment( tokens: &LogicalLineTokens, locator: &Locator, -) -> Vec<(Range, DiagnosticKind)> { + 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(Range::new( - Location::new(start.row(), 0), - Location::new(start.row(), start.column()), - )); + let range = token.range(); - let text = locator.slice(Range::new(start, end)); + let line = locator.slice(TextRange::new( + locator.line_start(range.start()), + range.start(), + )); + let text = locator.slice(range); 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 range.start() - prev_end < " ".text_len() { diagnostics.push(( - Range::new(prev_end, start), + TextRange::new(prev_end, range.start()), TooFewSpacesBeforeInlineComment.into(), )); } @@ -179,17 +179,14 @@ pub(crate) fn whitespace_before_comment( if is_inline_comment { if bad_prefix.is_some() || comment.chars().next().map_or(false, char::is_whitespace) { - diagnostics.push((Range::new(start, end), NoSpaceAfterInlineComment.into())); + diagnostics.push((range, 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((Range::new(start, end), NoSpaceAfterBlockComment.into())); + diagnostics.push((range, NoSpaceAfterBlockComment.into())); } else if !comment.is_empty() { - diagnostics.push(( - Range::new(start, end), - MultipleLeadingHashesForBlockComment.into(), - )); + diagnostics.push((range, MultipleLeadingHashesForBlockComment.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 a9c87d7651..f8600d5ee5 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,8 +1,7 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use ruff_python_ast::types::Range; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextRange, TextSize}; use super::LogicalLineTokens; @@ -57,13 +56,11 @@ 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, Range::new(start, end)); + let mut diagnostic = Diagnostic::new(kind, TextRange::new(start, end)); if autofix { diagnostic.set_fix(Edit::deletion(start, end)); diff --git a/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs b/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs index 169e5f679b..94aed8b41d 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs @@ -1,10 +1,8 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::StrExt; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for files missing a new line at the end of the file. @@ -42,19 +40,27 @@ pub fn no_newline_at_end_of_file( stylist: &Stylist, autofix: bool, ) -> Option { - if !locator.contents().ends_with(['\n', '\r']) { + let source = locator.contents(); + + // Ignore empty and BOM only files + if source.is_empty() || source == "\u{feff}" { + return None; + } + + if !source.ends_with(['\n', '\r']) { // Note: if `lines.last()` is `None`, then `contents` is empty (and so we don't // want to raise W292 anyway). - if let Some(line) = locator.contents().universal_newlines().last() { - // Both locations are at the end of the file (and thus the same). - let location = Location::new(locator.count_lines(), line.len()); - let mut diagnostic = - Diagnostic::new(MissingNewlineAtEndOfFile, Range::new(location, location)); - if autofix { - diagnostic.set_fix(Edit::insertion(stylist.line_ending().to_string(), location)); - } - return Some(diagnostic); + // Both locations are at the end of the file (and thus the same). + let range = TextRange::empty(locator.contents().text_len()); + + let mut diagnostic = Diagnostic::new(MissingNewlineAtEndOfFile, range); + if autofix { + diagnostic.set_fix(Edit::insertion( + stylist.line_ending().to_string(), + range.start(), + )); } + return Some(diagnostic); } None } diff --git a/crates/ruff/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs b/crates/ruff/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs index 7e12a5e0d9..c296adeb89 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs @@ -1,9 +1,8 @@ -use rustpython_parser::ast::Location; - use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::Line; use ruff_python_ast::whitespace::leading_space; +use ruff_text_size::{TextLen, TextRange}; /// ## What it does /// Checks for mixed tabs and spaces in indentation. @@ -36,16 +35,13 @@ impl Violation for MixedSpacesAndTabs { } /// E101 -pub fn mixed_spaces_and_tabs(lineno: usize, line: &str) -> Option { - let indent = leading_space(line); +pub(crate) fn mixed_spaces_and_tabs(line: &Line) -> Option { + let indent = leading_space(line.as_str()); if indent.contains(' ') && indent.contains('\t') { Some(Diagnostic::new( MixedSpacesAndTabs, - Range::new( - Location::new(lineno + 1, 0), - Location::new(lineno + 1, indent.chars().count()), - ), + TextRange::at(line.start(), indent.text_len()), )) } else { None diff --git a/crates/ruff/src/rules/pycodestyle/rules/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/mod.rs index 9e4f26579d..4a5c3a1ba0 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/mod.rs @@ -6,7 +6,7 @@ pub use compound_statements::{ compound_statements, MultipleStatementsOnOneLineColon, MultipleStatementsOnOneLineSemicolon, UselessSemicolon, }; -pub use doc_line_too_long::{doc_line_too_long, DocLineTooLong}; +pub(crate) use doc_line_too_long::{doc_line_too_long, DocLineTooLong}; pub use errors::{syntax_error, IOError, SyntaxError}; pub use imports::{ module_import_not_at_top_of_file, multiple_imports_on_one_line, ModuleImportNotAtTopOfFile, @@ -15,13 +15,15 @@ pub use imports::{ pub use invalid_escape_sequence::{invalid_escape_sequence, InvalidEscapeSequence}; pub use lambda_assignment::{lambda_assignment, LambdaAssignment}; -pub use line_too_long::{line_too_long, LineTooLong}; +pub(crate) use line_too_long::{line_too_long, LineTooLong}; pub use literal_comparisons::{literal_comparisons, NoneComparison, TrueFalseComparison}; pub use missing_newline_at_end_of_file::{no_newline_at_end_of_file, MissingNewlineAtEndOfFile}; -pub use mixed_spaces_and_tabs::{mixed_spaces_and_tabs, MixedSpacesAndTabs}; +pub(crate) use mixed_spaces_and_tabs::{mixed_spaces_and_tabs, MixedSpacesAndTabs}; pub use not_tests::{not_tests, NotInTest, NotIsTest}; -pub use tab_indentation::{tab_indentation, TabIndentation}; -pub use trailing_whitespace::{trailing_whitespace, BlankLineWithWhitespace, TrailingWhitespace}; +pub(crate) use tab_indentation::{tab_indentation, TabIndentation}; +pub(crate) use trailing_whitespace::{ + trailing_whitespace, BlankLineWithWhitespace, TrailingWhitespace, +}; pub use type_comparison::{type_comparison, TypeComparison}; mod ambiguous_class_name; diff --git a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs index c0cea258f7..8dd82ce08d 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Cmpop, Expr, ExprKind, Unaryop}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -98,12 +97,11 @@ pub fn not_tests( match op { Cmpop::In => { if check_not_in { - let mut diagnostic = Diagnostic::new(NotInTest, Range::from(operand)); + let mut diagnostic = Diagnostic::new(NotInTest, operand.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( compare(left, &[Cmpop::NotIn], comparators, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -111,12 +109,11 @@ pub fn not_tests( } Cmpop::Is => { if check_not_is { - let mut diagnostic = Diagnostic::new(NotIsTest, Range::from(operand)); + let mut diagnostic = Diagnostic::new(NotIsTest, operand.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( compare(left, &[Cmpop::IsNot], comparators, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pycodestyle/rules/tab_indentation.rs b/crates/ruff/src/rules/pycodestyle/rules/tab_indentation.rs index b15855b144..90c727b7ee 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/tab_indentation.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/tab_indentation.rs @@ -1,8 +1,8 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::Line; use ruff_python_ast::whitespace::leading_space; #[violation] @@ -16,50 +16,30 @@ impl Violation for TabIndentation { } /// W191 -pub fn tab_indentation(lineno: usize, line: &str, string_ranges: &[Range]) -> Option { +pub(crate) fn tab_indentation(line: &Line, string_ranges: &[TextRange]) -> Option { let indent = leading_space(line); if let Some(tab_index) = indent.find('\t') { - // 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 { + let tab_offset = line.start() + TextSize::try_from(tab_index).unwrap(); + + let string_range_index = string_ranges.binary_search_by(|range| { + if tab_offset < range.start() { std::cmp::Ordering::Greater - } else if end < lineno { - std::cmp::Ordering::Less - } else { + } else if range.contains(tab_offset) { std::cmp::Ordering::Equal + } else { + std::cmp::Ordering::Less } - }) { - 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; - } + // If the tab character is within a multi-line string, abort. + if string_range_index.is_ok() { + None + } else { + Some(Diagnostic::new( + TabIndentation, + TextRange::at(line.start(), indent.text_len()), + )) } - - Some(Diagnostic::new( - TabIndentation, - Range::new( - Location::new(lineno, 0), - Location::new(lineno, indent.chars().count()), - ), - )) } else { None } diff --git a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs index e23a7d14cf..da6074c65d 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -1,8 +1,8 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::Line; use crate::registry::Rule; use crate::settings::{flags, Settings}; @@ -75,35 +75,36 @@ impl AlwaysAutofixableViolation for BlankLineWithWhitespace { } /// W291, W293 -pub fn trailing_whitespace( - lineno: usize, - line: &str, +pub(crate) fn trailing_whitespace( + line: &Line, settings: &Settings, autofix: flags::Autofix, ) -> Option { - let whitespace_count = line.chars().rev().take_while(|c| c.is_whitespace()).count(); - if whitespace_count > 0 { - let line_char_count = line.chars().count(); - let start = Location::new(lineno + 1, line_char_count - whitespace_count); - let end = Location::new(lineno + 1, line_char_count); + let whitespace_len: TextSize = line + .chars() + .rev() + .take_while(|c| c.is_whitespace()) + .map(TextLen::text_len) + .sum(); + if whitespace_len > TextSize::from(0) { + let range = TextRange::new(line.end() - whitespace_len, line.end()); - if whitespace_count == line_char_count { + if range == line.range() { if settings.rules.enabled(Rule::BlankLineWithWhitespace) { - let mut diagnostic = - Diagnostic::new(BlankLineWithWhitespace, Range::new(start, end)); + let mut diagnostic = Diagnostic::new(BlankLineWithWhitespace, range); if matches!(autofix, flags::Autofix::Enabled) && settings.rules.should_fix(Rule::BlankLineWithWhitespace) { - diagnostic.set_fix(Edit::deletion(start, end)); + diagnostic.set_fix(Edit::range_deletion(range)); } return Some(diagnostic); } } else if settings.rules.enabled(Rule::TrailingWhitespace) { - let mut diagnostic = Diagnostic::new(TrailingWhitespace, Range::new(start, end)); + let mut diagnostic = Diagnostic::new(TrailingWhitespace, range); if matches!(autofix, flags::Autofix::Enabled) && settings.rules.should_fix(Rule::TrailingWhitespace) { - diagnostic.set_fix(Edit::deletion(start, end)); + diagnostic.set_fix(Edit::range_deletion(range)); } return Some(diagnostic); } diff --git a/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs b/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs index 3109617e4f..ad8b2aaf14 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/type_comparison.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind}; use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for object type comparisons without using isinstance(). @@ -57,7 +56,7 @@ pub fn type_comparison(checker: &mut Checker, expr: &Expr, ops: &[Cmpop], compar ) { checker .diagnostics - .push(Diagnostic::new(TypeComparison, Range::from(expr))); + .push(Diagnostic::new(TypeComparison, expr.range())); } } } @@ -76,7 +75,7 @@ pub fn type_comparison(checker: &mut Checker, expr: &Expr, ops: &[Cmpop], compar { checker .diagnostics - .push(Diagnostic::new(TypeComparison, Range::from(expr))); + .push(Diagnostic::new(TypeComparison, expr.range())); } } } diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E999_E999.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E999_E999.py.snap index 3c365978ca..48d6145618 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E999_E999.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E999_E999.py.snap @@ -5,7 +5,8 @@ E999.py:3:1: E999 SyntaxError: unindent does not match any outer indentation lev | 3 | def x(): 4 | - | E999 + | ^ E999 +5 | | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W191_W19.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W191_W19.py.snap index fc23f107e6..1dd9828f05 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W191_W19.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W191_W19.py.snap @@ -282,6 +282,16 @@ W19.py:133:1: W191 Indentation contains tabs 137 | def test_keys(self): | +W19.py:136:1: W191 Indentation contains tabs + | +136 | #: W191 W191 W191 W191 W191 +137 | def test_keys(self): +138 | """areas.json - All regions are accounted for.""" + | W191 +139 | expected = set([ +140 | u'Norrbotten', + | + W19.py:137:1: W191 Indentation contains tabs | 137 | def test_keys(self): diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__max_doc_length.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__max_doc_length.snap index 03608fd9d5..a1fc3e95e1 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__max_doc_length.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__max_doc_length.snap @@ -24,12 +24,21 @@ W505.py:10:51: W505 Doc line too long (56 > 50 characters) 12 | # Here's a standalone comment that's over the limit. | ^^^^^^ W505 13 | -14 | print("Here's a string that's over the limit, but it's not a docstring.") +14 | x = 2 | -W505.py:15:51: W505 Doc line too long (61 > 50 characters) +W505.py:13:51: W505 Doc line too long (93 > 50 characters) | -15 | "This is also considered a docstring, and is over the limit." +13 | x = 2 +14 | # Another standalone that is preceded by a newline and indent toke and is over the limit. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ W505 +15 | +16 | print("Here's a string that's over the limit, but it's not a docstring.") + | + +W505.py:18:51: W505 Doc line too long (61 > 50 characters) + | +18 | "This is also considered a docstring, and is over the limit." | ^^^^^^^^^^^ W505 | diff --git a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs index c2bff7cb9a..b29f3de2cc 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs @@ -3,7 +3,6 @@ use regex::Regex; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; @@ -32,7 +31,7 @@ pub fn backslashes(checker: &mut Checker, docstring: &Docstring) { if BACKSLASH_REGEX.is_match(contents) { checker.diagnostics.push(Diagnostic::new( EscapeSequenceInDocstring, - Range::from(docstring.expr), + docstring.range(), )); } } diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs index 913ca8d8cd..bbac7b3a72 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs @@ -1,11 +1,9 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator}; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; -use crate::message::Location; use crate::registry::AsRule; #[violation] @@ -42,9 +40,9 @@ impl Violation for BlankLineAfterSummary { /// D205 pub fn blank_after_summary(checker: &mut Checker, docstring: &Docstring) { - let body = docstring.body; + let body = docstring.body(); - let mut lines_count = 1; + let mut lines_count: usize = 1; let mut blanks_count = 0; for line in body.trim().universal_newlines().skip(1) { lines_count += 1; @@ -59,16 +57,26 @@ pub fn blank_after_summary(checker: &mut Checker, docstring: &Docstring) { BlankLineAfterSummary { num_lines: blanks_count, }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { if blanks_count > 1 { + let mut lines = UniversalNewlineIterator::with_offset(&body, body.start()); + let mut summary_end = body.start(); + // Find the "summary" line (defined as the first non-blank line). - let mut summary_line = 0; - for line in body.universal_newlines() { - if line.trim().is_empty() { - summary_line += 1; - } else { + for line in lines.by_ref() { + if !line.trim().is_empty() { + summary_end = line.full_end(); + break; + } + } + + // Find the last blank line + let mut blank_end = summary_end; + for line in lines { + if !line.trim().is_empty() { + blank_end = line.start(); break; } } @@ -76,11 +84,8 @@ pub fn blank_after_summary(checker: &mut Checker, docstring: &Docstring) { // Insert one blank line after the summary (replacing any existing lines). diagnostic.set_fix(Edit::replacement( checker.stylist.line_ending().to_string(), - Location::new(docstring.expr.location.row() + summary_line + 1, 0), - Location::new( - docstring.expr.location.row() + summary_line + 1 + blanks_count, - 0, - ), + summary_end, + blank_end, )); } } diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs index 52bd516d35..be9ed4141e 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs @@ -1,11 +1,10 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator}; +use ruff_text_size::{TextLen, TextRange}; use crate::checkers::ast::Checker; use crate::docstrings::definition::{DefinitionKind, Docstring}; -use crate::message::Location; use crate::registry::{AsRule, Rule}; #[violation] @@ -70,27 +69,34 @@ pub fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) { { let before = checker .locator - .slice(Range::new(parent.location, docstring.expr.location)); + .slice(TextRange::new(parent.start(), docstring.start())); + + let mut blank_lines_before = 0usize; + let mut lines = UniversalNewlineIterator::with_offset(before, parent.start()).rev(); + let mut blank_lines_start = lines.next().map(|line| line.start()).unwrap_or_default(); + + for line in lines { + if line.trim().is_empty() { + blank_lines_before += 1; + blank_lines_start = line.start(); + } else { + break; + } + } - let blank_lines_before = before - .universal_newlines() - .rev() - .skip(1) - .take_while(|line| line.trim().is_empty()) - .count(); if checker.settings.rules.enabled(Rule::BlankLineBeforeClass) { if blank_lines_before != 0 { let mut diagnostic = Diagnostic::new( BlankLineBeforeClass { lines: blank_lines_before, }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line before the class. diagnostic.set_fix(Edit::deletion( - Location::new(docstring.expr.location.row() - blank_lines_before, 0), - Location::new(docstring.expr.location.row(), 0), + blank_lines_start, + docstring.start() - docstring.indentation.text_len(), )); } checker.diagnostics.push(diagnostic); @@ -106,14 +112,14 @@ pub fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) { OneBlankLineBeforeClass { lines: blank_lines_before, }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Insert one blank line before the class. diagnostic.set_fix(Edit::replacement( checker.stylist.line_ending().to_string(), - Location::new(docstring.expr.location.row() - blank_lines_before, 0), - Location::new(docstring.expr.location.row(), 0), + blank_lines_start, + docstring.start() - docstring.indentation.text_len(), )); } checker.diagnostics.push(diagnostic); @@ -122,10 +128,9 @@ pub fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) { } if checker.settings.rules.enabled(Rule::OneBlankLineAfterClass) { - let after = checker.locator.slice(Range::new( - docstring.expr.end_location.unwrap(), - parent.end_location.unwrap(), - )); + let after = checker + .locator + .slice(TextRange::new(docstring.end(), parent.end())); let all_blank_after = after .universal_newlines() @@ -135,27 +140,33 @@ pub fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) { return; } - let blank_lines_after = after - .universal_newlines() - .skip(1) - .take_while(|line| line.trim().is_empty()) - .count(); + let mut blank_lines_after = 0usize; + let mut lines = UniversalNewlineIterator::with_offset(after, docstring.end()); + let first_line_start = lines.next().map(|l| l.start()).unwrap_or_default(); + let mut blank_lines_end = docstring.end(); + + for line in lines { + if line.trim().is_empty() { + blank_lines_end = line.end(); + blank_lines_after += 1; + } else { + break; + } + } + if blank_lines_after != 1 { let mut diagnostic = Diagnostic::new( OneBlankLineAfterClass { lines: blank_lines_after, }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Insert a blank line before the class (replacing any existing lines). diagnostic.set_fix(Edit::replacement( checker.stylist.line_ending().to_string(), - Location::new(docstring.expr.end_location.unwrap().row() + 1, 0), - Location::new( - docstring.expr.end_location.unwrap().row() + 1 + blank_lines_after, - 0, - ), + first_line_start, + blank_lines_end, )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs index c7e34fb0b9..25cd6ca58b 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs @@ -1,14 +1,13 @@ use once_cell::sync::Lazy; use regex::Regex; +use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator}; use crate::checkers::ast::Checker; use crate::docstrings::definition::{DefinitionKind, Docstring}; -use crate::message::Location; use crate::registry::{AsRule, Rule}; #[violation] @@ -65,26 +64,33 @@ pub fn blank_before_after_function(checker: &mut Checker, docstring: &Docstring) { let before = checker .locator - .slice(Range::new(parent.location, docstring.expr.location)); + .slice(TextRange::new(parent.start(), docstring.start())); + + let mut lines = UniversalNewlineIterator::with_offset(before, parent.start()).rev(); + let mut blank_lines_before = 0usize; + let mut blank_lines_start = lines.next().map(|l| l.end()).unwrap_or_default(); + + for line in lines { + if line.trim().is_empty() { + blank_lines_before += 1; + blank_lines_start = line.start(); + } else { + break; + } + } - let blank_lines_before = before - .universal_newlines() - .rev() - .skip(1) - .take_while(|line| line.trim().is_empty()) - .count(); if blank_lines_before != 0 { let mut diagnostic = Diagnostic::new( NoBlankLineBeforeFunction { num_lines: blank_lines_before, }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line before the docstring. diagnostic.set_fix(Edit::deletion( - Location::new(docstring.expr.location.row() - blank_lines_before, 0), - Location::new(docstring.expr.location.row(), 0), + blank_lines_start, + docstring.start() - docstring.indentation.text_len(), )); } checker.diagnostics.push(diagnostic); @@ -96,10 +102,9 @@ pub fn blank_before_after_function(checker: &mut Checker, docstring: &Docstring) .rules .enabled(Rule::NoBlankLineAfterFunction) { - let after = checker.locator.slice(Range::new( - docstring.expr.end_location.unwrap(), - parent.end_location.unwrap(), - )); + let after = checker + .locator + .slice(TextRange::new(docstring.end(), parent.end())); // If the docstring is only followed by blank and commented lines, abort. let all_blank_after = after @@ -111,19 +116,26 @@ pub fn blank_before_after_function(checker: &mut Checker, docstring: &Docstring) } // Count the number of blank lines after the docstring. - let blank_lines_after = after - .universal_newlines() - .skip(1) - .take_while(|line| line.trim().is_empty()) - .count(); + let mut blank_lines_after = 0usize; + let mut lines = UniversalNewlineIterator::with_offset(after, docstring.end()).peekable(); + let first_line_end = lines.next().map(|l| l.end()).unwrap_or_default(); + let mut blank_lines_end = first_line_end; + + while let Some(line) = lines.peek() { + if line.trim().is_empty() { + blank_lines_after += 1; + blank_lines_end = line.end(); + lines.next(); + } else { + break; + } + } // Avoid violations for blank lines followed by inner functions or classes. if blank_lines_after == 1 - && after - .universal_newlines() - .skip(1 + blank_lines_after) + && lines .find(|line| !line.trim_start().starts_with('#')) - .map_or(false, |line| INNER_FUNCTION_OR_CLASS_REGEX.is_match(line)) + .map_or(false, |line| INNER_FUNCTION_OR_CLASS_REGEX.is_match(&line)) { return; } @@ -133,17 +145,11 @@ pub fn blank_before_after_function(checker: &mut Checker, docstring: &Docstring) NoBlankLineAfterFunction { num_lines: blank_lines_after, }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line after the docstring. - diagnostic.set_fix(Edit::deletion( - Location::new(docstring.expr.end_location.unwrap().row() + 1, 0), - Location::new( - docstring.expr.end_location.unwrap().row() + 1 + blank_lines_after, - 0, - ), - )); + diagnostic.set_fix(Edit::deletion(first_line_end, blank_lines_end)); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs index da67fd7d2b..c6ecac7ed1 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs @@ -1,9 +1,7 @@ -use unicode_width::UnicodeWidthStr; +use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::str::leading_quote; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::docstrings::definition::{DefinitionKind, Docstring}; @@ -41,7 +39,7 @@ pub fn capitalized(checker: &mut Checker, docstring: &Docstring) { return; } - let body = docstring.body; + let body = docstring.body(); let Some(first_word) = body.split(' ').next() else { return @@ -69,20 +67,14 @@ pub fn capitalized(checker: &mut Checker, docstring: &Docstring) { first_word: first_word.to_string(), capitalized_word: capitalized_word.to_string(), }, - Range::from(docstring.expr), + docstring.expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - if let Some(pattern) = leading_quote(docstring.contents) { - diagnostic.set_fix(Edit::replacement( - capitalized_word, - docstring.expr.location.with_col_offset(pattern.width()), - docstring - .expr - .location - .with_col_offset(pattern.width() + first_word.width()), - )); - } + diagnostic.set_fix(Edit::range_replacement( + capitalized_word, + TextRange::at(body.start(), first_word.text_len()), + )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs b/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs index e36bad42b7..ffeb4af655 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs @@ -1,15 +1,13 @@ +use ruff_text_size::TextLen; use strum::IntoEnumIterator; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::str::leading_quote; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator}; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; use crate::docstrings::sections::SectionKind; -use crate::message::Location; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::logical_line; @@ -29,8 +27,7 @@ impl AlwaysAutofixableViolation for EndsInPeriod { /// D400 pub fn ends_with_period(checker: &mut Checker, docstring: &Docstring) { - let contents = docstring.contents; - let body = docstring.body; + let body = docstring.body(); if let Some(first_line) = body.trim().universal_newlines().next() { let trimmed = first_line.trim(); @@ -55,35 +52,22 @@ pub fn ends_with_period(checker: &mut Checker, docstring: &Docstring) { } } - if let Some(index) = logical_line(body) { - let line = body.universal_newlines().nth(index).unwrap(); + if let Some(index) = logical_line(body.as_str()) { + let mut lines = UniversalNewlineIterator::with_offset(&body, body.start()); + let line = lines.nth(index).unwrap(); let trimmed = line.trim_end(); if !trimmed.ends_with('.') { - let mut diagnostic = Diagnostic::new(EndsInPeriod, Range::from(docstring.expr)); + let mut diagnostic = Diagnostic::new(EndsInPeriod, docstring.range()); // Best-effort autofix: avoid adding a period after other punctuation marks. if checker.patch(diagnostic.kind.rule()) && !trimmed.ends_with(':') && !trimmed.ends_with(';') { - if let Some((row, column)) = if index == 0 { - leading_quote(contents).map(|pattern| { - ( - docstring.expr.location.row(), - docstring.expr.location.column() - + pattern.len() - + trimmed.chars().count(), - ) - }) - } else { - Some(( - docstring.expr.location.row() + index, - trimmed.chars().count(), - )) - } { - diagnostic - .set_fix(Edit::insertion(".".to_string(), Location::new(row, column))); - } + diagnostic.set_fix(Edit::insertion( + ".".to_string(), + line.start() + trimmed.text_len(), + )); } checker.diagnostics.push(diagnostic); }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs b/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs index cff79b49e4..b2ed83821f 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs @@ -1,15 +1,13 @@ +use ruff_text_size::TextLen; use strum::IntoEnumIterator; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::str::leading_quote; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator}; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; use crate::docstrings::sections::SectionKind; -use crate::message::Location; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::logical_line; @@ -29,8 +27,7 @@ impl AlwaysAutofixableViolation for EndsInPunctuation { /// D415 pub fn ends_with_punctuation(checker: &mut Checker, docstring: &Docstring) { - let contents = docstring.contents; - let body = docstring.body; + let body = docstring.body(); if let Some(first_line) = body.trim().universal_newlines().next() { let trimmed = first_line.trim(); @@ -55,34 +52,19 @@ pub fn ends_with_punctuation(checker: &mut Checker, docstring: &Docstring) { } } - if let Some(index) = logical_line(body) { - let line = body.universal_newlines().nth(index).unwrap(); + if let Some(index) = logical_line(body.as_str()) { + let mut lines = UniversalNewlineIterator::with_offset(&body, body.start()).skip(index); + let line = lines.next().unwrap(); let trimmed = line.trim_end(); - if !(trimmed.ends_with('.') || trimmed.ends_with('!') || trimmed.ends_with('?')) { - let mut diagnostic = Diagnostic::new(EndsInPunctuation, Range::from(docstring.expr)); + + if !trimmed.ends_with(['.', '!', '?']) { + let mut diagnostic = Diagnostic::new(EndsInPunctuation, docstring.range()); // Best-effort autofix: avoid adding a period after other punctuation marks. - if checker.patch(diagnostic.kind.rule()) - && !trimmed.ends_with(':') - && !trimmed.ends_with(';') - { - if let Some((row, column)) = if index == 0 { - leading_quote(contents).map(|pattern| { - ( - docstring.expr.location.row(), - docstring.expr.location.column() - + pattern.len() - + trimmed.chars().count(), - ) - }) - } else { - Some(( - docstring.expr.location.row() + index, - trimmed.chars().count(), - )) - } { - diagnostic - .set_fix(Edit::insertion(".".to_string(), Location::new(row, column))); - } + if checker.patch(diagnostic.kind.rule()) && !trimmed.ends_with([':', ';']) { + diagnostic.set_fix(Edit::insertion( + ".".to_string(), + line.start() + trimmed.text_len(), + )); } checker.diagnostics.push(diagnostic); }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/indent.rs b/crates/ruff/src/rules/pydocstyle/rules/indent.rs index c11376e02a..92fc84ce10 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/indent.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/indent.rs @@ -2,12 +2,11 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::newlines::NewlineWithTrailingNewline; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace; +use ruff_text_size::{TextLen, TextRange}; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; -use crate::message::Location; use crate::registry::{AsRule, Rule}; #[violation] @@ -50,10 +49,10 @@ impl AlwaysAutofixableViolation for OverIndentation { /// D206, D207, D208 pub fn indent(checker: &mut Checker, docstring: &Docstring) { - let body = docstring.body; + let body = docstring.body(); // Split the docstring into lines. - let lines: Vec<&str> = NewlineWithTrailingNewline::from(body).collect(); + let lines: Vec<_> = NewlineWithTrailingNewline::with_offset(&body, body.start()).collect(); if lines.len() <= 1 { return; } @@ -61,20 +60,22 @@ pub fn indent(checker: &mut Checker, docstring: &Docstring) { let mut has_seen_tab = docstring.indentation.contains('\t'); let mut is_over_indented = true; let mut over_indented_lines = vec![]; + for i in 0..lines.len() { // First lines and continuations doesn't need any indentation. if i == 0 || lines[i - 1].ends_with('\\') { continue; } + let line = &lines[i]; // Omit empty lines, except for the last line, which is non-empty by way of // containing the closing quotation marks. - let is_blank = lines[i].trim().is_empty(); + let is_blank = line.trim().is_empty(); if i < lines.len() - 1 && is_blank { continue; } - let line_indent = whitespace::leading_space(lines[i]); + let line_indent = whitespace::leading_space(line); // We only report tab indentation once, so only check if we haven't seen a tab // yet. @@ -86,18 +87,12 @@ pub fn indent(checker: &mut Checker, docstring: &Docstring) { if (i == lines.len() - 1 || !is_blank) && line_indent.len() < docstring.indentation.len() { - let mut diagnostic = Diagnostic::new( - UnderIndentation, - Range::new( - Location::new(docstring.expr.location.row() + i, 0), - Location::new(docstring.expr.location.row() + i, 0), - ), - ); + let mut diagnostic = + Diagnostic::new(UnderIndentation, TextRange::empty(line.start())); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( whitespace::clean(docstring.indentation), - Location::new(docstring.expr.location.row() + i, 0), - Location::new(docstring.expr.location.row() + i, line_indent.len()), + TextRange::at(line.start(), line_indent.text_len()), )); } checker.diagnostics.push(diagnostic); @@ -112,7 +107,7 @@ pub fn indent(checker: &mut Checker, docstring: &Docstring) { // the over-indentation status of every line. if i < lines.len() - 1 { if line_indent.len() > docstring.indentation.len() { - over_indented_lines.push(i); + over_indented_lines.push(TextRange::at(line.start(), line_indent.text_len())); } else { is_over_indented = false; } @@ -121,57 +116,44 @@ pub fn indent(checker: &mut Checker, docstring: &Docstring) { if checker.settings.rules.enabled(Rule::IndentWithSpaces) { if has_seen_tab { - checker.diagnostics.push(Diagnostic::new( - IndentWithSpaces, - Range::from(docstring.expr), - )); + checker + .diagnostics + .push(Diagnostic::new(IndentWithSpaces, docstring.range())); } } if checker.settings.rules.enabled(Rule::OverIndentation) { // If every line (except the last) is over-indented... if is_over_indented { - for i in over_indented_lines { - let line_indent = whitespace::leading_space(lines[i]); - if line_indent.len() > docstring.indentation.len() { - // We report over-indentation on every line. This isn't great, but - // enables autofix. - let mut diagnostic = Diagnostic::new( - OverIndentation, - Range::new( - Location::new(docstring.expr.location.row() + i, 0), - Location::new(docstring.expr.location.row() + i, 0), - ), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - whitespace::clean(docstring.indentation), - Location::new(docstring.expr.location.row() + i, 0), - Location::new(docstring.expr.location.row() + i, line_indent.len()), - )); - } - checker.diagnostics.push(diagnostic); + for over_indented in over_indented_lines { + // We report over-indentation on every line. This isn't great, but + // enables autofix. + let mut diagnostic = + Diagnostic::new(OverIndentation, TextRange::empty(over_indented.start())); + if checker.patch(diagnostic.kind.rule()) { + let new_indent = whitespace::clean(docstring.indentation); + + let edit = if new_indent.is_empty() { + Edit::range_deletion(over_indented) + } else { + Edit::range_replacement(new_indent, over_indented) + }; + diagnostic.set_fix(edit); } + checker.diagnostics.push(diagnostic); } } // If the last line is over-indented... - if !lines.is_empty() { - let i = lines.len() - 1; - let line_indent = whitespace::leading_space(lines[i]); + if let Some(last) = lines.last() { + let line_indent = whitespace::leading_space(last); if line_indent.len() > docstring.indentation.len() { - let mut diagnostic = Diagnostic::new( - OverIndentation, - Range::new( - Location::new(docstring.expr.location.row() + i, 0), - Location::new(docstring.expr.location.row() + i, 0), - ), - ); + let mut diagnostic = + Diagnostic::new(OverIndentation, TextRange::empty(last.start())); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( whitespace::clean(docstring.indentation), - Location::new(docstring.expr.location.row() + i, 0), - Location::new(docstring.expr.location.row() + i, line_indent.len()), + TextRange::at(last.start(), line_indent.text_len()), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs b/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs index bc462c6067..18e51fe435 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs @@ -1,12 +1,11 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::{NewlineWithTrailingNewline, StrExt}; +use ruff_python_ast::newlines::{NewlineWithTrailingNewline, UniversalNewlineIterator}; use ruff_python_ast::str::{is_triple_quote, leading_quote}; -use ruff_python_ast::types::Range; +use ruff_text_size::{TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::docstrings::definition::{DefinitionKind, Docstring}; -use crate::message::Location; use crate::registry::{AsRule, Rule}; #[violation] @@ -40,39 +39,40 @@ impl AlwaysAutofixableViolation for MultiLineSummarySecondLine { /// D212, D213 pub fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstring) { let contents = docstring.contents; - let body = docstring.body; + let body = docstring.body(); - if NewlineWithTrailingNewline::from(body).nth(1).is_none() { + if NewlineWithTrailingNewline::from(body.as_str()) + .nth(1) + .is_none() + { return; }; - let mut content_lines = contents.universal_newlines(); + let mut content_lines = UniversalNewlineIterator::with_offset(contents, docstring.start()); + let Some(first_line) = content_lines .next() else { return; }; - if is_triple_quote(first_line) { + + if is_triple_quote(&first_line) { if checker .settings .rules .enabled(Rule::MultiLineSummaryFirstLine) { - let mut diagnostic = - Diagnostic::new(MultiLineSummaryFirstLine, Range::from(docstring.expr)); + let mut diagnostic = Diagnostic::new(MultiLineSummaryFirstLine, docstring.range()); if checker.patch(diagnostic.kind.rule()) { - let location = docstring.expr.location; - let mut end_row = location.row() + 1; // Delete until first non-whitespace char. for line in content_lines { if let Some(end_column) = line.find(|c: char| !c.is_whitespace()) { - let start = - Location::new(location.row(), location.column() + first_line.len()); - let end = Location::new(end_row, end_column); - diagnostic.set_fix(Edit::deletion(start, end)); + diagnostic.set_fix(Edit::deletion( + first_line.end(), + line.start() + TextSize::try_from(end_column).unwrap(), + )); break; } - end_row += 1; } } checker.diagnostics.push(diagnostic); @@ -83,8 +83,7 @@ pub fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstring) { .rules .enabled(Rule::MultiLineSummarySecondLine) { - let mut diagnostic = - Diagnostic::new(MultiLineSummarySecondLine, Range::from(docstring.expr)); + let mut diagnostic = Diagnostic::new(MultiLineSummarySecondLine, docstring.range()); if checker.patch(diagnostic.kind.rule()) { let mut indentation = String::from(docstring.indentation); let mut fixable = true; @@ -99,10 +98,11 @@ pub fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstring) { | DefinitionKind::NestedFunction(parent) | DefinitionKind::Method(parent) = &docstring.kind { - let parent_indentation = checker.locator.slice(Range::new( - Location::new(parent.location.row(), 0), - Location::new(parent.location.row(), parent.location.column()), - )); + let parent_line_start = checker.locator.line_start(parent.start()); + let parent_indentation = checker + .locator + .slice(TextRange::new(parent_line_start, parent.start())); + if parent_indentation.chars().all(char::is_whitespace) { indentation.clear(); indentation.push_str(parent_indentation); @@ -113,7 +113,6 @@ pub fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstring) { } if fixable { - let location = docstring.expr.location; let prefix = leading_quote(contents).unwrap(); // Use replacement instead of insert to trim possible whitespace between leading // quote and text. @@ -123,11 +122,8 @@ pub fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstring) { indentation, first_line.strip_prefix(prefix).unwrap().trim_start() ); - diagnostic.set_fix(Edit::replacement( - repl, - Location::new(location.row(), location.column() + prefix.len()), - Location::new(location.row(), location.column() + first_line.len()), - )); + + diagnostic.set_fix(Edit::replacement(repl, body.start(), first_line.end())); } } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs b/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs index 5f7e9ca5f4..3728a42579 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs @@ -1,12 +1,11 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::newlines::{NewlineWithTrailingNewline, StrExt}; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace; +use ruff_text_size::{TextLen, TextSize}; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; -use crate::message::Location; use crate::registry::AsRule; #[violation] @@ -26,27 +25,32 @@ impl AlwaysAutofixableViolation for NewLineAfterLastParagraph { /// D209 pub fn newline_after_last_paragraph(checker: &mut Checker, docstring: &Docstring) { let contents = docstring.contents; - let body = docstring.body; + let body = docstring.body(); let mut line_count = 0; - for line in NewlineWithTrailingNewline::from(body) { + for line in NewlineWithTrailingNewline::from(body.as_str()) { if !line.trim().is_empty() { line_count += 1; } if line_count > 1 { - if let Some(last_line) = contents.universal_newlines().last().map(str::trim) { + if let Some(last_line) = contents + .universal_newlines() + .last() + .map(|l| l.as_str().trim()) + { if last_line != "\"\"\"" && last_line != "'''" { let mut diagnostic = - Diagnostic::new(NewLineAfterLastParagraph, Range::from(docstring.expr)); + Diagnostic::new(NewLineAfterLastParagraph, docstring.range()); if checker.patch(diagnostic.kind.rule()) { // Insert a newline just before the end-quote(s). - let num_trailing_quotes = "'''".len(); - let num_trailing_spaces = last_line + let num_trailing_quotes = "'''".text_len(); + let num_trailing_spaces: TextSize = last_line .chars() .rev() - .skip(num_trailing_quotes) + .skip(usize::from(num_trailing_quotes)) .take_while(|c| c.is_whitespace()) - .count(); + .map(TextLen::text_len) + .sum(); let content = format!( "{}{}", checker.stylist.line_ending().as_str(), @@ -54,16 +58,8 @@ pub fn newline_after_last_paragraph(checker: &mut Checker, docstring: &Docstring ); diagnostic.set_fix(Edit::replacement( content, - Location::new( - docstring.expr.end_location.unwrap().row(), - docstring.expr.end_location.unwrap().column() - - num_trailing_spaces - - num_trailing_quotes, - ), - Location::new( - docstring.expr.end_location.unwrap().row(), - docstring.expr.end_location.unwrap().column() - num_trailing_quotes, - ), + docstring.expr.end() - num_trailing_quotes - num_trailing_spaces, + docstring.expr.end() - num_trailing_quotes, )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs index 5546a24cf7..8d635ed0f9 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::StmtKind; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::docstrings::definition::{DefinitionKind, Docstring}; @@ -31,15 +30,17 @@ pub fn no_signature(checker: &mut Checker, docstring: &Docstring) { return; }; - let body = docstring.body; + let body = docstring.body(); let Some(first_line) = body.trim().universal_newlines().next() else { return; }; + if !first_line.contains(&format!("{name}(")) { return; }; + checker .diagnostics - .push(Diagnostic::new(NoSignature, Range::from(docstring.expr))); + .push(Diagnostic::new(NoSignature, docstring.range())); } diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs b/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs index ced81d26ca..9503907576 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs @@ -1,12 +1,10 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::newlines::NewlineWithTrailingNewline; -use ruff_python_ast::str::leading_quote; -use ruff_python_ast::types::Range; +use ruff_text_size::{TextLen, TextRange}; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; -use crate::message::Location; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::ends_with_backslash; @@ -26,10 +24,9 @@ impl AlwaysAutofixableViolation for SurroundingWhitespace { /// D210 pub fn no_surrounding_whitespace(checker: &mut Checker, docstring: &Docstring) { - let contents = docstring.contents; - let body = docstring.body; + let body = docstring.body(); - let mut lines = NewlineWithTrailingNewline::from(body); + let mut lines = NewlineWithTrailingNewline::from(body.as_str()); let Some(line) = lines.next() else { return; }; @@ -40,27 +37,17 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, docstring: &Docstring) { if line == trimmed { return; } - let mut diagnostic = Diagnostic::new(SurroundingWhitespace, Range::from(docstring.expr)); + let mut diagnostic = Diagnostic::new(SurroundingWhitespace, docstring.range()); if checker.patch(diagnostic.kind.rule()) { - if let Some(pattern) = leading_quote(contents) { - // If removing whitespace would lead to an invalid string of quote - // characters, avoid applying the fix. - if !trimmed.ends_with(pattern.chars().last().unwrap()) - && !trimmed.starts_with(pattern.chars().last().unwrap()) - && !ends_with_backslash(trimmed) - { - diagnostic.set_fix(Edit::replacement( - trimmed.to_string(), - Location::new( - docstring.expr.location.row(), - docstring.expr.location.column() + pattern.len(), - ), - Location::new( - docstring.expr.location.row(), - docstring.expr.location.column() + pattern.len() + line.chars().count(), - ), - )); - } + let quote = docstring.contents.chars().last().unwrap(); + // If removing whitespace would lead to an invalid string of quote + // characters, avoid applying the fix. + if !trimmed.ends_with(quote) && !trimmed.starts_with(quote) && !ends_with_backslash(trimmed) + { + diagnostic.set_fix(Edit::range_replacement( + trimmed.to_string(), + TextRange::at(body.start(), line.text_len()), + )); } } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs index 91a38951d8..c2b5a63bfd 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -8,7 +8,6 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::{from_qualified_name, CallPath}; use ruff_python_ast::cast; use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::visibility::{is_property, is_test}; use crate::checkers::ast::Checker; @@ -46,11 +45,11 @@ pub fn non_imperative_mood( return; } - let body = docstring.body; + let body = docstring.body(); // Find first line, disregarding whitespace. let line = match body.trim().universal_newlines().next() { - Some(line) => line.trim(), + Some(line) => line.as_str().trim(), None => return, }; // Find the first word on that line and normalize it to lower-case. @@ -62,10 +61,7 @@ pub fn non_imperative_mood( return; } if let Some(false) = MOOD.is_imperative(&first_word_norm) { - let diagnostic = Diagnostic::new( - NonImperativeMood(line.to_string()), - Range::from(docstring.expr), - ); + let diagnostic = Diagnostic::new(NonImperativeMood(line.to_string()), docstring.range()); checker.diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs b/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs index f8f7c2bd19..1fcaa7a29b 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs @@ -1,6 +1,5 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; @@ -18,14 +17,14 @@ impl Violation for EmptyDocstring { /// D419 pub fn not_empty(checker: &mut Checker, docstring: &Docstring) -> bool { - if !docstring.body.trim().is_empty() { + if !docstring.body().trim().is_empty() { return true; } if checker.settings.rules.enabled(Rule::EmptyDocstring) { checker .diagnostics - .push(Diagnostic::new(EmptyDocstring, Range::from(docstring.expr))); + .push(Diagnostic::new(EmptyDocstring, docstring.range())); } false } diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index dd3ed14616..d5107b970f 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -2,14 +2,13 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::visibility::{ is_call, is_init, is_magic, is_new, is_overload, is_override, Visibility, }; +use ruff_text_size::TextRange; use crate::checkers::ast::Checker; use crate::docstrings::definition::{Definition, DefinitionKind}; -use crate::message::Location; use crate::registry::Rule; #[violation] @@ -107,7 +106,7 @@ pub fn not_missing(checker: &mut Checker, definition: &Definition, visibility: V { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicModule, - Range::new(Location::new(1, 0), Location::new(1, 0)), + TextRange::default(), )); } false @@ -120,7 +119,7 @@ pub fn not_missing(checker: &mut Checker, definition: &Definition, visibility: V { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicPackage, - Range::new(Location::new(1, 0), Location::new(1, 0)), + TextRange::default(), )); } false diff --git a/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs b/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs index d7cce0f985..dfbbc32eea 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs @@ -2,7 +2,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::newlines::NewlineWithTrailingNewline; use ruff_python_ast::str::{leading_quote, trailing_quote}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; @@ -26,7 +25,7 @@ impl AlwaysAutofixableViolation for FitsOnOneLine { pub fn one_liner(checker: &mut Checker, docstring: &Docstring) { let mut line_count = 0; let mut non_empty_line_count = 0; - for line in NewlineWithTrailingNewline::from(docstring.body) { + for line in NewlineWithTrailingNewline::from(docstring.body().as_str()) { line_count += 1; if !line.trim().is_empty() { non_empty_line_count += 1; @@ -37,7 +36,7 @@ pub fn one_liner(checker: &mut Checker, docstring: &Docstring) { } if non_empty_line_count == 1 && line_count > 1 { - let mut diagnostic = Diagnostic::new(FitsOnOneLine, Range::from(docstring.expr)); + let mut diagnostic = Diagnostic::new(FitsOnOneLine, docstring.range()); if checker.patch(diagnostic.kind.rule()) { if let (Some(leading), Some(trailing)) = ( leading_quote(docstring.contents), @@ -45,14 +44,14 @@ pub fn one_liner(checker: &mut Checker, docstring: &Docstring) { ) { // If removing whitespace would lead to an invalid string of quote // characters, avoid applying the fix. - let trimmed = docstring.body.trim(); + let body = docstring.body(); + let trimmed = body.trim(); if !trimmed.ends_with(trailing.chars().last().unwrap()) && !trimmed.starts_with(leading.chars().last().unwrap()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( format!("{leading}{trimmed}{trailing}"), - docstring.expr.location, - docstring.expr.end_location.unwrap(), + docstring.range(), )); } } diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 9dd96e464c..abfd5b827a 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -1,6 +1,7 @@ use itertools::Itertools; use once_cell::sync::Lazy; use regex::Regex; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_hash::FxHashSet; use rustpython_parser::ast::StmtKind; @@ -9,15 +10,13 @@ use ruff_diagnostics::{Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::identifier_range; use ruff_python_ast::newlines::NewlineWithTrailingNewline; -use ruff_python_ast::types::Range; use ruff_python_ast::{cast, whitespace}; use ruff_python_semantic::analyze::visibility::is_staticmethod; use crate::checkers::ast::Checker; use crate::docstrings::definition::{DefinitionKind, Docstring}; -use crate::docstrings::sections::{section_contexts, SectionContext, SectionKind}; +use crate::docstrings::sections::{SectionContext, SectionContexts, SectionKind}; use crate::docstrings::styles::SectionStyle; -use crate::message::Location; use crate::registry::{AsRule, Rule}; use crate::rules::pydocstyle::settings::Convention; @@ -271,26 +270,19 @@ impl AlwaysAutofixableViolation for BlankLinesBetweenHeaderAndContent { /// D212, D214, D215, D405, D406, D407, D408, D409, D410, D411, D412, D413, /// D414, D416, D417 pub fn sections(checker: &mut Checker, docstring: &Docstring, convention: Option<&Convention>) { - let body = docstring.body; - - let lines: Vec<&str> = NewlineWithTrailingNewline::from(body).collect(); - if lines.len() < 2 { - return; - } - match convention { Some(Convention::Google) => { parse_google_sections( checker, docstring, - §ion_contexts(&lines, SectionStyle::Google), + &SectionContexts::from_docstring(docstring, SectionStyle::Google), ); } Some(Convention::Numpy) => { parse_numpy_sections( checker, docstring, - §ion_contexts(&lines, SectionStyle::Numpy), + &SectionContexts::from_docstring(docstring, SectionStyle::Numpy), ); } Some(Convention::Pep257) | None => { @@ -300,10 +292,10 @@ pub fn sections(checker: &mut Checker, docstring: &Docstring, convention: Option // If the docstring contains `Parameters:` or `Other Parameters:`, use the NumPy // convention. - let numpy_sections = section_contexts(&lines, SectionStyle::Numpy); + let numpy_sections = SectionContexts::from_docstring(docstring, SectionStyle::Numpy); if numpy_sections.iter().any(|context| { matches!( - context.kind, + context.kind(), SectionKind::Parameters | SectionKind::OtherParams | SectionKind::OtherParameters @@ -314,10 +306,10 @@ pub fn sections(checker: &mut Checker, docstring: &Docstring, convention: Option } // If the docstring contains any argument specifier, use the Google convention. - let google_sections = section_contexts(&lines, SectionStyle::Google); + let google_sections = SectionContexts::from_docstring(docstring, SectionStyle::Google); if google_sections.iter().any(|context| { matches!( - context.kind, + context.kind(), SectionKind::Args | SectionKind::Arguments | SectionKind::KeywordArgs @@ -346,245 +338,213 @@ fn blanks_and_section_underline( context: &SectionContext, ) { let mut blank_lines_after_header = 0; - for line in context.following_lines { - if !line.trim().is_empty() { + let mut blank_lines_end = context.following_range().start(); + let mut following_lines = context.following_lines().peekable(); + + while let Some(line) = following_lines.peek() { + if line.trim().is_empty() { + blank_lines_end = line.full_end(); + blank_lines_after_header += 1; + following_lines.next(); + } else { break; } - blank_lines_after_header += 1; } - // Nothing but blank lines after the section header. - if blank_lines_after_header == context.following_lines.len() { - if checker - .settings - .rules - .enabled(Rule::DashedUnderlineAfterSection) - { - let mut diagnostic = Diagnostic::new( - DashedUnderlineAfterSection { - name: context.section_name.to_string(), - }, - Range::from(docstring.expr), - ); - if checker.patch(diagnostic.kind.rule()) { - // Add a dashed line (of the appropriate length) under the section header. - let content = format!( - "{}{}{}", - checker.stylist.line_ending().as_str(), - whitespace::clean(docstring.indentation), - "-".repeat(context.section_name.len()), - ); - diagnostic.set_fix(Edit::insertion( - content, - Location::new( - docstring.expr.location.row() + context.original_index, - context.line.trim_end().chars().count(), - ), - )); - } - checker.diagnostics.push(diagnostic); - } - if checker.settings.rules.enabled(Rule::EmptyDocstringSection) { - checker.diagnostics.push(Diagnostic::new( - EmptyDocstringSection { - name: context.section_name.to_string(), - }, - Range::from(docstring.expr), - )); - } - return; - } - - let non_empty_line = context.following_lines[blank_lines_after_header]; - let dash_line_found = non_empty_line - .chars() - .all(|char| char.is_whitespace() || char == '-'); - - if dash_line_found { - if blank_lines_after_header > 0 { - if checker - .settings - .rules - .enabled(Rule::SectionUnderlineAfterName) - { - let mut diagnostic = Diagnostic::new( - SectionUnderlineAfterName { - name: context.section_name.to_string(), - }, - Range::from(docstring.expr), - ); - if checker.patch(diagnostic.kind.rule()) { - // Delete any blank lines between the header and the underline. - diagnostic.set_fix(Edit::deletion( - Location::new( - docstring.expr.location.row() + context.original_index + 1, - 0, - ), - Location::new( - docstring.expr.location.row() - + context.original_index - + 1 - + blank_lines_after_header, - 0, - ), - )); - } - checker.diagnostics.push(diagnostic); - } - } - - if non_empty_line - .trim() + if let Some(non_blank_line) = following_lines.next() { + let dash_line_found = non_blank_line .chars() - .filter(|char| *char == '-') - .count() - != context.section_name.len() - { + .all(|char| char.is_whitespace() || char == '-'); + + if dash_line_found { + if blank_lines_after_header > 0 { + if checker + .settings + .rules + .enabled(Rule::SectionUnderlineAfterName) + { + let mut diagnostic = Diagnostic::new( + SectionUnderlineAfterName { + name: context.section_name().to_string(), + }, + docstring.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + let range = + TextRange::new(context.following_range().start(), blank_lines_end); + // Delete any blank lines between the header and the underline. + diagnostic.set_fix(Edit::range_deletion(range)); + } + checker.diagnostics.push(diagnostic); + } + } + + if non_blank_line + .trim() + .chars() + .filter(|char| *char == '-') + .count() + != context.section_name().len() + { + if checker + .settings + .rules + .enabled(Rule::SectionUnderlineMatchesSectionLength) + { + let mut diagnostic = Diagnostic::new( + SectionUnderlineMatchesSectionLength { + name: context.section_name().to_string(), + }, + docstring.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + // Replace the existing underline with a line of the appropriate length. + let content = format!( + "{}{}{}", + whitespace::clean(docstring.indentation), + "-".repeat(context.section_name().len()), + checker.stylist.line_ending().as_str() + ); + diagnostic.set_fix(Edit::replacement( + content, + blank_lines_end, + non_blank_line.full_end(), + )); + }; + checker.diagnostics.push(diagnostic); + } + } + if checker .settings .rules - .enabled(Rule::SectionUnderlineMatchesSectionLength) + .enabled(Rule::SectionUnderlineNotOverIndented) { - let mut diagnostic = Diagnostic::new( - SectionUnderlineMatchesSectionLength { - name: context.section_name.to_string(), - }, - Range::from(docstring.expr), - ); - if checker.patch(diagnostic.kind.rule()) { - // Replace the existing underline with a line of the appropriate length. - let content = format!( - "{}{}{}", - whitespace::clean(docstring.indentation), - "-".repeat(context.section_name.len()), - checker.stylist.line_ending().as_str() + let leading_space = whitespace::leading_space(&non_blank_line); + if leading_space.len() > docstring.indentation.len() { + let mut diagnostic = Diagnostic::new( + SectionUnderlineNotOverIndented { + name: context.section_name().to_string(), + }, + docstring.range(), ); - diagnostic.set_fix(Edit::replacement( - content, - Location::new( - docstring.expr.location.row() - + context.original_index - + 1 - + blank_lines_after_header, - 0, - ), - Location::new( - docstring.expr.location.row() - + context.original_index - + 1 - + blank_lines_after_header - + 1, - 0, - ), - )); - }; - checker.diagnostics.push(diagnostic); - } - } + if checker.patch(diagnostic.kind.rule()) { + let range = TextRange::at( + blank_lines_end, + leading_space.text_len() + TextSize::from(1), + ); - if checker - .settings - .rules - .enabled(Rule::SectionUnderlineNotOverIndented) - { - let leading_space = whitespace::leading_space(non_empty_line); - if leading_space.len() > docstring.indentation.len() { - let mut diagnostic = Diagnostic::new( - SectionUnderlineNotOverIndented { - name: context.section_name.to_string(), - }, - Range::from(docstring.expr), - ); - if checker.patch(diagnostic.kind.rule()) { - // Replace the existing indentation with whitespace of the appropriate length. - diagnostic.set_fix(Edit::replacement( - whitespace::clean(docstring.indentation), - Location::new( - docstring.expr.location.row() - + context.original_index - + 1 - + blank_lines_after_header, - 0, - ), - Location::new( - docstring.expr.location.row() - + context.original_index - + 1 - + blank_lines_after_header, - 1 + leading_space.len(), - ), - )); - }; - checker.diagnostics.push(diagnostic); - } - } - - let line_after_dashes_index = blank_lines_after_header + 1; - - if line_after_dashes_index < context.following_lines.len() { - let line_after_dashes = context.following_lines[line_after_dashes_index]; - if line_after_dashes.trim().is_empty() { - let rest_of_lines = &context.following_lines[line_after_dashes_index..]; - let blank_lines_after_dashes = rest_of_lines - .iter() - .take_while(|line| line.trim().is_empty()) - .count(); - if blank_lines_after_dashes == rest_of_lines.len() { - if checker.settings.rules.enabled(Rule::EmptyDocstringSection) { - checker.diagnostics.push(Diagnostic::new( - EmptyDocstringSection { - name: context.section_name.to_string(), - }, - Range::from(docstring.expr), + // Replace the existing indentation with whitespace of the appropriate length. + diagnostic.set_fix(Edit::range_replacement( + whitespace::clean(docstring.indentation), + range, )); + }; + checker.diagnostics.push(diagnostic); + } + } + + if let Some(line_after_dashes) = following_lines.next() { + if line_after_dashes.trim().is_empty() { + let mut blank_lines_after_dashes_end = line_after_dashes.full_end(); + while let Some(line) = following_lines.peek() { + if line.trim().is_empty() { + blank_lines_after_dashes_end = line.full_end(); + following_lines.next(); + } else { + break; + } } - } else { - if checker + + if following_lines.peek().is_none() { + if checker.settings.rules.enabled(Rule::EmptyDocstringSection) { + checker.diagnostics.push(Diagnostic::new( + EmptyDocstringSection { + name: context.section_name().to_string(), + }, + docstring.range(), + )); + } + } else if checker .settings .rules .enabled(Rule::BlankLinesBetweenHeaderAndContent) { let mut diagnostic = Diagnostic::new( BlankLinesBetweenHeaderAndContent { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Delete any blank lines between the header and content. diagnostic.set_fix(Edit::deletion( - Location::new( - docstring.expr.location.row() - + context.original_index - + 1 - + line_after_dashes_index, - 0, - ), - Location::new( - docstring.expr.location.row() - + context.original_index - + 1 - + line_after_dashes_index - + blank_lines_after_dashes, - 0, - ), + line_after_dashes.start(), + blank_lines_after_dashes_end, )); } checker.diagnostics.push(diagnostic); } } + } else { + if checker.settings.rules.enabled(Rule::EmptyDocstringSection) { + checker.diagnostics.push(Diagnostic::new( + EmptyDocstringSection { + name: context.section_name().to_string(), + }, + docstring.range(), + )); + } } } else { - if checker.settings.rules.enabled(Rule::EmptyDocstringSection) { - checker.diagnostics.push(Diagnostic::new( - EmptyDocstringSection { - name: context.section_name.to_string(), + if checker + .settings + .rules + .enabled(Rule::DashedUnderlineAfterSection) + { + let mut diagnostic = Diagnostic::new( + DashedUnderlineAfterSection { + name: context.section_name().to_string(), }, - Range::from(docstring.expr), - )); + docstring.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + // Add a dashed line (of the appropriate length) under the section header. + let content = format!( + "{}{}{}", + checker.stylist.line_ending().as_str(), + whitespace::clean(docstring.indentation), + "-".repeat(context.section_name().len()), + ); + diagnostic.set_fix(Edit::insertion(content, context.summary_range().end())); + } + checker.diagnostics.push(diagnostic); + } + if blank_lines_after_header > 0 { + if checker + .settings + .rules + .enabled(Rule::BlankLinesBetweenHeaderAndContent) + { + let mut diagnostic = Diagnostic::new( + BlankLinesBetweenHeaderAndContent { + name: context.section_name().to_string(), + }, + docstring.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + let range = + TextRange::new(context.following_range().start(), blank_lines_end); + // Delete any blank lines between the header and content. + diagnostic.set_fix(Edit::range_deletion(range)); + } + checker.diagnostics.push(diagnostic); + } } } - } else { + } + // Nothing but blank lines after the section header. + else { if checker .settings .rules @@ -592,9 +552,9 @@ fn blanks_and_section_underline( { let mut diagnostic = Diagnostic::new( DashedUnderlineAfterSection { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Add a dashed line (of the appropriate length) under the section header. @@ -602,104 +562,61 @@ fn blanks_and_section_underline( "{}{}{}", checker.stylist.line_ending().as_str(), whitespace::clean(docstring.indentation), - "-".repeat(context.section_name.len()), + "-".repeat(context.section_name().len()), ); - diagnostic.set_fix(Edit::insertion( - content, - Location::new( - docstring.expr.location.row() + context.original_index, - context.line.trim_end().chars().count(), - ), - )); + + diagnostic.set_fix(Edit::insertion(content, context.summary_range().end())); } checker.diagnostics.push(diagnostic); } - if blank_lines_after_header > 0 { - if checker - .settings - .rules - .enabled(Rule::BlankLinesBetweenHeaderAndContent) - { - let mut diagnostic = Diagnostic::new( - BlankLinesBetweenHeaderAndContent { - name: context.section_name.to_string(), - }, - Range::from(docstring.expr), - ); - if checker.patch(diagnostic.kind.rule()) { - // Delete any blank lines between the header and content. - diagnostic.set_fix(Edit::deletion( - Location::new( - docstring.expr.location.row() + context.original_index + 1, - 0, - ), - Location::new( - docstring.expr.location.row() - + context.original_index - + 1 - + blank_lines_after_header, - 0, - ), - )); - } - checker.diagnostics.push(diagnostic); - } + if checker.settings.rules.enabled(Rule::EmptyDocstringSection) { + checker.diagnostics.push(Diagnostic::new( + EmptyDocstringSection { + name: context.section_name().to_string(), + }, + docstring.range(), + )); } } } fn common_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) { if checker.settings.rules.enabled(Rule::CapitalizeSectionName) { - let capitalized_section_name = context.kind.as_str(); - if context.section_name != capitalized_section_name { + let capitalized_section_name = context.kind().as_str(); + if context.section_name() != capitalized_section_name { let mut diagnostic = Diagnostic::new( CapitalizeSectionName { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Replace the section title with the capitalized variant. This requires // locating the start and end of the section name. - if let Some(index) = context.line.find(context.section_name) { - // Map from bytes to characters. - let section_name_start = &context.line[..index].chars().count(); - let section_name_length = &context.section_name.chars().count(); - diagnostic.set_fix(Edit::replacement( - capitalized_section_name.to_string(), - Location::new( - docstring.expr.location.row() + context.original_index, - *section_name_start, - ), - Location::new( - docstring.expr.location.row() + context.original_index, - section_name_start + section_name_length, - ), - )); - } + let section_range = context.section_name_range(); + diagnostic.set_fix(Edit::range_replacement( + capitalized_section_name.to_string(), + section_range, + )); } checker.diagnostics.push(diagnostic); } } if checker.settings.rules.enabled(Rule::SectionNotOverIndented) { - let leading_space = whitespace::leading_space(context.line); + let leading_space = whitespace::leading_space(context.summary_line()); if leading_space.len() > docstring.indentation.len() { let mut diagnostic = Diagnostic::new( SectionNotOverIndented { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Replace the existing indentation with whitespace of the appropriate length. - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( whitespace::clean(docstring.indentation), - Location::new(docstring.expr.location.row() + context.original_index, 0), - Location::new( - docstring.expr.location.row() + context.original_index, - leading_space.len(), - ), + TextRange::at(context.range().start(), leading_space.text_len()), )); }; checker.diagnostics.push(diagnostic); @@ -707,12 +624,9 @@ fn common_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio } let line_end = checker.stylist.line_ending().as_str(); - if context - .following_lines - .last() - .map_or(true, |line| !line.trim().is_empty()) - { - if context.is_last_section { + let last_line = context.following_lines().last(); + if last_line.map_or(true, |line| !line.trim().is_empty()) { + if context.is_last() { if checker .settings .rules @@ -720,21 +634,15 @@ fn common_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio { let mut diagnostic = Diagnostic::new( BlankLineAfterLastSection { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Add a newline after the section. - let line = context.following_lines.last().unwrap_or(&context.line); diagnostic.set_fix(Edit::insertion( format!("{}{}", line_end, docstring.indentation), - Location::new( - docstring.expr.location.row() - + context.original_index - + context.following_lines.len(), - line.trim_end().chars().count(), - ), + context.range().end(), )); } checker.diagnostics.push(diagnostic); @@ -747,22 +655,14 @@ fn common_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio { let mut diagnostic = Diagnostic::new( NoBlankLineAfterSection { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Add a newline after the section. - let line = context.following_lines.last().unwrap_or(&context.line); - diagnostic.set_fix(Edit::insertion( - line_end.to_string(), - Location::new( - docstring.expr.location.row() - + context.original_index - + context.following_lines.len(), - line.trim_end().chars().count(), - ), - )); + diagnostic + .set_fix(Edit::insertion(line_end.to_string(), context.range().end())); } checker.diagnostics.push(diagnostic); } @@ -774,18 +674,18 @@ fn common_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio .rules .enabled(Rule::NoBlankLineBeforeSection) { - if !context.previous_line.is_empty() { + if !context.previous_line().map_or(false, str::is_empty) { let mut diagnostic = Diagnostic::new( NoBlankLineBeforeSection { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Add a blank line before the section. diagnostic.set_fix(Edit::insertion( line_end.to_string(), - Location::new(docstring.expr.location.row() + context.original_index, 0), + context.range().start(), )); } checker.diagnostics.push(diagnostic); @@ -872,16 +772,17 @@ static GOOGLE_ARGS_REGEX: Lazy = Lazy::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap()); fn args_section(context: &SectionContext) -> FxHashSet { - if context.following_lines.is_empty() { + let mut following_lines = context.following_lines().peekable(); + let Some(first_line) = following_lines.next() else { return FxHashSet::default(); - } + }; // Normalize leading whitespace, by removing any lines with less indentation // than the first. - let leading_space = whitespace::leading_space(context.following_lines[0]); - let relevant_lines = context - .following_lines - .iter() + let leading_space = whitespace::leading_space(first_line.as_str()); + let relevant_lines = std::iter::once(first_line) + .chain(following_lines) + .map(|l| l.as_str()) .filter(|line| line.starts_with(leading_space) || line.is_empty()) .join("\n"); let args_content = textwrap::dedent(&relevant_lines); @@ -922,24 +823,28 @@ fn args_section(context: &SectionContext) -> FxHashSet { fn parameters_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) { // Collect the list of arguments documented in the docstring. let mut docstring_args: FxHashSet = FxHashSet::default(); - let section_level_indent = whitespace::leading_space(context.line); + let section_level_indent = whitespace::leading_space(context.summary_line()); // Join line continuations, then resplit by line. - let adjusted_following_lines = context.following_lines.join("\n").replace("\\\n", ""); + let adjusted_following_lines = context + .following_lines() + .map(|l| l.as_str()) + .join("\n") + .replace("\\\n", ""); let mut lines = NewlineWithTrailingNewline::from(&adjusted_following_lines); if let Some(mut current_line) = lines.next() { for next_line in lines { - let current_leading_space = whitespace::leading_space(current_line); + let current_leading_space = whitespace::leading_space(current_line.as_str()); if current_leading_space == section_level_indent - && (whitespace::leading_space(next_line).len() > current_leading_space.len()) + && (whitespace::leading_space(&next_line).len() > current_leading_space.len()) && !next_line.trim().is_empty() { let parameters = if let Some(semi_index) = current_line.find(':') { // If the parameter has a type annotation, exclude it. - ¤t_line[..semi_index] + ¤t_line.as_str()[..semi_index] } else { // Otherwise, it's just a list of parameters on the current line. - current_line.trim() + current_line.as_str().trim() }; // Notably, NumPy lets you put multiple parameters of the same type on the same // line. @@ -964,44 +869,29 @@ fn numpy_section(checker: &mut Checker, docstring: &Docstring, context: &Section .rules .enabled(Rule::NewLineAfterSectionName) { - let suffix = context - .line - .trim() - .strip_prefix(context.section_name) - .unwrap(); + let suffix = context.summary_after_section_name(); + if !suffix.is_empty() { let mut diagnostic = Diagnostic::new( NewLineAfterSectionName { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { - // Delete the suffix. This requires locating the end of the section name. - if let Some(index) = context.line.find(context.section_name) { - // Map from bytes to characters. - let suffix_start = &context.line[..index + context.section_name.len()] - .chars() - .count(); - let suffix_length = suffix.chars().count(); - diagnostic.set_fix(Edit::deletion( - Location::new( - docstring.expr.location.row() + context.original_index, - *suffix_start, - ), - Location::new( - docstring.expr.location.row() + context.original_index, - suffix_start + suffix_length, - ), - )); - } + let section_range = context.section_name_range(); + diagnostic.set_fix(Edit::range_deletion(TextRange::at( + section_range.end(), + suffix.text_len(), + ))); } + checker.diagnostics.push(diagnostic); } } if checker.settings.rules.enabled(Rule::UndocumentedParam) { - if matches!(context.kind, SectionKind::Parameters) { + if matches!(context.kind(), SectionKind::Parameters) { parameters_section(checker, docstring, context); } } @@ -1011,38 +901,21 @@ fn google_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio common_section(checker, docstring, context); if checker.settings.rules.enabled(Rule::SectionNameEndsInColon) { - let suffix = context - .line - .trim() - .strip_prefix(context.section_name) - .unwrap(); + let suffix = context.summary_after_section_name(); if suffix != ":" { let mut diagnostic = Diagnostic::new( SectionNameEndsInColon { - name: context.section_name.to_string(), + name: context.section_name().to_string(), }, - Range::from(docstring.expr), + docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { - // Replace the suffix. This requires locating the end of the section name. - if let Some(index) = context.line.find(context.section_name) { - // Map from bytes to characters. - let suffix_start = &context.line[..index + context.section_name.len()] - .chars() - .count(); - let suffix_length = suffix.chars().count(); - diagnostic.set_fix(Edit::replacement( - ":".to_string(), - Location::new( - docstring.expr.location.row() + context.original_index, - *suffix_start, - ), - Location::new( - docstring.expr.location.row() + context.original_index, - suffix_start + suffix_length, - ), - )); - } + // Replace the suffix. + let section_name_range = context.section_name_range(); + diagnostic.set_fix(Edit::range_replacement( + ":".to_string(), + TextRange::at(section_name_range.end(), suffix.text_len()), + )); } checker.diagnostics.push(diagnostic); } @@ -1052,20 +925,20 @@ fn google_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio fn parse_numpy_sections( checker: &mut Checker, docstring: &Docstring, - section_contexts: &[SectionContext], + section_contexts: &SectionContexts, ) { for section_context in section_contexts { - numpy_section(checker, docstring, section_context); + numpy_section(checker, docstring, §ion_context); } } fn parse_google_sections( checker: &mut Checker, docstring: &Docstring, - section_contexts: &[SectionContext], + section_contexts: &SectionContexts, ) { for section_context in section_contexts { - google_section(checker, docstring, section_context); + google_section(checker, docstring, §ion_context); } if checker.settings.rules.enabled(Rule::UndocumentedParam) { @@ -1076,7 +949,7 @@ fn parse_google_sections( // variants) can list arguments, we need to unify the sets of arguments mentioned in both // then check for missing arguments at the end of the section check. if matches!( - section_context.kind, + section_context.kind(), SectionKind::Args | SectionKind::Arguments | SectionKind::KeywordArgs @@ -1085,7 +958,7 @@ fn parse_google_sections( | SectionKind::OtherArguments ) { has_args = true; - documented_args.extend(args_section(section_context)); + documented_args.extend(args_section(§ion_context)); } } if has_args { diff --git a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs index cfad610cf4..18a9b8c0c3 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs @@ -1,6 +1,5 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; @@ -18,7 +17,7 @@ impl Violation for DocstringStartsWithThis { /// D404 pub fn starts_with_this(checker: &mut Checker, docstring: &Docstring) { - let body = docstring.body; + let body = docstring.body(); let trimmed = body.trim(); if trimmed.is_empty() { @@ -31,8 +30,7 @@ pub fn starts_with_this(checker: &mut Checker, docstring: &Docstring) { if normalize_word(first_word) != "this" { return; } - checker.diagnostics.push(Diagnostic::new( - DocstringStartsWithThis, - Range::from(docstring.expr), - )); + checker + .diagnostics + .push(Diagnostic::new(DocstringStartsWithThis, docstring.range())); } diff --git a/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs b/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs index 6e6a2d9571..e5cd02e154 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs @@ -1,7 +1,5 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::newlines::StrExt; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::docstrings::definition::Docstring; @@ -18,30 +16,21 @@ impl Violation for TripleSingleQuotes { /// D300 pub fn triple_quotes(checker: &mut Checker, docstring: &Docstring) { - let contents = docstring.contents; - let body = docstring.body; + let body = docstring.body(); + + let leading_quote = docstring.leading_quote().to_ascii_lowercase(); - let Some(first_line) = contents.universal_newlines() - .next() - .map(str::to_lowercase) else - { - return; - }; let starts_with_triple = if body.contains("\"\"\"") { - first_line.starts_with("'''") - || first_line.starts_with("u'''") - || first_line.starts_with("r'''") - || first_line.starts_with("ur'''") + matches!(leading_quote.as_str(), "'''" | "u'''" | "r'''" | "ur'''") } else { - first_line.starts_with("\"\"\"") - || first_line.starts_with("u\"\"\"") - || first_line.starts_with("r\"\"\"") - || first_line.starts_with("ur\"\"\"") + matches!( + leading_quote.as_str(), + "\"\"\"" | "u\"\"\"" | "r\"\"\"" | "ur\"\"\"" + ) }; if !starts_with_triple { - checker.diagnostics.push(Diagnostic::new( - TripleSingleQuotes, - Range::from(docstring.expr), - )); + checker + .diagnostics + .push(Diagnostic::new(TripleSingleQuotes, docstring.range())); } } diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 6a3603920d..52f848fc4a 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,12 +1,12 @@ use anyhow::{bail, Ok, Result}; use libcst_native::{Codegen, CodegenState, DictElement, Expression}; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Excepthandler, Expr}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::str::raw_contents; -use ruff_python_ast::types::Range; use rustpython_common::format::{ FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate, }; @@ -22,7 +22,7 @@ pub fn remove_unused_format_arguments_from_dict( locator: &Locator, stylist: &Stylist, ) -> Result { - let module_text = locator.slice(stmt); + let module_text = locator.slice(stmt.range()); let mut tree = match_expression(module_text)?; let dict = match_dict(&mut tree)?; @@ -40,17 +40,13 @@ pub fn remove_unused_format_arguments_from_dict( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - stmt.location, - stmt.end_location.unwrap(), - )) + Ok(Edit::range_replacement(state.to_string(), stmt.range())) } /// Generate a [`Edit`] to remove unused keyword arguments from a `format` call. pub fn remove_unused_keyword_arguments_from_format_call( unused_arguments: &[&str], - location: Range, + location: TextRange, locator: &Locator, stylist: &Stylist, ) -> Result { @@ -68,11 +64,7 @@ pub fn remove_unused_keyword_arguments_from_format_call( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - location.location, - location.end_location, - )) + Ok(Edit::range_replacement(state.to_string(), location)) } fn unparse_format_part(format_part: FormatPart) -> String { @@ -136,7 +128,7 @@ fn update_field_types(format_string: &FormatString, min_unused: usize) -> String /// Generate a [`Edit`] to remove unused positional arguments from a `format` call. pub fn remove_unused_positional_arguments_from_format_call( unused_arguments: &[usize], - location: Range, + location: TextRange, locator: &Locator, stylist: &Stylist, format_string: &FormatString, @@ -176,11 +168,7 @@ pub fn remove_unused_positional_arguments_from_format_call( }; tree.codegen(&mut state); - Ok(Edit::replacement( - state.to_string(), - location.location, - location.end_location, - )) + Ok(Edit::range_replacement(state.to_string(), location)) } /// Generate a [`Edit`] to remove the binding from an exception handler. @@ -188,23 +176,22 @@ pub fn remove_exception_handler_assignment( excepthandler: &Excepthandler, locator: &Locator, ) -> Result { - let contents = locator.slice(excepthandler); + let contents = locator.slice(excepthandler.range()); let mut fix_start = None; let mut fix_end = None; // End of the token just before the `as` to the semicolon. let mut prev = None; - for (start, tok, end) in - lexer::lex_located(contents, Mode::Module, excepthandler.location).flatten() + for (tok, range) in lexer::lex_located(contents, Mode::Module, excepthandler.start()).flatten() { if matches!(tok, Tok::As) { fix_start = prev; } if matches!(tok, Tok::Colon) { - fix_end = Some(start); + fix_end = Some(range.start()); break; } - prev = Some(end); + prev = Some(range.end()); } if let (Some(start), Some(end)) = (fix_start, fix_end) { diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 66b8ee0741..6df5ea86f3 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -11,6 +11,7 @@ mod tests { use anyhow::Result; use regex::Regex; + use ruff_diagnostics::Diagnostic; use rustpython_parser::lexer::LexResult; use test_case::test_case; use textwrap::dedent; @@ -254,16 +255,19 @@ mod tests { let tokens: Vec = ruff_rustpython::tokenize(&contents); let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); - let indexer: Indexer = tokens.as_slice().into(); - let directives = - directives::extract_directives(&tokens, directives::Flags::from_settings(&settings)); + let indexer = Indexer::from_tokens(&tokens, &locator); + let directives = directives::extract_directives( + &tokens, + directives::Flags::from_settings(&settings), + &locator, + &indexer, + ); let LinterResult { data: (mut diagnostics, _imports), .. } = check_path( Path::new(""), None, - &contents, tokens, &locator, &stylist, @@ -273,7 +277,7 @@ mod tests { flags::Noqa::Enabled, flags::Autofix::Enabled, ); - diagnostics.sort_by_key(|diagnostic| diagnostic.location); + diagnostics.sort_by_key(Diagnostic::start); let actual = diagnostics .iter() .map(|diagnostic| diagnostic.kind.rule()) diff --git a/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs b/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs index 06443636b7..f91a09ca9c 100644 --- a/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs +++ b/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -22,7 +21,7 @@ pub fn assert_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) { if !elts.is_empty() { checker .diagnostics - .push(Diagnostic::new(AssertTuple, Range::from(stmt))); + .push(Diagnostic::new(AssertTuple, stmt.range())); } } } diff --git a/crates/ruff/src/rules/pyflakes/rules/break_outside_loop.rs b/crates/ruff/src/rules/pyflakes/rules/break_outside_loop.rs index a77b7dcea3..d40f6a1714 100644 --- a/crates/ruff/src/rules/pyflakes/rules/break_outside_loop.rs +++ b/crates/ruff/src/rules/pyflakes/rules/break_outside_loop.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct BreakOutsideLoop; @@ -44,6 +43,6 @@ pub fn break_outside_loop<'a>( if allowed { None } else { - Some(Diagnostic::new(BreakOutsideLoop, Range::from(stmt))) + Some(Diagnostic::new(BreakOutsideLoop, stmt.range())) } } diff --git a/crates/ruff/src/rules/pyflakes/rules/continue_outside_loop.rs b/crates/ruff/src/rules/pyflakes/rules/continue_outside_loop.rs index 11958e01e9..6bee0d00b7 100644 --- a/crates/ruff/src/rules/pyflakes/rules/continue_outside_loop.rs +++ b/crates/ruff/src/rules/pyflakes/rules/continue_outside_loop.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct ContinueOutsideLoop; @@ -44,6 +43,6 @@ pub fn continue_outside_loop<'a>( if allowed { None } else { - Some(Diagnostic::new(ContinueOutsideLoop, Range::from(stmt))) + Some(Diagnostic::new(ContinueOutsideLoop, stmt.range())) } } diff --git a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index 341fee3f97..6f1d5d916a 100644 --- a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -1,10 +1,10 @@ -use rustpython_parser::ast::{Expr, ExprKind, Location}; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{Expr, ExprKind}; use rustpython_parser::{lexer, Mode, StringKind, Tok}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -52,35 +52,27 @@ impl AlwaysAutofixableViolation for FStringMissingPlaceholders { fn find_useless_f_strings<'a>( expr: &'a Expr, locator: &'a Locator, -) -> impl Iterator + 'a { - let contents = locator.slice(expr); - lexer::lex_located(contents, Mode::Module, expr.location) +) -> impl Iterator + 'a { + let contents = locator.slice(expr.range()); + lexer::lex_located(contents, Mode::Module, expr.start()) .flatten() - .filter_map(|(location, tok, end_location)| match tok { + .filter_map(|(tok, range)| match tok { Tok::String { kind: StringKind::FString | StringKind::RawFString, .. } => { - let first_char = locator.slice(Range { - location, - end_location: Location::new(location.row(), location.column() + 1), - }); + let first_char = + &locator.contents()[TextRange::at(range.start(), TextSize::from(1))]; // f"..." => f_position = 0 // fr"..." => f_position = 0 // rf"..." => f_position = 1 - let f_position = usize::from(!(first_char == "f" || first_char == "F")); + let f_position = u32::from(!(first_char == "f" || first_char == "F")); Some(( - Range { - location: Location::new(location.row(), location.column() + f_position), - end_location: Location::new( - location.row(), - location.column() + f_position + 1, - ), - }, - Range { - location, - end_location, - }, + TextRange::at( + range.start() + TextSize::from(f_position), + TextSize::from(1), + ), + range, )) } _ => None, @@ -92,18 +84,15 @@ fn unescape_f_string(content: &str) -> String { } fn fix_f_string_missing_placeholders( - prefix_range: &Range, - tok_range: &Range, + prefix_range: TextRange, + tok_range: TextRange, checker: &mut Checker, ) -> Edit { - let content = checker.locator.slice(Range::new( - prefix_range.end_location, - tok_range.end_location, - )); + let content = &checker.locator.contents()[TextRange::new(prefix_range.end(), tok_range.end())]; Edit::replacement( unescape_f_string(content), - prefix_range.location, - tok_range.end_location, + prefix_range.start(), + tok_range.end(), ) } @@ -117,8 +106,8 @@ pub fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range); if checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(fix_f_string_missing_placeholders( - &prefix_range, - &tok_range, + prefix_range, + tok_range, checker, )); } diff --git a/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs b/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs index d949740d31..70607fb0a9 100644 --- a/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs +++ b/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -22,7 +21,7 @@ pub fn if_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) { if !elts.is_empty() { checker .diagnostics - .push(Diagnostic::new(IfTuple, Range::from(stmt))); + .push(Diagnostic::new(IfTuple, stmt.range())); } } } diff --git a/crates/ruff/src/rules/pyflakes/rules/imports.rs b/crates/ruff/src/rules/pyflakes/rules/imports.rs index 339d710b9a..e81aa0567a 100644 --- a/crates/ruff/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff/src/rules/pyflakes/rules/imports.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::Alias; use ruff_diagnostics::Diagnostic; use ruff_diagnostics::{AutofixKind, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::source_code::OneIndexed; use ruff_python_stdlib::future::ALL_FEATURE_NAMES; use crate::checkers::ast::Checker; @@ -60,7 +60,7 @@ impl Violation for UnusedImport { #[violation] pub struct ImportShadowedByLoopVar { pub name: String, - pub line: usize, + pub line: OneIndexed, } impl Violation for ImportShadowedByLoopVar { @@ -144,7 +144,7 @@ pub fn future_feature_not_defined(checker: &mut Checker, alias: &Alias) { FutureFeatureNotDefined { name: alias.node.name.to_string(), }, - Range::from(alias), + alias.range(), )); } } diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index d616bbb7f1..2763551a5a 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -1,12 +1,12 @@ use itertools::izip; use log::error; use once_cell::unsync::Lazy; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Cmpop, Expr}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -57,7 +57,7 @@ pub fn invalid_literal_comparison( left: &Expr, ops: &[Cmpop], comparators: &[Expr], - location: Range, + location: TextRange, ) { let located = Lazy::new(|| helpers::locate_cmpops(checker.locator.slice(location))); let mut left = left; @@ -78,13 +78,9 @@ pub fn invalid_literal_comparison( None } } { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( content, - helpers::to_absolute(located_op.location, location.location), - helpers::to_absolute( - located_op.end_location.unwrap(), - location.location, - ), + located_op.range() + location.start(), )); } } else { diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs index ba9313f496..d00bf69105 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -29,5 +28,5 @@ pub fn invalid_print_syntax(checker: &mut Checker, left: &Expr) { }; checker .diagnostics - .push(Diagnostic::new(InvalidPrintSyntax, Range::from(left))); + .push(Diagnostic::new(InvalidPrintSyntax, left.range())); } diff --git a/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs b/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs index 02cb7d1802..15cc3e9f3b 100644 --- a/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs +++ b/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -45,12 +44,11 @@ pub fn raise_not_implemented(checker: &mut Checker, expr: &Expr) { let Some(expr) = match_not_implemented(expr) else { return; }; - let mut diagnostic = Diagnostic::new(RaiseNotImplemented, Range::from(expr)); + let mut diagnostic = Diagnostic::new(RaiseNotImplemented, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "NotImplementedError".to_string(), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyflakes/rules/redefined_while_unused.rs b/crates/ruff/src/rules/pyflakes/rules/redefined_while_unused.rs index 3a49c07e08..b32c762209 100644 --- a/crates/ruff/src/rules/pyflakes/rules/redefined_while_unused.rs +++ b/crates/ruff/src/rules/pyflakes/rules/redefined_while_unused.rs @@ -1,10 +1,11 @@ use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::OneIndexed; #[violation] pub struct RedefinedWhileUnused { pub name: String, - pub line: usize, + pub line: OneIndexed, } impl Violation for RedefinedWhileUnused { diff --git a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs index 7dd7cd9475..a85c484cd4 100644 --- a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs @@ -7,7 +7,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr}; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; @@ -106,13 +105,13 @@ pub fn repeated_keys(checker: &mut Checker, keys: &[Option], values: &[Exp name: unparse_expr(key, checker.stylist), repeated_value: is_duplicate_value, }, - Range::from(key), + key.range(), ); if is_duplicate_value { if checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(Edit::deletion( - values[i - 1].end_location.unwrap(), - values[i].end_location.unwrap(), + values[i - 1].end(), + values[i].end(), )); } } else { @@ -134,13 +133,13 @@ pub fn repeated_keys(checker: &mut Checker, keys: &[Option], values: &[Exp name: dict_key.to_string(), repeated_value: is_duplicate_value, }, - Range::from(key), + key.range(), ); if is_duplicate_value { if checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(Edit::deletion( - values[i - 1].end_location.unwrap(), - values[i].end_location.unwrap(), + values[i - 1].end(), + values[i].end(), )); } } else { diff --git a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs index 73254b6e47..43f11928bf 100644 --- a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::scope::ScopeKind; use crate::checkers::ast::Checker; @@ -25,7 +24,7 @@ pub fn return_outside_function(checker: &mut Checker, stmt: &Stmt) { ) { checker .diagnostics - .push(Diagnostic::new(ReturnOutsideFunction, Range::from(stmt))); + .push(Diagnostic::new(ReturnOutsideFunction, stmt.range())); } } } diff --git a/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs b/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs index 1707bb148e..10697f2b16 100644 --- a/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs +++ b/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs @@ -1,8 +1,8 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct ExpressionsInStarAssignment; @@ -29,7 +29,7 @@ pub fn starred_expressions( elts: &[Expr], check_too_many_expressions: bool, check_two_starred_expressions: bool, - location: Range, + location: TextRange, ) -> Option { let mut has_starred: bool = false; let mut starred_index: Option = None; diff --git a/crates/ruff/src/rules/pyflakes/rules/strings.rs b/crates/ruff/src/rules/pyflakes/rules/strings.rs index 9e72866fbb..81cf684d7f 100644 --- a/crates/ruff/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff/src/rules/pyflakes/rules/strings.rs @@ -1,3 +1,4 @@ +use ruff_text_size::TextRange; use std::string::ToString; use rustc_hash::FxHashSet; @@ -5,7 +6,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, KeywordData}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -225,7 +225,7 @@ pub(crate) fn percent_format_expected_mapping( checker: &mut Checker, summary: &CFormatSummary, right: &Expr, - location: Range, + location: TextRange, ) { if !summary.keywords.is_empty() { // Tuple, List, Set (+comprehensions) @@ -248,7 +248,7 @@ pub(crate) fn percent_format_expected_sequence( checker: &mut Checker, summary: &CFormatSummary, right: &Expr, - location: Range, + location: TextRange, ) { if summary.num_positional > 1 && matches!( @@ -267,7 +267,7 @@ pub(crate) fn percent_format_extra_named_arguments( checker: &mut Checker, summary: &CFormatSummary, right: &Expr, - location: Range, + location: TextRange, ) { if summary.num_positional > 0 { return; @@ -328,7 +328,7 @@ pub(crate) fn percent_format_missing_arguments( checker: &mut Checker, summary: &CFormatSummary, right: &Expr, - location: Range, + location: TextRange, ) { if summary.num_positional > 0 { return; @@ -375,7 +375,7 @@ pub(crate) fn percent_format_missing_arguments( pub(crate) fn percent_format_mixed_positional_and_named( checker: &mut Checker, summary: &CFormatSummary, - location: Range, + location: TextRange, ) { if !(summary.num_positional == 0 || summary.keywords.is_empty()) { checker.diagnostics.push(Diagnostic::new( @@ -390,7 +390,7 @@ pub(crate) fn percent_format_positional_count_mismatch( checker: &mut Checker, summary: &CFormatSummary, right: &Expr, - location: Range, + location: TextRange, ) { if !summary.keywords.is_empty() { return; @@ -425,7 +425,7 @@ pub(crate) fn percent_format_star_requires_sequence( checker: &mut Checker, summary: &CFormatSummary, right: &Expr, - location: Range, + location: TextRange, ) { if summary.starred { match &right.node { @@ -442,7 +442,7 @@ pub(crate) fn string_dot_format_extra_named_arguments( checker: &mut Checker, summary: &FormatSummary, keywords: &[Keyword], - location: Range, + location: TextRange, ) { if has_star_star_kwargs(keywords) { return; @@ -491,7 +491,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments( checker: &mut Checker, summary: &FormatSummary, args: &[Expr], - location: Range, + location: TextRange, ) { let missing: Vec = args .iter() @@ -537,7 +537,7 @@ pub(crate) fn string_dot_format_missing_argument( summary: &FormatSummary, args: &[Expr], keywords: &[Keyword], - location: Range, + location: TextRange, ) { if has_star_args(args) || has_star_star_kwargs(keywords) { return; @@ -578,7 +578,7 @@ pub(crate) fn string_dot_format_missing_argument( pub(crate) fn string_dot_format_mixing_automatic( checker: &mut Checker, summary: &FormatSummary, - location: Range, + location: TextRange, ) { if !(summary.autos.is_empty() || summary.indices.is_empty()) { checker diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs index 2b2ed7313f..d5a4417d17 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs @@ -1,7 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::scope::Scope; +use ruff_text_size::TextRange; #[violation] pub struct UndefinedExport { @@ -17,7 +17,7 @@ impl Violation for UndefinedExport { } /// F822 -pub fn undefined_export(names: &[&str], range: &Range, scope: &Scope) -> Vec { +pub fn undefined_export(names: &[&str], range: TextRange, scope: &Scope) -> Vec { let mut diagnostics = Vec::new(); if !scope.uses_star_imports() { for name in names { @@ -26,7 +26,7 @@ pub fn undefined_export(names: &[&str], range: &Range, scope: &Scope) -> Vec(located: &Located, locator: &Locator, f: F) -> Range +fn match_token_after(located: &Located, locator: &Locator, f: F) -> TextRange where F: Fn(Tok) -> bool, { - let contents = locator.after(located.location); + let contents = locator.after(located.start()); // Track the bracket depth. let mut par_count = 0; let mut sqb_count = 0; let mut brace_count = 0; - for ((_, tok, _), (start, _, end)) in - lexer::lex_located(contents, Mode::Module, located.location) - .flatten() - .tuple_windows() + for ((tok, _), (_, range)) in lexer::lex_located(contents, Mode::Module, located.start()) + .flatten() + .tuple_windows() { match tok { Tok::Lpar => { @@ -117,27 +117,26 @@ where } if f(tok) { - return Range::new(start, end); + return range; } } unreachable!("No token after matched"); } -/// Return the start and end [`Location`] of the token matching the predicate, +/// Return the [`TextRange`] of the token matching the predicate, /// skipping over any bracketed expressions. -fn match_token(located: &Located, locator: &Locator, f: F) -> Range +fn match_token(located: &Located, locator: &Locator, f: F) -> TextRange where F: Fn(Tok) -> bool, { - let contents = locator.after(located.location); + let contents = locator.after(located.start()); // Track the bracket depth. let mut par_count = 0; let mut sqb_count = 0; let mut brace_count = 0; - for (start, tok, end) in lexer::lex_located(contents, Mode::Module, located.location).flatten() - { + for (tok, range) in lexer::lex_located(contents, Mode::Module, located.start()).flatten() { match tok { Tok::Lpar => { par_count += 1; @@ -177,7 +176,7 @@ where } if f(tok) { - return Range::new(start, end); + return range; } } unreachable!("No token after matched"); @@ -190,17 +189,15 @@ enum DeletionKind { } /// Generate a [`Edit`] to remove an unused variable assignment, given the -/// enclosing [`Stmt`] and the [`Range`] of the variable binding. +/// enclosing [`Stmt`] and the [`TextRange`] of the variable binding. fn remove_unused_variable( stmt: &Stmt, - range: &Range, + range: TextRange, checker: &Checker, ) -> Option<(DeletionKind, Edit)> { // First case: simple assignment (`x = 1`) if let StmtKind::Assign { targets, value, .. } = &stmt.node { - if let Some(target) = targets.iter().find(|target| { - range.location == target.location && range.end_location == target.end_location.unwrap() - }) { + if let Some(target) = targets.iter().find(|target| range == target.range()) { if matches!(target.node, ExprKind::Name { .. }) { return if targets.len() > 1 || contains_effect(value, |id| checker.ctx.is_builtin(id)) @@ -210,9 +207,9 @@ fn remove_unused_variable( Some(( DeletionKind::Partial, Edit::deletion( - target.location, + target.start(), match_token_after(target, checker.locator, |tok| tok == Tok::Equal) - .location, + .start(), ), )) } else { @@ -256,8 +253,8 @@ fn remove_unused_variable( Some(( DeletionKind::Partial, Edit::deletion( - stmt.location, - match_token_after(stmt, checker.locator, |tok| tok == Tok::Equal).location, + stmt.start(), + match_token_after(stmt, checker.locator, |tok| tok == Tok::Equal).start(), ), )) } else { @@ -292,19 +289,17 @@ fn remove_unused_variable( // TODO(charlie): Store the `Withitem` in the `Binding`. for item in items { if let Some(optional_vars) = &item.optional_vars { - if optional_vars.location == range.location - && optional_vars.end_location.unwrap() == range.end_location - { + if optional_vars.range() == range { return Some(( DeletionKind::Partial, Edit::deletion( - item.context_expr.end_location.unwrap(), + item.context_expr.end(), // The end of the `Withitem` is the colon, comma, or closing // parenthesis following the `optional_vars`. match_token(&item.context_expr, checker.locator, |tok| { tok == Tok::Colon || tok == Tok::Comma || tok == Tok::Rpar }) - .location, + .start(), ), )); } @@ -341,7 +336,7 @@ pub fn unused_variable(checker: &mut Checker, scope: ScopeId) { ); if checker.patch(diagnostic.kind.rule()) { if let Some(stmt) = binding.source.as_ref().map(Into::into) { - if let Some((kind, fix)) = remove_unused_variable(stmt, &binding.range, checker) + if let Some((kind, fix)) = remove_unused_variable(stmt, binding.range, checker) { if matches!(kind, DeletionKind::Whole) { checker.deletions.insert(RefEquality(stmt)); diff --git a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs index 5f58c6fadf..e2573faa85 100644 --- a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::scope::ScopeKind; use crate::checkers::ast::Checker; @@ -52,7 +51,7 @@ pub fn yield_outside_function(checker: &mut Checker, expr: &Expr) { }; checker.diagnostics.push(Diagnostic::new( YieldOutsideFunction { keyword }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_noqa.rs b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_noqa.rs index 8dafd2439a..82a77fe523 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_noqa.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_noqa.rs @@ -1,10 +1,10 @@ use once_cell::sync::Lazy; use regex::Regex; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::Line; #[violation] pub struct BlanketNOQA; @@ -20,15 +20,13 @@ static BLANKET_NOQA_REGEX: Lazy = Lazy::new(|| Regex::new(r"(?i)# noqa($|\s|:[^ ])").unwrap()); /// PGH004 -pub fn blanket_noqa(diagnostics: &mut Vec, lineno: usize, line: &str) { - if let Some(match_) = BLANKET_NOQA_REGEX.find(line) { - let start = line[..match_.start()].chars().count(); - let end = start + line[match_.start()..match_.end()].chars().count(); +pub(crate) fn blanket_noqa(diagnostics: &mut Vec, line: &Line) { + if let Some(match_) = BLANKET_NOQA_REGEX.find(line.as_str()) { diagnostics.push(Diagnostic::new( BlanketNOQA, - Range::new( - Location::new(lineno + 1, start), - Location::new(lineno + 1, end), + TextRange::at( + line.start() + TextSize::try_from(match_.start()).unwrap(), + match_.as_str().text_len(), ), )); } 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 022ae04282..629d1f8db6 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,11 +1,11 @@ use anyhow::{anyhow, Result}; use once_cell::sync::Lazy; use regex::Regex; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::Line; #[violation] pub struct BlanketTypeIgnore; @@ -18,17 +18,15 @@ impl Violation for BlanketTypeIgnore { } /// PGH003 -pub fn blanket_type_ignore(diagnostics: &mut Vec, lineno: usize, line: &str) { +pub(crate) fn blanket_type_ignore(diagnostics: &mut Vec, line: &Line) { for match_ in TYPE_IGNORE_PATTERN.find_iter(line) { if let Ok(codes) = parse_type_ignore_tag(line[match_.end()..].trim()) { if codes.is_empty() { - let start = line[..match_.start()].chars().count(); - let end = start + line[match_.start()..match_.end()].chars().count(); diagnostics.push(Diagnostic::new( BlanketTypeIgnore, - Range::new( - Location::new(lineno + 1, start), - Location::new(lineno + 1, end), + TextRange::at( + line.start() + TextSize::try_from(match_.start()).unwrap(), + match_.as_str().text_len(), ), )); } diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs index 98d9709ad9..5d83e83e32 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -27,6 +26,6 @@ pub fn deprecated_log_warn(checker: &mut Checker, func: &Expr) { { checker .diagnostics - .push(Diagnostic::new(DeprecatedLogWarn, Range::from(func))); + .push(Diagnostic::new(DeprecatedLogWarn, func.range())); } } diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs b/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs index 4454dad548..3cdb1112e0 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs @@ -1,5 +1,5 @@ -pub use blanket_noqa::{blanket_noqa, BlanketNOQA}; -pub use blanket_type_ignore::{blanket_type_ignore, BlanketTypeIgnore}; +pub(crate) use blanket_noqa::{blanket_noqa, BlanketNOQA}; +pub(crate) use blanket_type_ignore::{blanket_type_ignore, BlanketTypeIgnore}; pub use deprecated_log_warn::{deprecated_log_warn, DeprecatedLogWarn}; pub use no_eval::{no_eval, Eval}; diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs index 6f767b7618..e9cf461e7a 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -29,5 +28,5 @@ pub fn no_eval(checker: &mut Checker, func: &Expr) { } checker .diagnostics - .push(Diagnostic::new(Eval, Range::from(func))); + .push(Diagnostic::new(Eval, func.range())); } diff --git a/crates/ruff/src/rules/pylint/rules/assert_on_string_literal.rs b/crates/ruff/src/rules/pylint/rules/assert_on_string_literal.rs index f2575b5635..3a1fc68d55 100644 --- a/crates/ruff/src/rules/pylint/rules/assert_on_string_literal.rs +++ b/crates/ruff/src/rules/pylint/rules/assert_on_string_literal.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -55,7 +54,7 @@ pub fn assert_on_string_literal(checker: &mut Checker, test: &Expr) { Kind::NonEmpty }, }, - Range::from(test), + test.range(), )); } Constant::Bytes(value) => { @@ -67,7 +66,7 @@ pub fn assert_on_string_literal(checker: &mut Checker, test: &Expr) { Kind::NonEmpty }, }, - Range::from(test), + test.range(), )); } _ => {} @@ -97,7 +96,7 @@ pub fn assert_on_string_literal(checker: &mut Checker, test: &Expr) { Kind::Unknown }, }, - Range::from(test), + test.range(), )); } _ => {} diff --git a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs index f5b9317c8d..fdc54f37e8 100644 --- a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::scope::{FunctionDef, ScopeKind}; use crate::checkers::ast::Checker; @@ -33,6 +32,6 @@ pub fn await_outside_async(checker: &mut Checker, expr: &Expr) { { checker .diagnostics - .push(Diagnostic::new(AwaitOutsideAsync, Range::from(expr))); + .push(Diagnostic::new(AwaitOutsideAsync, expr.range())); } } diff --git a/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs b/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs index 2762518a16..233f04e97e 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs @@ -5,7 +5,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion; @@ -132,7 +131,7 @@ pub fn bad_str_strip_call(checker: &mut Checker, func: &Expr, args: &[Expr]) { }; checker.diagnostics.push(Diagnostic::new( BadStrStripCall { strip, removal }, - Range::from(arg), + arg.range(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs index 8a9650aaad..7870c0395a 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs @@ -1,14 +1,14 @@ +use ruff_text_size::TextRange; use std::str::FromStr; use rustc_hash::FxHashMap; use rustpython_common::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString}; -use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Operator}; +use rustpython_parser::ast::{Constant, Expr, ExprKind, Operator}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::str::{leading_quote, trailing_quote}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -243,11 +243,11 @@ fn is_valid_dict( /// PLE1307 pub fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right: &Expr) { // Grab each string segment (in case there's an implicit concatenation). - let content = checker.locator.slice(expr); - let mut strings: Vec<(Location, Location)> = vec![]; - for (start, tok, end) in lexer::lex_located(content, Mode::Module, expr.location).flatten() { + let content = checker.locator.slice(expr.range()); + let mut strings: Vec = vec![]; + for (tok, range) in lexer::lex_located(content, Mode::Module, expr.start()).flatten() { if matches!(tok, Tok::String { .. }) { - strings.push((start, end)); + strings.push(range); } else if matches!(tok, Tok::Percent) { // Break as soon as we find the modulo symbol. break; @@ -261,8 +261,8 @@ pub fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right: &Expr) // Parse each string segment. let mut format_strings = vec![]; - for (start, end) in &strings { - let string = checker.locator.slice(Range::new(*start, *end)); + for range in &strings { + let string = checker.locator.slice(*range); let (Some(leader), Some(trailer)) = (leading_quote(string), trailing_quote(string)) else { return; }; @@ -284,6 +284,6 @@ pub fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right: &Expr) if !is_valid { checker .diagnostics - .push(Diagnostic::new(BadStringFormatType, Range::from(expr))); + .push(Diagnostic::new(BadStringFormatType, expr.range())); } } diff --git a/crates/ruff/src/rules/pylint/rules/bidirectional_unicode.rs b/crates/ruff/src/rules/pylint/rules/bidirectional_unicode.rs index 7138ce28fe..769ac2de90 100644 --- a/crates/ruff/src/rules/pylint/rules/bidirectional_unicode.rs +++ b/crates/ruff/src/rules/pylint/rules/bidirectional_unicode.rs @@ -1,8 +1,6 @@ -use rustpython_parser::ast::Location; - use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::Line; const BIDI_UNICODE: [char; 10] = [ '\u{202A}', //{LEFT-TO-RIGHT EMBEDDING} @@ -35,16 +33,10 @@ impl Violation for BidirectionalUnicode { } /// PLE2502 -pub fn bidirectional_unicode(lineno: usize, line: &str) -> Vec { +pub fn bidirectional_unicode(line: &Line) -> Vec { let mut diagnostics = Vec::new(); if line.contains(BIDI_UNICODE) { - diagnostics.push(Diagnostic::new( - BidirectionalUnicode, - Range::new( - Location::new(lineno + 1, 0), - Location::new((lineno + 1) + 1, 0), - ), - )); + diagnostics.push(Diagnostic::new(BidirectionalUnicode, line.full_range())); } diagnostics } diff --git a/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs b/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs index e6b197ca62..d5ea82c807 100644 --- a/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs +++ b/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -76,6 +75,6 @@ pub fn binary_op_exception(checker: &mut Checker, excepthandler: &Excepthandler) checker.diagnostics.push(Diagnostic::new( BinaryOpException { op: op.into() }, - Range::from(type_), + type_.range(), )); } diff --git a/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs b/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs index f127abb0b1..acea825011 100644 --- a/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs +++ b/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; #[violation] pub struct CollapsibleElseIf; @@ -21,14 +20,8 @@ pub fn collapsible_else_if(orelse: &[Stmt], locator: &Locator) -> Option Diagnostic { - Diagnostic::new(InvalidAllFormat, Range::from(expr)) + Diagnostic::new(InvalidAllFormat, expr.range()) } diff --git a/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs b/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs index ed8d41becc..542182b163 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; #[violation] pub struct InvalidAllObject; @@ -16,5 +15,5 @@ impl Violation for InvalidAllObject { /// PLE0604 pub fn invalid_all_object(expr: &Expr) -> Diagnostic { - Diagnostic::new(InvalidAllObject, Range::from(expr)) + Diagnostic::new(InvalidAllObject, expr.range()) } diff --git a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs index 22d4aa1379..1298dc155c 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Operator}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -104,7 +103,7 @@ pub fn invalid_envvar_default( if !is_valid_default(expr) { checker .diagnostics - .push(Diagnostic::new(InvalidEnvvarDefault, Range::from(expr))); + .push(Diagnostic::new(InvalidEnvvarDefault, expr.range())); } } } diff --git a/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs b/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs index 6c8bc3b084..7c364a5148 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Operator}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -101,7 +100,7 @@ pub fn invalid_envvar_value( if !is_valid_key(expr) { checker .diagnostics - .push(Diagnostic::new(InvalidEnvvarValue, Range::from(expr))); + .push(Diagnostic::new(InvalidEnvvarValue, expr.range())); } } } 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 3b05fedb1a..d57a2f6152 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs @@ -1,13 +1,10 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::AlwaysAutofixableViolation; use ruff_diagnostics::Edit; use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; -use ruff_python_ast::newlines::UniversalNewlineIterator; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for strings that contain the control character `BS`. @@ -176,40 +173,33 @@ impl AlwaysAutofixableViolation for InvalidCharacterZeroWidthSpace { /// PLE2510, PLE2512, PLE2513, PLE2514, PLE2515 pub fn invalid_string_characters( locator: &Locator, - start: Location, - end: Location, + range: TextRange, autofix: bool, ) -> Vec { let mut diagnostics = Vec::new(); - let text = locator.slice(Range::new(start, end)); + let text = locator.slice(range); - for (row, line) in UniversalNewlineIterator::from(text).enumerate() { - let mut char_offset = 0; - for char in line.chars() { - let (replacement, rule): (&str, DiagnosticKind) = match char { - '\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 = helpers::to_absolute(Location::new(row + 1, char_offset), start); - let end_location = Location::new(location.row(), location.column() + 1); - let mut diagnostic = Diagnostic::new(rule, Range::new(location, end_location)); - if autofix { - diagnostic.set_fix(Edit::replacement( - replacement.to_string(), - location, - end_location, - )); + for (column, match_) in text.match_indices(&['\x08', '\x1A', '\x1B', '\0', '\u{200b}']) { + 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()), + _ => { + continue; } - diagnostics.push(diagnostic); - char_offset += 1; + }; + + let location = range.start() + TextSize::try_from(column).unwrap(); + let range = TextRange::at(location, c.text_len()); + + let mut diagnostic = Diagnostic::new(rule, range); + if autofix { + diagnostic.set_fix(Edit::range_replacement(replacement.to_string(), range)); } + diagnostics.push(diagnostic); } diagnostics diff --git a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs index 291c4c0b43..219260e76e 100644 --- a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::source_code::OneIndexed; use ruff_python_semantic::scope::ScopeKind; use crate::checkers::ast::Checker; @@ -10,7 +10,7 @@ use crate::checkers::ast::Checker; #[violation] pub struct LoadBeforeGlobalDeclaration { pub name: String, - pub line: usize, + pub line: OneIndexed, } impl Violation for LoadBeforeGlobalDeclaration { @@ -28,13 +28,15 @@ pub fn load_before_global_declaration(checker: &mut Checker, name: &str, expr: & _ => return, }; if let Some(stmt) = globals.get(name) { - if expr.location < stmt.location { + if expr.start() < stmt.start() { + #[allow(deprecated)] + let location = checker.locator.compute_source_location(stmt.start()); checker.diagnostics.push(Diagnostic::new( LoadBeforeGlobalDeclaration { name: name.to_string(), - line: stmt.location.row(), + line: location.row, }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/logging.rs b/crates/ruff/src/rules/pylint/rules/logging.rs index 2242b4ca81..3dd5f7f9cb 100644 --- a/crates/ruff/src/rules/pylint/rules/logging.rs +++ b/crates/ruff/src/rules/pylint/rules/logging.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::SimpleCallArgs; -use ruff_python_ast::types::Range; use ruff_python_semantic::analyze::logging; use ruff_python_stdlib::logging::LoggingLevel; @@ -128,7 +127,7 @@ pub fn logging_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: if summary.num_positional < message_args { checker .diagnostics - .push(Diagnostic::new(LoggingTooManyArgs, Range::from(func))); + .push(Diagnostic::new(LoggingTooManyArgs, func.range())); } } @@ -139,7 +138,7 @@ pub fn logging_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: { checker .diagnostics - .push(Diagnostic::new(LoggingTooFewArgs, Range::from(func))); + .push(Diagnostic::new(LoggingTooFewArgs, func.range())); } } } diff --git a/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs b/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs index d10f94bdd1..866412af86 100644 --- a/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs +++ b/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Unaryop}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::rules::pylint::settings::ConstantType; @@ -81,7 +80,7 @@ pub fn magic_value_comparison(checker: &mut Checker, left: &Expr, comparators: & MagicValueComparison { value: unparse_expr(comparison_expr, checker.stylist), }, - Range::from(comparison_expr), + comparison_expr.range(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs index a0c08f886b..2658c00e55 100644 --- a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs +++ b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Alias, AliasData, Located, Stmt, StmtKind}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_stmt, unparse_stmt}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -51,27 +50,25 @@ pub fn manual_from_import(checker: &mut Checker, stmt: &Stmt, alias: &Alias, nam name: name.to_string(), fixable, }, - Range::from(alias), + alias.range(), ); if fixable && checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_stmt( &create_stmt(StmtKind::ImportFrom { module: Some(module.to_string()), - names: vec![Located::new( - stmt.location, - stmt.end_location.unwrap(), + names: vec![Located::with_range( AliasData { name: asname.into(), asname: None, }, + stmt.range(), )], level: Some(0), }), checker.stylist, ), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs index 228961dd96..0824fbc199 100644 --- a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs @@ -7,7 +7,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::{Node, Range}; +use ruff_python_ast::types::Node; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::context::Context; @@ -396,7 +396,7 @@ pub fn redefined_loop_name<'a, 'b>(checker: &'a mut Checker<'b>, node: &Node<'b> outer_kind: outer_assignment_target.binding_kind, inner_kind: inner_assignment_target.binding_kind, }, - Range::from(inner_assignment_target.expr), + inner_assignment_target.expr.range(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs index abd7388318..0fbde30015 100644 --- a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs +++ b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs @@ -6,7 +6,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::hashable::HashableExpr; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -68,7 +67,7 @@ pub fn repeated_isinstance_calls(checker: &mut Checker, expr: &Expr, op: &Boolop .sorted() .collect(), }, - Range::from(expr), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/return_in_init.rs b/crates/ruff/src/rules/pylint/rules/return_in_init.rs index fd87ddf34d..ddf1c715ae 100644 --- a/crates/ruff/src/rules/pylint/rules/return_in_init.rs +++ b/crates/ruff/src/rules/pylint/rules/return_in_init.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::in_dunder_init; @@ -66,6 +65,6 @@ pub fn return_in_init(checker: &mut Checker, stmt: &Stmt) { if in_dunder_init(checker) { checker .diagnostics - .push(Diagnostic::new(ReturnInInit, Range::from(stmt))); + .push(Diagnostic::new(ReturnInInit, stmt.range())); } } diff --git a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs index a724af3382..cc563eba2b 100644 --- a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::autofix::actions::get_or_import_symbol; use crate::checkers::ast::Checker; @@ -43,7 +42,7 @@ pub fn sys_exit_alias(checker: &mut Checker, func: &Expr) { SysExitAlias { name: name.to_string(), }, - Range::from(func), + func.range(), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { @@ -54,8 +53,7 @@ pub fn sys_exit_alias(checker: &mut Checker, func: &Expr) { &checker.importer, checker.locator, )?; - let reference_edit = - Edit::replacement(binding, func.location, func.end_location.unwrap()); + let reference_edit = Edit::range_replacement(binding, func.range()); Ok(Fix::from_iter([import_edit, reference_edit])) }); } diff --git a/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs b/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs index 57de373055..c9fcee7335 100644 --- a/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs +++ b/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -19,9 +18,8 @@ impl Violation for UnnecessaryDirectLambdaCall { /// PLC3002 pub fn unnecessary_direct_lambda_call(checker: &mut Checker, expr: &Expr, func: &Expr) { if let ExprKind::Lambda { .. } = &func.node { - checker.diagnostics.push(Diagnostic::new( - UnnecessaryDirectLambdaCall, - Range::from(expr), - )); + checker + .diagnostics + .push(Diagnostic::new(UnnecessaryDirectLambdaCall, expr.range())); } } diff --git a/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs index 83b9505d92..8cb327d34b 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Alias; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -33,13 +32,9 @@ pub fn useless_import_alias(checker: &mut Checker, alias: &Alias) { return; } - let mut diagnostic = Diagnostic::new(UselessImportAlias, Range::from(alias)); + let mut diagnostic = Diagnostic::new(UselessImportAlias, alias.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - asname.to_string(), - alias.location, - alias.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(asname.to_string(), alias.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pylint/rules/useless_return.rs b/crates/ruff/src/rules/pylint/rules/useless_return.rs index 3e6c1ca688..52c24da5f7 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_return.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_return.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{is_const_none, ReturnStatementVisitor}; -use ruff_python_ast::types::{Range, RefEquality}; +use ruff_python_ast::types::RefEquality; use ruff_python_ast::visitor::Visitor; use crate::autofix::actions::delete_stmt; @@ -105,7 +105,7 @@ pub fn useless_return<'a>( return; } - let mut diagnostic = Diagnostic::new(UselessReturn, Range::from(last_stmt)); + let mut diagnostic = Diagnostic::new(UselessReturn, last_stmt.range()); if checker.patch(diagnostic.kind.rule()) { let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect(); match delete_stmt( diff --git a/crates/ruff/src/rules/pylint/rules/yield_in_init.rs b/crates/ruff/src/rules/pylint/rules/yield_in_init.rs index 5904bf27fd..cee9a9cffd 100644 --- a/crates/ruff/src/rules/pylint/rules/yield_in_init.rs +++ b/crates/ruff/src/rules/pylint/rules/yield_in_init.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::in_dunder_init; @@ -43,6 +42,6 @@ pub fn yield_in_init(checker: &mut Checker, expr: &Expr) { if in_dunder_init(checker) { checker .diagnostics - .push(Diagnostic::new(YieldInInit, Range::from(expr))); + .push(Diagnostic::new(YieldInInit, expr.range())); } } diff --git a/crates/ruff/src/rules/pyupgrade/fixes.rs b/crates/ruff/src/rules/pyupgrade/fixes.rs index ea001bb01e..baf259423c 100644 --- a/crates/ruff/src/rules/pyupgrade/fixes.rs +++ b/crates/ruff/src/rules/pyupgrade/fixes.rs @@ -3,18 +3,18 @@ use libcst_native::{ Codegen, CodegenState, CompoundStatement, Expression, ParenthesizableWhitespace, SmallStatement, Statement, Suite, }; -use rustpython_parser::ast::{Expr, Location}; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::Expr; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use crate::cst::matchers::match_module; -/// Safely adjust the indentation of the indented block at [`Range`]. +/// Safely adjust the indentation of the indented block at [`TextRange`]. pub fn adjust_indentation( - range: Range, + range: TextRange, indentation: &str, locator: &Locator, stylist: &Stylist, @@ -51,7 +51,7 @@ pub fn adjust_indentation( /// Generate a fix to remove arguments from a `super` call. pub fn remove_super_arguments(locator: &Locator, stylist: &Stylist, expr: &Expr) -> Option { - let range = Range::from(expr); + let range = expr.range(); let contents = locator.slice(range); let mut tree = libcst_native::parse_module(contents, None).ok()?; @@ -77,38 +77,35 @@ pub fn remove_super_arguments(locator: &Locator, stylist: &Stylist, expr: &Expr) }; tree.codegen(&mut state); - Some(Edit::replacement( - state.to_string(), - range.location, - range.end_location, - )) + Some(Edit::range_replacement(state.to_string(), range)) } /// Remove any imports matching `members` from an import-from statement. pub fn remove_import_members(contents: &str, members: &[&str]) -> String { - let mut names: Vec = vec![]; - let mut commas: Vec = vec![]; + let mut names: Vec = vec![]; + let mut commas: Vec = vec![]; let mut removal_indices: Vec = vec![]; // Find all Tok::Name tokens that are not preceded by Tok::As, and all // Tok::Comma tokens. let mut prev_tok = None; - for (start, tok, end) in lexer::lex(contents, Mode::Module) + for (tok, range) in lexer::lex(contents, Mode::Module) .flatten() - .skip_while(|(_, tok, _)| !matches!(tok, Tok::Import)) + .skip_while(|(tok, _)| !matches!(tok, Tok::Import)) { if let Tok::Name { name } = &tok { if matches!(prev_tok, Some(Tok::As)) { // Adjust the location to take the alias into account. - names.last_mut().unwrap().end_location = end; + let last_range = names.last_mut().unwrap(); + *last_range = TextRange::new(last_range.start(), range.end()); } else { if members.contains(&name.as_str()) { removal_indices.push(names.len()); } - names.push(Range::new(start, end)); + names.push(range); } } else if matches!(tok, Tok::Comma) { - commas.push(Range::new(start, end)); + commas.push(range); } prev_tok = Some(tok); } @@ -116,7 +113,7 @@ pub fn remove_import_members(contents: &str, members: &[&str]) -> String { // Reconstruct the source code by skipping any names that are in `members`. let locator = Locator::new(contents); let mut output = String::with_capacity(contents.len()); - let mut last_pos: Location = Location::new(1, 0); + let mut last_pos = TextSize::default(); let mut is_first = true; for index in 0..names.len() { if !removal_indices.contains(&index) { @@ -124,21 +121,21 @@ pub fn remove_import_members(contents: &str, members: &[&str]) -> String { continue; } - let (start_location, end_location) = if is_first { - (names[index].location, names[index + 1].location) + let range = if is_first { + TextRange::new(names[index].start(), names[index + 1].start()) } else { - (commas[index - 1].location, names[index].end_location) + TextRange::new(commas[index - 1].start(), names[index].end()) }; // Add all contents from `last_pos` to `fix.location`. // It's possible that `last_pos` is after `fix.location`, if we're removing the // first _two_ members. - if start_location > last_pos { - let slice = locator.slice(Range::new(last_pos, start_location)); + if range.start() > last_pos { + let slice = locator.slice(TextRange::new(last_pos, range.start())); output.push_str(slice); } - last_pos = end_location; + last_pos = range.end(); } // Add the remaining content. diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index 4d2a57a641..32aedf8cc1 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -6,7 +6,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_expr, create_stmt, unparse_stmt}; use ruff_python_ast::source_code::Stylist; -use ruff_python_ast::types::Range; use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; @@ -164,10 +163,9 @@ fn convert_to_class( base_class: &Expr, stylist: &Stylist, ) -> Edit { - Edit::replacement( + Edit::range_replacement( unparse_stmt(&create_class_def_stmt(typename, body, base_class), stylist), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), ) } @@ -194,13 +192,13 @@ pub fn convert_named_tuple_functional_to_class( } }; // TODO(charlie): Preserve indentation, to remove the first-column requirement. - let fixable = stmt.location.column() == 0; + let fixable = checker.locator.is_at_start_of_line(stmt.start()); let mut diagnostic = Diagnostic::new( ConvertNamedTupleFunctionalToClass { name: typename.to_string(), fixable, }, - Range::from(stmt), + stmt.range(), ); if fixable && checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(convert_to_class( diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index 4ae005de8b..b700ce5b0b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -6,7 +6,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_expr, create_stmt, unparse_stmt}; use ruff_python_ast::source_code::Stylist; -use ruff_python_ast::types::Range; use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; @@ -211,13 +210,12 @@ fn convert_to_class( base_class: &Expr, stylist: &Stylist, ) -> Edit { - Edit::replacement( + Edit::range_replacement( unparse_stmt( &create_class_def_stmt(class_name, body, total_keyword, base_class), stylist, ), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), ) } @@ -242,13 +240,13 @@ pub fn convert_typed_dict_functional_to_class( } }; // TODO(charlie): Preserve indentation, to remove the first-column requirement. - let fixable = stmt.location.column() == 0; + let fixable = checker.locator.is_at_start_of_line(stmt.start()); let mut diagnostic = Diagnostic::new( ConvertTypedDictFunctionalToClass { name: class_name.to_string(), fixable, }, - Range::from(stmt), + stmt.range(), ); if fixable && checker.patch(diagnostic.kind.rule()) { diagnostic.set_fix(convert_to_class( diff --git a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs index 86f109de62..f31e304947 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -42,14 +41,12 @@ pub fn datetime_utc_alias(checker: &mut Checker, expr: &Expr) { let straight_import = collect_call_path(expr).map_or(false, |call_path| { call_path.as_slice() == ["datetime", "timezone", "utc"] }); - let mut diagnostic = - Diagnostic::new(DatetimeTimezoneUTC { straight_import }, Range::from(expr)); + let mut diagnostic = Diagnostic::new(DatetimeTimezoneUTC { straight_import }, expr.range()); if checker.patch(diagnostic.kind.rule()) { if straight_import { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "datetime.UTC".to_string(), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs index e5408dd7fe..7af7c123e4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Located, Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -22,13 +21,12 @@ impl AlwaysAutofixableViolation for DeprecatedCElementTree { } fn add_check_for_node(checker: &mut Checker, node: &Located) { - let mut diagnostic = Diagnostic::new(DeprecatedCElementTree, Range::from(node)); + let mut diagnostic = Diagnostic::new(DeprecatedCElementTree, node.range()); if checker.patch(diagnostic.kind.rule()) { - let contents = checker.locator.slice(node); - diagnostic.set_fix(Edit::replacement( + let contents = checker.locator.slice(node.range()); + diagnostic.set_fix(Edit::range_replacement( contents.replacen("cElementTree", "ElementTree", 1), - node.location, - node.end_location.unwrap(), + node.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index df68b0d701..781c5b8dde 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Alias, AliasData, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace::indentation; use crate::checkers::ast::Checker; @@ -456,7 +455,7 @@ impl<'a> ImportReplacer<'a> { let matched = ImportReplacer::format_import_from(&matched_names, target); let unmatched = fixes::remove_import_members( - self.locator.slice(self.stmt), + self.locator.slice(self.stmt.range()), &matched_names .iter() .map(|name| name.name.as_str()) @@ -548,15 +547,11 @@ pub fn deprecated_import( DeprecatedImport { deprecation: Deprecation::WithoutRename(operation), }, - Range::from(stmt), + stmt.range(), ); if checker.patch(Rule::DeprecatedImport) { if let Some(content) = fix { - diagnostic.set_fix(Edit::replacement( - content, - stmt.location, - stmt.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(content, stmt.range())); } } checker.diagnostics.push(diagnostic); @@ -567,7 +562,7 @@ pub fn deprecated_import( DeprecatedImport { deprecation: Deprecation::WithRename(operation), }, - Range::from(stmt), + stmt.range(), ); checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs index 2bf1aaa787..e638aa459f 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -10,7 +10,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace::indentation; use crate::checkers::ast::Checker; @@ -126,7 +125,7 @@ fn format_import( locator: &Locator, stylist: &Stylist, ) -> Result { - let module_text = locator.slice(stmt); + let module_text = locator.slice(stmt.range()); let mut tree = match_module(module_text)?; let mut import = match_import(&mut tree)?; @@ -160,7 +159,7 @@ fn format_import_from( locator: &Locator, stylist: &Stylist, ) -> Result { - let module_text = locator.slice(stmt); + let module_text = locator.slice(stmt.range()); let mut tree = match_module(module_text).unwrap(); let mut import = match_import_from(&mut tree)?; @@ -255,14 +254,10 @@ pub fn deprecated_mock_attribute(checker: &mut Checker, expr: &Expr) { DeprecatedMockImport { reference_type: MockReference::Attribute, }, - Range::from(value), + value.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "mock".to_string(), - value.location, - value.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("mock".to_string(), value.range())); } checker.diagnostics.push(diagnostic); } @@ -302,14 +297,11 @@ pub fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) { DeprecatedMockImport { reference_type: MockReference::Import, }, - Range::from(name), + name.range(), ); if let Some(content) = content.as_ref() { - diagnostic.set_fix(Edit::replacement( - content.clone(), - stmt.location, - stmt.end_location.unwrap(), - )); + diagnostic + .set_fix(Edit::range_replacement(content.clone(), stmt.range())); } checker.diagnostics.push(diagnostic); } @@ -330,20 +322,13 @@ pub fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) { DeprecatedMockImport { reference_type: MockReference::Import, }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { if let Some(indent) = indentation(checker.locator, stmt) { diagnostic.try_set_fix(|| { - format_import_from(stmt, indent, checker.locator, checker.stylist).map( - |content| { - Edit::replacement( - content, - stmt.location, - stmt.end_location.unwrap(), - ) - }, - ) + format_import_from(stmt, indent, checker.locator, checker.stylist) + .map(|content| Edit::range_replacement(content, stmt.range())) }); } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs index 7c66768f8c..73db5418ea 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -67,13 +66,12 @@ pub fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) { alias: attr.to_string(), target: target.to_string(), }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( format!("self.{target}"), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs index 49085b0279..d7dc4964ad 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -1,10 +1,10 @@ +use ruff_text_size::TextRange; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::registry::Rule; use crate::settings::{flags, Settings}; @@ -31,7 +31,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok((_, tok, _)) = &tokens[i] else { + let Ok((tok, _)) = &tokens[i] else { return None; }; match tok { @@ -57,7 +57,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok((_, tok, _)) = &tokens[i] else { + let Ok((tok, _)) = &tokens[i] else { return None; }; @@ -78,7 +78,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if (start + 1..i).all(|i| { matches!( tokens[i], - Ok((_, Tok::Comment(..) | Tok::NonLogicalNewline, _)) + Ok((Tok::Comment(..) | Tok::NonLogicalNewline, _)) ) }) { return None; @@ -90,7 +90,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok((_, tok, _)) = &tokens[i] else { + let Ok(( tok, _)) = &tokens[i] else { return None; }; match tok { @@ -106,7 +106,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok((_, tok, _)) = &tokens[i] else { + let Ok(( tok, _)) = &tokens[i] else { return None; }; if matches!(tok, Tok::Rpar) { @@ -126,23 +126,26 @@ pub fn extraneous_parentheses( let mut diagnostics = vec![]; let mut i = 0; while i < tokens.len() { - if matches!(tokens[i], Ok((_, Tok::Lpar, _))) { + if matches!(tokens[i], Ok((Tok::Lpar, _))) { if let Some((start, end)) = match_extraneous_parentheses(tokens, i) { i = end + 1; - let Ok((start, ..)) = &tokens[start] else { + let Ok((_, start_range)) = &tokens[start] else { return diagnostics; }; - let Ok((.., end)) = &tokens[end] else { + let Ok((.., end_range)) = &tokens[end] else { return diagnostics; }; - let mut diagnostic = - Diagnostic::new(ExtraneousParentheses, Range::new(*start, *end)); + let mut diagnostic = Diagnostic::new( + ExtraneousParentheses, + TextRange::new(start_range.start(), end_range.end()), + ); if autofix.into() && settings.rules.should_fix(Rule::ExtraneousParentheses) { - let contents = locator.slice(Range::new(*start, *end)); + let contents = + locator.slice(TextRange::new(start_range.start(), end_range.end())); diagnostic.set_fix(Edit::replacement( contents[1..contents.len() - 1].to_string(), - *start, - *end, + start_range.start(), + end_range.end(), )); } diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs index 252fa46ba1..6415e6744c 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs @@ -1,13 +1,13 @@ +use ruff_text_size::TextRange; use rustc_hash::FxHashMap; use rustpython_common::format::{ FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate, }; -use rustpython_parser::ast::{Constant, Expr, ExprKind, KeywordData, Location}; +use rustpython_parser::ast::{Constant, Expr, ExprKind, KeywordData}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::str::{is_implicit_concatenation, leading_quote, trailing_quote}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -44,7 +44,7 @@ impl<'a> FormatSummaryValues<'a> { let mut extracted_kwargs: FxHashMap<&str, String> = FxHashMap::default(); if let ExprKind::Call { args, keywords, .. } = &expr.node { for arg in args { - let arg = checker.locator.slice(arg); + let arg = checker.locator.slice(arg.range()); if contains_invalids(arg) { return None; } @@ -53,7 +53,7 @@ impl<'a> FormatSummaryValues<'a> { for keyword in keywords { let KeywordData { arg, value } = &keyword.node; if let Some(key) = arg { - let kwarg = checker.locator.slice(value); + let kwarg = checker.locator.slice(value.range()); if contains_invalids(kwarg) { return None; } @@ -124,7 +124,7 @@ fn try_convert_to_f_string(checker: &Checker, expr: &Expr) -> Option { return None; }; - let contents = checker.locator.slice(value); + let contents = checker.locator.slice(value.range()); // Skip implicit string concatenations. if is_implicit_concatenation(contents) { @@ -241,7 +241,7 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E } // Avoid refactoring multi-line strings. - if expr.location.row() != expr.end_location.unwrap().row() { + if checker.locator.contains_line_break(expr.range()) { return; } @@ -252,30 +252,21 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E }; // Avoid refactors that increase the resulting string length. - let existing = checker.locator.slice(expr); + let existing = checker.locator.slice(expr.range()); if contents.len() > existing.len() { return; } // If necessary, add a space between any leading keyword (`return`, `yield`, `assert`, etc.) // and the string. For example, `return"foo"` is valid, but `returnf"foo"` is not. - if expr.location.column() > 0 { - let existing = checker.locator.slice(Range::new( - Location::new(expr.location.row(), expr.location.column() - 1), - expr.end_location.unwrap(), - )); - if existing.chars().next().unwrap().is_ascii_alphabetic() { - contents.insert(0, ' '); - } + let existing = checker.locator.slice(TextRange::up_to(expr.start())); + if existing.chars().last().unwrap().is_ascii_alphabetic() { + contents.insert(0, ' '); } - let mut diagnostic = Diagnostic::new(FString, Range::from(expr)); + let mut diagnostic = Diagnostic::new(FString, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - contents, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(contents, expr.range())); }; checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs index a7e0f55562..d912872e77 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs @@ -7,7 +7,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::cst::matchers::{match_call, match_expression}; @@ -86,7 +85,7 @@ fn generate_call( locator: &Locator, stylist: &Stylist, ) -> Result { - let module_text = locator.slice(expr); + let module_text = locator.slice(expr.range()); let mut expression = match_expression(module_text)?; let mut call = match_call(&mut expression)?; @@ -139,18 +138,14 @@ pub(crate) fn format_literals(checker: &mut Checker, summary: &FormatSummary, ex return; } - let mut diagnostic = Diagnostic::new(FormatLiterals, Range::from(expr)); + let mut diagnostic = Diagnostic::new(FormatLiterals, expr.range()); if checker.patch(diagnostic.kind.rule()) { // Currently, the only issue we know of is in LibCST: // https://github.com/Instagram/LibCST/issues/846 if let Ok(contents) = generate_call(expr, &summary.indices, checker.locator, checker.stylist) { - diagnostic.set_fix(Edit::replacement( - contents, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(contents, expr.range())); }; } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs index 793814ef86..c0f266e968 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs @@ -1,8 +1,8 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Constant, Expr, ExprKind, KeywordData}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::autofix::actions::get_or_import_symbol; use crate::checkers::ast::Checker; @@ -55,7 +55,7 @@ pub fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: &[Expr { let mut diagnostic = Diagnostic::new( LRUCacheWithMaxsizeNone, - Range::new(func.end_location.unwrap(), expr.end_location.unwrap()), + TextRange::new(func.end(), expr.end()), ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { @@ -66,8 +66,7 @@ pub fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: &[Expr &checker.importer, checker.locator, )?; - let reference_edit = - Edit::replacement(binding, expr.location, expr.end_location.unwrap()); + let reference_edit = Edit::range_replacement(binding, expr.range()); Ok(Fix::from_iter([import_edit, reference_edit])) }); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs index f4f4fdb72d..fb15799b05 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs @@ -1,9 +1,9 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -45,13 +45,12 @@ pub fn lru_cache_without_parameters(checker: &mut Checker, decorator_list: &[Exp { let mut diagnostic = Diagnostic::new( LRUCacheWithoutParameters, - Range::new(func.end_location.unwrap(), expr.end_location.unwrap()), + TextRange::new(func.end(), expr.end()), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(func, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs index ab121ac7ec..734caa7d56 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs @@ -5,7 +5,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::str::is_implicit_concatenation; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -63,9 +62,9 @@ pub fn native_literals( LiteralType::Str } else { LiteralType::Bytes - }}, Range::from(expr)); + }}, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( if id == "bytes" { let mut content = String::with_capacity(3); content.push('b'); @@ -78,8 +77,7 @@ pub fn native_literals( content.push(checker.stylist.quote().into()); content }, - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); @@ -113,7 +111,7 @@ pub fn native_literals( } // Skip implicit string concatenations. - let arg_code = checker.locator.slice(arg); + let arg_code = checker.locator.slice(arg.range()); if is_implicit_concatenation(arg_code) { return; } @@ -126,14 +124,10 @@ pub fn native_literals( LiteralType::Bytes }, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - arg_code.to_string(), - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(arg_code.to_string(), expr.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs index 1c04120cda..c6db74f64f 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -37,13 +36,9 @@ pub fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) { .ctx .find_binding("open") .map_or(true, |binding| binding.kind.is_builtin()); - let mut diagnostic = Diagnostic::new(OpenAlias { fixable }, Range::from(expr)); + let mut diagnostic = Diagnostic::new(OpenAlias { fixable }, expr.range()); if fixable && checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "open".to_string(), - func.location, - func.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("open".to_string(), func.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 4e026acbf6..794220b864 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -4,7 +4,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::compose_call_path; use ruff_python_ast::helpers::{create_expr, unparse_expr}; -use ruff_python_ast::types::Range; use ruff_python_semantic::context::Context; use crate::checkers::ast::Checker; @@ -61,13 +60,12 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) { OSErrorAlias { name: compose_call_path(target), }, - Range::from(target), + target.range(), ); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "OSError".to_string(), - target.location, - target.end_location.unwrap(), + target.range(), )); } checker.diagnostics.push(diagnostic); @@ -75,7 +73,7 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) { /// Create a [`Diagnostic`] for a tuple of expressions. fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) { - let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, Range::from(target)); + let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, target.range()); if checker.patch(diagnostic.kind.rule()) { let ExprKind::Tuple { elts, ..} = &target.node else { panic!("Expected ExprKind::Tuple"); @@ -105,13 +103,12 @@ fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) { } if remaining.len() == 1 { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( "OSError".to_string(), - target.location, - target.end_location.unwrap(), + target.range(), )); } else { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( format!( "({})", unparse_expr( @@ -122,8 +119,7 @@ fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) { checker.stylist, ) ), - target.location, - target.end_location.unwrap(), + target.range(), )); } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs index 691346577c..be064dec61 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -2,13 +2,14 @@ use std::cmp::Ordering; use log::error; use num_bigint::{BigInt, Sign}; -use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Located, Location, Stmt}; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Located, Stmt}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::{Range, RefEquality}; +use ruff_python_ast::types::RefEquality; use ruff_python_ast::whitespace::indentation; use crate::autofix::actions::delete_stmt; @@ -36,13 +37,13 @@ struct BlockMetadata { /// The first non-whitespace token in the block. starter: Tok, /// The location of the first `elif` token, if any. - elif: Option, + elif: Option, /// The location of the `else` token, if any. - else_: Option, + else_: Option, } impl BlockMetadata { - const fn new(starter: Tok, elif: Option, else_: Option) -> Self { + const fn new(starter: Tok, elif: Option, else_: Option) -> Self { Self { starter, elif, @@ -54,39 +55,36 @@ impl BlockMetadata { fn metadata(locator: &Locator, located: &Located) -> Option { indentation(locator, located)?; + let line_start = locator.line_start(located.start()); // Start the selection at the start-of-line. This ensures consistent indentation // in the token stream, in the event that the entire block is indented. - let text = locator.slice(Range::new( - Location::new(located.location.row(), 0), - located.end_location.unwrap(), - )); + let text = locator.slice(TextRange::new(line_start, located.end())); let mut starter: Option = None; let mut elif = None; let mut else_ = None; - for (start, tok, _) in - lexer::lex_located(text, Mode::Module, Location::new(located.location.row(), 0)) - .flatten() - .filter(|(_, tok, _)| { - !matches!( - tok, - Tok::Indent - | Tok::Dedent - | Tok::NonLogicalNewline - | Tok::Newline - | Tok::Comment(..) - ) - }) + for (tok, range) in lexer::lex_located(text, Mode::Module, line_start) + .flatten() + .filter(|(tok, _)| { + !matches!( + tok, + Tok::Indent + | Tok::Dedent + | Tok::NonLogicalNewline + | Tok::Newline + | Tok::Comment(..) + ) + }) { if starter.is_none() { starter = Some(tok.clone()); } else { if matches!(tok, Tok::Elif) && elif.is_none() { - elif = Some(start); + elif = Some(range.start()); } if matches!(tok, Tok::Else) && else_.is_none() { - else_ = Some(start); + else_ = Some(range.start()); } } if starter.is_some() && elif.is_some() && else_.is_some() { @@ -195,22 +193,18 @@ fn fix_py2_block( if indentation(checker.locator, start).is_none() { // Inline `else` block (e.g., `else: x = 1`). - Some(Edit::replacement( + Some(Edit::range_replacement( checker .locator - .slice(Range::new(start.location, end.end_location.unwrap())) + .slice(TextRange::new(start.start(), end.end())) .to_string(), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), )) } else { indentation(checker.locator, stmt) .and_then(|indentation| { adjust_indentation( - Range::new( - Location::new(start.location.row(), 0), - end.end_location.unwrap(), - ), + TextRange::new(checker.locator.line_start(start.start()), end.end()), indentation, checker.locator, checker.stylist, @@ -220,28 +214,26 @@ fn fix_py2_block( .map(|contents| { Edit::replacement( contents, - Location::new(stmt.location.row(), 0), - stmt.end_location.unwrap(), + checker.locator.line_start(stmt.start()), + stmt.end(), ) }) } } else { - let mut end_location = orelse.last().unwrap().location; + let mut end_location = orelse.last().unwrap().start(); if block.starter == Tok::If && block.elif.is_some() { // Turn the `elif` into an `if`. - end_location = block.elif.unwrap(); - end_location.go_right(); - end_location.go_right(); + end_location = block.elif.unwrap() + TextSize::from(2); } else if block.starter == Tok::Elif { if let Some(elif) = block.elif { end_location = elif; } else if let Some(else_) = block.else_ { end_location = else_; } else { - end_location = body.last().unwrap().end_location.unwrap(); + end_location = body.last().unwrap().end(); } } - Some(Edit::deletion(stmt.location, end_location)) + Some(Edit::deletion(stmt.start(), end_location)) } } @@ -262,22 +254,18 @@ fn fix_py3_block( if indentation(checker.locator, start).is_none() { // Inline `if` block (e.g., `if ...: x = 1`). - Some(Edit::replacement( + Some(Edit::range_replacement( checker .locator - .slice(Range::new(start.location, end.end_location.unwrap())) + .slice(TextRange::new(start.start(), end.end())) .to_string(), - stmt.location, - stmt.end_location.unwrap(), + stmt.range(), )) } else { indentation(checker.locator, stmt) .and_then(|indentation| { adjust_indentation( - Range::new( - Location::new(start.location.row(), 0), - end.end_location.unwrap(), - ), + TextRange::new(checker.locator.line_start(start.start()), end.end()), indentation, checker.locator, checker.stylist, @@ -287,8 +275,8 @@ fn fix_py3_block( .map(|contents| { Edit::replacement( contents, - Location::new(stmt.location.row(), 0), - stmt.end_location.unwrap(), + checker.locator.line_start(stmt.start()), + stmt.end(), ) }) } @@ -297,15 +285,8 @@ fn fix_py3_block( // Replace the `elif` with an `else, preserve the body of the elif, and remove // the rest. let end = body.last().unwrap(); - let text = checker.locator.slice(Range::new( - test.end_location.unwrap(), - end.end_location.unwrap(), - )); - Some(Edit::replacement( - format!("else{text}"), - stmt.location, - stmt.end_location.unwrap(), - )) + let text = checker.locator.slice(TextRange::new(test.end(), end.end())); + Some(Edit::range_replacement(format!("else{text}"), stmt.range())) } _ => None, } @@ -346,8 +327,7 @@ pub fn outdated_version_block( let target = checker.settings.target_version; if op == &Cmpop::Lt || op == &Cmpop::LtE { if compare_version(&version, target, op == &Cmpop::LtE) { - let mut diagnostic = - Diagnostic::new(OutdatedVersionBlock, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt) { if let Some(fix) = @@ -361,8 +341,7 @@ pub fn outdated_version_block( } } else if op == &Cmpop::Gt || op == &Cmpop::GtE { if compare_version(&version, target, op == &Cmpop::GtE) { - let mut diagnostic = - Diagnostic::new(OutdatedVersionBlock, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt) { if let Some(fix) = fix_py3_block(checker, stmt, test, body, &block) @@ -381,7 +360,7 @@ pub fn outdated_version_block( } => { let version_number = bigint_to_u32(number); if version_number == 2 && op == &Cmpop::Eq { - let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt) { if let Some(fix) = fix_py2_block(checker, stmt, body, orelse, &block) { @@ -391,7 +370,7 @@ pub fn outdated_version_block( } checker.diagnostics.push(diagnostic); } else if version_number == 3 && op == &Cmpop::Eq { - let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, Range::from(stmt)); + let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt) { if let Some(fix) = fix_py3_block(checker, stmt, test, body, &block) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs index 142e4752e0..d8438bf764 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -1,15 +1,16 @@ +use ruff_text_size::TextRange; use std::str::FromStr; use rustpython_common::cformat::{ CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString, }; -use rustpython_parser::ast::{Constant, Expr, ExprKind, Location}; +use rustpython_parser::ast::{Constant, Expr, ExprKind}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Locator; use ruff_python_ast::str::{leading_quote, trailing_quote}; -use ruff_python_ast::types::Range; use ruff_python_ast::whitespace::indentation; use ruff_python_stdlib::identifiers::is_identifier; @@ -137,11 +138,11 @@ fn percent_to_format(format_string: &CFormatString) -> String { } /// If a tuple has one argument, remove the comma; otherwise, return it as-is. -fn clean_params_tuple(checker: &mut Checker, right: &Expr) -> String { - let mut contents = checker.locator.slice(right).to_string(); +fn clean_params_tuple(checker: &mut Checker, right: &Expr, locator: &Locator) -> String { + let mut contents = checker.locator.slice(right.range()).to_string(); if let ExprKind::Tuple { elts, .. } = &right.node { if elts.len() == 1 { - if right.location.row() == right.end_location.unwrap().row() { + if !locator.contains_line_break(right.range()) { for (i, character) in contents.chars().rev().enumerate() { if character == ',' { let correct_index = contents.len() - i - 1; @@ -157,8 +158,12 @@ fn clean_params_tuple(checker: &mut Checker, right: &Expr) -> String { /// Converts a dictionary to a function call while preserving as much styling as /// possible. -fn clean_params_dictionary(checker: &mut Checker, right: &Expr) -> Option { - let is_multi_line = right.location.row() < right.end_location.unwrap().row(); +fn clean_params_dictionary( + checker: &mut Checker, + right: &Expr, + locator: &Locator, +) -> Option { + let is_multi_line = locator.contains_line_break(right.range()); let mut contents = String::new(); if let ExprKind::Dict { keys, values } = &right.node { let mut arguments: Vec = vec![]; @@ -187,7 +192,7 @@ fn clean_params_dictionary(checker: &mut Checker, right: &Expr) -> Option Option { - let value_string = checker.locator.slice(value); + let value_string = checker.locator.slice(value.range()); arguments.push(format!("**{value_string}")); } } @@ -296,18 +301,27 @@ fn convertible(format_string: &CFormatString, params: &Expr) -> bool { } /// UP031 -pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right: &Expr) { +pub(crate) fn printf_string_formatting( + checker: &mut Checker, + expr: &Expr, + right: &Expr, + locator: &Locator, +) { // Grab each string segment (in case there's an implicit concatenation). - let mut strings: Vec<(Location, Location)> = vec![]; + let mut strings: Vec = vec![]; let mut extension = None; - for (start, tok, end) in - lexer::lex_located(checker.locator.slice(expr), Mode::Module, expr.location).flatten() + for (tok, range) in lexer::lex_located( + checker.locator.slice(expr.range()), + Mode::Module, + expr.start(), + ) + .flatten() { if matches!(tok, Tok::String { .. }) { - strings.push((start, end)); + strings.push(range); } else if matches!(tok, Tok::Rpar) { // If we hit a right paren, we have to preserve it. - extension = Some((start, end)); + extension = Some(range); } else if matches!(tok, Tok::Percent) { // Break as soon as we find the modulo symbol. break; @@ -323,8 +337,8 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right let mut num_positional_arguments = 0; let mut num_keyword_arguments = 0; let mut format_strings = Vec::with_capacity(strings.len()); - for (start, end) in &strings { - let string = checker.locator.slice(Range::new(*start, *end)); + for range in &strings { + let string = checker.locator.slice(*range); let (Some(leader), Some(trailer)) = (leading_quote(string), trailing_quote(string)) else { return; }; @@ -358,7 +372,7 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right // Parse the parameters. let params_string = match right.node { ExprKind::Constant { .. } | ExprKind::JoinedStr { .. } => { - format!("({})", checker.locator.slice(right)) + format!("({})", checker.locator.slice(right.range())) } ExprKind::Name { .. } | ExprKind::Attribute { .. } @@ -366,11 +380,11 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right | ExprKind::Call { .. } => { if num_keyword_arguments > 0 { // If we have _any_ named fields, assume the right-hand side is a mapping. - format!("(**{})", checker.locator.slice(right)) + format!("(**{})", checker.locator.slice(right.range())) } else if num_positional_arguments > 1 { // If we have multiple fields, but no named fields, assume the right-hand side is a // tuple. - format!("(*{})", checker.locator.slice(right)) + format!("(*{})", checker.locator.slice(right.range())) } else { // Otherwise, if we have a single field, we can't make any assumptions about the // right-hand side. It _could_ be a tuple, but it could also be a single value, @@ -384,9 +398,9 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right return; } } - ExprKind::Tuple { .. } => clean_params_tuple(checker, right), + ExprKind::Tuple { .. } => clean_params_tuple(checker, right, locator), ExprKind::Dict { .. } => { - if let Some(params_string) = clean_params_dictionary(checker, right) { + if let Some(params_string) = clean_params_dictionary(checker, right, locator) { params_string } else { return; @@ -398,35 +412,39 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right // Reconstruct the string. let mut contents = String::new(); let mut prev = None; - for ((start, end), format_string) in strings.iter().zip(format_strings) { + for (range, format_string) in strings.iter().zip(format_strings) { // Add the content before the string segment. match prev { None => { - contents.push_str(checker.locator.slice(Range::new(expr.location, *start))); + contents.push_str( + checker + .locator + .slice(TextRange::new(expr.start(), range.start())), + ); } Some(prev) => { - contents.push_str(checker.locator.slice(Range::new(prev, *start))); + contents.push_str(checker.locator.slice(TextRange::new(prev, range.start()))); } } // Add the string itself. contents.push_str(&format_string); - prev = Some(*end); + prev = Some(range.end()); } - if let Some((.., end)) = extension { - contents.push_str(checker.locator.slice(Range::new(prev.unwrap(), end))); + if let Some(range) = extension { + contents.push_str( + checker + .locator + .slice(TextRange::new(prev.unwrap(), range.end())), + ); } // Add the `.format` call. contents.push_str(&format!(".format{params_string}")); - let mut diagnostic = Diagnostic::new(PrintfStringFormatting, Range::from(expr)); + let mut diagnostic = Diagnostic::new(PrintfStringFormatting, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - contents, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(contents, expr.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs index 61167eb515..dad06f86c1 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_text_size::TextRange; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -20,14 +20,10 @@ impl AlwaysAutofixableViolation for QuotedAnnotation { } /// UP037 -pub fn quoted_annotation(checker: &mut Checker, annotation: &str, range: Range) { +pub fn quoted_annotation(checker: &mut Checker, annotation: &str, range: TextRange) { let mut diagnostic = Diagnostic::new(QuotedAnnotation, range); if checker.patch(Rule::QuotedAnnotation) { - diagnostic.set_fix(Edit::replacement( - annotation.to_string(), - range.location, - range.end_location, - )); + diagnostic.set_fix(Edit::range_replacement(annotation.to_string(), range)); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs index fca436bdf0..4b89564af2 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -1,14 +1,14 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; -use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Location}; +use ruff_text_size::TextSize; +use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::find_keyword; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -112,14 +112,13 @@ fn create_check( RedundantOpenModes { replacement: replacement_value.map(ToString::to_string), }, - Range::from(expr), + expr.range(), ); if patch { if let Some(content) = replacement_value { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( content.to_string(), - mode_param.location, - mode_param.end_location.unwrap(), + mode_param.range(), )); } else { diagnostic.try_set_fix(|| create_remove_param_fix(locator, expr, mode_param)); @@ -129,34 +128,34 @@ fn create_check( } fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> Result { - let content = locator.slice(expr); + let content = locator.slice(expr.range()); // Find the last comma before mode_param and create a deletion fix // starting from the comma and ending after mode_param. - let mut fix_start: Option = None; - let mut fix_end: Option = None; + let mut fix_start: Option = None; + let mut fix_end: Option = None; let mut is_first_arg: bool = false; let mut delete_first_arg: bool = false; - for (start, tok, end) in lexer::lex_located(content, Mode::Module, expr.location).flatten() { - if start == mode_param.location { + for (tok, range) in lexer::lex_located(content, Mode::Module, expr.start()).flatten() { + if range.start() == mode_param.start() { if is_first_arg { delete_first_arg = true; continue; } - fix_end = Some(end); + fix_end = Some(range.end()); break; } if delete_first_arg && matches!(tok, Tok::Name { .. }) { - fix_end = Some(start); + fix_end = Some(range.start()); break; } if matches!(tok, Tok::Lpar) { is_first_arg = true; - fix_start = Some(end); + fix_start = Some(range.end()); } if matches!(tok, Tok::Comma) { is_first_arg = false; if !delete_first_arg { - fix_start = Some(start); + fix_start = Some(range.start()); } } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 35e8062905..d39394b88f 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -5,7 +5,6 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::find_keyword; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::autofix::actions::remove_argument; use crate::checkers::ast::Checker; @@ -34,26 +33,14 @@ fn generate_fix( stdout: &Keyword, stderr: &Keyword, ) -> Result { - let (first, second) = if stdout.location < stderr.location { + let (first, second) = if stdout.start() < stderr.start() { (stdout, stderr) } else { (stderr, stdout) }; Ok(Fix::new(vec![ - Edit::replacement( - "capture_output=True".to_string(), - first.location, - first.end_location.unwrap(), - ), - remove_argument( - locator, - func.location, - second.location, - second.end_location.unwrap(), - args, - keywords, - false, - )?, + Edit::range_replacement("capture_output=True".to_string(), first.range()), + remove_argument(locator, func.start(), second.range(), args, keywords, false)?, ])) } @@ -97,7 +84,7 @@ pub fn replace_stdout_stderr( return; } - let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, Range::from(expr)); + let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, expr.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { generate_fix(checker.locator, func, args, keywords, stdout, stderr) diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs index ea385ab6f8..7fb590ae01 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -1,9 +1,9 @@ -use rustpython_parser::ast::{Expr, Keyword, Location}; +use ruff_text_size::{TextLen, TextRange}; +use rustpython_parser::ast::{Expr, Keyword}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::find_keyword; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -32,20 +32,10 @@ pub fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwargs: &[ }) { let Some(kwarg) = find_keyword(kwargs, "universal_newlines") else { return; }; - let range = Range::new( - kwarg.location, - Location::new( - kwarg.location.row(), - kwarg.location.column() + "universal_newlines".len(), - ), - ); + let range = TextRange::at(kwarg.start(), "universal_newlines".text_len()); let mut diagnostic = Diagnostic::new(ReplaceUniversalNewlines, range); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "text".to_string(), - range.location, - range.end_location, - )); + diagnostic.set_fix(Edit::range_replacement("text".to_string(), range)); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs index 156173d128..b221ced002 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{ArgData, Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::scope::ScopeKind; use crate::checkers::ast::Checker; @@ -96,7 +95,7 @@ pub fn super_call_with_parameters(checker: &mut Checker, expr: &Expr, func: &Exp return; } - let mut diagnostic = Diagnostic::new(SuperCallWithParameters, Range::from(expr)); + let mut diagnostic = Diagnostic::new(SuperCallWithParameters, expr.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(fix) = fixes::remove_super_arguments(checker.locator, checker.stylist, expr) { diagnostic.set_fix(fix); diff --git a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs index 65f30b1b3d..b0cf937777 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -45,13 +44,9 @@ pub fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, args: let Some(primitive) = Primitive::from_constant(value) else { return; }; - let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, Range::from(expr)); + let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - primitive.builtin(), - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(primitive.builtin(), expr.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 04c3affb1e..b9b6b9a0d8 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -30,13 +29,9 @@ pub fn typing_text_str_alias(checker: &mut Checker, expr: &Expr) { call_path.as_slice() == ["typing", "Text"] }) { - let mut diagnostic = Diagnostic::new(TypingTextStrAlias, Range::from(expr)); + let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( - "str".to_string(), - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement("str".to_string(), expr.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs b/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs index 66caff749f..d07cb4792c 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs @@ -1,8 +1,8 @@ -use rustpython_parser::ast::{Expr, Location}; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::Expr; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -25,12 +25,12 @@ impl AlwaysAutofixableViolation for UnicodeKindPrefix { pub fn unicode_kind_prefix(checker: &mut Checker, expr: &Expr, kind: Option<&str>) { if let Some(const_kind) = kind { if const_kind.to_lowercase() == "u" { - let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, Range::from(expr)); + let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::deletion( - expr.location, - Location::new(expr.location.row(), expr.location.column() + 1), - )); + diagnostic.set_fix(Edit::range_deletion(TextRange::at( + expr.start(), + TextSize::from(1), + ))); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs index 20586c10b4..6867c381a0 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Alias, AliasData, Located, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::autofix; use crate::checkers::ast::Checker; @@ -101,7 +100,7 @@ pub fn unnecessary_builtin_import( .sorted() .collect(), }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs index 4a6c189770..b73fc9b626 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs @@ -1,10 +1,9 @@ use once_cell::sync::Lazy; use regex::Regex; -use rustpython_parser::ast::Location; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; +use ruff_python_ast::newlines::Line; // TODO: document referencing [PEP 3120]: https://peps.python.org/pep-3120/ #[violation] @@ -26,18 +25,12 @@ static CODING_COMMENT_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*utf-?8").unwrap()); /// UP009 -pub fn unnecessary_coding_comment(lineno: usize, line: &str, autofix: bool) -> Option { +pub(crate) fn unnecessary_coding_comment(line: &Line, autofix: bool) -> Option { // PEP3120 makes utf-8 the default encoding. - if CODING_COMMENT_REGEX.is_match(line) { - let mut diagnostic = Diagnostic::new( - UTF8EncodingDeclaration, - Range::new(Location::new(lineno + 1, 0), Location::new(lineno + 2, 0)), - ); + if CODING_COMMENT_REGEX.is_match(line.as_str()) { + let mut diagnostic = Diagnostic::new(UTF8EncodingDeclaration, line.full_range()); if autofix { - diagnostic.set_fix(Edit::deletion( - Location::new(lineno + 1, 0), - Location::new(lineno + 2, 0), - )); + diagnostic.set_fix(Edit::deletion(line.start(), line.full_end())); } Some(diagnostic) } else { diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index 2fd43e201a..6854f175b7 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -1,10 +1,10 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::autofix::actions::remove_argument; use crate::checkers::ast::Checker; @@ -109,28 +109,27 @@ fn match_encoding_arg<'a>(args: &'a [Expr], kwargs: &'a [Keyword]) -> Option Edit { // Build up a replacement string by prefixing all string tokens with `b`. - let contents = locator.slice(constant); + let contents = locator.slice(constant.range()); let mut replacement = String::with_capacity(contents.len() + 1); let mut prev = None; - for (start, tok, end) in lexer::lex_located(contents, Mode::Module, constant.location).flatten() - { + for (tok, range) in lexer::lex_located(contents, Mode::Module, constant.start()).flatten() { if matches!(tok, Tok::String { .. }) { if let Some(prev) = prev { - replacement.push_str(locator.slice(Range::new(prev, start))); + replacement.push_str(locator.slice(TextRange::new(prev, range.start()))); } - let string = locator.slice(Range::new(start, end)); + let string = locator.slice(range); replacement.push_str(&format!( "b{}", &string.trim_start_matches('u').trim_start_matches('U') )); } else { if let Some(prev) = prev { - replacement.push_str(locator.slice(Range::new(prev, end))); + replacement.push_str(locator.slice(TextRange::new(prev, range.end()))); } } - prev = Some(end); + prev = Some(range.end()); } - Edit::replacement(replacement, expr.location, expr.end_location.unwrap()) + Edit::range_replacement(replacement, expr.range()) } /// UP012 @@ -157,7 +156,7 @@ pub fn unnecessary_encode_utf8( UnnecessaryEncodeUTF8 { reason: Reason::BytesLiteral, }, - Range::from(expr), + expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { diagnostic.set_fix(replace_with_bytes_literal( @@ -174,15 +173,14 @@ pub fn unnecessary_encode_utf8( UnnecessaryEncodeUTF8 { reason: Reason::DefaultArgument, }, - Range::from(expr), + expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.location, - kwarg.location, - kwarg.end_location.unwrap(), + func.start(), + kwarg.range(), args, kwargs, false, @@ -196,15 +194,14 @@ pub fn unnecessary_encode_utf8( UnnecessaryEncodeUTF8 { reason: Reason::DefaultArgument, }, - Range::from(expr), + expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.location, - arg.location, - arg.end_location.unwrap(), + func.start(), + arg.range(), args, kwargs, false, @@ -225,15 +222,14 @@ pub fn unnecessary_encode_utf8( UnnecessaryEncodeUTF8 { reason: Reason::DefaultArgument, }, - Range::from(expr), + expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.location, - kwarg.location, - kwarg.end_location.unwrap(), + func.start(), + kwarg.range(), args, kwargs, false, @@ -247,15 +243,14 @@ pub fn unnecessary_encode_utf8( UnnecessaryEncodeUTF8 { reason: Reason::DefaultArgument, }, - Range::from(expr), + expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.location, - arg.location, - arg.end_location.unwrap(), + func.start(), + arg.range(), args, kwargs, false, diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs index 8aa16847b6..df31d7695a 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{Alias, AliasData, Located, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::autofix; use crate::checkers::ast::Checker; @@ -81,7 +80,7 @@ pub fn unnecessary_future_import(checker: &mut Checker, stmt: &Stmt, names: &[Lo .sorted() .collect(), }, - Range::from(stmt), + stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs index fcd3ffa6a4..da53487ff0 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -94,19 +93,15 @@ pub fn unpacked_list_comprehension(checker: &mut Checker, targets: &[Expr], valu return; } - let mut diagnostic = Diagnostic::new(UnpackedListComprehension, Range::from(value)); + let mut diagnostic = Diagnostic::new(UnpackedListComprehension, value.range()); if checker.patch(diagnostic.kind.rule()) { - let existing = checker.locator.slice(value); + let existing = checker.locator.slice(value.range()); let mut content = String::with_capacity(existing.len()); content.push('('); content.push_str(&existing[1..existing.len() - 1]); content.push(')'); - diagnostic.set_fix(Edit::replacement( - content, - value.location, - value.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(content, value.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 55a07268cf..2bea49df43 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_ast::typing::AnnotationKind; use crate::checkers::ast::Checker; @@ -51,16 +50,12 @@ pub fn use_pep585_annotation(checker: &mut Checker, expr: &Expr) { name: binding.to_string(), fixable, }, - Range::from(expr), + expr.range(), ); if fixable && checker.patch(diagnostic.kind.rule()) { let binding = binding.to_lowercase(); if checker.ctx.is_builtin(&binding) { - diagnostic.set_fix(Edit::replacement( - binding, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(binding, expr.range())); } } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index cecf1c387f..2c51cb2d32 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -1,9 +1,9 @@ -use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Operator}; +use ruff_text_size::TextSize; +use rustpython_parser::ast::{Constant, Expr, ExprKind, Operator}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use ruff_python_ast::typing::AnnotationKind; use crate::checkers::ast::Checker; @@ -29,14 +29,14 @@ impl Violation for NonPEP604Annotation { fn optional(expr: &Expr) -> Expr { Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::BinOp { left: Box::new(expr.clone()), op: Operator::BitOr, right: Box::new(Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::Constant { value: Constant::None, kind: None, @@ -51,8 +51,8 @@ fn union(elts: &[Expr]) -> Expr { elts[0].clone() } else { Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::BinOp { left: Box::new(union(&elts[..elts.len() - 1])), op: Operator::BitOr, @@ -109,38 +109,33 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s match typing_member { TypingMember::Optional => { - let mut diagnostic = - Diagnostic::new(NonPEP604Annotation { fixable }, Range::from(expr)); + let mut diagnostic = Diagnostic::new(NonPEP604Annotation { fixable }, expr.range()); if fixable && checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&optional(slice), checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } checker.diagnostics.push(diagnostic); } TypingMember::Union => { - let mut diagnostic = - Diagnostic::new(NonPEP604Annotation { fixable }, Range::from(expr)); + let mut diagnostic = Diagnostic::new(NonPEP604Annotation { fixable }, expr.range()); if fixable && checker.patch(diagnostic.kind.rule()) { match &slice.node { ExprKind::Slice { .. } => { // Invalid type annotation. } ExprKind::Tuple { elts, .. } => { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&union(elts), checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } _ => { // Single argument. - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(slice, checker.stylist), - expr.location, - expr.end_location.unwrap(), + expr.range(), )); } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index 0eb4a40990..3745d05ae2 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -1,11 +1,11 @@ +use ruff_text_size::TextSize; use std::fmt; -use rustpython_parser::ast::{Expr, ExprKind, Location, Operator}; +use rustpython_parser::ast::{Expr, ExprKind, Operator}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::unparse_expr; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -56,8 +56,8 @@ fn union(elts: &[Expr]) -> Expr { elts[0].clone() } else { Expr::new( - Location::default(), - Location::default(), + TextSize::default(), + TextSize::default(), ExprKind::BinOp { left: Box::new(union(&elts[..elts.len() - 1])), op: Operator::BitOr, @@ -91,13 +91,11 @@ pub fn use_pep604_isinstance(checker: &mut Checker, expr: &Expr, func: &Expr, ar return; } - let mut diagnostic = - Diagnostic::new(NonPEP604Isinstance { kind }, Range::from(expr)); + let mut diagnostic = Diagnostic::new(NonPEP604Isinstance { kind }, expr.range()); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( unparse_expr(&union(elts), checker.stylist), - types.location, - types.end_location.unwrap(), + types.range(), )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs index 16c94bc650..e1b2839828 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -1,9 +1,9 @@ use log::error; +use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, ExprKind, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::autofix::actions; use crate::checkers::ast::Checker; @@ -23,7 +23,7 @@ impl AlwaysAutofixableViolation for UselessMetaclassType { } } -fn rule(targets: &[Expr], value: &Expr, location: Range) -> Option { +fn rule(targets: &[Expr], value: &Expr, location: TextRange) -> Option { if targets.len() != 1 { return None; } @@ -45,7 +45,7 @@ fn rule(targets: &[Expr], value: &Expr, location: Range) -> Option { /// UP001 pub fn useless_metaclass_type(checker: &mut Checker, stmt: &Stmt, value: &Expr, targets: &[Expr]) { let Some(mut diagnostic) = - rule(targets, value, Range::from(stmt)) else { + rule(targets, value, stmt.range()) else { return; }; if checker.patch(diagnostic.kind.rule()) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs index 014d4d6eb4..1390e58833 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_semantic::binding::{Binding, BindingKind, Bindings}; use ruff_python_semantic::scope::Scope; @@ -48,7 +47,7 @@ fn rule(name: &str, bases: &[Expr], scope: &Scope, bindings: &Bindings) -> Optio UselessObjectInheritance { name: name.to_string(), }, - Range::from(expr), + expr.range(), )); } @@ -65,14 +64,12 @@ pub fn useless_object_inheritance( ) { if let Some(mut diagnostic) = rule(name, bases, checker.ctx.scope(), &checker.ctx.bindings) { if checker.patch(diagnostic.kind.rule()) { - let location = diagnostic.location; - let end_location = diagnostic.end_location; + let expr_range = diagnostic.range(); diagnostic.try_set_fix(|| { remove_argument( checker.locator, - stmt.location, - location, - end_location, + stmt.start(), + expr_range, bases, keywords, true, diff --git a/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs index ba07ddf344..263d4441a4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, ExprContext, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::{Range, RefEquality}; +use ruff_python_ast::types::RefEquality; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -172,15 +172,11 @@ pub fn yield_in_for_loop(checker: &mut Checker, stmt: &Stmt) { continue; } - let mut diagnostic = Diagnostic::new(YieldInForLoop, Range::from(item.stmt)); + let mut diagnostic = Diagnostic::new(YieldInForLoop, item.stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let contents = checker.locator.slice(item.iter); + let contents = checker.locator.slice(item.iter.range()); let contents = format!("yield from {contents}"); - diagnostic.set_fix(Edit::replacement( - contents, - item.stmt.location, - item.stmt.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(contents, item.stmt.range())); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs index 3c661302c0..707fc21db3 100644 --- a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -1,12 +1,11 @@ use once_cell::sync::Lazy; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_hash::FxHashMap; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, DiagnosticKind, Edit}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; -use crate::message::Location; use crate::registry::AsRule; use crate::rules::ruff::rules::Context; use crate::settings::{flags, Settings}; @@ -1685,30 +1684,25 @@ static CONFUSABLES: Lazy> = Lazy::new(|| { pub fn ambiguous_unicode_character( locator: &Locator, - start: Location, - end: Location, + range: TextRange, context: Context, settings: &Settings, autofix: flags::Autofix, ) -> Vec { let mut diagnostics = vec![]; - let text = locator.slice(Range::new(start, end)); + let text = locator.slice(range); - let mut col_offset = 0; - let mut row_offset = 0; - for current_char in text.chars() { + for (relative_offset, current_char) in text.char_indices() { if !current_char.is_ascii() { // Search for confusing characters. if let Some(representant) = CONFUSABLES.get(&(current_char as u32)).copied() { if !settings.allowed_confusables.contains(¤t_char) { - let col = if row_offset == 0 { - start.column() + col_offset - } else { - col_offset - }; - let location = Location::new(start.row() + row_offset, col); - let end_location = Location::new(location.row(), location.column() + 1); + let char_range = TextRange::at( + TextSize::try_from(relative_offset).unwrap() + range.start(), + current_char.text_len(), + ); + let mut diagnostic = Diagnostic::new::( match context { Context::String => AmbiguousUnicodeCharacterString { @@ -1727,14 +1721,13 @@ pub fn ambiguous_unicode_character( } .into(), }, - Range::new(location, end_location), + char_range, ); if settings.rules.enabled(diagnostic.kind.rule()) { if autofix.into() && settings.rules.should_fix(diagnostic.kind.rule()) { - diagnostic.set_fix(Edit::replacement( + diagnostic.set_fix(Edit::range_replacement( (representant as char).to_string(), - location, - end_location, + char_range, )); } diagnostics.push(diagnostic); @@ -1742,14 +1735,6 @@ pub fn ambiguous_unicode_character( } } } - - // Track the offset from the start position as we iterate over the body. - if current_char == '\n' { - col_offset = 0; - row_offset += 1; - } else { - col_offset += 1; - } } diagnostics diff --git a/crates/ruff/src/rules/ruff/rules/asyncio_dangling_task.rs b/crates/ruff/src/rules/ruff/rules/asyncio_dangling_task.rs index 07deed868e..e82bdcc682 100644 --- a/crates/ruff/src/rules/ruff/rules/asyncio_dangling_task.rs +++ b/crates/ruff/src/rules/ruff/rules/asyncio_dangling_task.rs @@ -5,7 +5,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::CallPath; -use ruff_python_ast::types::Range; /// ## What it does /// Checks for `asyncio.create_task` and `asyncio.ensure_future` calls @@ -89,13 +88,13 @@ where AsyncioDanglingTask { method: Method::CreateTask, }, - Range::from(expr), + expr.range(), )), Some(["asyncio", "ensure_future"]) => Some(Diagnostic::new( AsyncioDanglingTask { method: Method::EnsureFuture, }, - Range::from(expr), + expr.range(), )), _ => None, } diff --git a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs index 85f97a159e..82b2fe8ca6 100644 --- a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Expr, ExprContext, ExprKind, Operator}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{create_expr, has_comments, unparse_expr}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -59,12 +58,9 @@ enum Kind { /// This suggestion could be unsafe if the non-literal expression in the /// expression has overridden the `__add__` (or `__radd__`) magic methods. pub fn collection_literal_concatenation(checker: &mut Checker, expr: &Expr) { - let ExprKind::BinOp { op, left, right } = &expr.node else { + let ExprKind::BinOp { left, op: Operator::Add, right } = &expr.node else { return; }; - if !matches!(op, Operator::Add) { - return; - } // Figure out which way the splat is, and what the kind of the collection is. let (kind, splat_element, other_elements, splat_at_left, ctx) = match (&left.node, &right.node) @@ -108,15 +104,11 @@ pub fn collection_literal_concatenation(checker: &mut Checker, expr: &Expr) { expr: contents.clone(), fixable, }, - Range::from(expr), + expr.range(), ); if checker.patch(diagnostic.kind.rule()) { if fixable { - diagnostic.set_fix(Edit::replacement( - contents, - expr.location, - expr.end_location.unwrap(), - )); + diagnostic.set_fix(Edit::range_replacement(contents, expr.range())); } } checker.diagnostics.push(diagnostic); 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 8d1d9839ef..68c2c1bfec 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 @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::{call_path::compose_call_path, helpers::map_callable, types::Range}; +use ruff_python_ast::{call_path::compose_call_path, helpers::map_callable}; use ruff_python_semantic::analyze::typing::is_immutable_annotation; use ruff_python_semantic::context::Context; @@ -177,7 +177,7 @@ pub fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) FunctionCallInDataclassDefaultArgument { name: compose_call_path(func), }, - Range::from(expr), + expr.range(), )); } } @@ -200,14 +200,14 @@ pub fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { { checker .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, Range::from(value))); + .push(Diagnostic::new(MutableDataclassDefault, value.range())); } } StmtKind::Assign { value, .. } => { 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/src/rules/ruff/rules/pairwise_over_zipped.rs b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs index 8eb11d5074..0e05c76424 100644 --- a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs +++ b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs @@ -5,7 +5,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -use crate::Range; #[violation] pub struct PairwiseOverZipped; @@ -137,5 +136,5 @@ pub fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[Expr]) { checker .diagnostics - .push(Diagnostic::new(PairwiseOverZipped, Range::from(func))); + .push(Diagnostic::new(PairwiseOverZipped, func.range())); } diff --git a/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs b/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs index abd471a491..3557348e35 100644 --- a/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs +++ b/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor::Visitor; use crate::checkers::ast::Checker; @@ -67,7 +66,7 @@ pub fn error_instead_of_exception(checker: &mut Checker, handlers: &[Excepthandl if attr == "error" { checker .diagnostics - .push(Diagnostic::new(ErrorInsteadOfException, Range::from(expr))); + .push(Diagnostic::new(ErrorInsteadOfException, expr.range())); } } } diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs index 008c928109..600e4e1a82 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -50,38 +49,40 @@ impl Violation for RaiseVanillaArgs { } } -fn collect_strings_impl<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) { +fn any_string(expr: &Expr, predicate: F) -> bool +where + F: (Fn(&str) -> bool) + Copy, +{ match &expr.node { ExprKind::JoinedStr { values } => { for value in values { - collect_strings_impl(value, parts); + if any_string(value, predicate) { + return true; + } } } ExprKind::Constant { value: Constant::Str(val), .. - } => parts.push(val), + } => { + if predicate(val.as_str()) { + return true; + } + } _ => {} } -} -fn collect_strings(expr: &Expr) -> Vec<&str> { - let mut parts = Vec::new(); - collect_strings_impl(expr, &mut parts); - parts + false } /// TRY003 pub fn raise_vanilla_args(checker: &mut Checker, expr: &Expr) { if let ExprKind::Call { args, .. } = &expr.node { if let Some(arg) = args.first() { - if collect_strings(arg) - .iter() - .any(|part| part.chars().any(char::is_whitespace)) - { + if any_string(arg, |part| part.chars().any(char::is_whitespace)) { checker .diagnostics - .push(Diagnostic::new(RaiseVanillaArgs, Range::from(expr))); + .push(Diagnostic::new(RaiseVanillaArgs, expr.range())); } } } diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs index 7805655e94..dd315bda2e 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -74,6 +73,6 @@ pub fn raise_vanilla_class(checker: &mut Checker, expr: &Expr) { { checker .diagnostics - .push(Diagnostic::new(RaiseVanillaClass, Range::from(expr))); + .push(Diagnostic::new(RaiseVanillaClass, expr.range())); } } diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs index 15931a68a0..88ad74dbfa 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor::{self, Visitor}; use crate::checkers::ast::Checker; @@ -85,6 +84,6 @@ pub fn raise_within_try(checker: &mut Checker, body: &[Stmt]) { for stmt in raises { checker .diagnostics - .push(Diagnostic::new(RaiseWithinTry, Range::from(stmt))); + .push(Diagnostic::new(RaiseWithinTry, stmt.range())); } } diff --git a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs index b3e3643879..db322c19e1 100644 --- a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs +++ b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs @@ -3,7 +3,6 @@ use rustpython_parser::ast::{Excepthandler, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::contains_effect; -use ruff_python_ast::types::Range; use crate::checkers::ast::Checker; @@ -73,7 +72,7 @@ pub fn try_consider_else( } checker .diagnostics - .push(Diagnostic::new(TryConsiderElse, Range::from(stmt))); + .push(Diagnostic::new(TryConsiderElse, stmt.range())); } } } diff --git a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs index c9e2213e44..a1fc850401 100644 --- a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs +++ b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -159,10 +158,9 @@ fn check_raise_type(checker: &mut Checker, exc: &Expr) -> bool { fn check_raise(checker: &mut Checker, exc: &Expr, item: &Stmt) { if check_raise_type(checker, exc) { - checker.diagnostics.push(Diagnostic::new( - TypeCheckWithoutTypeError, - Range::from(item), - )); + checker + .diagnostics + .push(Diagnostic::new(TypeCheckWithoutTypeError, item.range())); } } diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs index 8d4edb22ec..9ca1f8cb53 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -98,7 +97,7 @@ pub fn verbose_log_message(checker: &mut Checker, handlers: &[Excepthandler]) { if id == target { checker .diagnostics - .push(Diagnostic::new(VerboseLogMessage, Range::from(expr))); + .push(Diagnostic::new(VerboseLogMessage, expr.range())); } } } diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs index 4833feceb2..426e359ae8 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs @@ -2,7 +2,6 @@ use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, S use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -96,7 +95,7 @@ pub fn verbose_raise(checker: &mut Checker, handlers: &[Excepthandler]) { if id == exception_name { checker .diagnostics - .push(Diagnostic::new(VerboseRaise, Range::from(exc))); + .push(Diagnostic::new(VerboseRaise, exc.range())); } } } diff --git a/crates/ruff/src/test.rs b/crates/ruff/src/test.rs index 7da48e00ae..f600f03c14 100644 --- a/crates/ruff/src/test.rs +++ b/crates/ruff/src/test.rs @@ -29,9 +29,13 @@ pub fn test_path(path: impl AsRef, settings: &Settings) -> Result = ruff_rustpython::tokenize(&contents); let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); - let indexer: Indexer = tokens.as_slice().into(); - let directives = - directives::extract_directives(&tokens, directives::Flags::from_settings(settings)); + let indexer = Indexer::from_tokens(&tokens, &locator); + let directives = directives::extract_directives( + &tokens, + directives::Flags::from_settings(settings), + &locator, + &indexer, + ); let LinterResult { data: (diagnostics, _imports), .. @@ -39,7 +43,6 @@ pub fn test_path(path: impl AsRef, settings: &Settings) -> Result, settings: &Settings) -> Result = ruff_rustpython::tokenize(&contents); let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); - let indexer: Indexer = tokens.as_slice().into(); - let directives = - directives::extract_directives(&tokens, directives::Flags::from_settings(settings)); + let indexer = Indexer::from_tokens(&tokens, &locator); + let directives = directives::extract_directives( + &tokens, + directives::Flags::from_settings(settings), + &locator, + &indexer, + ); let LinterResult { data: (diagnostics, _imports), .. } = check_path( &path, None, - &contents, tokens, &locator, &stylist, @@ -88,9 +94,37 @@ pub fn test_path(path: impl AsRef, settings: &Settings) -> Result = diagnostics + .into_iter() + .map(|diagnostic| { + // Not strictly necessary but adds some coverage for this code path + let noqa = directives.noqa_line_for.resolve(diagnostic.start()); + + Message::from_diagnostic(diagnostic, source_code.clone(), noqa) + }) + .collect(); + + let mut output: Vec = Vec::new(); + TextEmitter::default() + .with_show_fix(true) + .with_show_source(true) + .emit( + &mut output, + &messages, + &EmitterContext::new(&FxHashMap::default()), + ) + .unwrap(); + + let output_str = String::from_utf8(output).unwrap(); panic!( "Failed to converge after {max_iterations} iterations. This likely \ - indicates a bug in the implementation of the fix." + indicates a bug in the implementation of the fix. Last diagnostics:\n{output_str}" ); } } else { @@ -99,13 +133,20 @@ pub fn test_path(path: impl AsRef, settings: &Settings) -> Result String { TextEmitter::default() .with_show_fix_status(true) .with_show_fix(true) + .with_show_source(true) .emit( &mut output, messages, diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 7df093bd53..0edb930eaa 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -26,6 +26,7 @@ ruff = { path = "../ruff", features = ["clap"] } ruff_cache = { path = "../ruff_cache" } ruff_diagnostics = { path = "../ruff_diagnostics" } ruff_python_ast = { path = "../ruff_python_ast" } +ruff_text_size = { workspace = true } annotate-snippets = { version = "0.9.1", features = ["color"] } anyhow = { workspace = true } diff --git a/crates/ruff_cli/src/cache.rs b/crates/ruff_cli/src/cache.rs index ac69581ee9..2d7bd0dd36 100644 --- a/crates/ruff_cli/src/cache.rs +++ b/crates/ruff_cli/src/cache.rs @@ -8,12 +8,13 @@ use anyhow::Result; use filetime::FileTime; use log::error; use path_absolutize::Absolutize; -use ruff::message::{Location, Message}; +use ruff::message::Message; use ruff::settings::{flags, AllSettings, Settings}; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_diagnostics::{DiagnosticKind, Fix}; use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::SourceFileBuilder; +use ruff_text_size::{TextRange, TextSize}; use serde::ser::{SerializeSeq, SerializeStruct}; use serde::{Deserialize, Serialize, Serializer}; #[cfg(unix)] @@ -22,8 +23,8 @@ use std::os::unix::fs::PermissionsExt; const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Vec storing all source files. The tuple is (filename, source code). -type Files<'a> = Vec<(&'a str, Option<&'a str>)>; -type FilesBuf = Vec<(String, Option)>; +type Files<'a> = Vec<(&'a str, &'a str)>; +type FilesBuf = Vec<(String, String)>; struct CheckResultRef<'a> { imports: &'a ImportMap, @@ -100,19 +101,17 @@ impl Serialize for SerializeMessage<'_> { { let Message { kind, - location, - end_location, + range, fix, // Serialized manually for all files file: _, - noqa_row, + noqa_offset: noqa_row, } = self.message; - let mut s = serializer.serialize_struct("Message", 6)?; + let mut s = serializer.serialize_struct("Message", 5)?; s.serialize_field("kind", &kind)?; - s.serialize_field("location", &location)?; - s.serialize_field("end_location", &end_location)?; + s.serialize_field("range", &range)?; s.serialize_field("fix", &fix)?; s.serialize_field("file_id", &self.file_id)?; s.serialize_field("noqa_row", &noqa_row)?; @@ -124,11 +123,10 @@ impl Serialize for SerializeMessage<'_> { #[derive(Deserialize)] struct MessageHeader { kind: DiagnosticKind, - location: Location, - end_location: Location, + range: TextRange, fix: Fix, file_id: usize, - noqa_row: usize, + noqa_row: TextSize, } #[derive(Deserialize)] @@ -223,15 +221,7 @@ pub fn get( let source_files: Vec<_> = sources .into_iter() - .map(|(filename, text)| { - let mut builder = SourceFileBuilder::from_string(filename); - - if let Some(text) = text { - builder.set_source_text_string(text); - } - - builder.finish() - }) + .map(|(filename, text)| SourceFileBuilder::new(filename, text).finish()) .collect(); for header in headers { @@ -242,11 +232,10 @@ pub fn get( messages.push(Message { kind: header.kind, - location: header.location, - end_location: header.end_location, + range: header.range, fix: header.fix, file: source_file.clone(), - noqa_row: header.noqa_row, + noqa_offset: header.noqa_row, }); } diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index 171a64d7c4..64da002afc 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -8,12 +8,13 @@ use ignore::Error; use log::{debug, error, warn}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; +use ruff_text_size::{TextRange, TextSize}; -use ruff::message::{Location, Message}; +use ruff::message::Message; use ruff::registry::Rule; use ruff::resolver::PyprojectDiscovery; use ruff::settings::{flags, AllSettings}; -use ruff::{fs, packaging, resolver, warn_user_once, IOError, Range}; +use ruff::{fs, packaging, resolver, warn_user_once, IOError}; use ruff_diagnostics::Diagnostic; use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::SourceFileBuilder; @@ -117,16 +118,14 @@ pub fn run( ); let settings = resolver.resolve(path, pyproject_strategy); if settings.rules.enabled(Rule::IOError) { - let file = SourceFileBuilder::new(&path.to_string_lossy()).finish(); + let file = + SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(); Diagnostics::new( vec![Message::from_diagnostic( - Diagnostic::new( - IOError { message }, - Range::new(Location::default(), Location::default()), - ), + Diagnostic::new(IOError { message }, TextRange::default()), file, - 1, + TextSize::default(), )], ImportMap::default(), ) diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index a63792d19f..2a7f2a26e9 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -9,16 +9,18 @@ use std::path::Path; use anyhow::{anyhow, Result}; use colored::Colorize; use log::{debug, error}; +use ruff_text_size::TextSize; use rustc_hash::FxHashMap; use similar::TextDiff; use ruff::fs; use ruff::jupyter::{is_jupyter_notebook, JupyterIndex, JupyterNotebook}; use ruff::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult}; +use ruff::logging::DisplayParseError; use ruff::message::Message; use ruff::settings::{flags, AllSettings, Settings}; use ruff_python_ast::imports::ImportMap; -use ruff_python_ast::source_code::SourceFileBuilder; +use ruff_python_ast::source_code::{LineIndex, SourceCode, SourceFileBuilder}; use crate::cache; @@ -86,8 +88,8 @@ fn load_jupyter_notebook(path: &Path) -> Result<(String, JupyterIndex), Box Result { return Ok(ExitStatus::Success); } + let top_level_settings = pyproject_strategy.top_level_settings(); + // Extract options that are included in `Settings`, but only apply at the top // level. let CliSettings { @@ -122,7 +124,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { show_fixes, update_check, .. - } = pyproject_strategy.top_level_settings().cli.clone(); + } = top_level_settings.cli.clone(); // Autofix rules are as follows: // - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or @@ -153,6 +155,10 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { printer_flags |= PrinterFlags::SHOW_FIXES; } + if top_level_settings.lib.show_source { + printer_flags |= PrinterFlags::SHOW_SOURCE; + } + #[cfg(debug_assertions)] if cache { // `--no-cache` doesn't respect code changes, and so is often confusing during diff --git a/crates/ruff_cli/src/printer.rs b/crates/ruff_cli/src/printer.rs index 5a74aa9d4f..d82d6b181f 100644 --- a/crates/ruff_cli/src/printer.rs +++ b/crates/ruff_cli/src/printer.rs @@ -30,6 +30,7 @@ bitflags! { pub(crate) struct Flags: u8 { const SHOW_VIOLATIONS = 0b0000_0001; const SHOW_FIXES = 0b0000_0010; + const SHOW_SOURCE = 0b000_0100; } } @@ -180,6 +181,7 @@ impl Printer { SerializationFormat::Text => { TextEmitter::default() .with_show_fix_status(show_fix_status(self.autofix_level)) + .with_show_source(self.flags.contains(Flags::SHOW_SOURCE)) .emit(writer, &diagnostics.messages, &context)?; if self.flags.contains(Flags::SHOW_FIXES) { @@ -194,6 +196,7 @@ impl Printer { } SerializationFormat::Grouped => { GroupedEmitter::default() + .with_show_source(self.flags.contains(Flags::SHOW_SOURCE)) .with_show_fix_status(show_fix_status(self.autofix_level)) .emit(writer, &diagnostics.messages, &context)?; @@ -346,6 +349,7 @@ impl Printer { let context = EmitterContext::new(&diagnostics.jupyter_index); TextEmitter::default() .with_show_fix_status(show_fix_status(self.autofix_level)) + .with_show_source(self.flags.contains(Flags::SHOW_SOURCE)) .emit(&mut stdout, &diagnostics.messages, &context)?; } stdout.flush()?; diff --git a/crates/ruff_dev/src/print_tokens.rs b/crates/ruff_dev/src/print_tokens.rs index 5e771e746f..176ef4ec95 100644 --- a/crates/ruff_dev/src/print_tokens.rs +++ b/crates/ruff_dev/src/print_tokens.rs @@ -16,8 +16,12 @@ pub struct Args { pub fn main(args: &Args) -> Result<()> { let contents = fs::read_to_string(&args.file)?; - for (start, tok, end) in lexer::lex(&contents, Mode::Module).flatten() { - println!("{start:#?} {tok:#?} {end:#?}"); + for (tok, range) in lexer::lex(&contents, Mode::Module).flatten() { + println!( + "{start:#?} {tok:#?} {end:#?}", + start = range.start(), + end = range.end() + ); } Ok(()) } diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 5565d595a4..6aaad1e20a 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -10,7 +10,5 @@ rust-version = { workspace = true } [dependencies] anyhow = { workspace = true } log = { workspace = true } -ruff_python_ast = { path = "../ruff_python_ast" } - -rustpython-parser = { workspace = true } +ruff_text_size = { workspace = true } serde = { workspace = true, optional = true, features = [] } diff --git a/crates/ruff_diagnostics/src/diagnostic.rs b/crates/ruff_diagnostics/src/diagnostic.rs index 86d45440d4..2c48005a61 100644 --- a/crates/ruff_diagnostics/src/diagnostic.rs +++ b/crates/ruff_diagnostics/src/diagnostic.rs @@ -1,11 +1,10 @@ use anyhow::Result; use log::error; -use rustpython_parser::ast::Location; +use ruff_text_size::{TextRange, TextSize}; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use ruff_python_ast::types::Range; - use crate::Fix; #[derive(Debug, PartialEq, Eq)] @@ -24,18 +23,16 @@ pub struct DiagnosticKind { #[derive(Debug, PartialEq, Eq)] pub struct Diagnostic { pub kind: DiagnosticKind, - pub location: Location, - pub end_location: Location, + pub range: TextRange, pub fix: Fix, - pub parent: Option, + pub parent: Option, } impl Diagnostic { - pub fn new>(kind: T, range: Range) -> Self { + pub fn new>(kind: T, range: TextRange) -> Self { Self { kind: kind.into(), - location: range.location, - end_location: range.end_location, + range, fix: Fix::empty(), parent: None, } @@ -65,9 +62,21 @@ impl Diagnostic { } } + pub const fn range(&self) -> TextRange { + self.range + } + + pub const fn start(&self) -> TextSize { + self.range.start() + } + + pub const fn end(&self) -> TextSize { + self.range.end() + } + /// Set the location of the diagnostic's parent node. #[inline] - pub fn set_parent(&mut self, parent: Location) { + pub fn set_parent(&mut self, parent: TextSize) { self.parent = Some(parent); } } diff --git a/crates/ruff_diagnostics/src/edit.rs b/crates/ruff_diagnostics/src/edit.rs index ab2afa623d..733d5c049d 100644 --- a/crates/ruff_diagnostics/src/edit.rs +++ b/crates/ruff_diagnostics/src/edit.rs @@ -1,49 +1,58 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::{TextRange, TextSize}; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; /// A text edit to be applied to a source file. Inserts, deletes, or replaces /// content at a given location. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Edit { /// The start location of the edit. - location: Location, - /// The end location of the edit. - end_location: Location, + range: TextRange, /// The replacement content to insert between the start and end locations. content: Option>, } impl Edit { /// Creates an edit that deletes the content in the `start` to `end` range. - pub const fn deletion(start: Location, end: Location) -> Self { + #[inline] + pub const fn deletion(start: TextSize, end: TextSize) -> Self { + Self::range_deletion(TextRange::new(start, end)) + } + + /// Creates an edit that deletes the content in `range`. + pub const fn range_deletion(range: TextRange) -> Self { Self { content: None, - location: start, - end_location: end, + range, } } /// Creates an edit that replaces the content in the `start` to `end` range with `content`. - pub fn replacement(content: String, start: Location, end: Location) -> Self { + #[inline] + pub fn replacement(content: String, start: TextSize, end: TextSize) -> Self { + Self::range_replacement(content, TextRange::new(start, end)) + } + + /// Creates an edit that replaces the content in `range` with `content`. + pub fn range_replacement(content: String, range: TextRange) -> Self { debug_assert!(!content.is_empty(), "Prefer `Fix::deletion`"); Self { content: Some(Box::from(content)), - location: start, - end_location: end, + range, } } - /// Creates an edit that inserts `content` at the [`Location`] `at`. - pub fn insertion(content: String, at: Location) -> Self { + /// Creates an edit that inserts `content` at the [`TextSize`] `at`. + pub fn insertion(content: String, at: TextSize) -> Self { debug_assert!(!content.is_empty(), "Insert content is empty"); Self { content: Some(Box::from(content)), - location: at, - end_location: at, + range: TextRange::new(at, at), } } @@ -53,19 +62,23 @@ impl Edit { } /// Returns the start location of the edit in the source document. - pub const fn location(&self) -> Location { - self.location + pub const fn start(&self) -> TextSize { + self.range.start() + } + + pub const fn range(&self) -> TextRange { + self.range } /// Returns the edit's end location in the source document. - pub const fn end_location(&self) -> Location { - self.end_location + pub const fn end(&self) -> TextSize { + self.range.end() } fn kind(&self) -> EditOperationKind { if self.content.is_none() { EditOperationKind::Deletion - } else if self.location == self.end_location { + } else if self.range.is_empty() { EditOperationKind::Insertion } else { EditOperationKind::Replacement @@ -91,6 +104,21 @@ impl Edit { } } +impl Ord for Edit { + fn cmp(&self, other: &Self) -> Ordering { + self.start() + .cmp(&other.start()) + .then_with(|| self.end().cmp(&other.end())) + .then_with(|| self.content.cmp(&other.content)) + } +} + +impl PartialOrd for Edit { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[derive(Copy, Clone, Eq, PartialEq, Debug)] enum EditOperationKind { /// Edit that inserts new content into the source document. diff --git a/crates/ruff_diagnostics/src/fix.rs b/crates/ruff_diagnostics/src/fix.rs index 60edebef9e..577a86674f 100644 --- a/crates/ruff_diagnostics/src/fix.rs +++ b/crates/ruff_diagnostics/src/fix.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::Location; +use ruff_text_size::TextSize; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -27,9 +27,9 @@ impl Fix { self.edits.is_empty() } - /// Return the [`Location`] of the first [`Edit`] in the [`Fix`]. - pub fn min_location(&self) -> Option { - self.edits.iter().map(Edit::location).min() + /// Return the [`TextSize`] of the first [`Edit`] in the [`Fix`]. + pub fn min_start(&self) -> Option { + self.edits.iter().map(Edit::start).min() } /// Return a slice of the [`Edit`] elements in the [`Fix`]. diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index e4e6cf243e..a9c82b91a8 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } rust-version = { workspace = true } [dependencies] -ruff_text_size = { path = "../ruff_text_size" } +ruff_text_size = { workspace = true } drop_bomb = { version = "0.1.5" } rustc-hash = { workspace = true } @@ -19,4 +19,5 @@ unicode-width = { version = "0.1.10" } insta = { workspace = true } [features] -serde = ["dep:serde", "schemars"] +serde = ["dep:serde", "ruff_text_size/serde"] +schemars = ["dep:schemars", "ruff_text_size/schemars"] diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index cf9ec26767..d20db48eed 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -9,7 +9,7 @@ rust-version = { workspace = true } [dependencies] ruff_rustpython = { path = "../ruff_rustpython" } -ruff_text_size = { path = "../ruff_text_size" } +ruff_text_size = { workspace = true, features = ["serde"] } anyhow = { workspace = true } bitflags = { workspace = true } @@ -23,5 +23,5 @@ regex = { workspace = true } rustc-hash = { workspace = true } rustpython-common = { workspace = true } rustpython-parser = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, optional = true } smallvec = { workspace = true } diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 39d73b1798..213ae1e770 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -6,28 +6,29 @@ use log::error; use num_traits::Zero; use once_cell::sync::Lazy; use regex::Regex; +use ruff_text_size::{TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; use rustpython_parser::ast::{ Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, - KeywordData, Located, Location, MatchCase, Pattern, PatternKind, Stmt, StmtKind, + KeywordData, Located, MatchCase, Pattern, PatternKind, Stmt, StmtKind, }; use rustpython_parser::{lexer, Mode, Tok}; use smallvec::SmallVec; use crate::call_path::CallPath; +use crate::newlines::UniversalNewlineIterator; use crate::source_code::{Generator, Indexer, Locator, Stylist}; -use crate::types::Range; use crate::visitor; use crate::visitor::Visitor; /// Create an `Expr` with default location from an `ExprKind`. pub fn create_expr(node: ExprKind) -> Expr { - Expr::new(Location::default(), Location::default(), node) + Expr::with_range(node, TextRange::default()) } /// Create a `Stmt` with a default location from a `StmtKind`. pub fn create_stmt(node: StmtKind) -> Stmt { - Stmt::new(Location::default(), Location::default(), node) + Stmt::with_range(node, TextRange::default()) } /// Generate source code from an [`Expr`]. @@ -617,24 +618,27 @@ pub fn map_callable(decorator: &Expr) -> &Expr { /// Returns `true` if a statement or expression includes at least one comment. pub fn has_comments(located: &Located, locator: &Locator) -> bool { - let start = if match_leading_content(located, locator) { - located.location + let start = if has_leading_content(located, locator) { + located.start() } else { - Location::new(located.location.row(), 0) + locator.line_start(located.start()) }; - let end = if match_trailing_content(located, locator) { - located.end_location.unwrap() + let end = if has_trailing_content(located, locator) { + located.end() } else { - Location::new(located.end_location.unwrap().row() + 1, 0) + locator.line_end(located.end()) }; - has_comments_in(Range::new(start, end), locator) + + has_comments_in(TextRange::new(start, end), locator) } -/// Returns `true` if a [`Range`] includes at least one comment. -pub fn has_comments_in(range: Range, locator: &Locator) -> bool { - for tok in lexer::lex_located(locator.slice(range), Mode::Module, range.location) { +/// Returns `true` if a [`TextRange`] includes at least one comment. +pub fn has_comments_in(range: TextRange, locator: &Locator) -> bool { + let source = &locator.contents()[range]; + + for tok in lexer::lex_located(source, Mode::Module, range.start()) { match tok { - Ok((_, tok, _)) => { + Ok((tok, _)) => { if matches!(tok, Tok::Comment(..)) { return true; } @@ -836,7 +840,7 @@ where /// A [`Visitor`] that collects all `raise` statements in a function or method. #[derive(Default)] pub struct RaiseStatementVisitor<'a> { - pub raises: Vec<(Range, Option<&'a Expr>, Option<&'a Expr>)>, + pub raises: Vec<(TextRange, Option<&'a Expr>, Option<&'a Expr>)>, } impl<'a, 'b> Visitor<'b> for RaiseStatementVisitor<'b> @@ -847,7 +851,7 @@ where match &stmt.node { StmtKind::Raise { exc, cause } => { self.raises - .push((Range::from(stmt), exc.as_deref(), cause.as_deref())); + .push((stmt.range(), exc.as_deref(), cause.as_deref())); } StmtKind::ClassDef { .. } | StmtKind::FunctionDef { .. } @@ -907,45 +911,19 @@ pub fn extract_globals(body: &[Stmt]) -> FxHashMap<&str, &Stmt> { visitor.globals } -/// Convert a location within a file (relative to `base`) to an absolute -/// position. -pub fn to_absolute(relative: Location, base: Location) -> Location { - if relative.row() == 1 { - Location::new( - relative.row() + base.row() - 1, - relative.column() + base.column(), - ) - } else { - Location::new(relative.row() + base.row() - 1, relative.column()) - } -} - -pub fn to_relative(absolute: Location, base: Location) -> Location { - if absolute.row() == base.row() { - Location::new( - absolute.row() - base.row() + 1, - absolute.column() - base.column(), - ) - } else { - Location::new(absolute.row() - base.row() + 1, absolute.column()) - } -} - /// Return `true` if a [`Located`] has leading content. -pub fn match_leading_content(located: &Located, locator: &Locator) -> bool { - let range = Range::new(Location::new(located.location.row(), 0), located.location); - let prefix = locator.slice(range); - prefix.chars().any(|char| !char.is_whitespace()) +pub fn has_leading_content(located: &Located, locator: &Locator) -> bool { + let line_start = locator.line_start(located.start()); + let leading = &locator.contents()[TextRange::new(line_start, located.start())]; + leading.chars().any(|char| !char.is_whitespace()) } /// Return `true` if a [`Located`] has trailing content. -pub fn match_trailing_content(located: &Located, locator: &Locator) -> bool { - let range = Range::new( - located.end_location.unwrap(), - Location::new(located.end_location.unwrap().row() + 1, 0), - ); - let suffix = locator.slice(range); - for char in suffix.chars() { +pub fn has_trailing_content(located: &Located, locator: &Locator) -> bool { + let line_end = locator.line_end(located.end()); + let trailing = &locator.contents()[TextRange::new(located.end(), line_end)]; + + for char in trailing.chars() { if char == '#' { return false; } @@ -957,55 +935,66 @@ pub fn match_trailing_content(located: &Located, locator: &Locator) -> boo } /// If a [`Located`] has a trailing comment, return the index of the hash. -pub fn match_trailing_comment(located: &Located, locator: &Locator) -> Option { - let range = Range::new( - located.end_location.unwrap(), - Location::new(located.end_location.unwrap().row() + 1, 0), - ); - let suffix = locator.slice(range); - for (i, char) in suffix.chars().enumerate() { +pub fn trailing_comment_start_offset( + located: &Located, + locator: &Locator, +) -> Option { + let line_end = locator.line_end(located.end()); + + let trailing = &locator.contents()[TextRange::new(located.end(), line_end)]; + + for (i, char) in trailing.chars().enumerate() { if char == '#' { - return Some(i); + return TextSize::try_from(i).ok(); } if !char.is_whitespace() { return None; } } + None } -/// Return the number of trailing empty lines following a statement. -pub fn count_trailing_lines(stmt: &Stmt, locator: &Locator) -> usize { - let suffix = locator.after(Location::new(stmt.end_location.unwrap().row() + 1, 0)); - suffix - .lines() +/// Return the end offset at which the empty lines following a statement. +pub fn trailing_lines_end(stmt: &Stmt, locator: &Locator) -> TextSize { + let line_end = locator.full_line_end(stmt.end()); + let rest = &locator.contents()[usize::from(line_end)..]; + + UniversalNewlineIterator::with_offset(rest, line_end) .take_while(|line| line.trim().is_empty()) - .count() + .last() + .map_or(line_end, |l| l.full_end()) } -/// Return the range of the first parenthesis pair after a given [`Location`]. -pub fn match_parens(start: Location, locator: &Locator) -> Option { - let contents = locator.after(start); +/// Return the range of the first parenthesis pair after a given [`TextSize`]. +pub fn match_parens(start: TextSize, locator: &Locator) -> Option { + let contents = &locator.contents()[usize::from(start)..]; + let mut fix_start = None; let mut fix_end = None; let mut count: usize = 0; - for (start, tok, end) in lexer::lex_located(contents, Mode::Module, start).flatten() { - if matches!(tok, Tok::Lpar) { - if count == 0 { - fix_start = Some(start); + + for (tok, range) in lexer::lex_located(contents, Mode::Module, start).flatten() { + match tok { + Tok::Lpar => { + if count == 0 { + fix_start = Some(range.start()); + } + count += 1; } - count += 1; - } - if matches!(tok, Tok::Rpar) { - count -= 1; - if count == 0 { - fix_end = Some(end); - break; + Tok::Rpar => { + count -= 1; + if count == 0 { + fix_end = Some(range.end()); + break; + } } + _ => {} } } + match (fix_start, fix_end) { - (Some(start), Some(end)) => Some(Range::new(start, end)), + (Some(start), Some(end)) => Some(TextRange::new(start, end)), _ => None, } } @@ -1013,182 +1002,175 @@ pub fn match_parens(start: Location, locator: &Locator) -> Option { /// Return the appropriate visual `Range` for any message that spans a `Stmt`. /// Specifically, this method returns the range of a function or class name, /// rather than that of the entire function or class body. -pub fn identifier_range(stmt: &Stmt, locator: &Locator) -> Range { +pub fn identifier_range(stmt: &Stmt, locator: &Locator) -> TextRange { if matches!( stmt.node, StmtKind::ClassDef { .. } | StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } ) { - let contents = locator.slice(stmt); - for (start, tok, end) in lexer::lex_located(contents, Mode::Module, stmt.location).flatten() - { + let contents = &locator.contents()[stmt.range()]; + + for (tok, range) in lexer::lex_located(contents, Mode::Module, stmt.start()).flatten() { if matches!(tok, Tok::Name { .. }) { - return Range::new(start, end); + return range; } } error!("Failed to find identifier for {:?}", stmt); } - Range::from(stmt) + + stmt.range() } /// Return the ranges of [`Tok::Name`] tokens within a specified node. pub fn find_names<'a, T>( located: &'a Located, locator: &'a Locator, -) -> impl Iterator + 'a { - let contents = locator.slice(located); - lexer::lex_located(contents, Mode::Module, located.location) +) -> impl Iterator + 'a { + let contents = locator.slice(located.range()); + + lexer::lex_located(contents, Mode::Module, located.start()) .flatten() - .filter(|(_, tok, _)| matches!(tok, Tok::Name { .. })) - .map(|(start, _, end)| Range { - location: start, - end_location: end, - }) + .filter(|(tok, _)| matches!(tok, Tok::Name { .. })) + .map(|(_, range)| range) } /// Return the `Range` of `name` in `Excepthandler`. -pub fn excepthandler_name_range(handler: &Excepthandler, locator: &Locator) -> Option { +pub fn excepthandler_name_range(handler: &Excepthandler, locator: &Locator) -> Option { let ExcepthandlerKind::ExceptHandler { name, type_, body, .. } = &handler.node; + match (name, type_) { (Some(_), Some(type_)) => { - let type_end_location = type_.end_location.unwrap(); - let contents = locator.slice(Range::new(type_end_location, body[0].location)); - let range = lexer::lex_located(contents, Mode::Module, type_end_location) + let contents = &locator.contents()[TextRange::new(type_.end(), body[0].start())]; + + lexer::lex_located(contents, Mode::Module, type_.end()) .flatten() .tuple_windows() .find(|(tok, next_tok)| { - matches!(tok.1, Tok::As) && matches!(next_tok.1, Tok::Name { .. }) + matches!(tok.0, Tok::As) && matches!(next_tok.0, Tok::Name { .. }) }) - .map(|((..), (location, _, end_location))| Range::new(location, end_location)); - range + .map(|((..), (_, range))| range) } _ => None, } } /// Return the `Range` of `except` in `Excepthandler`. -pub fn except_range(handler: &Excepthandler, locator: &Locator) -> Range { +pub fn except_range(handler: &Excepthandler, locator: &Locator) -> TextRange { let ExcepthandlerKind::ExceptHandler { body, type_, .. } = &handler.node; let end = if let Some(type_) = type_ { - type_.location + type_.end() } else { - body.first() - .expect("Expected body to be non-empty") - .location + body.first().expect("Expected body to be non-empty").start() }; - let contents = locator.slice(Range { - location: handler.location, - end_location: end, - }); - let range = lexer::lex_located(contents, Mode::Module, handler.location) + let contents = &locator.contents()[TextRange::new(handler.start(), end)]; + + lexer::lex_located(contents, Mode::Module, handler.start()) .flatten() - .find(|(_, kind, _)| matches!(kind, Tok::Except { .. })) - .map(|(location, _, end_location)| Range { - location, - end_location, - }) - .expect("Failed to find `except` range"); - range + .find(|(kind, _)| matches!(kind, Tok::Except { .. })) + .map(|(_, range)| range) + .expect("Failed to find `except` range") } /// Return the `Range` of `else` in `For`, `AsyncFor`, and `While` statements. -pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option { +pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option { match &stmt.node { StmtKind::For { body, orelse, .. } | StmtKind::AsyncFor { body, orelse, .. } | StmtKind::While { body, orelse, .. } if !orelse.is_empty() => { - let body_end = body - .last() - .expect("Expected body to be non-empty") - .end_location - .unwrap(); - let contents = locator.slice(Range { - location: body_end, - end_location: orelse - .first() - .expect("Expected orelse to be non-empty") - .location, - }); - let range = lexer::lex_located(contents, Mode::Module, body_end) + let body_end = body.last().expect("Expected body to be non-empty").end(); + let or_else_start = orelse + .first() + .expect("Expected orelse to be non-empty") + .start(); + let contents = &locator.contents()[TextRange::new(body_end, or_else_start)]; + + lexer::lex_located(contents, Mode::Module, body_end) .flatten() - .find(|(_, kind, _)| matches!(kind, Tok::Else)) - .map(|(location, _, end_location)| Range { - location, - end_location, - }); - range + .find(|(kind, _)| matches!(kind, Tok::Else)) + .map(|(_, range)| range) } _ => None, } } /// Return the `Range` of the first `Tok::Colon` token in a `Range`. -pub fn first_colon_range(range: Range, locator: &Locator) -> Option { - let contents = locator.slice(range); - let range = lexer::lex_located(contents, Mode::Module, range.location) +pub fn first_colon_range(range: TextRange, locator: &Locator) -> Option { + let contents = &locator.contents()[range]; + let range = lexer::lex_located(contents, Mode::Module, range.start()) .flatten() - .find(|(_, kind, _)| matches!(kind, Tok::Colon)) - .map(|(location, _, end_location)| Range { - location, - end_location, - }); + .find(|(kind, _)| matches!(kind, Tok::Colon)) + .map(|(_, range)| range); range } /// Return the `Range` of the first `Elif` or `Else` token in an `If` statement. -pub fn elif_else_range(stmt: &Stmt, locator: &Locator) -> Option { +pub fn elif_else_range(stmt: &Stmt, locator: &Locator) -> Option { let StmtKind::If { body, orelse, .. } = &stmt.node else { return None; }; - let start = body - .last() - .expect("Expected body to be non-empty") - .end_location - .unwrap(); + let start = body.last().expect("Expected body to be non-empty").end(); + let end = match &orelse[..] { [Stmt { node: StmtKind::If { test, .. }, .. - }] => test.location, - [stmt, ..] => stmt.location, + }] => test.start(), + [stmt, ..] => stmt.start(), _ => return None, }; - let contents = locator.slice(Range::new(start, end)); - let range = lexer::lex_located(contents, Mode::Module, start) + + let contents = &locator.contents()[TextRange::new(start, end)]; + lexer::lex_located(contents, Mode::Module, start) .flatten() - .find(|(_, kind, _)| matches!(kind, Tok::Elif | Tok::Else)) - .map(|(location, _, end_location)| Range { - location, - end_location, - }); - range + .find(|(kind, _)| matches!(kind, Tok::Elif | Tok::Else)) + .map(|(_, range)| range) } /// Return `true` if a `Stmt` appears to be part of a multi-statement line, with /// other statements preceding it. -pub fn preceded_by_continuation(stmt: &Stmt, indexer: &Indexer) -> bool { - stmt.location.row() > 1 - && indexer - .continuation_lines() - .contains(&(stmt.location.row() - 1)) +pub fn preceded_by_continuation(stmt: &Stmt, indexer: &Indexer, locator: &Locator) -> bool { + let previous_line_end = locator.line_start(stmt.start()); + let newline_pos = usize::from(previous_line_end).saturating_sub(1); + + // Compute start of preceding line + let newline_len = match locator.contents().as_bytes()[newline_pos] { + b'\n' => { + if locator + .contents() + .as_bytes() + .get(newline_pos.saturating_sub(1)) + == Some(&b'\r') + { + 2 + } else { + 1 + } + } + b'\r' => 1, + // No preceding line + _ => return false, + }; + + // See if the position is in the continuation line starts + indexer.is_continuation(previous_line_end - TextSize::from(newline_len), locator) } /// Return `true` if a `Stmt` appears to be part of a multi-statement line, with /// other statements preceding it. pub fn preceded_by_multi_statement_line(stmt: &Stmt, locator: &Locator, indexer: &Indexer) -> bool { - match_leading_content(stmt, locator) || preceded_by_continuation(stmt, indexer) + has_leading_content(stmt, locator) || preceded_by_continuation(stmt, indexer, locator) } /// Return `true` if a `Stmt` appears to be part of a multi-statement line, with /// other statements following it. pub fn followed_by_multi_statement_line(stmt: &Stmt, locator: &Locator) -> bool { - match_trailing_content(stmt, locator) + has_trailing_content(stmt, locator) } /// Return `true` if a `Stmt` is a docstring. @@ -1370,7 +1352,7 @@ pub fn locate_cmpops(contents: &str) -> Vec { let mut ops: Vec = vec![]; let mut count: usize = 0; loop { - let Some((start, tok, end)) = tok_iter.next() else { + let Some((tok, range)) = tok_iter.next() else { break; }; if matches!(tok, Tok::Lpar) { @@ -1383,42 +1365,46 @@ pub fn locate_cmpops(contents: &str) -> Vec { if count == 0 { match tok { Tok::Not => { - if let Some((_, _, end)) = - tok_iter.next_if(|(_, tok, _)| matches!(tok, Tok::In)) + if let Some((_, next_range)) = + tok_iter.next_if(|(tok, _)| matches!(tok, Tok::In)) { - ops.push(LocatedCmpop::new(start, end, Cmpop::NotIn)); + ops.push(LocatedCmpop::new( + range.start(), + next_range.end(), + Cmpop::NotIn, + )); } } Tok::In => { - ops.push(LocatedCmpop::new(start, end, Cmpop::In)); + ops.push(LocatedCmpop::with_range(Cmpop::In, range)); } Tok::Is => { - let op = if let Some((_, _, end)) = - tok_iter.next_if(|(_, tok, _)| matches!(tok, Tok::Not)) + let op = if let Some((_, next_range)) = + tok_iter.next_if(|(tok, _)| matches!(tok, Tok::Not)) { - LocatedCmpop::new(start, end, Cmpop::IsNot) + LocatedCmpop::new(range.start(), next_range.end(), Cmpop::IsNot) } else { - LocatedCmpop::new(start, end, Cmpop::Is) + LocatedCmpop::with_range(Cmpop::Is, range) }; ops.push(op); } Tok::NotEqual => { - ops.push(LocatedCmpop::new(start, end, Cmpop::NotEq)); + ops.push(LocatedCmpop::with_range(Cmpop::NotEq, range)); } Tok::EqEqual => { - ops.push(LocatedCmpop::new(start, end, Cmpop::Eq)); + ops.push(LocatedCmpop::with_range(Cmpop::Eq, range)); } Tok::GreaterEqual => { - ops.push(LocatedCmpop::new(start, end, Cmpop::GtE)); + ops.push(LocatedCmpop::with_range(Cmpop::GtE, range)); } Tok::Greater => { - ops.push(LocatedCmpop::new(start, end, Cmpop::Gt)); + ops.push(LocatedCmpop::with_range(Cmpop::Gt, range)); } Tok::LessEqual => { - ops.push(LocatedCmpop::new(start, end, Cmpop::LtE)); + ops.push(LocatedCmpop::with_range(Cmpop::LtE, range)); } Tok::Less => { - ops.push(LocatedCmpop::new(start, end, Cmpop::Lt)); + ops.push(LocatedCmpop::with_range(Cmpop::Lt, range)); } _ => {} } @@ -1524,15 +1510,15 @@ mod tests { use std::borrow::Cow; use anyhow::Result; + use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser as parser; - use rustpython_parser::ast::{Cmpop, Location}; + use rustpython_parser::ast::Cmpop; use crate::helpers::{ - elif_else_range, else_range, first_colon_range, identifier_range, locate_cmpops, - match_trailing_content, resolve_imported_module_path, LocatedCmpop, + elif_else_range, else_range, first_colon_range, has_trailing_content, identifier_range, + locate_cmpops, resolve_imported_module_path, LocatedCmpop, }; use crate::source_code::Locator; - use crate::types::Range; #[test] fn trailing_content() -> Result<()> { @@ -1540,25 +1526,25 @@ mod tests { let program = parser::parse_program(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!match_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt, &locator)); let contents = "x = 1; y = 2"; let program = parser::parse_program(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(match_trailing_content(stmt, &locator)); + assert!(has_trailing_content(stmt, &locator)); let contents = "x = 1 "; let program = parser::parse_program(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!match_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt, &locator)); let contents = "x = 1 # Comment"; let program = parser::parse_program(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!match_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt, &locator)); let contents = r#" x = 1 @@ -1568,7 +1554,7 @@ y = 2 let program = parser::parse_program(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!match_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt, &locator)); Ok(()) } @@ -1581,7 +1567,7 @@ y = 2 let locator = Locator::new(contents); assert_eq!( identifier_range(stmt, &locator), - Range::new(Location::new(1, 4), Location::new(1, 5),) + TextRange::new(TextSize::from(4), TextSize::from(5)) ); let contents = r#" @@ -1595,7 +1581,7 @@ def \ let locator = Locator::new(contents); assert_eq!( identifier_range(stmt, &locator), - Range::new(Location::new(2, 2), Location::new(2, 3),) + TextRange::new(TextSize::from(8), TextSize::from(9)) ); let contents = "class Class(): pass".trim(); @@ -1604,7 +1590,7 @@ def \ let locator = Locator::new(contents); assert_eq!( identifier_range(stmt, &locator), - Range::new(Location::new(1, 6), Location::new(1, 11),) + TextRange::new(TextSize::from(6), TextSize::from(11)) ); let contents = "class Class: pass".trim(); @@ -1613,7 +1599,7 @@ def \ let locator = Locator::new(contents); assert_eq!( identifier_range(stmt, &locator), - Range::new(Location::new(1, 6), Location::new(1, 11),) + TextRange::new(TextSize::from(6), TextSize::from(11)) ); let contents = r#" @@ -1627,7 +1613,7 @@ class Class(): let locator = Locator::new(contents); assert_eq!( identifier_range(stmt, &locator), - Range::new(Location::new(2, 6), Location::new(2, 11),) + TextRange::new(TextSize::from(19), TextSize::from(24)) ); let contents = r#"x = y + 1"#.trim(); @@ -1636,7 +1622,7 @@ class Class(): let locator = Locator::new(contents); assert_eq!( identifier_range(stmt, &locator), - Range::new(Location::new(1, 0), Location::new(1, 9),) + TextRange::new(TextSize::from(0), TextSize::from(9)) ); Ok(()) @@ -1692,10 +1678,11 @@ else: let stmt = program.first().unwrap(); let locator = Locator::new(contents); let range = else_range(stmt, &locator).unwrap(); - assert_eq!(range.location.row(), 3); - assert_eq!(range.location.column(), 0); - assert_eq!(range.end_location.row(), 3); - assert_eq!(range.end_location.column(), 4); + assert_eq!(&contents[range], "else"); + assert_eq!( + range, + TextRange::new(TextSize::from(21), TextSize::from(25)) + ); Ok(()) } @@ -1704,14 +1691,12 @@ else: let contents = "with a: pass"; let locator = Locator::new(contents); let range = first_colon_range( - Range::new(Location::new(1, 0), Location::new(1, contents.len())), + TextRange::new(TextSize::from(0), contents.text_len()), &locator, ) .unwrap(); - assert_eq!(range.location.row(), 1); - assert_eq!(range.location.column(), 6); - assert_eq!(range.end_location.row(), 1); - assert_eq!(range.end_location.column(), 7); + assert_eq!(&contents[range], ":"); + assert_eq!(range, TextRange::new(TextSize::from(6), TextSize::from(7))); } #[test] @@ -1727,10 +1712,9 @@ elif b: let stmt = program.first().unwrap(); let locator = Locator::new(contents); let range = elif_else_range(stmt, &locator).unwrap(); - assert_eq!(range.location.row(), 3); - assert_eq!(range.location.column(), 0); - assert_eq!(range.end_location.row(), 3); - assert_eq!(range.end_location.column(), 4); + assert_eq!(range.start(), TextSize::from(14)); + assert_eq!(range.end(), TextSize::from(18)); + let contents = " if a: ... @@ -1742,10 +1726,9 @@ else: let stmt = program.first().unwrap(); let locator = Locator::new(contents); let range = elif_else_range(stmt, &locator).unwrap(); - assert_eq!(range.location.row(), 3); - assert_eq!(range.location.column(), 0); - assert_eq!(range.end_location.row(), 3); - assert_eq!(range.end_location.column(), 4); + assert_eq!(range.start(), TextSize::from(14)); + assert_eq!(range.end(), TextSize::from(18)); + Ok(()) } @@ -1754,8 +1737,8 @@ else: assert_eq!( locate_cmpops("x == 1"), vec![LocatedCmpop::new( - Location::new(1, 2), - Location::new(1, 4), + TextSize::from(2), + TextSize::from(4), Cmpop::Eq )] ); @@ -1763,8 +1746,8 @@ else: assert_eq!( locate_cmpops("x != 1"), vec![LocatedCmpop::new( - Location::new(1, 2), - Location::new(1, 4), + TextSize::from(2), + TextSize::from(4), Cmpop::NotEq )] ); @@ -1772,8 +1755,8 @@ else: assert_eq!( locate_cmpops("x is 1"), vec![LocatedCmpop::new( - Location::new(1, 2), - Location::new(1, 4), + TextSize::from(2), + TextSize::from(4), Cmpop::Is )] ); @@ -1781,8 +1764,8 @@ else: assert_eq!( locate_cmpops("x is not 1"), vec![LocatedCmpop::new( - Location::new(1, 2), - Location::new(1, 8), + TextSize::from(2), + TextSize::from(8), Cmpop::IsNot )] ); @@ -1790,8 +1773,8 @@ else: assert_eq!( locate_cmpops("x in 1"), vec![LocatedCmpop::new( - Location::new(1, 2), - Location::new(1, 4), + TextSize::from(2), + TextSize::from(4), Cmpop::In )] ); @@ -1799,8 +1782,8 @@ else: assert_eq!( locate_cmpops("x not in 1"), vec![LocatedCmpop::new( - Location::new(1, 2), - Location::new(1, 8), + TextSize::from(2), + TextSize::from(8), Cmpop::NotIn )] ); @@ -1808,8 +1791,8 @@ else: assert_eq!( locate_cmpops("x != (1 is not 2)"), vec![LocatedCmpop::new( - Location::new(1, 2), - Location::new(1, 4), + TextSize::from(2), + TextSize::from(4), Cmpop::NotEq )] ); diff --git a/crates/ruff_python_ast/src/imports.rs b/crates/ruff_python_ast/src/imports.rs index 0cc4ddea68..f3913d225d 100644 --- a/crates/ruff_python_ast/src/imports.rs +++ b/crates/ruff_python_ast/src/imports.rs @@ -1,8 +1,8 @@ +use ruff_text_size::TextRange; use rustc_hash::FxHashMap; -use rustpython_parser::ast::Location; -use serde::{Deserialize, Serialize}; -use crate::types::Range; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; /// A representation of an individual name imported via any import statement. #[derive(Debug, Clone, PartialEq, Eq)] @@ -102,31 +102,28 @@ impl FutureImport for AnyImport<'_> { } /// A representation of a module reference in an import statement. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ModuleImport { module: String, - location: Location, - end_location: Location, + range: TextRange, } impl ModuleImport { - pub fn new(module: String, location: Location, end_location: Location) -> Self { - Self { - module, - location, - end_location, - } + pub fn new(module: String, range: TextRange) -> Self { + Self { module, range } } } -impl From<&ModuleImport> for Range { - fn from(import: &ModuleImport) -> Range { - Range::new(import.location, import.end_location) +impl From<&ModuleImport> for TextRange { + fn from(import: &ModuleImport) -> TextRange { + import.range } } /// A representation of the import dependencies between modules. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ImportMap { /// A map from dot-delimited module name to the list of imports in that module. module_to_imports: FxHashMap>, diff --git a/crates/ruff_python_ast/src/newlines.rs b/crates/ruff_python_ast/src/newlines.rs index 371f80a352..54edacaaa7 100644 --- a/crates/ruff_python_ast/src/newlines.rs +++ b/crates/ruff_python_ast/src/newlines.rs @@ -1,4 +1,6 @@ +use ruff_text_size::{TextLen, TextRange, TextSize}; use std::iter::FusedIterator; +use std::ops::Deref; /// Extension trait for [`str`] that provides a [`UniversalNewlineIterator`]. pub trait StrExt { @@ -17,32 +19,42 @@ impl StrExt for str { /// ## Examples /// /// ```rust -/// use ruff_python_ast::newlines::UniversalNewlineIterator; -/// +/// # use ruff_text_size::TextSize; +/// # use ruff_python_ast::newlines::{Line, UniversalNewlineIterator}; /// let mut lines = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop"); /// -/// assert_eq!(lines.next_back(), Some("bop")); -/// assert_eq!(lines.next(), Some("foo")); -/// assert_eq!(lines.next_back(), Some("baz")); -/// assert_eq!(lines.next(), Some("bar")); -/// assert_eq!(lines.next_back(), Some("")); +/// assert_eq!(lines.next_back(), Some(Line::new("bop", TextSize::from(14)))); +/// assert_eq!(lines.next(), Some(Line::new("foo\n", TextSize::from(0)))); +/// assert_eq!(lines.next_back(), Some(Line::new("baz\r", TextSize::from(10)))); +/// assert_eq!(lines.next(), Some(Line::new("bar\n", TextSize::from(4)))); +/// assert_eq!(lines.next_back(), Some(Line::new("\r\n", TextSize::from(8)))); /// assert_eq!(lines.next(), None); /// ``` pub struct UniversalNewlineIterator<'a> { text: &'a str, + offset: TextSize, + offset_back: TextSize, } impl<'a> UniversalNewlineIterator<'a> { + pub fn with_offset(text: &'a str, offset: TextSize) -> UniversalNewlineIterator<'a> { + UniversalNewlineIterator { + text, + offset, + offset_back: offset + text.text_len(), + } + } + pub fn from(text: &'a str) -> UniversalNewlineIterator<'a> { - UniversalNewlineIterator { text } + Self::with_offset(text, TextSize::default()) } } impl<'a> Iterator for UniversalNewlineIterator<'a> { - type Item = &'a str; + type Item = Line<'a>; #[inline] - fn next(&mut self) -> Option<&'a str> { + fn next(&mut self) -> Option> { if self.text.is_empty() { return None; } @@ -50,21 +62,32 @@ impl<'a> Iterator for UniversalNewlineIterator<'a> { let line = match self.text.find(['\n', '\r']) { // Non-last line Some(line_end) => { - let (line, remainder) = self.text.split_at(line_end); - - self.text = match remainder.as_bytes()[0] { + let offset: usize = match self.text.as_bytes()[line_end] { // Explicit branch for `\n` as this is the most likely path - b'\n' => &remainder[1..], + b'\n' => 1, // '\r\n' - b'\r' if remainder.as_bytes().get(1) == Some(&b'\n') => &remainder[2..], + b'\r' if self.text.as_bytes().get(line_end + 1) == Some(&b'\n') => 2, // '\r' - _ => &remainder[1..], + _ => 1, }; + let (text, remainder) = self.text.split_at(line_end + offset); + + let line = Line { + offset: self.offset, + text, + }; + + self.text = remainder; + self.offset += text.text_len(); + line } // Last line - None => std::mem::take(&mut self.text), + None => Line { + offset: self.offset, + text: std::mem::take(&mut self.text), + }, }; Some(line) @@ -85,7 +108,7 @@ impl DoubleEndedIterator for UniversalNewlineIterator<'_> { let len = self.text.len(); // Trim any trailing newlines. - self.text = match self.text.as_bytes()[len - 1] { + let haystack = match self.text.as_bytes()[len - 1] { b'\n' if len > 1 && self.text.as_bytes()[len - 2] == b'\r' => &self.text[..len - 2], b'\n' | b'\r' => &self.text[..len - 1], _ => self.text, @@ -93,16 +116,23 @@ impl DoubleEndedIterator for UniversalNewlineIterator<'_> { // Find the end of the previous line. The previous line is the text up to, but not including // the newline character. - let line = match self.text.rfind(['\n', '\r']) { + let line = if let Some(line_end) = haystack.rfind(['\n', '\r']) { // '\n' or '\r' or '\r\n' - Some(line_end) => { - let (remainder, line) = self.text.split_at(line_end + 1); - self.text = remainder; + let (remainder, line) = self.text.split_at(line_end + 1); + self.text = remainder; + self.offset_back -= line.text_len(); - line + Line { + text: line, + offset: self.offset_back, } + } else { // Last line - None => std::mem::take(&mut self.text), + let offset = self.offset_back - self.text.text_len(); + Line { + text: std::mem::take(&mut self.text), + offset, + } }; Some(line) @@ -113,16 +143,23 @@ impl FusedIterator for UniversalNewlineIterator<'_> {} /// Like [`UniversalNewlineIterator`], but includes a trailing newline as an empty line. pub struct NewlineWithTrailingNewline<'a> { - trailing: Option<&'a str>, + trailing: Option>, underlying: UniversalNewlineIterator<'a>, } impl<'a> NewlineWithTrailingNewline<'a> { pub fn from(input: &'a str) -> NewlineWithTrailingNewline<'a> { + Self::with_offset(input, TextSize::default()) + } + + pub fn with_offset(input: &'a str, offset: TextSize) -> Self { NewlineWithTrailingNewline { - underlying: UniversalNewlineIterator::from(input), + underlying: UniversalNewlineIterator::with_offset(input, offset), trailing: if input.ends_with(['\r', '\n']) { - Some("") + Some(Line { + text: "", + offset: offset + input.text_len(), + }) } else { None }, @@ -131,37 +168,159 @@ impl<'a> NewlineWithTrailingNewline<'a> { } impl<'a> Iterator for NewlineWithTrailingNewline<'a> { - type Item = &'a str; + type Item = Line<'a>; #[inline] - fn next(&mut self) -> Option<&'a str> { + fn next(&mut self) -> Option> { self.underlying.next().or_else(|| self.trailing.take()) } } +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Line<'a> { + text: &'a str, + offset: TextSize, +} + +impl<'a> Line<'a> { + pub fn new(text: &'a str, offset: TextSize) -> Self { + Self { text, offset } + } + + #[inline] + pub const fn start(&self) -> TextSize { + self.offset + } + + /// Returns the byte offset where the line ends, including its terminating new line character. + #[inline] + pub fn full_end(&self) -> TextSize { + self.offset + self.full_text_len() + } + + /// Returns the byte offset where the line ends, excluding its new line character + #[inline] + pub fn end(&self) -> TextSize { + self.offset + self.as_str().text_len() + } + + /// Returns the range of the line, including its terminating new line character. + #[inline] + pub fn full_range(&self) -> TextRange { + TextRange::at(self.offset, self.text.text_len()) + } + + /// Returns the range of the line, excluding its terminating new line character + #[inline] + pub fn range(&self) -> TextRange { + TextRange::new(self.start(), self.end()) + } + + /// Returns the text of the line, excluding the terminating new line character. + #[inline] + pub fn as_str(&self) -> &'a str { + let mut bytes = self.text.bytes().rev(); + + let newline_len = match bytes.next() { + Some(b'\n') => { + if bytes.next() == Some(b'\r') { + 2 + } else { + 1 + } + } + Some(b'\r') => 1, + _ => 0, + }; + + &self.text[..self.text.len() - newline_len] + } + + /// Returns the line's text, including the terminating new line character. + #[inline] + pub fn as_full_str(&self) -> &'a str { + self.text + } + + #[inline] + pub fn full_text_len(&self) -> TextSize { + self.text.text_len() + } +} + +impl Deref for Line<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl PartialEq<&str> for Line<'_> { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl PartialEq> for &str { + fn eq(&self, other: &Line<'_>) -> bool { + *self == other.as_str() + } +} + #[cfg(test)] mod tests { use super::UniversalNewlineIterator; + use crate::newlines::Line; + use ruff_text_size::TextSize; #[test] fn universal_newlines_empty_str() { let lines: Vec<_> = UniversalNewlineIterator::from("").collect(); - assert_eq!(lines, Vec::<&str>::default()); + assert_eq!(lines, Vec::::new()); let lines: Vec<_> = UniversalNewlineIterator::from("").rev().collect(); - assert_eq!(lines, Vec::<&str>::default()); + assert_eq!(lines, Vec::::new()); } #[test] fn universal_newlines_forward() { let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop").collect(); - assert_eq!(lines, vec!["foo", "bar", "", "baz", "bop"]); + assert_eq!( + lines, + vec![ + Line::new("foo\n", TextSize::from(0)), + Line::new("bar\n", TextSize::from(4)), + Line::new("\r\n", TextSize::from(8)), + Line::new("baz\r", TextSize::from(10)), + Line::new("bop", TextSize::from(14)), + ] + ); let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop\n").collect(); - assert_eq!(lines, vec!["foo", "bar", "", "baz", "bop"]); + assert_eq!( + lines, + vec![ + Line::new("foo\n", TextSize::from(0)), + Line::new("bar\n", TextSize::from(4)), + Line::new("\r\n", TextSize::from(8)), + Line::new("baz\r", TextSize::from(10)), + Line::new("bop\n", TextSize::from(14)), + ] + ); let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop\n\n").collect(); - assert_eq!(lines, vec!["foo", "bar", "", "baz", "bop", ""]); + assert_eq!( + lines, + vec![ + Line::new("foo\n", TextSize::from(0)), + Line::new("bar\n", TextSize::from(4)), + Line::new("\r\n", TextSize::from(8)), + Line::new("baz\r", TextSize::from(10)), + Line::new("bop\n", TextSize::from(14)), + Line::new("\n", TextSize::from(18)), + ] + ); } #[test] @@ -169,24 +328,52 @@ mod tests { let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop") .rev() .collect(); - assert_eq!(lines, vec!["bop", "baz", "", "bar", "foo"]); + assert_eq!( + lines, + vec![ + Line::new("bop", TextSize::from(14)), + Line::new("baz\r", TextSize::from(10)), + Line::new("\r\n", TextSize::from(8)), + Line::new("bar\n", TextSize::from(4)), + Line::new("foo\n", TextSize::from(0)), + ] + ); let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\nbaz\rbop\n") .rev() + .map(|line| line.as_str()) .collect(); - assert_eq!(lines, vec!["bop", "baz", "", "bar", "foo"]); + assert_eq!( + lines, + vec![ + Line::new("bop\n", TextSize::from(13)), + Line::new("baz\r", TextSize::from(9)), + Line::new("\n", TextSize::from(8)), + Line::new("bar\n", TextSize::from(4)), + Line::new("foo\n", TextSize::from(0)), + ] + ); } #[test] fn universal_newlines_mixed() { let mut lines = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop"); - assert_eq!(lines.next_back(), Some("bop")); - assert_eq!(lines.next(), Some("foo")); - assert_eq!(lines.next_back(), Some("baz")); - assert_eq!(lines.next(), Some("bar")); - assert_eq!(lines.next_back(), Some("")); + assert_eq!( + lines.next_back(), + Some(Line::new("bop", TextSize::from(14))) + ); + assert_eq!(lines.next(), Some(Line::new("foo\n", TextSize::from(0)))); + assert_eq!( + lines.next_back(), + Some(Line::new("baz\r", TextSize::from(10))) + ); + assert_eq!(lines.next(), Some(Line::new("bar\n", TextSize::from(4)))); + assert_eq!( + lines.next_back(), + Some(Line::new("\r\n", TextSize::from(8))) + ); assert_eq!(lines.next(), None); } } diff --git a/crates/ruff_python_ast/src/relocate.rs b/crates/ruff_python_ast/src/relocate.rs index 586dc89d92..a618105f1a 100644 --- a/crates/ruff_python_ast/src/relocate.rs +++ b/crates/ruff_python_ast/src/relocate.rs @@ -1,18 +1,15 @@ +use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, ExprKind, Keyword}; -use crate::types::Range; - -fn relocate_keyword(keyword: &mut Keyword, location: Range) { - keyword.location = location.location; - keyword.end_location = Some(location.end_location); +fn relocate_keyword(keyword: &mut Keyword, location: TextRange) { + keyword.range = location; relocate_expr(&mut keyword.node.value, location); } /// Change an expression's location (recursively) to match a desired, fixed /// location. -pub fn relocate_expr(expr: &mut Expr, location: Range) { - expr.location = location.location; - expr.end_location = Some(location.end_location); +pub fn relocate_expr(expr: &mut Expr, location: TextRange) { + expr.range = location; match &mut expr.node { ExprKind::BoolOp { values, .. } => { for expr in values { diff --git a/crates/ruff_python_ast/src/source_code/generator.rs b/crates/ruff_python_ast/src/source_code/generator.rs index 98c6b35fa1..600d2f41c9 100644 --- a/crates/ruff_python_ast/src/source_code/generator.rs +++ b/crates/ruff_python_ast/src/source_code/generator.rs @@ -3,10 +3,11 @@ use std::ops::Deref; use rustpython_parser::ast::{ - Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, ConversionFlag, Excepthandler, + Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, MatchCase, Operator, Pattern, PatternKind, Stmt, StmtKind, Suite, Withitem, }; +use rustpython_parser::ConversionFlag; use ruff_rustpython::vendor::{bytes, str}; diff --git a/crates/ruff_python_ast/src/source_code/indexer.rs b/crates/ruff_python_ast/src/source_code/indexer.rs index d282e12bb0..f6a954fb30 100644 --- a/crates/ruff_python_ast/src/source_code/indexer.rs +++ b/crates/ruff_python_ast/src/source_code/indexer.rs @@ -1,98 +1,135 @@ //! Struct used to index source code, to enable efficient lookup of tokens that //! are omitted from the AST (e.g., commented lines). -use rustpython_parser::ast::Location; +use crate::source_code::Locator; +use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; -use crate::types::Range; - pub struct Indexer { - commented_lines: Vec, - continuation_lines: Vec, - string_ranges: Vec, + /// Stores the ranges of comments sorted by [`TextRange::start`] in increasing order. No two ranges are overlapping. + comment_ranges: Vec, + + /// Stores the start offset of continuation lines. + continuation_lines: Vec, + + /// The range of all triple quoted strings in the source document. The ranges are sorted by their + /// [`TextRange::start`] position in increasing order. No two ranges are overlapping. + triple_quoted_string_ranges: Vec, } impl Indexer { - /// Return a slice of all lines that include a comment. - pub fn commented_lines(&self) -> &[usize] { - &self.commented_lines - } + pub fn from_tokens(tokens: &[LexResult], locator: &Locator) -> Self { + assert!(TextSize::try_from(locator.contents().len()).is_ok()); - /// Return a slice of all lines that end with a continuation (backslash). - pub fn continuation_lines(&self) -> &[usize] { - &self.continuation_lines - } - - /// Return a slice of all ranges that include a triple-quoted string. - pub fn string_ranges(&self) -> &[Range] { - &self.string_ranges - } -} - -impl From<&[LexResult]> for Indexer { - fn from(lxr: &[LexResult]) -> Self { let mut commented_lines = Vec::new(); let mut continuation_lines = Vec::new(); let mut string_ranges = Vec::new(); - let mut prev: Option<(&Location, &Tok, &Location)> = None; - for (start, tok, end) in lxr.iter().flatten() { + // Token, end + let mut prev_end = TextSize::default(); + let mut prev_token: Option<&Tok> = None; + let mut line_start = TextSize::default(); + + for (tok, range) in tokens.iter().flatten() { + let trivia = &locator.contents()[TextRange::new(prev_end, range.start())]; + + // Get the trivia between the previous and the current token and detect any newlines. + // This is necessary because `RustPython` doesn't emit `[Tok::Newline]` tokens + // between any two tokens that form a continuation nor multiple newlines in a row. + // That's why we have to extract the newlines "manually". + for (index, text) in trivia.match_indices(['\n', '\r']) { + if text == "\r" && trivia.as_bytes().get(index + 1) == Some(&b'\n') { + continue; + } + + // Newlines after a comment or new-line never form a continuation. + if !matches!( + prev_token, + Some(Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..)) | None + ) { + continuation_lines.push(line_start); + } + + // SAFETY: Safe because of the len assertion at the top of the function. + #[allow(clippy::cast_possible_truncation)] + { + line_start = prev_end + TextSize::new((index + 1) as u32); + } + } + match tok { - Tok::Comment(..) => commented_lines.push(start.row()), + Tok::Comment(..) => { + commented_lines.push(*range); + } + Tok::Newline | Tok::NonLogicalNewline => { + line_start = range.end(); + } Tok::String { triple_quoted: true, .. - } => string_ranges.push(Range::new(*start, *end)), - _ => (), + } => string_ranges.push(*range), + _ => {} } - if let Some((.., prev_tok, prev_end)) = prev { - if !matches!( - prev_tok, - Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..) - ) { - for line in prev_end.row()..start.row() { - continuation_lines.push(line); - } - } - } - prev = Some((start, tok, end)); + prev_token = Some(tok); + prev_end = range.end(); } Self { - commented_lines, + comment_ranges: commented_lines, continuation_lines, - string_ranges, + triple_quoted_string_ranges: string_ranges, } } + + /// Returns the byte offset ranges of comments + pub fn comment_ranges(&self) -> &[TextRange] { + &self.comment_ranges + } + + /// Returns the line start positions of continuations (backslash). + pub fn continuation_line_starts(&self) -> &[TextSize] { + &self.continuation_lines + } + + /// Return a slice of all ranges that include a triple-quoted string. The ranges are sorted by + /// [`TextRange::start`] in increasing order. No two ranges are overlapping. + pub fn triple_quoted_string_ranges(&self) -> &[TextRange] { + &self.triple_quoted_string_ranges + } + + pub fn is_continuation(&self, offset: TextSize, locator: &Locator) -> bool { + let line_start = locator.line_start(offset); + self.continuation_lines.binary_search(&line_start).is_ok() + } } #[cfg(test)] mod tests { - use rustpython_parser::ast::Location; + use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::lexer::LexResult; use rustpython_parser::{lexer, Mode}; - use crate::source_code::Indexer; - use crate::types::Range; + use crate::source_code::{Indexer, Locator}; #[test] fn continuation() { let contents = r#"x = 1"#; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - let indexer: Indexer = lxr.as_slice().into(); - assert_eq!(indexer.continuation_lines(), Vec::::new().as_slice()); + let indexer = Indexer::from_tokens(&lxr, &Locator::new(contents)); + assert_eq!(indexer.continuation_line_starts(), &[]); let contents = r#" -# Hello, world! + # Hello, world! x = 1 y = 2 -"# + "# .trim(); + let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - let indexer: Indexer = lxr.as_slice().into(); - assert_eq!(indexer.continuation_lines(), Vec::::new().as_slice()); + let indexer = Indexer::from_tokens(&lxr, &Locator::new(contents)); + assert_eq!(indexer.continuation_line_starts(), &[]); let contents = r#" x = \ @@ -111,8 +148,20 @@ if True: "# .trim(); let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - let indexer: Indexer = lxr.as_slice().into(); - assert_eq!(indexer.continuation_lines(), [1, 5, 6, 11]); + let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents)); + assert_eq!( + indexer.continuation_line_starts(), + [ + // row 1 + TextSize::from(0), + // row 5 + TextSize::from(22), + // row 6 + TextSize::from(32), + // row 11 + TextSize::from(71), + ] + ); let contents = r#" x = 1; import sys @@ -131,16 +180,24 @@ import os "# .trim(); let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - let indexer: Indexer = lxr.as_slice().into(); - assert_eq!(indexer.continuation_lines(), [9, 12]); + let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents)); + assert_eq!( + indexer.continuation_line_starts(), + [ + // row 9 + TextSize::from(84), + // row 12 + TextSize::from(116) + ] + ); } #[test] fn string_ranges() { let contents = r#""this is a single-quoted string""#; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - let indexer: Indexer = lxr.as_slice().into(); - assert_eq!(indexer.string_ranges(), &vec![]); + let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents)); + assert_eq!(indexer.triple_quoted_string_ranges(), []); let contents = r#" """ @@ -148,10 +205,10 @@ import os """ "#; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - let indexer: Indexer = lxr.as_slice().into(); + let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents)); assert_eq!( - indexer.string_ranges(), - &vec![Range::new(Location::new(2, 12), Location::new(4, 15))] + indexer.triple_quoted_string_ranges(), + [TextRange::new(TextSize::from(13), TextSize::from(71))] ); let contents = r#" @@ -160,10 +217,10 @@ import os """ "#; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - let indexer: Indexer = lxr.as_slice().into(); + let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents)); assert_eq!( - indexer.string_ranges(), - &vec![Range::new(Location::new(2, 12), Location::new(4, 15))] + indexer.triple_quoted_string_ranges(), + [TextRange::new(TextSize::from(13), TextSize::from(107))] ); let contents = r#" @@ -177,12 +234,12 @@ import os """ "#; let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); - let indexer: Indexer = lxr.as_slice().into(); + let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents)); assert_eq!( - indexer.string_ranges(), - &vec![ - Range::new(Location::new(2, 12), Location::new(5, 15)), - Range::new(Location::new(6, 12), Location::new(9, 15)) + indexer.triple_quoted_string_ranges(), + &[ + TextRange::new(TextSize::from(13), TextSize::from(85)), + TextRange::new(TextSize::from(98), TextSize::from(161)) ] ); } diff --git a/crates/ruff_python_ast/src/source_code/line_index.rs b/crates/ruff_python_ast/src/source_code/line_index.rs index d79fa11081..ac3ce82d88 100644 --- a/crates/ruff_python_ast/src/source_code/line_index.rs +++ b/crates/ruff_python_ast/src/source_code/line_index.rs @@ -1,12 +1,14 @@ +use crate::source_code::SourceLocation; use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::ast::Location; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use std::fmt; use std::fmt::{Debug, Formatter}; use std::num::NonZeroUsize; use std::ops::Deref; use std::sync::Arc; -/// Index for fast [`Location`] to [byte offset](TextSize) conversions. +/// Index for fast [byte offset](TextSize) to [`SourceLocation`] conversions. /// /// Cloning a [`LineIndex`] is cheap because it only requires bumping a reference count. #[derive(Clone)] @@ -58,28 +60,63 @@ impl LineIndex { self.inner.kind } - /// Converts a [`Location`] to it's [byte offset](TextSize) in the source code. - pub fn location_offset(&self, location: Location, contents: &str) -> TextSize { - let line_index = OneIndexed::new(location.row()).unwrap(); - let line_range = self.line_range(line_index, contents); + /// Returns the row and column index for an offset. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::TextSize; + /// # use ruff_python_ast::source_code::{LineIndex, OneIndexed, SourceLocation}; + /// let source = "def a():\n pass"; + /// let index = LineIndex::from_source_text(source); + /// + /// assert_eq!( + /// index.source_location(TextSize::from(0), source), + /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } + /// ); + /// + /// assert_eq!( + /// index.source_location(TextSize::from(4), source), + /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } + /// ); + /// assert_eq!( + /// index.source_location(TextSize::from(13), source), + /// SourceLocation { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } + /// ); + /// ``` + /// + /// ## Panics + /// + /// If the offset is out of bounds. + pub fn source_location(&self, offset: TextSize, content: &str) -> SourceLocation { + match self.line_starts().binary_search(&offset) { + // Offset is at the start of a line + Ok(row) => SourceLocation { + row: OneIndexed::from_zero_indexed(row), + column: OneIndexed::from_zero_indexed(0), + }, + Err(next_row) => { + // SAFETY: Safe because the index always contains an entry for the offset 0 + let row = next_row - 1; + let mut line_start = self.line_starts()[row]; - let column_offset = match self.kind() { - IndexKind::Ascii => TextSize::try_from(location.column()).unwrap(), - IndexKind::Utf8 => { - let line = &contents[line_range]; + let column = if self.kind().is_ascii() { + usize::from(offset) - usize::from(line_start) + } else { + // Don't count the BOM character as a column. + if line_start == TextSize::from(0) && content.starts_with('\u{feff}') { + line_start = '\u{feff}'.text_len(); + } - // Skip the bom character - let bom_len = - usize::from(line_index.to_zero_indexed() == 0 && line.starts_with('\u{feff}')); + content[TextRange::new(line_start, offset)].chars().count() + }; - match line.char_indices().nth(location.column() + bom_len) { - Some((offset, _)) => TextSize::try_from(offset).unwrap(), - None => line_range.len(), + SourceLocation { + row: OneIndexed::from_zero_indexed(row), + column: OneIndexed::from_zero_indexed(column), } } - }; - - line_range.start() + column_offset + } } /// Return the number of lines in the source code. @@ -87,6 +124,35 @@ impl LineIndex { self.line_starts().len() } + /// Returns the row number for a given offset. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::TextSize; + /// # use ruff_python_ast::source_code::{LineIndex, OneIndexed, SourceLocation}; + /// let source = "def a():\n pass"; + /// let index = LineIndex::from_source_text(source); + /// + /// assert_eq!(index.line_index(TextSize::from(0)), OneIndexed::from_zero_indexed(0)); + /// assert_eq!(index.line_index(TextSize::from(4)), OneIndexed::from_zero_indexed(0)); + /// assert_eq!(index.line_index(TextSize::from(13)), OneIndexed::from_zero_indexed(1)); + /// ``` + /// + /// ## Panics + /// + /// If the offset is out of bounds. + pub fn line_index(&self, offset: TextSize) -> OneIndexed { + match self.line_starts().binary_search(&offset) { + // Offset is at the start of a line + Ok(row) => OneIndexed::from_zero_indexed(row), + Err(row) => { + // SAFETY: Safe because the index always contains an entry for the offset 0 + OneIndexed::from_zero_indexed(row - 1) + } + } + } + /// Returns the [byte offset](TextSize) for the `line` with the given index. pub(crate) fn line_start(&self, line: OneIndexed, contents: &str) -> TextSize { let row_index = line.to_zero_indexed(); @@ -159,12 +225,19 @@ enum IndexKind { Utf8, } +impl IndexKind { + const fn is_ascii(self) -> bool { + matches!(self, IndexKind::Ascii) + } +} + /// Type-safe wrapper for a value whose logical range starts at `1`, for /// instance the line or column numbers in a file /// /// Internally this is represented as a [`NonZeroUsize`], this enables some /// memory optimizations #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct OneIndexed(NonZeroUsize); impl OneIndexed { @@ -238,8 +311,8 @@ const fn unwrap(option: Option) -> T { #[cfg(test)] mod tests { use crate::source_code::line_index::LineIndex; + use crate::source_code::{OneIndexed, SourceLocation}; use ruff_text_size::TextSize; - use rustpython_parser::ast::Location; #[test] fn ascii_index() { @@ -265,21 +338,38 @@ mod tests { } #[test] - fn ascii_byte_offset() { + fn ascii_source_location() { let contents = "x = 1\ny = 2"; let index = LineIndex::from_source_text(contents); // First row. - let loc = index.location_offset(Location::new(1, 0), contents); - assert_eq!(loc, TextSize::from(0)); + let loc = index.source_location(TextSize::from(2), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(2) + } + ); // Second row. - let loc = index.location_offset(Location::new(2, 0), contents); - assert_eq!(loc, TextSize::from(6)); + let loc = index.source_location(TextSize::from(6), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } + ); - // One-past-the-end. - let loc = index.location_offset(Location::new(3, 0), contents); - assert_eq!(loc, TextSize::from(11)); + let loc = index.source_location(TextSize::from(11), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(5) + } + ); } #[test] @@ -289,16 +379,25 @@ mod tests { assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]); assert_eq!( - index.location_offset(Location::new(1, 4), contents), - TextSize::from(4) + index.source_location(TextSize::from(4), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(4) + } ); assert_eq!( - index.location_offset(Location::new(2, 0), contents), - TextSize::from(6) + index.source_location(TextSize::from(6), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } ); assert_eq!( - index.location_offset(Location::new(2, 1), contents), - TextSize::from(7) + index.source_location(TextSize::from(7), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(1) + } ); } @@ -309,16 +408,25 @@ mod tests { assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(7)]); assert_eq!( - index.location_offset(Location::new(1, 4), contents), - TextSize::from(4) + index.source_location(TextSize::from(4), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(4) + } ); assert_eq!( - index.location_offset(Location::new(2, 0), contents), - TextSize::from(7) + index.source_location(TextSize::from(7), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } ); assert_eq!( - index.location_offset(Location::new(2, 1), contents), - TextSize::from(8) + index.source_location(TextSize::from(8), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(1) + } ); } @@ -367,16 +475,25 @@ mod tests { // Second ' assert_eq!( - index.location_offset(Location::new(1, 6), contents), - TextSize::from(9) + index.source_location(TextSize::from(9), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(6) + } ); assert_eq!( - index.location_offset(Location::new(2, 0), contents), - TextSize::from(11) + index.source_location(TextSize::from(11), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } ); assert_eq!( - index.location_offset(Location::new(2, 1), contents), - TextSize::from(12) + index.source_location(TextSize::from(12), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(1) + } ); } @@ -392,16 +509,25 @@ mod tests { // Second ' assert_eq!( - index.location_offset(Location::new(1, 6), contents), - TextSize::from(9) + index.source_location(TextSize::from(9), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(6) + } ); assert_eq!( - index.location_offset(Location::new(2, 0), contents), - TextSize::from(12) + index.source_location(TextSize::from(12), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } ); assert_eq!( - index.location_offset(Location::new(2, 1), contents), - TextSize::from(13) + index.source_location(TextSize::from(13), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(1) + } ); } @@ -415,23 +541,51 @@ mod tests { ); // First row. - let loc = index.location_offset(Location::new(1, 0), contents); - assert_eq!(loc, TextSize::from(0)); + let loc = index.source_location(TextSize::from(0), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(0) + } + ); - let loc = index.location_offset(Location::new(1, 5), contents); - assert_eq!(loc, TextSize::from(5)); - assert_eq!(&"x = '☃'\ny = 2"[usize::from(loc)..], "☃'\ny = 2"); + let loc = index.source_location(TextSize::from(5), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(5) + } + ); - let loc = index.location_offset(Location::new(1, 6), contents); - assert_eq!(loc, TextSize::from(8)); - assert_eq!(&"x = '☃'\ny = 2"[usize::from(loc)..], "'\ny = 2"); + let loc = index.source_location(TextSize::from(8), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(6) + } + ); // Second row. - let loc = index.location_offset(Location::new(2, 0), contents); - assert_eq!(loc, TextSize::from(10)); + let loc = index.source_location(TextSize::from(10), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } + ); // One-past-the-end. - let loc = index.location_offset(Location::new(3, 0), contents); - assert_eq!(loc, TextSize::from(15)); + let loc = index.source_location(TextSize::from(15), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(5) + } + ); } } diff --git a/crates/ruff_python_ast/src/source_code/locator.rs b/crates/ruff_python_ast/src/source_code/locator.rs index bd26136289..24932d6e4e 100644 --- a/crates/ruff_python_ast/src/source_code/locator.rs +++ b/crates/ruff_python_ast/src/source_code/locator.rs @@ -1,61 +1,399 @@ //! Struct used to efficiently slice source code at (row, column) Locations. -use crate::source_code::line_index::LineIndex; -use crate::source_code::SourceCode; +use crate::source_code::{LineIndex, OneIndexed, SourceCode, SourceLocation}; use once_cell::unsync::OnceCell; -use ruff_text_size::TextSize; -use rustpython_parser::ast::Location; - -use crate::types::Range; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use std::ops::Add; pub struct Locator<'a> { contents: &'a str, - line_index: OnceCell, + index: OnceCell, } impl<'a> Locator<'a> { pub const fn new(contents: &'a str) -> Self { Self { contents, - line_index: OnceCell::new(), + index: OnceCell::new(), } } - fn get_or_init_index(&self) -> &LineIndex { - self.line_index + #[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) + } + + #[deprecated( + note = "This is expensive, avoid using outside of the diagnostic phase. Prefer the other `Locator` methods instead." + )] + pub fn compute_source_location(&self, offset: TextSize) -> SourceLocation { + self.to_source_code().source_location(offset) + } + + fn to_index(&self) -> &LineIndex { + self.index .get_or_init(|| LineIndex::from_source_text(self.contents)) } - #[inline] - pub fn to_source_code(&self) -> SourceCode<'a, '_> { + pub fn line_index(&self) -> Option<&LineIndex> { + self.index.get() + } + + pub fn to_source_code(&self) -> SourceCode { SourceCode { - index: self.get_or_init_index(), + index: self.to_index(), text: self.contents, } } - /// Take the source code up to the given [`Location`]. - #[inline] - pub fn up_to(&self, location: Location) -> &'a str { - self.to_source_code().up_to(location) + /// Computes the start position of the line of `offset`. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::TextSize; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\rthird line"); + /// + /// assert_eq!(locator.line_start(TextSize::from(0)), TextSize::from(0)); + /// assert_eq!(locator.line_start(TextSize::from(4)), TextSize::from(0)); + /// + /// assert_eq!(locator.line_start(TextSize::from(14)), TextSize::from(11)); + /// assert_eq!(locator.line_start(TextSize::from(28)), TextSize::from(23)); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + pub fn line_start(&self, offset: TextSize) -> TextSize { + if let Some(index) = self.contents[TextRange::up_to(offset)].rfind(['\n', '\r']) { + // SAFETY: Safe because `index < offset` + TextSize::try_from(index).unwrap().add(TextSize::from(1)) + } else { + TextSize::default() + } } - /// Take the source code after the given [`Location`]. - #[inline] - pub fn after(&self, location: Location) -> &'a str { - self.to_source_code().after(location) + pub fn is_at_start_of_line(&self, offset: TextSize) -> bool { + offset == TextSize::from(0) + || self.contents[TextRange::up_to(offset)].ends_with(['\n', '\r']) } - /// Take the source code between the given [`Range`]. - #[inline] - pub fn slice>(&self, range: R) -> &'a str { - self.to_source_code().slice(range) + /// Computes the offset that is right after the newline character that ends `offset`'s line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!(locator.full_line_end(TextSize::from(3)), TextSize::from(11)); + /// assert_eq!(locator.full_line_end(TextSize::from(14)), TextSize::from(24)); + /// assert_eq!(locator.full_line_end(TextSize::from(28)), TextSize::from(34)); + /// ``` + /// + /// ## Panics + /// + /// If `offset` is passed the end of the content. + pub fn full_line_end(&self, offset: TextSize) -> TextSize { + let slice = &self.contents[usize::from(offset)..]; + if let Some(index) = slice.find(['\n', '\r']) { + let bytes = slice.as_bytes(); + + // `\r\n` + let relative_offset = if bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') { + TextSize::try_from(index + 2).unwrap() + } + // `\r` or `\n` + else { + TextSize::try_from(index + 1).unwrap() + }; + + offset.add(relative_offset) + } else { + self.contents.text_len() + } } - /// Return the byte offset of the given [`Location`]. + /// Computes the offset that is right before the newline character that ends `offset`'s line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!(locator.line_end(TextSize::from(3)), TextSize::from(10)); + /// assert_eq!(locator.line_end(TextSize::from(14)), TextSize::from(22)); + /// assert_eq!(locator.line_end(TextSize::from(28)), TextSize::from(34)); + /// ``` + /// + /// ## Panics + /// + /// If `offset` is passed the end of the content. + pub fn line_end(&self, offset: TextSize) -> TextSize { + let slice = &self.contents[usize::from(offset)..]; + if let Some(index) = slice.find(['\n', '\r']) { + offset + TextSize::try_from(index).unwrap() + } else { + self.contents.text_len() + } + } + + /// Computes the range of this `offset`s line. + /// + /// The range starts at the beginning of the line and goes up to, and including, the new line character + /// at the end of the line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!(locator.full_line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(11))); + /// assert_eq!(locator.full_line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(24))); + /// assert_eq!(locator.full_line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34))); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + pub fn full_line_range(&self, offset: TextSize) -> TextRange { + TextRange::new(self.line_start(offset), self.full_line_end(offset)) + } + + /// Computes the range of this `offset`s line ending before the newline character. + /// + /// The range starts at the beginning of the line and goes up to, but excluding, the new line character + /// at the end of the line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!(locator.line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(10))); + /// assert_eq!(locator.line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(22))); + /// assert_eq!(locator.line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34))); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + pub fn line_range(&self, offset: TextSize) -> TextRange { + TextRange::new(self.line_start(offset), self.line_end(offset)) + } + + /// Returns the text of the `offset`'s line. + /// + /// The line includes the newline characters at the end of the line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!(locator.full_line(TextSize::from(3)), "First line\n"); + /// assert_eq!(locator.full_line(TextSize::from(14)), "second line\r\n"); + /// assert_eq!(locator.full_line(TextSize::from(28)), "third line"); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + pub fn full_line(&self, offset: TextSize) -> &'a str { + &self.contents[self.full_line_range(offset)] + } + + /// Returns the text of the `offset`'s line. + /// + /// Excludes the newline characters at the end of the line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!(locator.line(TextSize::from(3)), "First line"); + /// assert_eq!(locator.line(TextSize::from(14)), "second line"); + /// assert_eq!(locator.line(TextSize::from(28)), "third line"); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + pub fn line(&self, offset: TextSize) -> &'a str { + &self.contents[self.line_range(offset)] + } + + /// Computes the range of all lines that this `range` covers. + /// + /// The range starts at the beginning of the line at `range.start()` and goes up to, and including, the new line character + /// at the end of `range.ends()`'s line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!( + /// locator.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// TextRange::new(TextSize::from(0), TextSize::from(11)) + /// ); + /// assert_eq!( + /// locator.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// TextRange::new(TextSize::from(0), TextSize::from(24)) + /// ); + /// ``` + /// + /// ## Panics + /// If the start or end of `range` is out of bounds. + pub fn full_lines_range(&self, range: TextRange) -> TextRange { + TextRange::new( + self.line_start(range.start()), + self.full_line_end(range.end()), + ) + } + + /// Computes the range of all lines that this `range` covers. + /// + /// The range starts at the beginning of the line at `range.start()` and goes up to, but excluding, the new line character + /// at the end of `range.end()`'s line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!( + /// locator.lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// TextRange::new(TextSize::from(0), TextSize::from(10)) + /// ); + /// assert_eq!( + /// locator.lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// TextRange::new(TextSize::from(0), TextSize::from(22)) + /// ); + /// ``` + /// + /// ## Panics + /// If the start or end of `range` is out of bounds. + pub fn lines_range(&self, range: TextRange) -> TextRange { + TextRange::new(self.line_start(range.start()), self.line_end(range.end())) + } + + /// Returns true if the text of `range` contains any line break. + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert!( + /// !locator.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// ); + /// assert!( + /// locator.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// ); + /// ``` + /// + /// ## Panics + /// If the `range` is out of bounds. + pub fn contains_line_break(&self, range: TextRange) -> bool { + let text = &self.contents[range]; + text.contains(['\n', '\r']) + } + + /// Returns the text of all lines that include `range`. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!( + /// locator.lines(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// "First line" + /// ); + /// assert_eq!( + /// locator.lines(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// "First line\nsecond line" + /// ); + /// ``` + /// + /// ## Panics + /// If the start or end of `range` is out of bounds. + pub fn lines(&self, range: TextRange) -> &'a str { + &self.contents[self.lines_range(range)] + } + + /// Returns the text of all lines that include `range`. + /// + /// Includes the newline characters of the last line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{TextRange, TextSize}; + /// # use ruff_python_ast::source_code::Locator; + /// + /// let locator = Locator::new("First line\nsecond line\r\nthird line"); + /// + /// assert_eq!( + /// locator.full_lines(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// "First line\n" + /// ); + /// assert_eq!( + /// locator.full_lines(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// "First line\nsecond line\r\n" + /// ); + /// ``` + /// + /// ## Panics + /// If the start or end of `range` is out of bounds. + pub fn full_lines(&self, range: TextRange) -> &'a str { + &self.contents[self.full_lines_range(range)] + } + + /// Take the source code up to the given [`TextSize`]. #[inline] - pub fn offset(&self, location: Location) -> TextSize { - self.to_source_code().offset(location) + pub fn up_to(&self, offset: TextSize) -> &'a str { + &self.contents[TextRange::up_to(offset)] + } + + /// Take the source code after the given [`TextSize`]. + #[inline] + pub fn after(&self, offset: TextSize) -> &'a str { + &self.contents[usize::from(offset)..] + } + + /// Take the source code between the given [`TextRange`]. + #[inline] + pub fn slice(&self, range: TextRange) -> &'a str { + &self.contents[range] } /// Return the underlying source code. @@ -63,17 +401,15 @@ impl<'a> Locator<'a> { self.contents } - /// Return the number of lines in the source code. - pub fn count_lines(&self) -> usize { - let index = self.get_or_init_index(); - index.line_count() - } - /// Return the number of bytes in the source code. pub const fn len(&self) -> usize { self.contents.len() } + pub fn text_len(&self) -> TextSize { + self.contents.text_len() + } + /// Return `true` if the source code is empty. pub const fn is_empty(&self) -> bool { self.contents.is_empty() diff --git a/crates/ruff_python_ast/src/source_code/mod.rs b/crates/ruff_python_ast/src/source_code/mod.rs index 68223226b4..5c0b3769ed 100644 --- a/crates/ruff_python_ast/src/source_code/mod.rs +++ b/crates/ruff_python_ast/src/source_code/mod.rs @@ -5,17 +5,17 @@ mod locator; mod stylist; pub use crate::source_code::line_index::{LineIndex, OneIndexed}; -use crate::types::Range; pub use generator::Generator; pub use indexer::Indexer; pub use locator::Locator; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser as parser; -use rustpython_parser::ast::Location; use rustpython_parser::{lexer, Mode, ParseError}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Formatter}; - use std::sync::Arc; + pub use stylist::{LineEnding, Stylist}; /// Run round-trip source code generation on a given Python code. @@ -29,7 +29,7 @@ pub fn round_trip(code: &str, source_path: &str) -> Result { Ok(generator.generate()) } -/// Gives access to the source code of a file and allows mapping between [`Location`] and byte offsets. +/// Gives access to the source code of a file and allows mapping between [`TextSize`] and [`SourceLocation`]. #[derive(Debug)] pub struct SourceCode<'src, 'index> { text: &'src str, @@ -44,37 +44,34 @@ impl<'src, 'index> SourceCode<'src, 'index> { } } - /// Take the source code up to the given [`Location`]. - pub fn up_to(&self, location: Location) -> &'src str { - let offset = self.index.location_offset(location, self.text); + /// Computes the one indexed row and column numbers for `offset`. + #[inline] + pub fn source_location(&self, offset: TextSize) -> SourceLocation { + self.index.source_location(offset, self.text) + } + + #[inline] + pub fn line_index(&self, offset: TextSize) -> OneIndexed { + self.index.line_index(offset) + } + + /// Take the source code up to the given [`TextSize`]. + #[inline] + pub fn up_to(&self, offset: TextSize) -> &'src str { &self.text[TextRange::up_to(offset)] } - /// Take the source code after the given [`Location`]. - pub fn after(&self, location: Location) -> &'src str { - let offset = self.index.location_offset(location, self.text); + /// Take the source code after the given [`TextSize`]. + #[inline] + pub fn after(&self, offset: TextSize) -> &'src str { &self.text[usize::from(offset)..] } - /// Take the source code between the given [`Range`]. - pub fn slice>(&self, range: R) -> &'src str { - let range = self.text_range(range); + /// Take the source code between the given [`TextRange`]. + pub fn slice(&self, range: TextRange) -> &'src str { &self.text[range] } - /// Converts a [`Location`] range to a byte offset range - pub fn text_range>(&self, range: R) -> TextRange { - let range = range.into(); - let start = self.index.location_offset(range.location, self.text); - let end = self.index.location_offset(range.end_location, self.text); - TextRange::new(start, end) - } - - /// Return the byte offset of the given [`Location`]. - pub fn offset(&self, location: Location) -> TextSize { - self.index.location_offset(location, self.text) - } - pub fn line_start(&self, line: OneIndexed) -> TextSize { self.index.line_start(line, self.text) } @@ -87,20 +84,6 @@ impl<'src, 'index> SourceCode<'src, 'index> { self.index.line_range(line, self.text) } - /// Returns a string with the lines spawning between location and end location. - pub fn lines(&self, range: Range) -> &'src str { - let start_line = self - .index - .line_range(OneIndexed::new(range.location.row()).unwrap(), self.text); - - let end_line = self.index.line_range( - OneIndexed::new(range.end_location.row()).unwrap(), - self.text, - ); - - &self.text[TextRange::new(start_line.start(), end_line.end())] - } - /// Returns the source text of the line with the given index #[inline] pub fn line_text(&self, index: OneIndexed) -> &'src str { @@ -131,69 +114,43 @@ impl Eq for SourceCode<'_, '_> {} /// A Builder for constructing a [`SourceFile`] pub struct SourceFileBuilder { name: Box, - code: Option, + code: Box, + index: Option, } impl SourceFileBuilder { /// Creates a new builder for a file named `name`. - pub fn new(name: &str) -> Self { + pub fn new>, Code: Into>>(name: Name, code: Code) -> Self { Self { - name: Box::from(name), - code: None, + name: name.into(), + code: code.into(), + index: None, } } - /// Creates a enw builder for a file named `name` - pub fn from_string(name: String) -> Self { - Self { - name: Box::from(name), - code: None, - } - } - - /// Consumes `self` and returns a builder for a file with the source text and the [`LineIndex`] copied - /// from `source`. #[must_use] - pub fn source_code(mut self, source: &SourceCode) -> Self { - self.set_source_code(source); + pub fn line_index(mut self, index: LineIndex) -> Self { + self.index = Some(index); self } - /// Copies the source text and [`LineIndex`] from `source`. - pub fn set_source_code(&mut self, source: &SourceCode) { - self.code = Some(FileSourceCode { - text: Box::from(source.text()), - index: source.index.clone(), - }); - } - - /// Consumes `self` and returns a builder for a file with the source text `text`. Builds the [`LineIndex`] from `text`. - #[must_use] - pub fn source_text(self, text: &str) -> Self { - self.source_code(&SourceCode::new(text, &LineIndex::from_source_text(text))) - } - - /// Consumes `self` and returns a builder for a file with the source text `text`. Builds the [`LineIndex`] from `text`. - #[must_use] - pub fn source_text_string(mut self, text: String) -> Self { - self.set_source_text_string(text); - self - } - - /// Copies the source text `text` and builds the [`LineIndex`] from `text`. - pub fn set_source_text_string(&mut self, text: String) { - self.code = Some(FileSourceCode { - index: LineIndex::from_source_text(&text), - text: Box::from(text), - }); + pub fn set_line_index(&mut self, index: LineIndex) { + self.index = Some(index); } /// Consumes `self` and returns the [`SourceFile`]. pub fn finish(self) -> SourceFile { + let index = if let Some(index) = self.index { + once_cell::sync::OnceCell::with_value(index) + } else { + once_cell::sync::OnceCell::new() + }; + SourceFile { inner: Arc::new(SourceFileInner { name: self.name, code: self.code, + line_index: index, }), } } @@ -211,7 +168,7 @@ impl Debug for SourceFile { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("SourceFile") .field("name", &self.name()) - .field("code", &self.source_code()) + .field("code", &self.source_text()) .finish() } } @@ -223,38 +180,57 @@ impl SourceFile { &self.inner.name } - /// Returns `Some` with the source code if set, or `None`. #[inline] - pub fn source_code(&self) -> Option { - self.inner.code.as_ref().map(|code| SourceCode { - text: &code.text, - index: &code.index, - }) + pub fn slice(&self, range: TextRange) -> &str { + &self.source_text()[range] + } + + pub fn to_source_code(&self) -> SourceCode { + SourceCode { + text: self.source_text(), + index: self.index(), + } + } + + fn index(&self) -> &LineIndex { + self.inner + .line_index + .get_or_init(|| LineIndex::from_source_text(self.source_text())) } /// Returns `Some` with the source text if set, or `None`. #[inline] - pub fn source_text(&self) -> Option<&str> { - self.inner.code.as_ref().map(|code| &*code.text) + pub fn source_text(&self) -> &str { + &self.inner.code } } -#[derive(Eq, PartialEq)] struct SourceFileInner { name: Box, - code: Option, + code: Box, + line_index: once_cell::sync::OnceCell, } -struct FileSourceCode { - text: Box, - index: LineIndex, -} - -impl PartialEq for FileSourceCode { +impl PartialEq for SourceFileInner { fn eq(&self, other: &Self) -> bool { - // It should be safe to assume that the index for two source files are identical - self.text == other.text + self.name == other.name && self.code == other.code } } -impl Eq for FileSourceCode {} +impl Eq for SourceFileInner {} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SourceLocation { + pub row: OneIndexed, + pub column: OneIndexed, +} + +impl Debug for SourceLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SourceLocation") + .field("row", &self.row.get()) + .field("column", &self.column.get()) + .finish() + } +} diff --git a/crates/ruff_python_ast/src/source_code/stylist.rs b/crates/ruff_python_ast/src/source_code/stylist.rs index 31735a86cf..dcd25b8b8d 100644 --- a/crates/ruff_python_ast/src/source_code/stylist.rs +++ b/crates/ruff_python_ast/src/source_code/stylist.rs @@ -4,7 +4,6 @@ use std::fmt; use std::ops::Deref; use once_cell::unsync::OnceCell; -use rustpython_parser::ast::Location; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; @@ -12,48 +11,21 @@ use ruff_rustpython::vendor; use crate::source_code::Locator; use crate::str::leading_quote; -use crate::types::Range; pub struct Stylist<'a> { locator: &'a Locator<'a>, - indentation: OnceCell, - indent_end: Option, - quote: OnceCell, - quote_range: Option, + indentation: Indentation, + quote: Quote, line_ending: OnceCell, } impl<'a> Stylist<'a> { pub fn indentation(&'a self) -> &'a Indentation { - self.indentation.get_or_init(|| { - if let Some(indent_end) = self.indent_end { - let start = Location::new(indent_end.row(), 0); - let whitespace = self.locator.slice(Range::new(start, indent_end)); - Indentation(whitespace.to_string()) - } else { - Indentation::default() - } - }) + &self.indentation } pub fn quote(&'a self) -> Quote { - *self.quote.get_or_init(|| { - self.quote_range - .and_then(|quote_range| { - let content = self.locator.slice(quote_range); - leading_quote(content) - }) - .map(|pattern| { - if pattern.contains('\'') { - Quote::Single - } else if pattern.contains('"') { - Quote::Double - } else { - unreachable!("Expected string to start with a valid quote prefix") - } - }) - .unwrap_or_default() - }) + self.quote } pub fn line_ending(&'a self) -> LineEnding { @@ -63,33 +35,60 @@ impl<'a> Stylist<'a> { } pub fn from_tokens(tokens: &[LexResult], locator: &'a Locator<'a>) -> Self { - let indent_end = tokens.iter().flatten().find_map(|(_, t, end)| { - if matches!(t, Tok::Indent) { - Some(*end) - } else { - None - } - }); - - let quote_range = tokens.iter().flatten().find_map(|(start, t, end)| match t { - Tok::String { - triple_quoted: false, - .. - } => Some(Range::new(*start, *end)), - _ => None, - }); + let indentation = detect_indention(tokens, locator); Self { locator, - indentation: OnceCell::default(), - indent_end, - quote_range, - quote: OnceCell::default(), + indentation, + quote: detect_quote(tokens, locator), line_ending: OnceCell::default(), } } } +fn detect_quote(tokens: &[LexResult], locator: &Locator) -> Quote { + let quote_range = tokens.iter().flatten().find_map(|(t, range)| match t { + Tok::String { + triple_quoted: false, + .. + } => Some(*range), + _ => None, + }); + + if let Some(quote_range) = quote_range { + let content = &locator.slice(quote_range); + if let Some(quotes) = leading_quote(content) { + return if quotes.contains('\'') { + Quote::Single + } else if quotes.contains('"') { + Quote::Double + } else { + unreachable!("Expected string to start with a valid quote prefix") + }; + } + } + + Quote::default() +} + +fn detect_indention(tokens: &[LexResult], locator: &Locator) -> Indentation { + let indent_range = tokens.iter().flatten().find_map(|(t, range)| { + if matches!(t, Tok::Indent) { + Some(range) + } else { + None + } + }); + + if let Some(indent_range) = indent_range { + let whitespace = locator.slice(*indent_range); + + Indentation(whitespace.to_string()) + } else { + Indentation::default() + } +} + /// The quotation style used in Python source code. #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub enum Quote { @@ -198,17 +197,18 @@ impl Deref for LineEnding { /// Detect the line ending style of the given contents. fn detect_line_ending(contents: &str) -> Option { - if let Some(position) = contents.find('\n') { - let position = position.saturating_sub(1); - return if let Some('\r') = contents.chars().nth(position) { + if let Some(position) = contents.find(['\n', '\r']) { + let bytes = contents.as_bytes(); + if bytes[position] == b'\n' { + Some(LineEnding::Lf) + } else if bytes.get(position.saturating_add(1)) == Some(&b'\n') { Some(LineEnding::CrLf) } else { - Some(LineEnding::Lf) - }; - } else if contents.find('\r').is_some() { - return Some(LineEnding::Cr); + Some(LineEnding::Cr) + } + } else { + None } - None } #[cfg(test)] diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index 000cc086e4..7da1af24fe 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -1,3 +1,5 @@ +use ruff_text_size::{TextLen, TextRange}; + /// See: const TRIPLE_QUOTE_STR_PREFIXES: &[&str] = &[ "u\"\"\"", "u'''", "r\"\"\"", "r'''", "U\"\"\"", "U'''", "R\"\"\"", "R'''", "\"\"\"", "'''", @@ -21,9 +23,19 @@ const SINGLE_QUOTE_SUFFIXES: &[&str] = &["\"", "'"]; /// Assumes that the string is a valid string literal, but does not verify that the string /// is a "simple" string literal (i.e., that it does not contain any implicit concatenations). pub fn raw_contents(contents: &str) -> Option<&str> { + let range = raw_contents_range(contents)?; + + Some(&contents[range]) +} + +pub fn raw_contents_range(contents: &str) -> Option { let leading_quote_str = leading_quote(contents)?; let trailing_quote_str = trailing_quote(contents)?; - Some(&contents[leading_quote_str.len()..contents.len() - trailing_quote_str.len()]) + + Some(TextRange::new( + leading_quote_str.text_len(), + contents.text_len() - trailing_quote_str.text_len(), + )) } /// Return the leading quote for a string or byte literal (e.g., `"""`). diff --git a/crates/ruff_python_ast/src/types.rs b/crates/ruff_python_ast/src/types.rs index 466c95fe41..6ed092d684 100644 --- a/crates/ruff_python_ast/src/types.rs +++ b/crates/ruff_python_ast/src/types.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use rustpython_parser::ast::{Expr, Located, Location, Stmt}; +use rustpython_parser::ast::{Expr, Stmt}; #[derive(Clone)] pub enum Node<'a> { @@ -8,33 +8,6 @@ pub enum Node<'a> { Expr(&'a Expr), } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -pub struct Range { - pub location: Location, - pub end_location: Location, -} - -impl Range { - pub const fn new(location: Location, end_location: Location) -> Self { - Self { - location, - end_location, - } - } -} - -impl From<&Located> for Range { - fn from(located: &Located) -> Self { - Range::new(located.location, located.end_location.unwrap()) - } -} - -impl From<&Box>> for Range { - fn from(located: &Box>) -> Self { - Range::new(located.location, located.end_location.unwrap()) - } -} - #[derive(Debug)] pub struct RefEquality<'a, T>(pub &'a T); diff --git a/crates/ruff_python_ast/src/typing.rs b/crates/ruff_python_ast/src/typing.rs index 6beb47a406..5c5128e808 100644 --- a/crates/ruff_python_ast/src/typing.rs +++ b/crates/ruff_python_ast/src/typing.rs @@ -1,11 +1,11 @@ use anyhow::Result; +use ruff_text_size::{TextLen, TextRange}; use rustpython_parser as parser; -use rustpython_parser::ast::{Expr, Location}; +use rustpython_parser::ast::Expr; use crate::relocate::relocate_expr; use crate::source_code::Locator; use crate::str; -use crate::types::Range; #[derive(is_macro::Is, Copy, Clone)] pub enum AnnotationKind { @@ -24,10 +24,11 @@ pub enum AnnotationKind { /// Parse a type annotation from a string. pub fn parse_type_annotation( value: &str, - range: Range, + range: TextRange, locator: &Locator, ) -> Result<(Expr, AnnotationKind)> { - let expression = locator.slice(range); + let expression = &locator.contents()[range]; + if str::raw_contents(expression).map_or(false, |body| body == value) { // The annotation is considered "simple" if and only if the raw representation (e.g., // `List[int]` within "List[int]") exactly matches the parsed representation. This @@ -37,10 +38,7 @@ pub fn parse_type_annotation( let expr = parser::parse_expression_located( value, "", - Location::new( - range.location.row(), - range.location.column() + leading_quote.len(), - ), + range.start() + leading_quote.text_len(), )?; Ok((expr, AnnotationKind::Simple)) } else { diff --git a/crates/ruff_python_ast/src/whitespace.rs b/crates/ruff_python_ast/src/whitespace.rs index 64bdc35c8c..8893c2d087 100644 --- a/crates/ruff_python_ast/src/whitespace.rs +++ b/crates/ruff_python_ast/src/whitespace.rs @@ -1,15 +1,13 @@ -use rustpython_parser::ast::{Located, Location}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::Located; use crate::source_code::Locator; -use crate::types::Range; /// Extract the leading indentation from a line. -pub fn indentation<'a, T>(locator: &'a Locator, located: &'a Located) -> Option<&'a str> { - let range = Range::from(located); - let indentation = locator.slice(Range::new( - Location::new(range.location.row(), 0), - Location::new(range.location.row(), range.location.column()), - )); +pub fn indentation<'a, T>(locator: &'a Locator, located: &Located) -> Option<&'a str> { + let line_start = locator.line_start(located.start()); + let indentation = &locator.contents()[TextRange::new(line_start, located.start())]; + if indentation.chars().all(char::is_whitespace) { Some(indentation) } else { diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index d2772f4358..4ca22b16b8 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -9,7 +9,7 @@ rust-version = { workspace = true } ruff_formatter = { path = "../ruff_formatter" } ruff_python_ast = { path = "../ruff_python_ast" } ruff_rustpython = { path = "../ruff_rustpython" } -ruff_text_size = { path = "../ruff_text_size" } +ruff_text_size = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } diff --git a/crates/ruff_python_formatter/src/cst/helpers.rs b/crates/ruff_python_formatter/src/cst/helpers.rs index e103e77827..0770992463 100644 --- a/crates/ruff_python_formatter/src/cst/helpers.rs +++ b/crates/ruff_python_formatter/src/cst/helpers.rs @@ -1,9 +1,5 @@ -use rustpython_parser::ast::Location; - -use ruff_python_ast::newlines::StrExt; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; -use ruff_text_size::TextRange; +use ruff_text_size::{TextLen, TextRange, TextSize}; /// Return `true` if the given string is a radix literal (e.g., `0b101`). pub fn is_radix_literal(content: &str) -> bool { @@ -17,26 +13,22 @@ pub fn is_radix_literal(content: &str) -> bool { /// Find the first token in the given range that satisfies the given predicate. pub fn find_tok( - location: Location, - end_location: Location, + range: TextRange, locator: &Locator, f: impl Fn(rustpython_parser::Tok) -> bool, -) -> (Location, Location) { - for (start, tok, end) in rustpython_parser::lexer::lex_located( - locator.slice(Range::new(location, end_location)), +) -> TextRange { + for (tok, tok_range) in rustpython_parser::lexer::lex_located( + &locator.contents()[range], rustpython_parser::Mode::Module, - location, + range.start(), ) .flatten() { if f(tok) { - return (start, end); + return tok_range; } } - unreachable!( - "Failed to find token in range {:?}..{:?}", - location, end_location - ) + unreachable!("Failed to find token in range {:?}", range) } /// Expand the range of a compound statement. @@ -44,19 +36,17 @@ pub fn find_tok( /// `location` is the start of the compound statement (e.g., the `if` in `if x:`). /// `end_location` is the end of the last statement in the body. pub fn expand_indented_block( - location: Location, - end_location: Location, + location: TextSize, + end_location: TextSize, locator: &Locator, -) -> (Location, Location) { +) -> TextRange { let contents = locator.contents(); - let start_index = locator.offset(location); - let end_index = locator.offset(end_location); // Find the colon, which indicates the end of the header. let mut nesting = 0; let mut colon = None; - for (start, tok, _end) in rustpython_parser::lexer::lex_located( - &contents[TextRange::new(start_index, end_index)], + for (tok, tok_range) in rustpython_parser::lexer::lex_located( + &contents[TextRange::new(location, end_location)], rustpython_parser::Mode::Module, location, ) @@ -64,7 +54,7 @@ pub fn expand_indented_block( { match tok { rustpython_parser::Tok::Colon if nesting == 0 => { - colon = Some(start); + colon = Some(tok_range.start()); break; } rustpython_parser::Tok::Lpar @@ -77,55 +67,68 @@ pub fn expand_indented_block( } } let colon_location = colon.unwrap(); - let colon_index = locator.offset(colon_location); // From here, we have two options: simple statement or compound statement. let indent = rustpython_parser::lexer::lex_located( - &contents[TextRange::new(colon_index, end_index)], + &contents[TextRange::new(colon_location, end_location)], rustpython_parser::Mode::Module, colon_location, ) .flatten() - .find_map(|(start, tok, _end)| match tok { - rustpython_parser::Tok::Indent => Some(start), + .find_map(|(tok, range)| match tok { + rustpython_parser::Tok::Indent => Some(range.end()), _ => None, }); - let Some(indent_location) = indent else { + let line_end = locator.line_end(end_location); + let Some(indent_end) = indent else { + // Simple statement: from the colon to the end of the line. - return (colon_location, Location::new(end_location.row() + 1, 0)); + return TextRange::new(colon_location, line_end); }; + let indent_width = indent_end - locator.line_start(indent_end); + // Compound statement: from the colon to the end of the block. - let mut offset = 0; - for (index, line) in contents[usize::from(end_index)..] - .universal_newlines() - .skip(1) - .enumerate() - { - if line.is_empty() { - continue; + // For each line that follows, check that there's no content up to the expected indent. + let mut offset = TextSize::default(); + let mut line_offset = TextSize::default(); + // Issue, body goes to far.. it includes the whole try including the catch + + let rest = &contents[usize::from(line_end)..]; + for (relative_offset, c) in rest.char_indices() { + if line_offset < indent_width && !c.is_whitespace() { + break; // Found end of block } - if line - .chars() - .take(indent_location.column()) - .all(char::is_whitespace) - { - offset = index + 1; - } else { - break; + match c { + '\n' | '\r' => { + // Ignore empty lines + if line_offset > TextSize::from(0) { + offset = TextSize::try_from(relative_offset).unwrap() + TextSize::from(1); + } + line_offset = TextSize::from(0); + } + _ => { + line_offset += c.text_len(); + } } } - let end_location = Location::new(end_location.row() + 1 + offset, 0); - (colon_location, end_location) + // Reached end of file + let end = if line_offset >= indent_width { + contents.text_len() + } else { + line_end + offset + }; + + TextRange::new(colon_location, end) } /// Return true if the `orelse` block of an `if` statement is an `elif` statement. pub fn is_elif(orelse: &[rustpython_parser::ast::Stmt], locator: &Locator) -> bool { if orelse.len() == 1 && matches!(orelse[0].node, rustpython_parser::ast::StmtKind::If { .. }) { - let contents = locator.after(orelse[0].location); + let contents = locator.after(orelse[0].start()); if contents.starts_with("elif") { return true; } diff --git a/crates/ruff_python_formatter/src/cst/mod.rs b/crates/ruff_python_formatter/src/cst/mod.rs index 7c5f41999e..4731174fe5 100644 --- a/crates/ruff_python_formatter/src/cst/mod.rs +++ b/crates/ruff_python_formatter/src/cst/mod.rs @@ -1,13 +1,14 @@ #![allow(clippy::derive_partial_eq_without_eq)] use std::iter; +use std::ops::Deref; use itertools::Itertools; -use rustpython_parser::ast::{Constant, Location}; -use rustpython_parser::Mode; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::Constant; +use rustpython_parser::{ast, Mode}; use ruff_python_ast::source_code::Locator; -use ruff_python_ast::types::Range; use crate::cst::helpers::{expand_indented_block, find_tok, is_elif}; use crate::trivia::{Parenthesize, Trivia}; @@ -19,42 +20,47 @@ type Ident = String; #[derive(Clone, Debug, PartialEq)] pub struct Located { - pub location: Location, - pub end_location: Option, + pub range: TextRange, pub node: T, pub trivia: Vec, pub parentheses: Parenthesize, } impl Located { - pub fn new(location: Location, end_location: Location, node: T) -> Self { + pub fn new(range: TextRange, node: T) -> Self { Self { - location, - end_location: Some(end_location), + range, node, trivia: Vec::new(), parentheses: Parenthesize::Never, } } + pub const fn range(&self) -> TextRange { + self.range + } + + pub const fn start(&self) -> TextSize { + self.range.start() + } + + pub const fn end(&self) -> TextSize { + self.range.end() + } + pub fn add_trivia(&mut self, trivia: Trivia) { self.trivia.push(trivia); } pub fn id(&self) -> usize { - self as *const _ as usize + std::ptr::addr_of!(self.node) as usize } } -impl From<&Located> for Range { - fn from(located: &Located) -> Self { - Self::new(located.location, located.end_location.unwrap()) - } -} - -impl From<&Box>> for Range { - fn from(located: &Box>) -> Self { - Self::new(located.location, located.end_location.unwrap()) +impl Deref for Located { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.node } } @@ -190,8 +196,7 @@ pub type Body = Located>; impl From<(Vec, &Locator<'_>)> for Body { fn from((body, locator): (Vec, &Locator)) -> Self { Body { - location: body.first().unwrap().location, - end_location: body.last().unwrap().end_location, + range: body.first().unwrap().range(), node: body .into_iter() .map(|node| (node, locator).into()) @@ -568,8 +573,7 @@ pub type Pattern = Located; impl From<(rustpython_parser::ast::Alias, &Locator<'_>)> for Alias { fn from((alias, _locator): (rustpython_parser::ast::Alias, &Locator)) -> Self { Alias { - location: alias.location, - end_location: alias.end_location, + range: alias.range(), node: AliasData { name: alias.node.name, asname: alias.node.asname, @@ -598,14 +602,13 @@ impl From<(rustpython_parser::ast::Excepthandler, &Locator<'_>)> for Excepthandl // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - excepthandler.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + excepthandler.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -616,8 +619,7 @@ impl From<(rustpython_parser::ast::Excepthandler, &Locator<'_>)> for Excepthandl }; Excepthandler { - location: excepthandler.location, - end_location: body.end_location, + range: TextRange::new(excepthandler.range.start(), body.end()), node: ExcepthandlerKind::ExceptHandler { type_: type_.map(|type_| Box::new((*type_, locator).into())), name, @@ -632,8 +634,7 @@ impl From<(rustpython_parser::ast::Excepthandler, &Locator<'_>)> for Excepthandl impl From<(rustpython_parser::ast::Pattern, &Locator<'_>)> for Pattern { fn from((pattern, locator): (rustpython_parser::ast::Pattern, &Locator)) -> Self { Pattern { - location: pattern.location, - end_location: pattern.end_location, + range: pattern.range(), node: match pattern.node { rustpython_parser::ast::PatternKind::MatchValue { value } => { PatternKind::MatchValue { @@ -706,14 +707,13 @@ impl From<(rustpython_parser::ast::MatchCase, &Locator<'_>)> for MatchCase { fn from((match_case, locator): (rustpython_parser::ast::MatchCase, &Locator)) -> Self { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - match_case.pattern.location, - match_case.body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + match_case.pattern.start(), + match_case.body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: match_case .body .into_iter() @@ -738,8 +738,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { fn from((stmt, locator): (rustpython_parser::ast::Stmt, &Locator)) -> Self { match stmt.node { rustpython_parser::ast::StmtKind::Expr { value } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Expr { value: Box::new((*value, locator).into()), }, @@ -747,15 +746,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { parentheses: Parenthesize::Never, }, rustpython_parser::ast::StmtKind::Pass => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Pass, trivia: vec![], parentheses: Parenthesize::Never, }, rustpython_parser::ast::StmtKind::Return { value } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Return { value: value.map(|v| (*v, locator).into()), }, @@ -767,8 +764,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { value, type_comment, } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Assign { targets: targets .into_iter() @@ -789,14 +785,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -807,8 +802,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }; Stmt { - location: stmt.location, - end_location: body.end_location, + range: TextRange::new(stmt.range.start(), body.end()), node: StmtKind::ClassDef { name, bases: bases @@ -832,14 +826,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { rustpython_parser::ast::StmtKind::If { test, body, orelse } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -852,8 +845,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { if orelse.is_empty() { // No `else` block. Stmt { - location: stmt.location, - end_location: body.end_location, + range: TextRange::new(stmt.range.start(), body.end()), node: StmtKind::If { test: Box::new((*test, locator).into()), body, @@ -874,8 +866,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }; Stmt { - location: stmt.location, - end_location: elif.end_location, + range: TextRange::new(stmt.range.start(), elif.end()), node: StmtKind::If { test: Box::new((*test, locator).into()), body, @@ -887,14 +878,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } } else { // Find the start and end of the `else`. - let (orelse_location, orelse_end_location) = expand_indented_block( - body.end_location.unwrap(), - orelse.last().unwrap().end_location.unwrap(), + let orelse_range = expand_indented_block( + body.end(), + orelse.last().unwrap().end(), locator, ); let orelse = Body { - location: orelse_location, - end_location: Some(orelse_end_location), + range: orelse_range, node: orelse .into_iter() .map(|node| (node, locator).into()) @@ -904,8 +894,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }; Stmt { - location: stmt.location, - end_location: orelse.end_location, + range: TextRange::new(stmt.range.start(), orelse.end()), node: StmtKind::If { test: Box::new((*test, locator).into()), body, @@ -919,8 +908,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } } rustpython_parser::ast::StmtKind::Assert { test, msg } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Assert { test: Box::new((*test, locator).into()), msg: msg.map(|node| Box::new((*node, locator).into())), @@ -938,14 +926,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -956,8 +943,12 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }; Stmt { - location: decorator_list.first().map_or(stmt.location, |d| d.location), - end_location: body.end_location, + range: TextRange::new( + decorator_list + .first() + .map_or(stmt.range.start(), ast::Located::start), + body.end(), + ), node: StmtKind::FunctionDef { name, args: Box::new((*args, locator).into()), @@ -983,14 +974,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -1001,10 +991,12 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }; Stmt { - location: decorator_list - .first() - .map_or(stmt.location, |expr| expr.location), - end_location: body.end_location, + range: TextRange::new( + decorator_list + .first() + .map_or(stmt.range.start(), |expr| expr.range.start()), + body.end(), + ), node: StmtKind::AsyncFunctionDef { name, args: Box::new((*args, locator).into()), @@ -1021,8 +1013,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } } rustpython_parser::ast::StmtKind::Delete { targets } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Delete { targets: targets .into_iter() @@ -1033,8 +1024,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { parentheses: Parenthesize::Never, }, rustpython_parser::ast::StmtKind::AugAssign { target, op, value } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::AugAssign { op: { let target_tok = match &op { @@ -1078,13 +1068,11 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { rustpython_parser::Tok::DoubleSlashEqual } }; - let (op_location, op_end_location) = find_tok( - target.end_location.unwrap(), - value.location, - locator, - |tok| tok == target_tok, - ); - Operator::new(op_location, op_end_location, (&op).into()) + let op_range = + find_tok(TextRange::new(target.end(), value.end()), locator, |tok| { + tok == target_tok + }); + Operator::new(op_range, (&op).into()) }, target: Box::new((*target, locator).into()), value: Box::new((*value, locator).into()), @@ -1098,8 +1086,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { value, simple, } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::AnnAssign { target: Box::new((*target, locator).into()), annotation: Box::new((*annotation, locator).into()), @@ -1118,14 +1105,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -1137,14 +1123,10 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { // Find the start and end of the `orelse`. let orelse = (!orelse.is_empty()).then(|| { - let (orelse_location, orelse_end_location) = expand_indented_block( - body.end_location.unwrap(), - orelse.last().unwrap().end_location.unwrap(), - locator, - ); + let orelse_range = + expand_indented_block(body.end(), orelse.last().unwrap().end(), locator); Body { - location: orelse_location, - end_location: Some(orelse_end_location), + range: orelse_range, node: orelse .into_iter() .map(|node| (node, locator).into()) @@ -1155,8 +1137,10 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }); Stmt { - location: stmt.location, - end_location: orelse.as_ref().unwrap_or(&body).end_location, + range: TextRange::new( + stmt.range.start(), + orelse.as_ref().unwrap_or(&body).end(), + ), node: StmtKind::For { target: Box::new((*target, locator).into()), iter: Box::new((*iter, locator).into()), @@ -1177,14 +1161,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -1196,14 +1179,10 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { // Find the start and end of the `orelse`. let orelse = (!orelse.is_empty()).then(|| { - let (orelse_location, orelse_end_location) = expand_indented_block( - body.end_location.unwrap(), - orelse.last().unwrap().end_location.unwrap(), - locator, - ); + let orelse_range = + expand_indented_block(body.end(), orelse.last().unwrap().end(), locator); Body { - location: orelse_location, - end_location: Some(orelse_end_location), + range: orelse_range, node: orelse .into_iter() .map(|node| (node, locator).into()) @@ -1214,8 +1193,10 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }); Stmt { - location: stmt.location, - end_location: orelse.as_ref().unwrap_or(&body).end_location, + range: TextRange::new( + stmt.range.start(), + orelse.as_ref().unwrap_or(&body).end(), + ), node: StmtKind::AsyncFor { target: Box::new((*target, locator).into()), iter: Box::new((*iter, locator).into()), @@ -1230,14 +1211,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { rustpython_parser::ast::StmtKind::While { test, body, orelse } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -1249,14 +1229,10 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { // Find the start and end of the `orelse`. let orelse = (!orelse.is_empty()).then(|| { - let (orelse_location, orelse_end_location) = expand_indented_block( - body.end_location.unwrap(), - orelse.last().unwrap().end_location.unwrap(), - locator, - ); + let orelse_range = + expand_indented_block(body.end(), orelse.last().unwrap().end(), locator); Body { - location: orelse_location, - end_location: Some(orelse_end_location), + range: orelse_range, node: orelse .into_iter() .map(|node| (node, locator).into()) @@ -1267,8 +1243,10 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }); Stmt { - location: stmt.location, - end_location: orelse.as_ref().unwrap_or(&body).end_location, + range: TextRange::new( + stmt.range.start(), + orelse.as_ref().unwrap_or(&body).end(), + ), node: StmtKind::While { test: Box::new((*test, locator).into()), body, @@ -1285,14 +1263,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -1303,8 +1280,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }; Stmt { - location: stmt.location, - end_location: body.end_location, + range: TextRange::new(stmt.range.start(), body.end()), node: StmtKind::With { items: items .into_iter() @@ -1324,14 +1300,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -1342,8 +1317,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { }; Stmt { - location: stmt.location, - end_location: body.end_location, + range: TextRange::new(stmt.range.start(), body.end()), node: StmtKind::AsyncWith { items: items .into_iter() @@ -1357,8 +1331,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } } rustpython_parser::ast::StmtKind::Match { subject, cases } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Match { subject: Box::new((*subject, locator).into()), cases: cases @@ -1370,8 +1343,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { parentheses: Parenthesize::Never, }, rustpython_parser::ast::StmtKind::Raise { exc, cause } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Raise { exc: exc.map(|exc| Box::new((*exc, locator).into())), cause: cause.map(|cause| Box::new((*cause, locator).into())), @@ -1387,14 +1359,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -1411,18 +1382,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { // Find the start and end of the `orelse`. let orelse = (!orelse.is_empty()).then(|| { - let (orelse_location, orelse_end_location) = expand_indented_block( - handlers - .last() - .map_or(body.end_location.unwrap(), |handler| { - handler.end_location.unwrap() - }), - orelse.last().unwrap().end_location.unwrap(), + let orelse_range = expand_indented_block( + handlers.last().map_or(body.end(), Located::end), + orelse.last().unwrap().end(), locator, ); Body { - location: orelse_location, - end_location: Some(orelse_end_location), + range: orelse_range, node: orelse .into_iter() .map(|node| (node, locator).into()) @@ -1434,21 +1400,16 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { // Find the start and end of the `finalbody`. let finalbody = (!finalbody.is_empty()).then(|| { - let (finalbody_location, finalbody_end_location) = expand_indented_block( + let finalbody_range = expand_indented_block( orelse.as_ref().map_or( - handlers - .last() - .map_or(body.end_location.unwrap(), |handler| { - handler.end_location.unwrap() - }), - |orelse| orelse.end_location.unwrap(), + handlers.last().map_or(body.end(), Located::end), + Located::end, ), - finalbody.last().unwrap().end_location.unwrap(), + finalbody.last().unwrap().end(), locator, ); Body { - location: finalbody_location, - end_location: Some(finalbody_end_location), + range: finalbody_range, node: finalbody .into_iter() .map(|node| (node, locator).into()) @@ -1460,19 +1421,14 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { let end_location = finalbody.as_ref().map_or( orelse.as_ref().map_or( - handlers - .last() - .map_or(body.end_location.unwrap(), |handler| { - handler.end_location.unwrap() - }), - |orelse| orelse.end_location.unwrap(), + handlers.last().map_or(body.end(), Located::end), + Located::end, ), - |finalbody| finalbody.end_location.unwrap(), + Located::end, ); Stmt { - location: stmt.location, - end_location: Some(end_location), + range: TextRange::new(stmt.range.start(), end_location), node: StmtKind::Try { body, handlers, @@ -1491,14 +1447,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } => { // Find the start and end of the `body`. let body = { - let (body_location, body_end_location) = expand_indented_block( - stmt.location, - body.last().unwrap().end_location.unwrap(), + let body_range = expand_indented_block( + stmt.range.start(), + body.last().unwrap().end(), locator, ); Body { - location: body_location, - end_location: Some(body_end_location), + range: body_range, node: body .into_iter() .map(|node| (node, locator).into()) @@ -1515,18 +1470,13 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { // Find the start and end of the `orelse`. let orelse = (!orelse.is_empty()).then(|| { - let (orelse_location, orelse_end_location) = expand_indented_block( - handlers - .last() - .map_or(body.end_location.unwrap(), |handler| { - handler.end_location.unwrap() - }), - orelse.last().unwrap().end_location.unwrap(), + let orelse_range = expand_indented_block( + handlers.last().map_or(body.end(), Located::end), + orelse.last().unwrap().end(), locator, ); Body { - location: orelse_location, - end_location: Some(orelse_end_location), + range: orelse_range, node: orelse .into_iter() .map(|node| (node, locator).into()) @@ -1538,21 +1488,16 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { // Find the start and end of the `finalbody`. let finalbody = (!finalbody.is_empty()).then(|| { - let (finalbody_location, finalbody_end_location) = expand_indented_block( + let finalbody_range = expand_indented_block( orelse.as_ref().map_or( - handlers - .last() - .map_or(body.end_location.unwrap(), |handler| { - handler.end_location.unwrap() - }), - |orelse| orelse.end_location.unwrap(), + handlers.last().map_or(body.end(), Located::end), + Located::end, ), - finalbody.last().unwrap().end_location.unwrap(), + finalbody.last().unwrap().end(), locator, ); Body { - location: finalbody_location, - end_location: Some(finalbody_end_location), + range: finalbody_range, node: finalbody .into_iter() .map(|node| (node, locator).into()) @@ -1564,19 +1509,14 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { let end_location = finalbody.as_ref().map_or( orelse.as_ref().map_or( - handlers - .last() - .map_or(body.end_location.unwrap(), |handler| { - handler.end_location.unwrap() - }), - |orelse| orelse.end_location.unwrap(), + handlers.last().map_or(body.end(), Located::end), + Located::end, ), - |finalbody| finalbody.end_location.unwrap(), + Located::end, ); Stmt { - location: stmt.location, - end_location: Some(end_location), + range: TextRange::new(stmt.range.start(), end_location), node: StmtKind::TryStar { body, handlers, @@ -1588,8 +1528,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { } } rustpython_parser::ast::StmtKind::Import { names } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Import { names: names .into_iter() @@ -1604,8 +1543,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { names, level, } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::ImportFrom { module, names: names @@ -1618,29 +1556,25 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { parentheses: Parenthesize::Never, }, rustpython_parser::ast::StmtKind::Global { names } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Global { names }, trivia: vec![], parentheses: Parenthesize::Never, }, rustpython_parser::ast::StmtKind::Nonlocal { names } => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Nonlocal { names }, trivia: vec![], parentheses: Parenthesize::Never, }, rustpython_parser::ast::StmtKind::Break => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Break, trivia: vec![], parentheses: Parenthesize::Never, }, rustpython_parser::ast::StmtKind::Continue => Stmt { - location: stmt.location, - end_location: stmt.end_location, + range: stmt.range, node: StmtKind::Continue, trivia: vec![], parentheses: Parenthesize::Never, @@ -1652,8 +1586,7 @@ impl From<(rustpython_parser::ast::Stmt, &Locator<'_>)> for Stmt { impl From<(rustpython_parser::ast::Keyword, &Locator<'_>)> for Keyword { fn from((keyword, locator): (rustpython_parser::ast::Keyword, &Locator)) -> Self { Keyword { - location: keyword.location, - end_location: keyword.end_location, + range: keyword.range(), node: KeywordData { arg: keyword.node.arg, value: (keyword.node.value, locator).into(), @@ -1667,8 +1600,7 @@ impl From<(rustpython_parser::ast::Keyword, &Locator<'_>)> for Keyword { impl From<(rustpython_parser::ast::Arg, &Locator<'_>)> for Arg { fn from((arg, locator): (rustpython_parser::ast::Arg, &Locator)) -> Self { Arg { - location: arg.location, - end_location: arg.end_location, + range: arg.range(), node: ArgData { arg: arg.node.arg, annotation: arg @@ -1740,8 +1672,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { fn from((expr, locator): (rustpython_parser::ast::Expr, &Locator)) -> Self { match expr.node { rustpython_parser::ast::ExprKind::Name { id, ctx } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Name { id, ctx: ctx.into(), @@ -1750,8 +1681,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::BoolOp { op, values } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::BoolOp { ops: values .iter() @@ -1761,13 +1691,12 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { rustpython_parser::ast::Boolop::And => rustpython_parser::Tok::And, rustpython_parser::ast::Boolop::Or => rustpython_parser::Tok::Or, }; - let (op_location, op_end_location) = find_tok( - left.end_location.unwrap(), - right.location, + let op_range = find_tok( + TextRange::new(left.end(), right.start()), locator, |tok| tok == target_tok, ); - BoolOp::new(op_location, op_end_location, (&op).into()) + BoolOp::new(op_range, (&op).into()) }) .collect(), values: values @@ -1779,8 +1708,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::NamedExpr { target, value } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::NamedExpr { target: Box::new((*target, locator).into()), value: Box::new((*value, locator).into()), @@ -1789,8 +1717,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::BinOp { left, op, right } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::BinOp { op: { let target_tok = match &op { @@ -1822,11 +1749,11 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { rustpython_parser::Tok::DoubleSlash } }; - let (op_location, op_end_location) = - find_tok(left.end_location.unwrap(), right.location, locator, |tok| { + let op_range = + find_tok(TextRange::new(left.end(), right.start()), locator, |tok| { tok == target_tok }); - Operator::new(op_location, op_end_location, (&op).into()) + Operator::new(op_range, (&op).into()) }, left: Box::new((*left, locator).into()), right: Box::new((*right, locator).into()), @@ -1835,8 +1762,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::UnaryOp { op, operand } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::UnaryOp { op: { let target_tok = match &op { @@ -1847,11 +1773,12 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { rustpython_parser::ast::Unaryop::UAdd => rustpython_parser::Tok::Plus, rustpython_parser::ast::Unaryop::USub => rustpython_parser::Tok::Minus, }; - let (op_location, op_end_location) = - find_tok(expr.location, operand.location, locator, |tok| { - tok == target_tok - }); - UnaryOp::new(op_location, op_end_location, (&op).into()) + let op_range = find_tok( + TextRange::new(expr.range.start(), operand.start()), + locator, + |tok| tok == target_tok, + ); + UnaryOp::new(op_range, (&op).into()) }, operand: Box::new((*operand, locator).into()), }, @@ -1859,8 +1786,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Lambda { args, body } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Lambda { args: Box::new((*args, locator).into()), body: Box::new((*body, locator).into()), @@ -1869,8 +1795,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::IfExp { test, body, orelse } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::IfExp { test: Box::new((*test, locator).into()), body: Box::new((*body, locator).into()), @@ -1880,8 +1805,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Dict { keys, values } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Dict { keys: keys .into_iter() @@ -1896,8 +1820,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Set { elts } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Set { elts: elts .into_iter() @@ -1908,8 +1831,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::ListComp { elt, generators } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::ListComp { elt: Box::new((*elt, locator).into()), generators: generators @@ -1921,8 +1843,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::SetComp { elt, generators } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::SetComp { elt: Box::new((*elt, locator).into()), generators: generators @@ -1938,8 +1859,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { value, generators, } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::DictComp { key: Box::new((*key, locator).into()), value: Box::new((*value, locator).into()), @@ -1952,8 +1872,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::GeneratorExp { elt, generators } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::GeneratorExp { elt: Box::new((*elt, locator).into()), generators: generators @@ -1965,8 +1884,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Await { value } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Await { value: Box::new((*value, locator).into()), }, @@ -1974,8 +1892,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Yield { value } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Yield { value: value.map(|v| Box::new((*v, locator).into())), }, @@ -1983,8 +1900,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::YieldFrom { value } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::YieldFrom { value: Box::new((*value, locator).into()), }, @@ -1996,8 +1912,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { ops, comparators, } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Compare { ops: iter::once(left.as_ref()) .chain(comparators.iter()) @@ -2028,13 +1943,12 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { // TODO(charlie): Break this into two tokens. rustpython_parser::ast::Cmpop::NotIn => rustpython_parser::Tok::In, }; - let (op_location, op_end_location) = find_tok( - left.end_location.unwrap(), - right.location, + let op_range = find_tok( + TextRange::new(left.end(), right.start()), locator, |tok| tok == target_tok, ); - CmpOp::new(op_location, op_end_location, (&op).into()) + CmpOp::new(op_range, (&op).into()) }) .collect(), left: Box::new((*left, locator).into()), @@ -2051,8 +1965,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { args, keywords, } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Call { func: Box::new((*func, locator).into()), args: args @@ -2072,8 +1985,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { conversion, format_spec, } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::FormattedValue { value: Box::new((*value, locator).into()), conversion, @@ -2083,8 +1995,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::JoinedStr { values } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::JoinedStr { values: values .into_iter() @@ -2095,15 +2006,13 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Constant { value, kind } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Constant { value, kind }, trivia: vec![], parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Attribute { value, attr, ctx } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Attribute { value: Box::new((*value, locator).into()), attr, @@ -2113,8 +2022,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Subscript { value, slice, ctx } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Subscript { value: Box::new((*value, locator).into()), slice: Box::new((*slice, locator).into()), @@ -2124,8 +2032,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Starred { value, ctx } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Starred { value: Box::new((*value, locator).into()), ctx: ctx.into(), @@ -2134,8 +2041,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::List { elts, ctx } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::List { elts: elts .into_iter() @@ -2147,8 +2053,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { parentheses: Parenthesize::Never, }, rustpython_parser::ast::ExprKind::Tuple { elts, ctx } => Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Tuple { elts: elts .into_iter() @@ -2162,9 +2067,9 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { rustpython_parser::ast::ExprKind::Slice { lower, upper, step } => { // Locate the colon tokens, which indicate the number of index segments. let tokens = rustpython_parser::lexer::lex_located( - locator.slice(Range::new(expr.location, expr.end_location.unwrap())), + &locator.contents()[expr.range], Mode::Module, - expr.location, + expr.range.start(), ); // Find the first and (if it exists) second colon in the slice, avoiding any @@ -2173,7 +2078,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { let mut second_colon = None; let mut lambda = 0; let mut nesting = 0; - for (start, tok, ..) in tokens.flatten() { + for (tok, range) in tokens.flatten() { match tok { rustpython_parser::Tok::Lambda if nesting == 0 => lambda += 1, rustpython_parser::Tok::Colon if nesting == 0 => { @@ -2181,9 +2086,9 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { lambda -= 1; } else { if first_colon.is_none() { - first_colon = Some(start); + first_colon = Some(range.start()); } else { - second_colon = Some(start); + second_colon = Some(range.start()); break; } } @@ -2199,23 +2104,23 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { } let lower = SliceIndex::new( - expr.location, - first_colon.unwrap(), + TextRange::new(expr.range.start(), first_colon.unwrap()), lower.map_or(SliceIndexKind::Empty, |node| SliceIndexKind::Index { value: Box::new((*node, locator).into()), }), ); let upper = SliceIndex::new( - first_colon.unwrap(), - second_colon.unwrap_or(expr.end_location.unwrap()), + TextRange::new( + first_colon.unwrap(), + second_colon.unwrap_or(expr.range.end()), + ), upper.map_or(SliceIndexKind::Empty, |node| SliceIndexKind::Index { value: Box::new((*node, locator).into()), }), ); let step = second_colon.map(|second_colon| { SliceIndex::new( - second_colon, - expr.end_location.unwrap(), + TextRange::new(second_colon, expr.range.end()), step.map_or(SliceIndexKind::Empty, |node| SliceIndexKind::Index { value: Box::new((*node, locator).into()), }), @@ -2223,8 +2128,7 @@ impl From<(rustpython_parser::ast::Expr, &Locator<'_>)> for Expr { }); Expr { - location: expr.location, - end_location: expr.end_location, + range: expr.range, node: ExprKind::Slice { lower, upper, step }, trivia: vec![], parentheses: Parenthesize::Never, diff --git a/crates/ruff_python_formatter/src/format/builders.rs b/crates/ruff_python_formatter/src/format/builders.rs index 0cde20034a..31b156c109 100644 --- a/crates/ruff_python_formatter/src/format/builders.rs +++ b/crates/ruff_python_formatter/src/format/builders.rs @@ -1,6 +1,5 @@ use ruff_formatter::prelude::*; use ruff_formatter::{write, Format}; -use ruff_python_ast::types::Range; use ruff_text_size::{TextRange, TextSize}; use crate::context::ASTFormatContext; @@ -68,25 +67,22 @@ pub fn statements(suite: &[Stmt]) -> Statements { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Literal { - range: Range, + range: TextRange, } impl Format> for Literal { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let text = f.context().contents(); - let locator = f.context().locator(); - let start_index = locator.offset(self.range.location); - let end_index = locator.offset(self.range.end_location); f.write_element(FormatElement::StaticTextSlice { text, - range: TextRange::new(start_index, end_index), + range: self.range, }) } } #[inline] -pub const fn literal(range: Range) -> Literal { +pub const fn literal(range: TextRange) -> Literal { Literal { range } } diff --git a/crates/ruff_python_formatter/src/format/expr.rs b/crates/ruff_python_formatter/src/format/expr.rs index ef95842632..80f82cc15a 100644 --- a/crates/ruff_python_formatter/src/format/expr.rs +++ b/crates/ruff_python_formatter/src/format/expr.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::Constant; use ruff_formatter::prelude::*; use ruff_formatter::{format_args, write}; -use ruff_python_ast::types::Range; use ruff_text_size::TextSize; use crate::context::ASTFormatContext; @@ -39,7 +38,7 @@ fn format_name( expr: &Expr, _id: &str, ) -> FormatResult<()> { - write!(f, [literal(Range::from(expr))])?; + write!(f, [literal(expr.range())])?; write!(f, [end_of_line_comments(expr)])?; Ok(()) } @@ -97,62 +96,68 @@ fn format_tuple( } else if !elts.is_empty() { write!( f, - [group(&format_with(|f| { - if expr.parentheses.is_if_expanded() { - write!(f, [if_group_breaks(&text("("))])?; - } - if matches!( - expr.parentheses, - Parenthesize::IfExpanded | Parenthesize::Always - ) { - write!( - f, - [soft_block_indent(&format_with(|f| { - let magic_trailing_comma = - expr.trivia.iter().any(|c| c.kind.is_magic_trailing_comma()); - let is_unbroken = - expr.location.row() == expr.end_location.unwrap().row(); - if magic_trailing_comma { - write!(f, [expand_parent()])?; - } - for (i, elt) in elts.iter().enumerate() { - write!(f, [elt.format()])?; - if i < elts.len() - 1 { - write!(f, [text(",")])?; - write!(f, [soft_line_break_or_space()])?; - } else { - if magic_trailing_comma || is_unbroken { - write!(f, [if_group_breaks(&text(","))])?; - } - } - } - Ok(()) - }))] - )?; - } else { - let magic_trailing_comma = - expr.trivia.iter().any(|c| c.kind.is_magic_trailing_comma()); - let is_unbroken = expr.location.row() == expr.end_location.unwrap().row(); - if magic_trailing_comma { - write!(f, [expand_parent()])?; + [group(&format_with( + |f: &mut Formatter>| { + if expr.parentheses.is_if_expanded() { + write!(f, [if_group_breaks(&text("("))])?; } - for (i, elt) in elts.iter().enumerate() { - write!(f, [elt.format()])?; - if i < elts.len() - 1 { - write!(f, [text(",")])?; - write!(f, [soft_line_break_or_space()])?; - } else { - if magic_trailing_comma || is_unbroken { - write!(f, [if_group_breaks(&text(","))])?; + if matches!( + expr.parentheses, + Parenthesize::IfExpanded | Parenthesize::Always + ) { + write!( + f, + [soft_block_indent(&format_with( + |f: &mut Formatter>| { + let magic_trailing_comma = expr + .trivia + .iter() + .any(|c| c.kind.is_magic_trailing_comma()); + let is_unbroken = + !f.context().locator().contains_line_break(expr.range()); + if magic_trailing_comma { + write!(f, [expand_parent()])?; + } + for (i, elt) in elts.iter().enumerate() { + write!(f, [elt.format()])?; + if i < elts.len() - 1 { + write!(f, [text(",")])?; + write!(f, [soft_line_break_or_space()])?; + } else { + if magic_trailing_comma || is_unbroken { + write!(f, [if_group_breaks(&text(","))])?; + } + } + } + Ok(()) + } + ))] + )?; + } else { + let magic_trailing_comma = + expr.trivia.iter().any(|c| c.kind.is_magic_trailing_comma()); + let is_unbroken = !f.context().locator().contains_line_break(expr.range()); + if magic_trailing_comma { + write!(f, [expand_parent()])?; + } + for (i, elt) in elts.iter().enumerate() { + write!(f, [elt.format()])?; + if i < elts.len() - 1 { + write!(f, [text(",")])?; + write!(f, [soft_line_break_or_space()])?; + } else { + if magic_trailing_comma || is_unbroken { + write!(f, [if_group_breaks(&text(","))])?; + } } } } + if expr.parentheses.is_if_expanded() { + write!(f, [if_group_breaks(&text(")"))])?; + } + Ok(()) } - if expr.parentheses.is_if_expanded() { - write!(f, [if_group_breaks(&text(")"))])?; - } - Ok(()) - }))] + ))] )?; } Ok(()) @@ -577,7 +582,7 @@ fn format_joined_str( expr: &Expr, _values: &[Expr], ) -> FormatResult<()> { - write!(f, [literal(Range::from(expr))])?; + write!(f, [literal(expr.range())])?; write!(f, [end_of_line_comments(expr)])?; Ok(()) } @@ -598,11 +603,11 @@ fn format_constant( write!(f, [text("False")])?; } } - Constant::Int(_) => write!(f, [int_literal(Range::from(expr))])?, - Constant::Float(_) => write!(f, [float_literal(Range::from(expr))])?, + Constant::Int(_) => write!(f, [int_literal(expr.range())])?, + Constant::Float(_) => write!(f, [float_literal(expr.range())])?, Constant::Str(_) => write!(f, [string_literal(expr)])?, Constant::Bytes(_) => write!(f, [string_literal(expr)])?, - Constant::Complex { .. } => write!(f, [complex_literal(Range::from(expr))])?, + Constant::Complex { .. } => write!(f, [complex_literal(expr.range())])?, Constant::Tuple(_) => unreachable!("Constant::Tuple should be handled by format_tuple"), } write!(f, [end_of_line_comments(expr)])?; diff --git a/crates/ruff_python_formatter/src/format/numbers.rs b/crates/ruff_python_formatter/src/format/numbers.rs index eb315f1b19..84066e62a3 100644 --- a/crates/ruff_python_formatter/src/format/numbers.rs +++ b/crates/ruff_python_formatter/src/format/numbers.rs @@ -1,8 +1,7 @@ -use rustpython_parser::ast::Location; +use std::ops::{Add, Sub}; use ruff_formatter::prelude::*; use ruff_formatter::{write, Format}; -use ruff_python_ast::types::Range; use ruff_text_size::{TextRange, TextSize}; use crate::context::ASTFormatContext; @@ -10,17 +9,14 @@ use crate::format::builders::literal; #[derive(Debug, Copy, Clone, Eq, PartialEq)] struct FloatAtom { - range: Range, + range: TextRange, } impl Format> for FloatAtom { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let locator = f.context().locator(); let contents = f.context().contents(); - let start_index = locator.offset(self.range.location); - let end_index = locator.offset(self.range.end_location); - let content = &contents[TextRange::new(start_index, end_index)]; + let content = &contents[self.range]; if let Some(dot_index) = content.find('.') { let integer = &content[..dot_index]; let fractional = &content[dot_index + 1..]; @@ -30,12 +26,11 @@ impl Format> for FloatAtom { } else { write!( f, - [literal(Range::new( - self.range.location, - Location::new( - self.range.location.row(), - self.range.location.column() + dot_index - ), + [literal(TextRange::new( + self.range.start(), + self.range + .start() + .add(TextSize::try_from(dot_index).unwrap()) ))] )?; } @@ -47,12 +42,11 @@ impl Format> for FloatAtom { } else { write!( f, - [literal(Range::new( - Location::new( - self.range.location.row(), - self.range.location.column() + dot_index + 1 - ), - self.range.end_location + [literal(TextRange::new( + self.range + .start() + .add(TextSize::try_from(dot_index + 1).unwrap()), + self.range.end() ))] )?; } @@ -65,35 +59,31 @@ impl Format> for FloatAtom { } #[inline] -const fn float_atom(range: Range) -> FloatAtom { +const fn float_atom(range: TextRange) -> FloatAtom { FloatAtom { range } } #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct FloatLiteral { - range: Range, + range: TextRange, } impl Format> for FloatLiteral { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let locator = f.context().locator(); let contents = f.context().contents(); - let start_index = locator.offset(self.range.location); - let end_index = locator.offset(self.range.end_location); - let content = &contents[TextRange::new(start_index, end_index)]; + let content = &contents[self.range]; // Scientific notation if let Some(exponent_index) = content.find('e').or_else(|| content.find('E')) { // Write the base. write!( f, - [float_atom(Range::new( - self.range.location, - Location::new( - self.range.location.row(), - self.range.location.column() + exponent_index - ), + [float_atom(TextRange::new( + self.range.start(), + self.range + .start() + .add(TextSize::try_from(exponent_index).unwrap()) ))] )?; @@ -103,12 +93,11 @@ impl Format> for FloatLiteral { let plus = content[exponent_index + 1..].starts_with('+'); write!( f, - [literal(Range::new( - Location::new( - self.range.location.row(), - self.range.location.column() + exponent_index + 1 + usize::from(plus) - ), - self.range.end_location + [literal(TextRange::new( + self.range + .start() + .add(TextSize::try_from(exponent_index + 1 + usize::from(plus)).unwrap()), + self.range.end() ))] )?; } else { @@ -120,24 +109,21 @@ impl Format> for FloatLiteral { } #[inline] -pub const fn float_literal(range: Range) -> FloatLiteral { +pub const fn float_literal(range: TextRange) -> FloatLiteral { FloatLiteral { range } } #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct IntLiteral { - range: Range, + range: TextRange, } impl Format> for IntLiteral { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let locator = f.context().locator(); let contents = f.context().contents(); - let start_index = locator.offset(self.range.location); - let end_index = locator.offset(self.range.end_location); for prefix in ["0b", "0B", "0o", "0O", "0x", "0X"] { - let content = &contents[TextRange::new(start_index, end_index)]; + let content = &contents[self.range]; if content.starts_with(prefix) { // In each case, the prefix must be lowercase, while the suffix must be uppercase. let prefix = &content[..prefix.len()]; @@ -170,35 +156,28 @@ impl Format> for IntLiteral { } #[inline] -pub const fn int_literal(range: Range) -> IntLiteral { +pub const fn int_literal(range: TextRange) -> IntLiteral { IntLiteral { range } } #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct ComplexLiteral { - range: Range, + range: TextRange, } impl Format> for ComplexLiteral { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let locator = f.context().locator(); let contents = f.context().contents(); - let start_index = locator.offset(self.range.location); - let end_index = locator.offset(self.range.end_location); - - let content = &contents[TextRange::new(start_index, end_index)]; + let content = &contents[self.range]; if content.ends_with('j') { write!(f, [literal(self.range)])?; } else if content.ends_with('J') { write!( f, - [literal(Range::new( - self.range.location, - Location::new( - self.range.end_location.row(), - self.range.end_location.column() - 1 - ), + [literal(TextRange::new( + self.range.start(), + self.range.end().sub(TextSize::from(1)) ))] )?; write!(f, [text("j")])?; @@ -211,6 +190,6 @@ impl Format> for ComplexLiteral { } #[inline] -pub const fn complex_literal(range: Range) -> ComplexLiteral { +pub const fn complex_literal(range: TextRange) -> ComplexLiteral { ComplexLiteral { range } } diff --git a/crates/ruff_python_formatter/src/format/strings.rs b/crates/ruff_python_formatter/src/format/strings.rs index 499e72c49b..c008824832 100644 --- a/crates/ruff_python_formatter/src/format/strings.rs +++ b/crates/ruff_python_formatter/src/format/strings.rs @@ -3,7 +3,6 @@ use rustpython_parser::{Mode, Tok}; use ruff_formatter::prelude::*; use ruff_formatter::{write, Format}; use ruff_python_ast::str::{leading_quote, trailing_quote}; -use ruff_python_ast::types::Range; use ruff_text_size::{TextRange, TextSize}; use crate::context::ASTFormatContext; @@ -11,18 +10,15 @@ use crate::cst::Expr; #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct StringLiteralPart { - range: Range, + range: TextRange, } impl Format> for StringLiteralPart { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let locator = f.context().locator(); let contents = f.context().contents(); - let start_index = locator.offset(self.range.location); - let end_index = locator.offset(self.range.end_location); // Extract leading and trailing quotes. - let contents = &contents[TextRange::new(start_index, end_index)]; + let contents = &contents[self.range]; let leading_quote = leading_quote(contents).unwrap(); let trailing_quote = trailing_quote(contents).unwrap(); let body = &contents[leading_quote.len()..contents.len() - trailing_quote.len()]; @@ -114,7 +110,7 @@ impl Format> for StringLiteralPart { } #[inline] -pub const fn string_literal_part(range: Range) -> StringLiteralPart { +pub const fn string_literal_part(range: TextRange) -> StringLiteralPart { StringLiteralPart { range } } @@ -129,12 +125,12 @@ impl Format> for StringLiteral<'_> { // TODO(charlie): This tokenization needs to happen earlier, so that we can attach // comments to individual string literals. - let contents = f.context().locator().slice(expr); - let elts = rustpython_parser::lexer::lex_located(contents, Mode::Module, expr.location) + let contents = f.context().locator().slice(expr.range()); + let elts = rustpython_parser::lexer::lex_located(contents, Mode::Module, expr.start()) .flatten() - .filter_map(|(start, tok, end)| { + .filter_map(|(tok, range)| { if matches!(tok, Tok::String { .. }) { - Some(Range::new(start, end)) + Some(range) } else { None } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 9211c77f34..90ceed1c1a 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -28,7 +28,7 @@ pub fn fmt(contents: &str) -> Result> { let tokens: Vec = ruff_rustpython::tokenize(contents); // Extract trivia. - let trivia = trivia::extract_trivia_tokens(&tokens); + let trivia = trivia::extract_trivia_tokens(&tokens, contents); // Parse the AST. let python_ast = ruff_rustpython::parse_program_tokens(tokens, "")?; diff --git a/crates/ruff_python_formatter/src/parentheses.rs b/crates/ruff_python_formatter/src/parentheses.rs index 5ad1e15250..752c7cfdf2 100644 --- a/crates/ruff_python_formatter/src/parentheses.rs +++ b/crates/ruff_python_formatter/src/parentheses.rs @@ -155,7 +155,7 @@ impl<'a> Visitor<'a> for ParenthesesNormalizer<'_> { }, ) { // TODO(charlie): Encode this in the AST via separate node types. - if !is_radix_literal(self.locator.slice(&**value)) { + if !is_radix_literal(self.locator.slice(value.range())) { value.parentheses = Parenthesize::Always; } } diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index cc1902e782..7f553e50bf 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -1,16 +1,15 @@ +use ruff_text_size::{TextRange, TextSize}; use rustc_hash::FxHashMap; -use rustpython_parser::ast::Location; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; - -use ruff_python_ast::types::Range; +use std::ops::Add; use crate::cst::{ Alias, Arg, Body, BoolOp, CmpOp, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, Operator, Pattern, PatternKind, SliceIndex, SliceIndexKind, Stmt, StmtKind, UnaryOp, }; -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum Node<'a> { Alias(&'a Alias), Arg(&'a Arg), @@ -48,41 +47,41 @@ impl Node<'_> { } } - pub fn location(&self) -> Location { + pub fn start(&self) -> TextSize { match self { - Node::Alias(node) => node.location, - Node::Arg(node) => node.location, - Node::Body(node) => node.location, - Node::BoolOp(node) => node.location, - Node::CmpOp(node) => node.location, - Node::Excepthandler(node) => node.location, - Node::Expr(node) => node.location, - Node::Keyword(node) => node.location, + Node::Alias(node) => node.start(), + Node::Arg(node) => node.start(), + Node::Body(node) => node.start(), + Node::BoolOp(node) => node.start(), + Node::CmpOp(node) => node.start(), + Node::Excepthandler(node) => node.start(), + Node::Expr(node) => node.start(), + Node::Keyword(node) => node.start(), Node::Mod(..) => unreachable!("Node::Mod cannot be a child node"), - Node::Operator(node) => node.location, - Node::Pattern(node) => node.location, - Node::SliceIndex(node) => node.location, - Node::Stmt(node) => node.location, - Node::UnaryOp(node) => node.location, + Node::Operator(node) => node.start(), + Node::Pattern(node) => node.start(), + Node::SliceIndex(node) => node.start(), + Node::Stmt(node) => node.start(), + Node::UnaryOp(node) => node.start(), } } - pub fn end_location(&self) -> Location { + pub fn end(&self) -> TextSize { match self { - Node::Alias(node) => node.end_location.unwrap(), - Node::Arg(node) => node.end_location.unwrap(), - Node::Body(node) => node.end_location.unwrap(), - Node::BoolOp(node) => node.end_location.unwrap(), - Node::CmpOp(node) => node.end_location.unwrap(), - Node::Excepthandler(node) => node.end_location.unwrap(), - Node::Expr(node) => node.end_location.unwrap(), - Node::Keyword(node) => node.end_location.unwrap(), + Node::Alias(node) => node.end(), + Node::Arg(node) => node.end(), + Node::Body(node) => node.end(), + Node::BoolOp(node) => node.end(), + Node::CmpOp(node) => node.end(), + Node::Excepthandler(node) => node.end(), + Node::Expr(node) => node.end(), + Node::Keyword(node) => node.end(), Node::Mod(..) => unreachable!("Node::Mod cannot be a child node"), - Node::Operator(node) => node.end_location.unwrap(), - Node::Pattern(node) => node.end_location.unwrap(), - Node::SliceIndex(node) => node.end_location.unwrap(), - Node::Stmt(node) => node.end_location.unwrap(), - Node::UnaryOp(node) => node.end_location.unwrap(), + Node::Operator(node) => node.end(), + Node::Pattern(node) => node.end(), + Node::SliceIndex(node) => node.end(), + Node::Stmt(node) => node.end(), + Node::UnaryOp(node) => node.end(), } } } @@ -98,11 +97,20 @@ pub enum TriviaTokenKind { #[derive(Clone, Debug, PartialEq, Eq)] pub struct TriviaToken { - pub start: Location, - pub end: Location, + pub range: TextRange, pub kind: TriviaTokenKind, } +impl TriviaToken { + pub const fn start(&self) -> TextSize { + self.range.start() + } + + pub const fn end(&self) -> TextSize { + self.range.end() + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, is_macro::Is)] pub enum TriviaKind { /// A Comment that is separated by at least one line break from the @@ -115,7 +123,7 @@ pub enum TriviaKind { /// # This is an own-line comment. /// b = 2 /// ``` - OwnLineComment(Range), + OwnLineComment(TextRange), /// A comment that is on the same line as the preceding token. /// /// # Examples @@ -126,7 +134,7 @@ pub enum TriviaKind { /// a = 1 # This is an end-of-line comment. /// b = 2 /// ``` - EndOfLineComment(Range), + EndOfLineComment(TextRange), MagicTrailingComma, EmptyLine, Parentheses, @@ -167,11 +175,11 @@ impl Trivia { relationship, }, TriviaTokenKind::OwnLineComment => Self { - kind: TriviaKind::OwnLineComment(Range::new(token.start, token.end)), + kind: TriviaKind::OwnLineComment(token.range), relationship, }, TriviaTokenKind::EndOfLineComment => Self { - kind: TriviaKind::EndOfLineComment(Range::new(token.start, token.end)), + kind: TriviaKind::EndOfLineComment(token.range), relationship, }, TriviaTokenKind::Parentheses => Self { @@ -182,30 +190,53 @@ impl Trivia { } } -pub fn extract_trivia_tokens(lxr: &[LexResult]) -> Vec { +pub fn extract_trivia_tokens(lxr: &[LexResult], text: &str) -> Vec { let mut tokens = vec![]; - let mut prev_tok: Option<(&Location, &Tok, &Location)> = None; - let mut prev_non_newline_tok: Option<(&Location, &Tok, &Location)> = None; - let mut prev_semantic_tok: Option<(&Location, &Tok, &Location)> = None; + let mut prev_end = TextSize::default(); + let mut prev_tok: Option<(&Tok, TextRange)> = None; + let mut prev_semantic_tok: Option<(&Tok, TextRange)> = None; let mut parens = vec![]; - for (start, tok, end) in lxr.iter().flatten() { + + for (tok, range) in lxr.iter().flatten() { // Add empty lines. - if let Some((.., prev)) = prev_non_newline_tok { - for row in prev.row() + 1..start.row() { + let trivia = &text[TextRange::new(prev_end, range.start())]; + let bytes = trivia.as_bytes(); + + let mut bytes_iter = bytes.iter().enumerate(); + + let mut after_new_line = + matches!(prev_tok, Some((Tok::Newline | Tok::NonLogicalNewline, _))); + + while let Some((index, byte)) = bytes_iter.next() { + let len = match byte { + b'\r' if bytes.get(index + 1) == Some(&b'\n') => { + bytes_iter.next(); + TextSize::from(2) + } + b'\n' | b'\r' => TextSize::from(1), + _ => { + // Must be whitespace or the parser would generate a token + continue; + } + }; + + if after_new_line { + let new_line_start = prev_end.add(TextSize::try_from(index).unwrap()); tokens.push(TriviaToken { - start: Location::new(row, 0), - end: Location::new(row + 1, 0), + range: TextRange::new(new_line_start, new_line_start.add(len)), kind: TriviaTokenKind::EmptyLine, }); + } else { + after_new_line = true; } } // Add comments. if let Tok::Comment(_) = tok { tokens.push(TriviaToken { - start: *start, - end: *end, - kind: if prev_non_newline_tok.map_or(true, |(prev, ..)| prev.row() < start.row()) { + range: *range, + // Used to use prev_non-newline_tok + kind: if after_new_line || prev_tok.is_none() { TriviaTokenKind::OwnLineComment } else { TriviaTokenKind::EndOfLineComment @@ -218,11 +249,10 @@ pub fn extract_trivia_tokens(lxr: &[LexResult]) -> Vec { tok, Tok::Rpar | Tok::Rsqb | Tok::Rbrace | Tok::Equal | Tok::Newline ) { - if let Some((prev_start, prev_tok, prev_end)) = prev_semantic_tok { + if let Some((prev_tok, prev_range)) = prev_semantic_tok { if prev_tok == &Tok::Comma { tokens.push(TriviaToken { - start: *prev_start, - end: *prev_end, + range: prev_range, kind: TriviaTokenKind::MagicTrailingComma, }); } @@ -230,7 +260,7 @@ pub fn extract_trivia_tokens(lxr: &[LexResult]) -> Vec { } if matches!(tok, Tok::Lpar) { - if prev_tok.map_or(true, |(_, prev_tok, _)| { + if prev_tok.map_or(true, |(prev_tok, _)| { !matches!( prev_tok, Tok::Name { .. } @@ -240,40 +270,36 @@ pub fn extract_trivia_tokens(lxr: &[LexResult]) -> Vec { | Tok::String { .. } ) }) { - parens.push((start, true)); + parens.push((range.start(), true)); } else { - parens.push((start, false)); + parens.push((range.start(), false)); } } else if matches!(tok, Tok::Rpar) { let (start, explicit) = parens.pop().unwrap(); if explicit { tokens.push(TriviaToken { - start: *start, - end: *end, + range: TextRange::new(start, range.end()), kind: TriviaTokenKind::Parentheses, }); } } - prev_tok = Some((start, tok, end)); - - // Track the most recent non-whitespace token. - if !matches!(tok, Tok::Newline | Tok::NonLogicalNewline) { - prev_non_newline_tok = Some((start, tok, end)); - } + prev_tok = Some((tok, *range)); // Track the most recent semantic token. if !matches!( tok, Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..) ) { - prev_semantic_tok = Some((start, tok, end)); + prev_semantic_tok = Some((tok, *range)); } + + prev_end = range.end(); } tokens } -fn sorted_child_nodes_inner<'a>(node: &Node<'a>, result: &mut Vec>) { +fn sorted_child_nodes_inner<'a>(node: Node<'a>, result: &mut Vec>) { match node { Node::Mod(nodes) => { for stmt in nodes.iter() { @@ -281,9 +307,7 @@ fn sorted_child_nodes_inner<'a>(node: &Node<'a>, result: &mut Vec>) { } } Node::Body(body) => { - for stmt in &body.node { - result.push(Node::Stmt(stmt)); - } + result.extend(body.iter().map(Node::Stmt)); } Node::Stmt(stmt) => match &stmt.node { StmtKind::Return { value } => { @@ -734,15 +758,16 @@ fn sorted_child_nodes_inner<'a>(node: &Node<'a>, result: &mut Vec>) { } } -pub fn sorted_child_nodes<'a>(node: &Node<'a>) -> Vec> { +pub fn sorted_child_nodes(node: Node) -> Vec { let mut result = Vec::new(); sorted_child_nodes_inner(node, &mut result); + result } pub fn decorate_token<'a>( token: &TriviaToken, - node: &Node<'a>, + node: Node<'a>, enclosing_node: Option>, enclosed_node: Option>, cache: &mut FxHashMap>>, @@ -765,51 +790,45 @@ pub fn decorate_token<'a>( while left < right { let middle = (left + right) / 2; - let child = &child_nodes[middle]; - let start = child.location(); - let end = child.end_location(); + let child = child_nodes[middle]; + let start = child.start(); + let end = child.end(); if let Some(existing) = &enclosed_node { // Special-case: if we're dealing with a statement that's a single expression, // we want to treat the expression as the enclosed node. - let existing_start = existing.location(); - let existing_end = existing.end_location(); + let existing_start = existing.start(); + let existing_end = existing.end(); if start == existing_start && end == existing_end { - enclosed_node = Some(child.clone()); + enclosed_node = Some(child); } } else { - if token.start <= start && token.end >= end { - enclosed_node = Some(child.clone()); + if token.start() <= start && token.end() >= end { + enclosed_node = Some(child); } } // The comment is completely contained by this child node. - if token.start >= start && token.end <= end { - return decorate_token( - token, - &child.clone(), - Some(child.clone()), - enclosed_node, - cache, - ); + if token.start() >= start && token.end() <= end { + return decorate_token(token, child, Some(child), enclosed_node, cache); } - if end <= token.start { + if end <= token.start() { // This child node falls completely before the comment. // Because we will never consider this node or any nodes // before it again, this node must be the closest preceding // node we have encountered so far. - preceding_node = Some(child.clone()); + preceding_node = Some(child); left = middle + 1; continue; } - if token.end <= start { + if token.end() <= start { // This child node falls completely after the comment. // Because we will never consider this node or any nodes after // it again, this node must be the closest following node we // have encountered so far. - following_node = Some(child.clone()); + following_node = Some(child); right = middle; continue; } @@ -944,7 +963,7 @@ pub fn decorate_trivia(tokens: Vec, python_ast: &[Stmt]) -> TriviaI let mut cache = FxHashMap::default(); for token in &tokens { let (preceding_node, following_node, enclosing_node, enclosed_node) = - decorate_token(token, &Node::Mod(python_ast), None, None, &mut cache); + decorate_token(token, Node::Mod(python_ast), None, None, &mut cache); stack.push(( preceding_node, diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index 9eea6557c9..87ac6230c0 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -10,6 +10,7 @@ rust-version = { workspace = true } [dependencies] ruff_python_ast = { path = "../ruff_python_ast" } ruff_python_stdlib = { path = "../ruff_python_stdlib" } +ruff_text_size = { workspace = true } bitflags = { workspace = true } is-macro = { workspace = true } diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index a413606ca0..f531ff4f79 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -2,37 +2,38 @@ use std::num::TryFromIntError; use std::ops::{Deref, Index, IndexMut}; use bitflags::bitflags; +use ruff_text_size::TextRange; use rustpython_parser::ast::Stmt; -use ruff_python_ast::types::{Range, RefEquality}; +use ruff_python_ast::types::RefEquality; use crate::scope::ScopeId; #[derive(Debug, Clone)] pub struct Binding<'a> { pub kind: BindingKind<'a>, - pub range: Range, + pub range: TextRange, /// The context in which the binding was created. pub context: ExecutionContext, /// The statement in which the [`Binding`] was defined. pub source: Option>, /// Tuple of (scope index, range) indicating the scope and range at which /// the binding was last used in a runtime context. - pub runtime_usage: Option<(ScopeId, Range)>, + pub runtime_usage: Option<(ScopeId, TextRange)>, /// Tuple of (scope index, range) indicating the scope and range at which /// the binding was last used in a typing-time context. - pub typing_usage: Option<(ScopeId, Range)>, + pub typing_usage: Option<(ScopeId, TextRange)>, /// Tuple of (scope index, range) indicating the scope and range at which /// the binding was last used in a synthetic context. This is used for /// (e.g.) `__future__` imports, explicit re-exports, and other bindings /// that should be considered used even if they're never referenced. - pub synthetic_usage: Option<(ScopeId, Range)>, + pub synthetic_usage: Option<(ScopeId, TextRange)>, /// The exceptions that were handled when the binding was defined. pub exceptions: Exceptions, } impl<'a> Binding<'a> { - pub fn mark_used(&mut self, scope: ScopeId, range: Range, context: ExecutionContext) { + pub fn mark_used(&mut self, scope: ScopeId, range: TextRange, context: ExecutionContext) { match context { ExecutionContext::Runtime => self.runtime_usage = Some((scope, range)), ExecutionContext::Typing => self.typing_usage = Some((scope, range)), diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml deleted file mode 100644 index 357e06f6a2..0000000000 --- a/crates/ruff_text_size/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "ruff_text_size" -version = "0.0.0" -publish = false -edition = { workspace = true } -rust-version = { workspace = true } - -[dependencies] -serde = { workspace = true, optional = true } -schemars = { workspace = true, optional = true } - -[dev-dependencies] -serde_test = { version = "1.0.152" } -static_assertions = { version = "1.1.0" } - -[[test]] -name = "serde" -path = "tests/serde.rs" -required-features = ["serde"] diff --git a/crates/ruff_text_size/src/lib.rs b/crates/ruff_text_size/src/lib.rs deleted file mode 100644 index fd27c65523..0000000000 --- a/crates/ruff_text_size/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Newtypes for working with text sizes/ranges in a more type-safe manner. -//! -//! This library can help with two things: -//! * Reducing storage requirements for offsets and ranges, under the -//! assumption that 32 bits is enough. -//! * Providing standard vocabulary types for applications where text ranges -//! are pervasive. -//! -//! However, you should not use this library simply because you work with -//! strings. In the overwhelming majority of cases, using `usize` and -//! `std::ops::Range` is better. In particular, if you are publishing a -//! library, using only std types in the interface would make it more -//! interoperable. Similarly, if you are writing something like a lexer, which -//! produces, but does not *store* text ranges, then sticking to `usize` would -//! be better. -//! -//! Minimal Supported Rust Version: latest stable. - -#![forbid(unsafe_code)] -#![warn(missing_debug_implementations, missing_docs)] - -mod range; -mod size; -mod traits; - -#[cfg(feature = "schemars")] -mod schemars_impls; -#[cfg(feature = "serde")] -mod serde_impls; - -pub use crate::{range::TextRange, size::TextSize, traits::TextLen}; - -#[cfg(target_pointer_width = "16")] -compile_error!("text-size assumes usize >= u32 and does not work on 16-bit targets"); diff --git a/crates/ruff_text_size/src/range.rs b/crates/ruff_text_size/src/range.rs deleted file mode 100644 index 107b79d3dc..0000000000 --- a/crates/ruff_text_size/src/range.rs +++ /dev/null @@ -1,537 +0,0 @@ -use cmp::Ordering; - -use { - crate::TextSize, - std::{ - cmp, fmt, - ops::{Add, AddAssign, Bound, Index, IndexMut, Range, RangeBounds, Sub, SubAssign}, - }, -}; - -/// A range in text, represented as a pair of [`TextSize`][struct@TextSize]. -/// -/// It is a logic error for `start` to be greater than `end`. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct TextRange { - // Invariant: start <= end - start: TextSize, - end: TextSize, -} - -impl fmt::Debug for TextRange { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}..{}", self.start().raw, self.end().raw) - } -} - -impl TextRange { - /// Creates a new `TextRange` with the given `start` and `end` (`start..end`). - /// - /// # Panics - /// - /// Panics if `end < start`. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let start = TextSize::from(5); - /// let end = TextSize::from(10); - /// let range = TextRange::new(start, end); - /// - /// assert_eq!(range.start(), start); - /// assert_eq!(range.end(), end); - /// assert_eq!(range.len(), end - start); - /// ``` - #[inline] - pub fn new(start: TextSize, end: TextSize) -> TextRange { - assert!(start <= end); - TextRange { start, end } - } - - /// Create a new `TextRange` with the given `offset` and `len` (`offset..offset + len`). - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let text = "0123456789"; - /// - /// let offset = TextSize::from(2); - /// let length = TextSize::from(5); - /// let range = TextRange::at(offset, length); - /// - /// assert_eq!(range, TextRange::new(offset, offset + length)); - /// assert_eq!(&text[range], "23456") - /// ``` - #[inline] - pub fn at(offset: TextSize, len: TextSize) -> TextRange { - TextRange::new(offset, offset + len) - } - - /// Create a zero-length range at the specified offset (`offset..offset`). - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let point: TextSize; - /// # point = TextSize::from(3); - /// let range = TextRange::empty(point); - /// assert!(range.is_empty()); - /// assert_eq!(range, TextRange::new(point, point)); - /// ``` - #[inline] - pub fn empty(offset: TextSize) -> TextRange { - TextRange { - start: offset, - end: offset, - } - } - - /// Create a range up to the given end (`..end`). - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let point: TextSize; - /// # point = TextSize::from(12); - /// let range = TextRange::up_to(point); - /// - /// assert_eq!(range.len(), point); - /// assert_eq!(range, TextRange::new(0.into(), point)); - /// assert_eq!(range, TextRange::at(0.into(), point)); - /// ``` - #[inline] - pub fn up_to(end: TextSize) -> TextRange { - TextRange { - start: 0.into(), - end, - } - } -} - -/// Identity methods. -impl TextRange { - /// The start point of this range. - #[inline] - pub const fn start(self) -> TextSize { - self.start - } - - /// The end point of this range. - #[inline] - pub const fn end(self) -> TextSize { - self.end - } - - /// The size of this range. - #[inline] - pub const fn len(self) -> TextSize { - // HACK for const fn: math on primitives only - TextSize { - raw: self.end().raw - self.start().raw, - } - } - - /// Check if this range is empty. - #[inline] - pub const fn is_empty(self) -> bool { - // HACK for const fn: math on primitives only - self.start().raw == self.end().raw - } -} - -/// Manipulation methods. -impl TextRange { - /// Check if this range contains an offset. - /// - /// The end index is considered excluded. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let (start, end): (TextSize, TextSize); - /// # start = 10.into(); end = 20.into(); - /// let range = TextRange::new(start, end); - /// assert!(range.contains(start)); - /// assert!(!range.contains(end)); - /// ``` - #[inline] - pub fn contains(self, offset: TextSize) -> bool { - self.start() <= offset && offset < self.end() - } - - /// Check if this range contains an offset. - /// - /// The end index is considered included. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let (start, end): (TextSize, TextSize); - /// # start = 10.into(); end = 20.into(); - /// let range = TextRange::new(start, end); - /// assert!(range.contains_inclusive(start)); - /// assert!(range.contains_inclusive(end)); - /// ``` - #[inline] - pub fn contains_inclusive(self, offset: TextSize) -> bool { - self.start() <= offset && offset <= self.end() - } - - /// Check if this range completely contains another range. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let larger = TextRange::new(0.into(), 20.into()); - /// let smaller = TextRange::new(5.into(), 15.into()); - /// assert!(larger.contains_range(smaller)); - /// assert!(!smaller.contains_range(larger)); - /// - /// // a range always contains itself - /// assert!(larger.contains_range(larger)); - /// assert!(smaller.contains_range(smaller)); - /// ``` - #[inline] - pub fn contains_range(self, other: TextRange) -> bool { - self.start() <= other.start() && other.end() <= self.end() - } - - /// The range covered by both ranges, if it exists. - /// If the ranges touch but do not overlap, the output range is empty. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!( - /// TextRange::intersect( - /// TextRange::new(0.into(), 10.into()), - /// TextRange::new(5.into(), 15.into()), - /// ), - /// Some(TextRange::new(5.into(), 10.into())), - /// ); - /// ``` - #[inline] - pub fn intersect(self, other: TextRange) -> Option { - let start = cmp::max(self.start(), other.start()); - let end = cmp::min(self.end(), other.end()); - if end < start { - return None; - } - Some(TextRange::new(start, end)) - } - - /// Extends the range to cover `other` as well. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!( - /// TextRange::cover( - /// TextRange::new(0.into(), 5.into()), - /// TextRange::new(15.into(), 20.into()), - /// ), - /// TextRange::new(0.into(), 20.into()), - /// ); - /// ``` - #[inline] - #[must_use] - pub fn cover(self, other: TextRange) -> TextRange { - let start = cmp::min(self.start(), other.start()); - let end = cmp::max(self.end(), other.end()); - TextRange::new(start, end) - } - - /// Extends the range to cover `other` offsets as well. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!( - /// TextRange::empty(0.into()).cover_offset(20.into()), - /// TextRange::new(0.into(), 20.into()), - /// ) - /// ``` - #[inline] - #[must_use] - pub fn cover_offset(self, offset: TextSize) -> TextRange { - self.cover(TextRange::empty(offset)) - } - - /// Add an offset to this range. - /// - /// Note that this is not appropriate for changing where a `TextRange` is - /// within some string; rather, it is for changing the reference anchor - /// that the `TextRange` is measured against. - /// - /// The unchecked version (`Add::add`) will _always_ panic on overflow, - /// in contrast to primitive integers, which check in debug mode only. - #[inline] - pub fn checked_add(self, offset: TextSize) -> Option { - Some(TextRange { - start: self.start.checked_add(offset)?, - end: self.end.checked_add(offset)?, - }) - } - - /// Subtract an offset from this range. - /// - /// Note that this is not appropriate for changing where a `TextRange` is - /// within some string; rather, it is for changing the reference anchor - /// that the `TextRange` is measured against. - /// - /// The unchecked version (`Sub::sub`) will _always_ panic on overflow, - /// in contrast to primitive integers, which check in debug mode only. - #[inline] - pub fn checked_sub(self, offset: TextSize) -> Option { - Some(TextRange { - start: self.start.checked_sub(offset)?, - end: self.end.checked_sub(offset)?, - }) - } - - /// Relative order of the two ranges (overlapping ranges are considered - /// equal). - /// - /// - /// This is useful when, for example, binary searching an array of disjoint - /// ranges. - /// - /// # Examples - /// - /// ``` - /// # use ruff_text_size::*; - /// # use std::cmp::Ordering; - /// - /// let a = TextRange::new(0.into(), 3.into()); - /// let b = TextRange::new(4.into(), 5.into()); - /// assert_eq!(a.ordering(b), Ordering::Less); - /// - /// let a = TextRange::new(0.into(), 3.into()); - /// let b = TextRange::new(3.into(), 5.into()); - /// assert_eq!(a.ordering(b), Ordering::Less); - /// - /// let a = TextRange::new(0.into(), 3.into()); - /// let b = TextRange::new(2.into(), 5.into()); - /// assert_eq!(a.ordering(b), Ordering::Equal); - /// - /// let a = TextRange::new(0.into(), 3.into()); - /// let b = TextRange::new(2.into(), 2.into()); - /// assert_eq!(a.ordering(b), Ordering::Equal); - /// - /// let a = TextRange::new(2.into(), 3.into()); - /// let b = TextRange::new(2.into(), 2.into()); - /// assert_eq!(a.ordering(b), Ordering::Greater); - /// ``` - #[inline] - pub fn ordering(self, other: TextRange) -> Ordering { - if self.end() <= other.start() { - Ordering::Less - } else if other.end() <= self.start() { - Ordering::Greater - } else { - Ordering::Equal - } - } - - /// Subtracts an offset from the start position. - /// - /// - /// ## Panics - /// If `start - amount` is less than zero. - /// - /// ## Examples - /// - /// ``` - /// use ruff_text_size::{TextRange, TextSize}; - /// - /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); - /// assert_eq!(range.sub_start(TextSize::from(2)), TextRange::new(TextSize::from(3), TextSize::from(10))); - /// ``` - #[inline] - #[must_use] - pub fn sub_start(&self, amount: TextSize) -> TextRange { - TextRange::new(self.start() - amount, self.end()) - } - - /// Adds an offset to the start position. - /// - /// ## Panics - /// If `start + amount > end` - /// - /// ## Examples - /// - /// ``` - /// use ruff_text_size::{TextRange, TextSize}; - /// - /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); - /// assert_eq!(range.add_start(TextSize::from(3)), TextRange::new(TextSize::from(8), TextSize::from(10))); - /// ``` - #[inline] - #[must_use] - pub fn add_start(&self, amount: TextSize) -> TextRange { - TextRange::new(self.start() + amount, self.end()) - } - - /// Subtracts an offset from the end position. - /// - /// - /// ## Panics - /// If `end - amount < 0` or `end - amount < start` - /// - /// ## Examples - /// - /// ``` - /// use ruff_text_size::{TextRange, TextSize}; - /// - /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); - /// assert_eq!(range.sub_end(TextSize::from(2)), TextRange::new(TextSize::from(5), TextSize::from(8))); - /// ``` - #[inline] - #[must_use] - pub fn sub_end(&self, amount: TextSize) -> TextRange { - TextRange::new(self.start(), self.end() - amount) - } - - /// Adds an offset to the end position. - /// - /// - /// ## Panics - /// If `end + amount > u32::MAX` - /// - /// ## Examples - /// - /// ``` - /// use ruff_text_size::{TextRange, TextSize}; - /// - /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); - /// assert_eq!(range.add_end(TextSize::from(2)), TextRange::new(TextSize::from(5), TextSize::from(12))); - /// ``` - #[inline] - #[must_use] - pub fn add_end(&self, amount: TextSize) -> TextRange { - TextRange::new(self.start(), self.end() + amount) - } -} - -impl Index for str { - type Output = str; - #[inline] - fn index(&self, index: TextRange) -> &str { - &self[Range::::from(index)] - } -} - -impl Index for String { - type Output = str; - #[inline] - fn index(&self, index: TextRange) -> &str { - &self[Range::::from(index)] - } -} - -impl IndexMut for str { - #[inline] - fn index_mut(&mut self, index: TextRange) -> &mut str { - &mut self[Range::::from(index)] - } -} - -impl IndexMut for String { - #[inline] - fn index_mut(&mut self, index: TextRange) -> &mut str { - &mut self[Range::::from(index)] - } -} - -impl RangeBounds for TextRange { - fn start_bound(&self) -> Bound<&TextSize> { - Bound::Included(&self.start) - } - - fn end_bound(&self) -> Bound<&TextSize> { - Bound::Excluded(&self.end) - } -} - -impl From for Range -where - T: From, -{ - #[inline] - fn from(r: TextRange) -> Self { - r.start().into()..r.end().into() - } -} - -macro_rules! ops { - (impl $Op:ident for TextRange by fn $f:ident = $op:tt) => { - impl $Op<&TextSize> for TextRange { - type Output = TextRange; - #[inline] - fn $f(self, other: &TextSize) -> TextRange { - self $op *other - } - } - impl $Op for &TextRange - where - TextRange: $Op, - { - type Output = TextRange; - #[inline] - fn $f(self, other: T) -> TextRange { - *self $op other - } - } - }; -} - -impl Add for TextRange { - type Output = TextRange; - #[inline] - fn add(self, offset: TextSize) -> TextRange { - self.checked_add(offset) - .expect("TextRange +offset overflowed") - } -} - -impl Sub for TextRange { - type Output = TextRange; - #[inline] - fn sub(self, offset: TextSize) -> TextRange { - self.checked_sub(offset) - .expect("TextRange -offset overflowed") - } -} - -ops!(impl Add for TextRange by fn add = +); -ops!(impl Sub for TextRange by fn sub = -); - -impl AddAssign for TextRange -where - TextRange: Add, -{ - #[inline] - fn add_assign(&mut self, rhs: A) { - *self = *self + rhs; - } -} - -impl SubAssign for TextRange -where - TextRange: Sub, -{ - #[inline] - fn sub_assign(&mut self, rhs: S) { - *self = *self - rhs; - } -} diff --git a/crates/ruff_text_size/src/schemars_impls.rs b/crates/ruff_text_size/src/schemars_impls.rs deleted file mode 100644 index a1c7fa3618..0000000000 --- a/crates/ruff_text_size/src/schemars_impls.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! This module implements the [`JsonSchema`] trait from the `schemars` crate for -//! [`TextSize`] and [`TextRange`] if the `schemars` feature is enabled. This trait -//! exposes meta-information on how a given type is serialized and deserialized -//! using `serde`, and is currently used to generate autocomplete information -//! for the `rome.json` configuration file and TypeScript types for the node.js -//! bindings to the Workspace API - -use crate::{TextRange, TextSize}; -use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; - -impl JsonSchema for TextSize { - fn schema_name() -> String { - String::from("TextSize") - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - // TextSize is represented as a raw u32, see serde_impls.rs for the - // actual implementation - ::json_schema(gen) - } -} - -impl JsonSchema for TextRange { - fn schema_name() -> String { - String::from("TextRange") - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - // TextSize is represented as (TextSize, TextSize), see serde_impls.rs - // for the actual implementation - <(TextSize, TextSize)>::json_schema(gen) - } -} diff --git a/crates/ruff_text_size/src/serde_impls.rs b/crates/ruff_text_size/src/serde_impls.rs deleted file mode 100644 index b6885d674a..0000000000 --- a/crates/ruff_text_size/src/serde_impls.rs +++ /dev/null @@ -1,47 +0,0 @@ -use { - crate::{TextRange, TextSize}, - serde::{de, Deserialize, Deserializer, Serialize, Serializer}, -}; - -impl Serialize for TextSize { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.raw.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for TextSize { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - u32::deserialize(deserializer).map(TextSize::from) - } -} - -impl Serialize for TextRange { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - (self.start(), self.end()).serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for TextRange { - #[allow(clippy::nonminimal_bool)] - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (start, end) = Deserialize::deserialize(deserializer)?; - if !(start <= end) { - return Err(de::Error::custom(format!( - "invalid range: {start:?}..{end:?}" - ))); - } - Ok(TextRange::new(start, end)) - } -} diff --git a/crates/ruff_text_size/src/size.rs b/crates/ruff_text_size/src/size.rs deleted file mode 100644 index e9a378ee1d..0000000000 --- a/crates/ruff_text_size/src/size.rs +++ /dev/null @@ -1,161 +0,0 @@ -use { - crate::TextLen, - std::{ - convert::TryFrom, - fmt, iter, - num::TryFromIntError, - ops::{Add, AddAssign, Sub, SubAssign}, - u32, - }, -}; - -/// A measure of text length. Also, equivalently, an index into text. -/// -/// This is a UTF-8 bytes offset stored as `u32`, but -/// most clients should treat it as an opaque measure. -/// -/// For cases that need to escape `TextSize` and return to working directly -/// with primitive integers, `TextSize` can be converted losslessly to/from -/// `u32` via [`From`] conversions as well as losslessly be converted [`Into`] -/// `usize`. The `usize -> TextSize` direction can be done via [`TryFrom`]. -/// -/// These escape hatches are primarily required for unit testing and when -/// converting from UTF-8 size to another coordinate space, such as UTF-16. -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct TextSize { - pub(crate) raw: u32, -} - -impl fmt::Debug for TextSize { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.raw) - } -} - -impl TextSize { - /// The text size of some primitive text-like object. - /// - /// Accepts `char`, `&str`, and `&String`. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let char_size = TextSize::of('🦀'); - /// assert_eq!(char_size, TextSize::from(4)); - /// - /// let str_size = TextSize::of("rust-analyzer"); - /// assert_eq!(str_size, TextSize::from(13)); - /// ``` - #[inline] - pub fn of(text: T) -> TextSize { - text.text_len() - } -} - -/// Methods to act like a primitive integer type, where reasonably applicable. -// Last updated for parity with Rust 1.42.0. -impl TextSize { - /// Checked addition. Returns `None` if overflow occurred. - #[inline] - pub fn checked_add(self, rhs: TextSize) -> Option { - self.raw.checked_add(rhs.raw).map(|raw| TextSize { raw }) - } - - /// Checked subtraction. Returns `None` if overflow occurred. - #[inline] - pub fn checked_sub(self, rhs: TextSize) -> Option { - self.raw.checked_sub(rhs.raw).map(|raw| TextSize { raw }) - } -} - -impl From for TextSize { - #[inline] - fn from(raw: u32) -> Self { - TextSize { raw } - } -} - -impl From for u32 { - #[inline] - fn from(value: TextSize) -> Self { - value.raw - } -} - -impl TryFrom for TextSize { - type Error = TryFromIntError; - #[inline] - fn try_from(value: usize) -> Result { - Ok(u32::try_from(value)?.into()) - } -} - -impl From for usize { - #[inline] - fn from(value: TextSize) -> Self { - value.raw as usize - } -} - -macro_rules! ops { - (impl $Op:ident for TextSize by fn $f:ident = $op:tt) => { - impl $Op for TextSize { - type Output = TextSize; - #[inline] - fn $f(self, other: TextSize) -> TextSize { - TextSize { raw: self.raw $op other.raw } - } - } - impl $Op<&TextSize> for TextSize { - type Output = TextSize; - #[inline] - fn $f(self, other: &TextSize) -> TextSize { - self $op *other - } - } - impl $Op for &TextSize - where - TextSize: $Op, - { - type Output = TextSize; - #[inline] - fn $f(self, other: T) -> TextSize { - *self $op other - } - } - }; -} - -ops!(impl Add for TextSize by fn add = +); -ops!(impl Sub for TextSize by fn sub = -); - -impl AddAssign for TextSize -where - TextSize: Add, -{ - #[inline] - fn add_assign(&mut self, rhs: A) { - *self = *self + rhs; - } -} - -impl SubAssign for TextSize -where - TextSize: Sub, -{ - #[inline] - fn sub_assign(&mut self, rhs: S) { - *self = *self - rhs; - } -} - -impl iter::Sum for TextSize -where - TextSize: Add, -{ - #[inline] - fn sum>(iter: I) -> TextSize { - iter.fold(0.into(), Add::add) - } -} diff --git a/crates/ruff_text_size/src/traits.rs b/crates/ruff_text_size/src/traits.rs deleted file mode 100644 index 3f5261bfd8..0000000000 --- a/crates/ruff_text_size/src/traits.rs +++ /dev/null @@ -1,37 +0,0 @@ -use {crate::TextSize, std::convert::TryInto}; - -use priv_in_pub::Sealed; -mod priv_in_pub { - pub trait Sealed {} -} - -/// Primitives with a textual length that can be passed to [`TextSize::of`]. -pub trait TextLen: Copy + Sealed { - /// The textual length of this primitive. - fn text_len(self) -> TextSize; -} - -impl Sealed for &'_ str {} -impl TextLen for &'_ str { - #[inline] - fn text_len(self) -> TextSize { - self.len().try_into().unwrap() - } -} - -impl Sealed for &'_ String {} -impl TextLen for &'_ String { - #[inline] - fn text_len(self) -> TextSize { - self.as_str().text_len() - } -} - -impl Sealed for char {} -impl TextLen for char { - #[inline] - #[allow(clippy::cast_possible_truncation)] - fn text_len(self) -> TextSize { - (self.len_utf8() as u32).into() - } -} diff --git a/crates/ruff_text_size/tests/auto_traits.rs b/crates/ruff_text_size/tests/auto_traits.rs deleted file mode 100644 index 6adf8bd2d6..0000000000 --- a/crates/ruff_text_size/tests/auto_traits.rs +++ /dev/null @@ -1,18 +0,0 @@ -use { - ruff_text_size::{TextRange, TextSize}, - static_assertions::assert_impl_all, - std::{ - fmt::Debug, - hash::Hash, - marker::{Send, Sync}, - panic::{RefUnwindSafe, UnwindSafe}, - }, -}; - -// auto traits -assert_impl_all!(TextSize: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); -assert_impl_all!(TextRange: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); - -// common traits -assert_impl_all!(TextSize: Copy, Debug, Default, Hash, Ord); -assert_impl_all!(TextRange: Copy, Debug, Default, Hash, Eq); diff --git a/crates/ruff_text_size/tests/constructors.rs b/crates/ruff_text_size/tests/constructors.rs deleted file mode 100644 index d6f88f5042..0000000000 --- a/crates/ruff_text_size/tests/constructors.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ruff_text_size::TextSize; - -#[derive(Copy, Clone)] -struct BadRope<'a>(&'a [&'a str]); - -impl BadRope<'_> { - fn text_len(self) -> TextSize { - self.0.iter().copied().map(TextSize::of).sum() - } -} - -#[test] -fn main() { - let x: char = 'c'; - _ = TextSize::of(x); - - let x: &str = "hello"; - _ = TextSize::of(x); - - let x: &String = &"hello".into(); - _ = TextSize::of(x); - - _ = BadRope(&[""]).text_len(); -} diff --git a/crates/ruff_text_size/tests/indexing.rs b/crates/ruff_text_size/tests/indexing.rs deleted file mode 100644 index b4b4650403..0000000000 --- a/crates/ruff_text_size/tests/indexing.rs +++ /dev/null @@ -1,8 +0,0 @@ -use ruff_text_size::TextRange; - -#[test] -fn main() { - let range = TextRange::default(); - _ = &""[range]; - _ = &String::new()[range]; -} diff --git a/crates/ruff_text_size/tests/main.rs b/crates/ruff_text_size/tests/main.rs deleted file mode 100644 index 8d8dd091e9..0000000000 --- a/crates/ruff_text_size/tests/main.rs +++ /dev/null @@ -1,79 +0,0 @@ -use { - ruff_text_size::{TextRange, TextSize}, - std::ops, -}; - -fn size(x: u32) -> TextSize { - TextSize::from(x) -} - -fn range(x: ops::Range) -> TextRange { - TextRange::new(x.start.into(), x.end.into()) -} - -#[test] -fn sum() { - let xs: Vec = vec![size(0), size(1), size(2)]; - assert_eq!(xs.iter().sum::(), size(3)); - assert_eq!(xs.into_iter().sum::(), size(3)); -} - -#[test] -fn math() { - assert_eq!(size(10) + size(5), size(15)); - assert_eq!(size(10) - size(5), size(5)); -} - -#[test] -fn checked_math() { - assert_eq!(size(1).checked_add(size(1)), Some(size(2))); - assert_eq!(size(1).checked_sub(size(1)), Some(size(0))); - assert_eq!(size(1).checked_sub(size(2)), None); - assert_eq!(size(!0).checked_add(size(1)), None); -} - -#[test] -#[rustfmt::skip] -fn contains() { - assert!( range(2..4).contains_range(range(2..3))); - assert!( ! range(2..4).contains_range(range(1..3))); -} - -#[test] -fn intersect() { - assert_eq!(range(1..2).intersect(range(2..3)), Some(range(2..2))); - assert_eq!(range(1..5).intersect(range(2..3)), Some(range(2..3))); - assert_eq!(range(1..2).intersect(range(3..4)), None); -} - -#[test] -fn cover() { - assert_eq!(range(1..2).cover(range(2..3)), range(1..3)); - assert_eq!(range(1..5).cover(range(2..3)), range(1..5)); - assert_eq!(range(1..2).cover(range(4..5)), range(1..5)); -} - -#[test] -fn cover_offset() { - assert_eq!(range(1..3).cover_offset(size(0)), range(0..3)); - assert_eq!(range(1..3).cover_offset(size(1)), range(1..3)); - assert_eq!(range(1..3).cover_offset(size(2)), range(1..3)); - assert_eq!(range(1..3).cover_offset(size(3)), range(1..3)); - assert_eq!(range(1..3).cover_offset(size(4)), range(1..4)); -} - -#[test] -#[rustfmt::skip] -fn contains_point() { - assert!( ! range(1..3).contains(size(0))); - assert!( range(1..3).contains(size(1))); - assert!( range(1..3).contains(size(2))); - assert!( ! range(1..3).contains(size(3))); - assert!( ! range(1..3).contains(size(4))); - - assert!( ! range(1..3).contains_inclusive(size(0))); - assert!( range(1..3).contains_inclusive(size(1))); - assert!( range(1..3).contains_inclusive(size(2))); - assert!( range(1..3).contains_inclusive(size(3))); - assert!( ! range(1..3).contains_inclusive(size(4))); -} diff --git a/crates/ruff_text_size/tests/serde.rs b/crates/ruff_text_size/tests/serde.rs deleted file mode 100644 index 0d8f9d4a6a..0000000000 --- a/crates/ruff_text_size/tests/serde.rs +++ /dev/null @@ -1,83 +0,0 @@ -use { - ruff_text_size::{TextRange, TextSize}, - serde_test::{assert_de_tokens_error, assert_tokens, Token}, - std::ops, -}; - -fn size(x: u32) -> TextSize { - TextSize::from(x) -} - -fn range(x: ops::Range) -> TextRange { - TextRange::new(x.start.into(), x.end.into()) -} - -#[test] -fn size_serialization() { - assert_tokens(&size(00), &[Token::U32(00)]); - assert_tokens(&size(10), &[Token::U32(10)]); - assert_tokens(&size(20), &[Token::U32(20)]); - assert_tokens(&size(30), &[Token::U32(30)]); -} - -#[test] -fn range_serialization() { - assert_tokens( - &range(00..10), - &[ - Token::Tuple { len: 2 }, - Token::U32(00), - Token::U32(10), - Token::TupleEnd, - ], - ); - assert_tokens( - &range(10..20), - &[ - Token::Tuple { len: 2 }, - Token::U32(10), - Token::U32(20), - Token::TupleEnd, - ], - ); - assert_tokens( - &range(20..30), - &[ - Token::Tuple { len: 2 }, - Token::U32(20), - Token::U32(30), - Token::TupleEnd, - ], - ); - assert_tokens( - &range(30..40), - &[ - Token::Tuple { len: 2 }, - Token::U32(30), - Token::U32(40), - Token::TupleEnd, - ], - ); -} - -#[test] -fn invalid_range_deserialization() { - assert_tokens::( - &range(62..92), - &[ - Token::Tuple { len: 2 }, - Token::U32(62), - Token::U32(92), - Token::TupleEnd, - ], - ); - assert_de_tokens_error::( - &[ - Token::Tuple { len: 2 }, - Token::U32(92), - Token::U32(62), - Token::TupleEnd, - ], - "invalid range: 92..62", - ); -} diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 6290834916..9677084cfa 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -1,6 +1,5 @@ use std::path::Path; -use rustpython_parser::ast::Location; use rustpython_parser::lexer::LexResult; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -19,7 +18,7 @@ use ruff::settings::configuration::Configuration; use ruff::settings::options::Options; use ruff::settings::{defaults, flags, Settings}; use ruff_diagnostics::Edit; -use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; +use ruff_python_ast::source_code::{Indexer, Locator, SourceLocation, Stylist}; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -63,8 +62,8 @@ pub struct ExpandedFix { pub struct ExpandedMessage { pub code: String, pub message: String, - pub location: Location, - pub end_location: Location, + pub location: SourceLocation, + pub end_location: SourceLocation, pub fix: Option, } @@ -180,10 +179,11 @@ pub fn check(contents: &str, options: JsValue) -> Result { let stylist = Stylist::from_tokens(&tokens, &locator); // Extra indices from the code. - let indexer: Indexer = tokens.as_slice().into(); + let indexer = Indexer::from_tokens(&tokens, &locator); // Extract the `# noqa` and `# isort: skip` directives from the source. - let directives = directives::extract_directives(&tokens, directives::Flags::empty()); + let directives = + directives::extract_directives(&tokens, directives::Flags::empty(), &locator, &indexer); // Generate checks. let LinterResult { @@ -192,7 +192,6 @@ pub fn check(contents: &str, options: JsValue) -> Result { } = check_path( Path::new(""), None, - contents, tokens, &locator, &stylist, @@ -203,21 +202,28 @@ pub fn check(contents: &str, options: JsValue) -> Result { flags::Autofix::Enabled, ); + let source_code = locator.to_source_code(); + let messages: Vec = diagnostics .into_iter() - .map(|message| ExpandedMessage { - code: message.kind.rule().noqa_code().to_string(), - message: message.kind.body, - location: message.location, - end_location: message.end_location, - fix: if message.fix.is_empty() { - None - } else { - Some(ExpandedFix { - message: message.kind.suggestion, - edits: message.fix.into_edits(), - }) - }, + .map(|message| { + let start_location = source_code.source_location(message.start()); + let end_location = source_code.source_location(message.end()); + + ExpandedMessage { + code: message.kind.rule().noqa_code().to_string(), + message: message.kind.body, + location: start_location, + end_location, + fix: if message.fix.is_empty() { + None + } else { + Some(ExpandedFix { + message: message.kind.suggestion, + edits: message.fix.into_edits(), + }) + }, + } }) .collect(); diff --git a/crates/ruff_wasm/tests/api.rs b/crates/ruff_wasm/tests/api.rs index 432a86f3ea..02d4975154 100644 --- a/crates/ruff_wasm/tests/api.rs +++ b/crates/ruff_wasm/tests/api.rs @@ -1,10 +1,11 @@ #![cfg(target_arch = "wasm32")] use js_sys; -use rustpython_parser::ast::Location; + use wasm_bindgen_test::*; use ruff::registry::Rule; +use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; use ruff_wasm::*; macro_rules! check { @@ -28,8 +29,14 @@ fn empty_config() { [ExpandedMessage { code: Rule::IfTuple.noqa_code().to_string(), message: "If test is a tuple, which is always `True`".to_string(), - location: Location::new(1, 0), - end_location: Location::new(2, 8), + location: SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(0) + }, + end_location: SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(8) + }, fix: None, }] ); diff --git a/playground/src/Editor/SourceEditor.tsx b/playground/src/Editor/SourceEditor.tsx index da7677d1ad..c039cbf302 100644 --- a/playground/src/Editor/SourceEditor.tsx +++ b/playground/src/Editor/SourceEditor.tsx @@ -35,9 +35,9 @@ export default function SourceEditor({ "owner", diagnostics.map((diagnostic) => ({ startLineNumber: diagnostic.location.row, - startColumn: diagnostic.location.column + 1, + startColumn: diagnostic.location.column, endLineNumber: diagnostic.end_location.row, - endColumn: diagnostic.end_location.column + 1, + endColumn: diagnostic.end_location.column, message: `${diagnostic.code}: ${diagnostic.message}`, severity: MarkerSeverity.Error, tags: diff --git a/scripts/add_rule.py b/scripts/add_rule.py index 86ad8a3bf8..fe176a7742 100755 --- a/scripts/add_rule.py +++ b/scripts/add_rule.py @@ -11,6 +11,7 @@ Example usage: """ import argparse +import subprocess from _utils import ROOT_DIR, dir_name, get_indent, pascal_case, snake_case @@ -79,17 +80,10 @@ def main(*, name: str, prefix: str, code: str, linter: str) -> None: new_mod = f"mod {rule_name_snake};" if len(parts) == 2: - pub_use_contents = parts[0].split(";\n") - pub_use_contents.append(new_pub_use) - pub_use_contents.sort() - - mod_contents = parts[1].splitlines() - mod_contents.append(new_mod) - mod_contents.sort() - - new_contents = ";\n".join(pub_use_contents) + new_contents = parts[0] + new_contents += "\n" + new_pub_use + ";" new_contents += "\n\n" - new_contents += "\n".join(mod_contents) + new_contents += parts[1] + new_mod new_contents += "\n" rules_mod.write_text(new_contents) @@ -201,6 +195,12 @@ pub fn {rule_name_snake}(checker: &mut Checker) {{}} with (ROOT_DIR / "crates/ruff/src/codes.rs").open("w") as fp: fp.write(text) + _rustfmt(rules_mod) + + +def _rustfmt(path: str) -> None: + subprocess.run(["rustfmt", path]) + if __name__ == "__main__": parser = argparse.ArgumentParser(