diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py b/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py index 28d5af1f3b..bd2a3f2e02 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py @@ -108,3 +108,33 @@ def f(): def f(): toplevel = tt = 1 + + +def f(provided: int) -> int: + match provided: + case [_, *x]: + pass + + +def f(provided: int) -> int: + match provided: + case x: + pass + + +def f(provided: int) -> int: + match provided: + case Foo(bar) as x: + pass + + +def f(provided: int) -> int: + match provided: + case {"foo": 0, **x}: + pass + + +def f(provided: int) -> int: + match provided: + case {**x}: + pass diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 8e0ce79375..6dd4cc885c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -12,12 +12,13 @@ use rustpython_parser::ast::{ use ruff_diagnostics::{Diagnostic, Fix, IsolationLevel}; use ruff_python_ast::all::{extract_all_names, AllNamesFlags}; use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path}; +use ruff_python_ast::identifier::{Identifier, TryIdentifier}; use ruff_python_ast::source_code::{Generator, Indexer, Locator, Quote, Stylist}; use ruff_python_ast::str::trailing_quote; use ruff_python_ast::types::Node; use ruff_python_ast::typing::{parse_type_annotation, AnnotationKind}; use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor}; -use ruff_python_ast::{cast, helpers, str, visitor}; +use ruff_python_ast::{cast, helpers, identifier, str, visitor}; use ruff_python_semantic::analyze::{branch_detection, typing, visibility}; use ruff_python_semantic::{ Binding, BindingFlags, BindingId, BindingKind, ContextualizedDefinition, Exceptions, @@ -250,7 +251,7 @@ where // Pre-visit. match stmt { Stmt::Global(ast::StmtGlobal { names, range: _ }) => { - let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); + let ranges: Vec = identifier::names(stmt, self.locator).collect(); if !self.semantic.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. @@ -271,7 +272,7 @@ where } } Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { - let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); + let ranges: Vec = identifier::names(stmt, self.locator).collect(); if !self.semantic.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. @@ -367,7 +368,7 @@ where if self.enabled(Rule::AmbiguousFunctionName) { if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name, || { - helpers::identifier_range(stmt, self.locator) + stmt.identifier(self.locator) }) { self.diagnostics.push(diagnostic); @@ -692,7 +693,7 @@ where } if self.enabled(Rule::AmbiguousClassName) { if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name, || { - helpers::identifier_range(stmt, self.locator) + stmt.identifier(self.locator) }) { self.diagnostics.push(diagnostic); } @@ -808,7 +809,7 @@ where let name = alias.asname.as_ref().unwrap_or(&alias.name); self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::FutureImportation, BindingFlags::empty(), ); @@ -828,7 +829,7 @@ where let qualified_name = &alias.name; self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name, }), @@ -839,7 +840,7 @@ where let qualified_name = &alias.name; self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::Importation(Importation { qualified_name }), if alias .asname @@ -1084,7 +1085,7 @@ where self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::FutureImportation, BindingFlags::empty(), ); @@ -1145,7 +1146,7 @@ where helpers::format_import_from_member(level, module, &alias.name); self.add_binding( name, - alias.range(), + alias.identifier(self.locator), BindingKind::FromImportation(FromImportation { qualified_name }), if alias .asname @@ -1871,7 +1872,7 @@ where self.add_binding( name, - stmt.range(), + stmt.identifier(self.locator), BindingKind::FunctionDefinition, BindingFlags::empty(), ); @@ -2094,7 +2095,7 @@ where self.semantic.pop_definition(); self.add_binding( name, - stmt.range(), + stmt.identifier(self.locator), BindingKind::ClassDefinition, BindingFlags::empty(), ); @@ -3902,8 +3903,7 @@ where } match name { Some(name) => { - let range = helpers::excepthandler_name_range(excepthandler, self.locator) - .expect("Failed to find `name` range"); + let range = excepthandler.try_identifier(self.locator).unwrap(); if self.enabled(Rule::AmbiguousVariableName) { if let Some(diagnostic) = @@ -4019,7 +4019,7 @@ where // upstream. self.add_binding( &arg.arg, - arg.range(), + arg.identifier(self.locator), BindingKind::Argument, BindingFlags::empty(), ); @@ -4059,7 +4059,7 @@ where { self.add_binding( name, - pattern.range(), + pattern.try_identifier(self.locator).unwrap(), BindingKind::Assignment, BindingFlags::empty(), ); @@ -4268,16 +4268,14 @@ impl<'a> Checker<'a> { { if self.enabled(Rule::RedefinedWhileUnused) { #[allow(deprecated)] - let line = self.locator.compute_line_index( - shadowed.trimmed_range(&self.semantic, self.locator).start(), - ); + let line = self.locator.compute_line_index(shadowed.range.start()); let mut diagnostic = Diagnostic::new( pyflakes::rules::RedefinedWhileUnused { name: name.to_string(), line, }, - binding.trimmed_range(&self.semantic, self.locator), + binding.range, ); if let Some(range) = binding.parent_range(&self.semantic) { diagnostic.set_parent(range.start()); @@ -4890,9 +4888,7 @@ impl<'a> Checker<'a> { } #[allow(deprecated)] - let line = self.locator.compute_line_index( - shadowed.trimmed_range(&self.semantic, self.locator).start(), - ); + let line = self.locator.compute_line_index(shadowed.range.start()); let binding = self.semantic.binding(binding_id); let mut diagnostic = Diagnostic::new( @@ -4900,7 +4896,7 @@ impl<'a> Checker<'a> { name: (*name).to_string(), line, }, - binding.trimmed_range(&self.semantic, self.locator), + binding.range, ); if let Some(range) = binding.parent_range(&self.semantic) { diagnostic.set_parent(range.start()); diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index a5313ec9be..e839a092f7 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -2,9 +2,10 @@ use rustpython_parser::ast::{Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::cast; use ruff_python_ast::helpers::ReturnStatementVisitor; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::statement_visitor::StatementVisitor; -use ruff_python_ast::{cast, helpers}; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; use ruff_python_stdlib::typing::SIMPLE_MAGIC_RETURN_TYPES; @@ -640,7 +641,7 @@ pub(crate) fn definition( MissingReturnTypeClassMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } else if is_method @@ -651,7 +652,7 @@ pub(crate) fn definition( MissingReturnTypeStaticMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } else if is_method && visibility::is_init(name) { @@ -663,7 +664,7 @@ pub(crate) fn definition( MissingReturnTypeSpecialMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), ); if checker.patch(diagnostic.kind.rule()) { #[allow(deprecated)] @@ -680,7 +681,7 @@ pub(crate) fn definition( MissingReturnTypeSpecialMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), ); let return_type = SIMPLE_MAGIC_RETURN_TYPES.get(name); if let Some(return_type) = return_type { @@ -701,7 +702,7 @@ pub(crate) fn definition( MissingReturnTypeUndocumentedPublicFunction { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } @@ -711,7 +712,7 @@ pub(crate) fn definition( MissingReturnTypePrivateFunction { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } 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 6868c61673..16606d08ff 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,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_python_semantic::SemanticModel; @@ -134,7 +134,7 @@ pub(crate) fn abstract_base_class( AbstractBaseClassWithoutAbstractMethod { name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs index 5453b71569..8556160ba5 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -31,6 +31,6 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) { }; checker.diagnostics.push(Diagnostic::new( FStringDocstring, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } diff --git a/crates/ruff/src/rules/flake8_builtins/helpers.rs b/crates/ruff/src/rules/flake8_builtins/helpers.rs index d350c11447..5b6c45761e 100644 --- a/crates/ruff/src/rules/flake8_builtins/helpers.rs +++ b/crates/ruff/src/rules/flake8_builtins/helpers.rs @@ -1,7 +1,7 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_stdlib::builtins::BUILTINS; @@ -20,7 +20,7 @@ impl AnyShadowing<'_> { pub(crate) fn range(self, locator: &Locator) -> TextRange { match self { AnyShadowing::Expression(expr) => expr.range(), - AnyShadowing::Statement(stmt) => identifier_range(stmt, locator), + AnyShadowing::Statement(stmt) => stmt.identifier(locator), AnyShadowing::ExceptHandler(handler) => handler.range(), } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs index 5a3bb60e74..78155686c9 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -2,7 +2,8 @@ use rustpython_parser::ast::{self, Arguments, Decorator, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, map_subscript}; +use ruff_python_ast::helpers::map_subscript; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{is_abstract, is_final, is_overload}; use ruff_python_semantic::{ScopeKind, SemanticModel}; @@ -147,7 +148,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } return; @@ -161,7 +162,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } return; @@ -176,7 +177,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } return; @@ -192,7 +193,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } @@ -205,7 +206,7 @@ pub(crate) fn non_self_return_type( class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs index 0d40ff8f9f..3ae9eab2f8 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_abstract; use crate::autofix::edits::delete_stmt; @@ -90,7 +90,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { StrOrReprDefinedInStub { name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), ); if checker.patch(diagnostic.kind.rule()) { let stmt = checker.semantic().stmt(); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs index 3ab3613237..bfc0e70857 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs @@ -2,8 +2,8 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; use ruff_python_ast::helpers::is_docstring_stmt; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -32,6 +32,6 @@ pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt, checker.diagnostics.push(Diagnostic::new( StubBodyMultipleStatements, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } 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 3825e9929e..760aa8f4b6 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -9,10 +9,11 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix}; 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::identifier::Identifier; use ruff_python_ast::prelude::Decorator; use ruff_python_ast::source_code::Locator; +use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; -use ruff_python_ast::{helpers, visitor}; use ruff_python_semantic::analyze::visibility::is_abstract; use ruff_python_semantic::SemanticModel; @@ -378,7 +379,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestIncorrectFixtureNameUnderscore { function: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } else if checker.enabled(Rule::PytestMissingFixtureNameUnderscore) && !visitor.has_return_with_value @@ -389,7 +390,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestMissingFixtureNameUnderscore { function: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index c43f9f35fd..3a56fc07c6 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::prelude::Stmt; use crate::checkers::ast::Checker; @@ -77,7 +77,7 @@ pub(crate) fn no_slots_in_namedtuple_subclass( if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( NoSlotsInNamedtupleSubclass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index 8c0ae80b90..a1a5182c61 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Stmt, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::rules::flake8_slots::rules::helpers::has_slots; @@ -61,7 +61,7 @@ pub(crate) fn no_slots_in_str_subclass(checker: &mut Checker, stmt: &Stmt, class if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( NoSlotsInStrSubclass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index a4705c2e25..c57cd510b1 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -2,7 +2,8 @@ use rustpython_parser::ast::{Stmt, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, map_subscript}; +use ruff_python_ast::helpers::map_subscript; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::rules::flake8_slots::rules::helpers::has_slots; @@ -64,7 +65,7 @@ pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, cla if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( NoSlotsInTupleSubclass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index b9ffa0c236..6863584901 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -99,17 +99,18 @@ pub(crate) fn runtime_import_in_type_checking_block( let import = Import { qualified_name, reference_id, - trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + range: binding.range, parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored( - Rule::RuntimeImportInTypeCheckingBlock, - import.trimmed_range.start(), - ) || import.parent_range.map_or(false, |parent_range| { - checker - .rule_is_ignored(Rule::RuntimeImportInTypeCheckingBlock, parent_range.start()) - }) { + if checker.rule_is_ignored(Rule::RuntimeImportInTypeCheckingBlock, import.range.start()) + || import.parent_range.map_or(false, |parent_range| { + checker.rule_is_ignored( + Rule::RuntimeImportInTypeCheckingBlock, + parent_range.start(), + ) + }) + { ignores_by_statement .entry(stmt_id) .or_default() @@ -131,7 +132,7 @@ pub(crate) fn runtime_import_in_type_checking_block( for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports @@ -140,7 +141,7 @@ pub(crate) fn runtime_import_in_type_checking_block( RuntimeImportInTypeCheckingBlock { qualified_name: qualified_name.to_string(), }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -156,7 +157,7 @@ pub(crate) fn runtime_import_in_type_checking_block( // suppression comments aren't marked as unused. for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in ignores_by_statement.into_values().flatten() @@ -165,7 +166,7 @@ pub(crate) fn runtime_import_in_type_checking_block( RuntimeImportInTypeCheckingBlock { qualified_name: qualified_name.to_string(), }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -181,7 +182,7 @@ struct Import<'a> { /// The first reference to the imported symbol. reference_id: ReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 722a6bfbce..34e6eccbdf 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -278,11 +278,11 @@ pub(crate) fn typing_only_runtime_import( let import = Import { qualified_name, reference_id, - trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + range: binding.range, parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored(rule_for(import_type), import.trimmed_range.start()) + if checker.rule_is_ignored(rule_for(import_type), import.range.start()) || import.parent_range.map_or(false, |parent_range| { checker.rule_is_ignored(rule_for(import_type), parent_range.start()) }) @@ -311,14 +311,14 @@ pub(crate) fn typing_only_runtime_import( for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports { let mut diagnostic = Diagnostic::new( diagnostic_for(import_type, qualified_name.to_string()), - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -335,14 +335,14 @@ pub(crate) fn typing_only_runtime_import( for ((_, import_type), imports) in ignores_by_statement { for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports { let mut diagnostic = Diagnostic::new( diagnostic_for(import_type, qualified_name.to_string()), - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -359,7 +359,7 @@ struct Import<'a> { /// The first reference to the imported symbol. reference_id: ReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap index 49b96c6624..51e4ecc7c4 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:4:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap index c969cd7bc4..9ef1a60a95 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap @@ -117,12 +117,12 @@ strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 64 67 | 65 68 | def test(value: pkg.A): -strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block +strict.py:71:23: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 69 | def f(): 70 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 71 | import pkg.foo as F - | ^^^^^^^^^^^^ TCH002 + | ^ TCH002 72 | import pkg.foo.bar as B | = help: Move into type-checking block @@ -201,12 +201,12 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 93 96 | 94 97 | def test(value: pkg.A): -strict.py:101:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block +strict.py:101:23: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 99 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 100 | import pkg.bar as B 101 | import pkg.foo as F - | ^^^^^^^^^^^^ TCH002 + | ^ TCH002 102 | 103 | def test(value: F.Foo): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap index d208730b97..8eb591e7db 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap index e186f50adb..43ed4cbe88 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap index 5fa5f507ca..983345aae9 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: import os | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap index 3e490001a0..4246c582dc 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap index 19eb823d73..7005762cc5 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -TCH002.py:5:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:5:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | def f(): 5 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 6 | 7 | x: pd.DataFrame | @@ -53,11 +53,11 @@ TCH002.py:11:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 13 16 | x: DataFrame 14 17 | -TCH002.py:17:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block +TCH002.py:17:37: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 16 | def f(): 17 | from pandas import DataFrame as df # TCH002 - | ^^^^^^^^^^^^^^^ TCH002 + | ^^ TCH002 18 | 19 | x: df | @@ -81,11 +81,11 @@ TCH002.py:17:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 19 22 | x: df 20 23 | -TCH002.py:23:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:23:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 22 | def f(): 23 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 24 | 25 | x: pd.DataFrame = 1 | @@ -137,11 +137,11 @@ TCH002.py:29:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 31 34 | x: DataFrame = 2 32 35 | -TCH002.py:35:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block +TCH002.py:35:37: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 34 | def f(): 35 | from pandas import DataFrame as df # TCH002 - | ^^^^^^^^^^^^^^^ TCH002 + | ^^ TCH002 36 | 37 | x: df = 3 | @@ -165,11 +165,11 @@ TCH002.py:35:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 37 40 | x: df = 3 38 41 | -TCH002.py:41:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:41:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 40 | def f(): 41 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 42 | 43 | x: "pd.DataFrame" = 1 | @@ -193,11 +193,11 @@ TCH002.py:41:12: TCH002 [*] Move third-party import `pandas` into a type-checkin 43 46 | x: "pd.DataFrame" = 1 44 47 | -TCH002.py:47:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:47:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 46 | def f(): 47 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 48 | 49 | x = dict["pd.DataFrame", "pd.DataFrame"] | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap index de8fe6ddd7..c90b345fe7 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -runtime_evaluated_base_classes_2.py:3:8: TCH002 [*] Move third-party import `geopandas` into a type-checking block +runtime_evaluated_base_classes_2.py:3:21: TCH002 [*] Move third-party import `geopandas` into a type-checking block | 1 | from __future__ import annotations 2 | 3 | import geopandas as gpd # TCH002 - | ^^^^^^^^^^^^^^^^ TCH002 + | ^^^ TCH002 4 | import pydantic 5 | import pyproj # TCH002 | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap index 27235f0f48..41b6a8f207 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:4:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | from typing import TYPE_CHECKING | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap index 4c598b4014..271f52f2df 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 Move third-party import `pandas` into a type-checking block +:4:18: TCH002 Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap index 4530c20fc5..06752d7221 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs index 6393a65f3d..6b27ba7d62 100644 --- a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs +++ b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -152,7 +152,7 @@ pub(crate) fn function_is_too_complex( complexity, max_complexity, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } else { None diff --git a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs index 6067a04839..4f248b2040 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::{Scope, ScopeKind}; @@ -69,6 +69,6 @@ pub(crate) fn dunder_function_name( Some(Diagnostic::new( DunderFunctionName, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs index 50fcb36a1e..182ba1e404 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use crate::settings::types::IdentifierPattern; @@ -75,6 +75,6 @@ pub(crate) fn error_suffix_on_exception_name( ErrorSuffixOnExceptionName { name: name.to_string(), }, - identifier_range(class_def, locator), + class_def.identifier(locator), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs index 3c14cfa361..c6d7e72e99 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use crate::settings::types::IdentifierPattern; @@ -69,7 +69,7 @@ pub(crate) fn invalid_class_name( InvalidClassName { name: name.to_string(), }, - identifier_range(class_def, locator), + class_def.identifier(locator), )); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index 35fdc13a02..b23556566e 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Decorator, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::SemanticModel; @@ -81,6 +81,6 @@ pub(crate) fn invalid_function_name( InvalidFunctionName { name: name.to_string(), }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } diff --git a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs index 0b9eee1598..457bdd6347 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::except_range; +use ruff_python_ast::identifier::except; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -66,7 +66,7 @@ pub(crate) fn bare_except( .iter() .any(|stmt| matches!(stmt, Stmt::Raise(ast::StmtRaise { exc: None, .. }))) { - Some(Diagnostic::new(BareExcept, except_range(handler, locator))) + Some(Diagnostic::new(BareExcept, except(handler, locator))) } else { None } diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index c986fb6ac8..0f531d8d74 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -1,7 +1,7 @@ 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::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_overload; use ruff_python_semantic::{Definition, Member, MemberKind}; @@ -32,6 +32,6 @@ pub(crate) fn if_needed(checker: &mut Checker, docstring: &Docstring) { } checker.diagnostics.push(Diagnostic::new( OverloadWithDocstring, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index 7f2be87fff..1d78d99444 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -3,7 +3,7 @@ use ruff_text_size::TextRange; 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::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{ is_call, is_init, is_magic, is_new, is_overload, is_override, Visibility, }; @@ -135,7 +135,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicClass) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicClass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } false @@ -148,7 +148,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicNestedClass) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicNestedClass, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } false @@ -164,7 +164,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicFunction) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicFunction, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } false @@ -183,7 +183,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicInit) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicInit, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } true @@ -191,7 +191,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicMethod) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicMethod, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } true @@ -199,7 +199,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedMagicMethod) { checker.diagnostics.push(Diagnostic::new( UndocumentedMagicMethod, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } true @@ -207,7 +207,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicMethod) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicMethod, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } true diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index 30cbd6687c..26d3f06109 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -10,7 +10,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; use ruff_python_ast::docstrings::{clean_space, leading_space}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_staticmethod; use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::NewlineWithTrailingNewline; @@ -759,7 +759,7 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & let names = missing_arg_names.into_iter().sorted().collect(); checker.diagnostics.push(Diagnostic::new( UndocumentedParam { names }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs index e27c4bd1cd..26a760d2b7 100644 --- a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs +++ b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::except_range; +use ruff_python_ast::identifier::except; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -63,7 +63,7 @@ pub(crate) fn default_except_not_last( if type_.is_none() && idx < handlers.len() - 1 { return Some(Diagnostic::new( DefaultExceptNotLast, - except_range(handler, locator), + except(handler, locator), )); } } diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index 50898b388e..d004b6433c 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -117,11 +117,11 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let import = Import { qualified_name, - trimmed_range: binding.trimmed_range(checker.semantic(), checker.locator), + range: binding.range, parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored(Rule::UnusedImport, import.trimmed_range.start()) + if checker.rule_is_ignored(Rule::UnusedImport, import.range.start()) || import.parent_range.map_or(false, |parent_range| { checker.rule_is_ignored(Rule::UnusedImport, parent_range.start()) }) @@ -156,7 +156,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut for Import { qualified_name, - trimmed_range, + range, parent_range, } in imports { @@ -172,7 +172,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut }, multiple, }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -190,7 +190,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut // suppression comments aren't marked as unused. for Import { qualified_name, - trimmed_range, + range, parent_range, } in ignored.into_values().flatten() { @@ -200,7 +200,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut context: None, multiple: false, }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -214,7 +214,7 @@ struct Import<'a> { /// The qualified name of the import (e.g., `typing.List` for `from typing import List`). qualified_name: &'a str, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap index 67c97bb6ba..d7ce627b24 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap @@ -44,7 +44,7 @@ F401_0.py:12:8: F401 [*] `logging.handlers` imported but unused 10 | import multiprocessing.process 11 | import logging.config 12 | import logging.handlers - | ^^^^^^^^^^^^^^^^ F401 + | ^^^^^^^ F401 13 | from typing import ( 14 | TYPE_CHECKING, | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap index 1c7f2d6cab..2800a738ed 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap @@ -18,12 +18,12 @@ F401_5.py:2:17: F401 [*] `a.b.c` imported but unused 4 3 | import h.i 5 4 | import j.k as l -F401_5.py:3:17: F401 [*] `d.e.f` imported but unused +F401_5.py:3:22: F401 [*] `d.e.f` imported but unused | 1 | """Test: removal of multi-segment and aliases imports.""" 2 | from a.b import c 3 | from d.e import f as g - | ^^^^^^ F401 + | ^ F401 4 | import h.i 5 | import j.k as l | @@ -41,7 +41,7 @@ F401_5.py:4:8: F401 [*] `h.i` imported but unused 2 | from a.b import c 3 | from d.e import f as g 4 | import h.i - | ^^^ F401 + | ^ F401 5 | import j.k as l | = help: Remove unused import: `h.i` @@ -53,12 +53,12 @@ F401_5.py:4:8: F401 [*] `h.i` imported but unused 4 |-import h.i 5 4 | import j.k as l -F401_5.py:5:8: F401 [*] `j.k` imported but unused +F401_5.py:5:15: F401 [*] `j.k` imported but unused | 3 | from d.e import f as g 4 | import h.i 5 | import j.k as l - | ^^^^^^^^ F401 + | ^ F401 | = help: Remove unused import: `j.k` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap index bdfda37456..a6b21228c3 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap @@ -20,11 +20,11 @@ F401_6.py:7:25: F401 [*] `.background.BackgroundTasks` imported but unused 9 8 | # F401 `datastructures.UploadFile` imported but unused 10 9 | from .datastructures import UploadFile as FileUpload -F401_6.py:10:29: F401 [*] `.datastructures.UploadFile` imported but unused +F401_6.py:10:43: F401 [*] `.datastructures.UploadFile` imported but unused | 9 | # F401 `datastructures.UploadFile` imported but unused 10 | from .datastructures import UploadFile as FileUpload - | ^^^^^^^^^^^^^^^^^^^^^^^^ F401 + | ^^^^^^^^^^ F401 11 | 12 | # OK | @@ -58,11 +58,11 @@ F401_6.py:16:8: F401 [*] `background` imported but unused 18 17 | # F401 `datastructures` imported but unused 19 18 | import datastructures as structures -F401_6.py:19:8: F401 [*] `datastructures` imported but unused +F401_6.py:19:26: F401 [*] `datastructures` imported but unused | 18 | # F401 `datastructures` imported but unused 19 | import datastructures as structures - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F401 + | ^^^^^^^^^^ F401 | = help: Remove unused import: `datastructures` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap index 6f08dc26a2..365bef1c04 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap @@ -1,10 +1,10 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_1.py:1:18: F811 Redefinition of unused `FU` from line 1 +F811_1.py:1:25: F811 Redefinition of unused `FU` from line 1 | 1 | import fu as FU, bar as FU - | ^^^^^^^^^ F811 + | ^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap index 55c6dc663f..bf0275f23e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap @@ -1,10 +1,10 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_2.py:1:27: F811 Redefinition of unused `FU` from line 1 +F811_2.py:1:34: F811 Redefinition of unused `FU` from line 1 | 1 | from moo import fu as FU, bar as FU - | ^^^^^^^^^ F811 + | ^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap index 9a73c7bf9b..6f9198fc9d 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_23.py:4:8: F811 Redefinition of unused `foo` from line 3 +F811_23.py:4:15: F811 Redefinition of unused `foo` from line 3 | 3 | import foo as foo 4 | import bar as foo - | ^^^^^^^^^^ F811 + | ^^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap index fbf428003b..be49b03119 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap @@ -502,6 +502,9 @@ F841_3.py:110:5: F841 [*] Local variable `toplevel` is assigned to but never use 109 109 | def f(): 110 |- toplevel = tt = 1 110 |+ tt = 1 +111 111 | +112 112 | +113 113 | def f(provided: int) -> int: F841_3.py:110:16: F841 [*] Local variable `tt` is assigned to but never used | @@ -517,5 +520,68 @@ F841_3.py:110:16: F841 [*] Local variable `tt` is assigned to but never used 109 109 | def f(): 110 |- toplevel = tt = 1 110 |+ toplevel = 1 +111 111 | +112 112 | +113 113 | def f(provided: int) -> int: + +F841_3.py:115:19: F841 Local variable `x` is assigned to but never used + | +113 | def f(provided: int) -> int: +114 | match provided: +115 | case [_, *x]: + | ^ F841 +116 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:121:14: F841 Local variable `x` is assigned to but never used + | +119 | def f(provided: int) -> int: +120 | match provided: +121 | case x: + | ^ F841 +122 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:127:18: F841 Local variable `bar` is assigned to but never used + | +125 | def f(provided: int) -> int: +126 | match provided: +127 | case Foo(bar) as x: + | ^^^ F841 +128 | pass + | + = help: Remove assignment to unused variable `bar` + +F841_3.py:127:26: F841 Local variable `x` is assigned to but never used + | +125 | def f(provided: int) -> int: +126 | match provided: +127 | case Foo(bar) as x: + | ^ F841 +128 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:133:27: F841 Local variable `x` is assigned to but never used + | +131 | def f(provided: int) -> int: +132 | match provided: +133 | case {"foo": 0, **x}: + | ^ F841 +134 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:139:17: F841 Local variable `x` is assigned to but never used + | +137 | def f(provided: int) -> int: +138 | match provided: +139 | case {**x}: + | ^ F841 +140 | pass + | + = help: Remove assignment to unused variable `x` diff --git a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs index aaded36e14..1029a9d97f 100644 --- a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Arguments, Decorator, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -70,7 +70,7 @@ pub(crate) fn property_with_parameters( { checker.diagnostics.push(Diagnostic::new( PropertyWithParameters, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs index bd06b69c99..2d050ee75c 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Arguments, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -72,7 +72,7 @@ pub(crate) fn too_many_arguments(checker: &mut Checker, args: &Arguments, stmt: c_args: num_args, max_args: checker.settings.pylint.max_args, }, - identifier_range(stmt, checker.locator), + stmt.identifier(checker.locator), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs index 449a261d2b..8a9b0be78f 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -175,7 +175,7 @@ pub(crate) fn too_many_branches( branches, max_branches, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs index 2a48517067..ef5e45d2d3 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs @@ -2,7 +2,8 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, ReturnStatementVisitor}; +use ruff_python_ast::helpers::ReturnStatementVisitor; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_ast::statement_visitor::StatementVisitor; @@ -89,7 +90,7 @@ pub(crate) fn too_many_return_statements( returns, max_returns, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs index 87f3d57e9f..755daab2b0 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -158,7 +158,7 @@ pub(crate) fn too_many_statements( statements, max_statements, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs index 3b82d550f9..8277cb699e 100644 --- a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs +++ b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{Arguments, Decorator, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility::is_staticmethod; @@ -189,7 +189,7 @@ pub(crate) fn unexpected_special_method_signature( expected_params, actual_params, }, - identifier_range(stmt, locator), + stmt.identifier(locator), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs index 2073339f69..ab6f737cfb 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Excepthandler, MatchCase, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; +use ruff_python_ast::identifier; use crate::checkers::ast::Checker; @@ -102,7 +102,7 @@ pub(crate) fn useless_else_on_loop( if !orelse.is_empty() && !loop_exits_early(body) { checker.diagnostics.push(Diagnostic::new( UselessElseOnLoop, - helpers::else_range(stmt, checker.locator).unwrap(), + identifier::else_(stmt, checker.locator).unwrap(), )); } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs index faa8f49fbd..cd2483fd65 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -5,7 +5,7 @@ use rustpython_parser::ast::{self, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -53,7 +53,7 @@ pub(crate) fn unnecessary_class_parentheses( return; } - let offset = identifier_range(stmt, checker.locator).start(); + let offset = stmt.identifier(checker.locator).start(); let contents = checker.locator.after(offset); // Find the open and closing parentheses between the class name and the colon, if they exist. 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 d3a86d91fb..ad61c35c8b 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,7 @@ use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; @@ -73,7 +73,7 @@ pub(crate) fn useless_object_inheritance( diagnostic.try_set_fix(|| { let edit = remove_argument( checker.locator, - identifier_range(stmt, checker.locator).start(), + stmt.identifier(checker.locator).start(), expr.range(), &class_def.bases, &class_def.keywords, diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 0d86104608..38b750d00d 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -1,15 +1,13 @@ use std::borrow::Cow; -use std::ops::{Add, Sub}; +use std::ops::Sub; use std::path::Path; -use itertools::Itertools; -use log::error; use num_traits::Zero; use ruff_text_size::{TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; +use rustpython_ast::Cmpop; use rustpython_parser::ast::{ - self, Arguments, Cmpop, Constant, Excepthandler, Expr, Keyword, MatchCase, Pattern, Ranged, - Stmt, + self, Arguments, Constant, Excepthandler, Expr, Keyword, MatchCase, Pattern, Ranged, Stmt, }; use rustpython_parser::{lexer, Mode, Tok}; use smallvec::SmallVec; @@ -44,6 +42,7 @@ where range: _range, }) = expr { + // Ex) `list()` if args.is_empty() && keywords.is_empty() { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { if !is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { @@ -1071,185 +1070,6 @@ pub fn match_parens(start: TextSize, locator: &Locator) -> Option { } } -/// Return `true` if the given character is a valid identifier character. -fn is_identifier(c: char) -> bool { - c.is_alphanumeric() || c == '_' -} - -#[derive(Debug)] -enum IdentifierState { - /// We're in a comment, awaiting the identifier at the given index. - InComment { index: usize }, - /// We're looking for the identifier at the given index. - AwaitingIdentifier { index: usize }, - /// We're in the identifier at the given index, starting at the given character. - InIdentifier { index: usize, start: TextSize }, -} - -/// 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) -> TextRange { - match stmt { - Stmt::ClassDef(ast::StmtClassDef { - decorator_list, - range, - .. - }) - | Stmt::FunctionDef(ast::StmtFunctionDef { - decorator_list, - range, - .. - }) - | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, - range, - .. - }) => { - let header_range = decorator_list.last().map_or(*range, |last_decorator| { - TextRange::new(last_decorator.end(), range.end()) - }); - - // If the statement is an async function, we're looking for the third - // keyword-or-identifier (`foo` in `async def foo()`). Otherwise, it's the - // second keyword-or-identifier (`foo` in `def foo()` or `Foo` in `class Foo`). - let name_index = if stmt.is_async_function_def_stmt() { - 2 - } else { - 1 - }; - - let mut state = IdentifierState::AwaitingIdentifier { index: 0 }; - for (char_index, char) in locator.slice(header_range).char_indices() { - match state { - IdentifierState::InComment { index } => match char { - // Read until the end of the comment. - '\r' | '\n' => { - state = IdentifierState::AwaitingIdentifier { index }; - } - _ => {} - }, - IdentifierState::AwaitingIdentifier { index } => match char { - // Read until we hit an identifier. - '#' => { - state = IdentifierState::InComment { index }; - } - c if is_identifier(c) => { - state = IdentifierState::InIdentifier { - index, - start: TextSize::try_from(char_index).unwrap(), - }; - } - _ => {} - }, - IdentifierState::InIdentifier { index, start } => { - // We've reached the end of the identifier. - if !is_identifier(char) { - if index == name_index { - // We've found the identifier we're looking for. - let end = TextSize::try_from(char_index).unwrap(); - return TextRange::new( - header_range.start().add(start), - header_range.start().add(end), - ); - } - - // We're looking for a different identifier. - state = IdentifierState::AwaitingIdentifier { index: index + 1 }; - } - } - } - } - - error!("Failed to find identifier for {:?}", stmt); - header_range - } - _ => stmt.range(), - } -} - -/// Return the ranges of [`Tok::Name`] tokens within a specified node. -pub fn find_names<'a, T>( - located: &'a T, - locator: &'a Locator, -) -> impl Iterator + 'a -where - T: Ranged, -{ - let contents = locator.slice(located.range()); - - lexer::lex_starts_at(contents, Mode::Module, located.start()) - .flatten() - .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 { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { - name, - type_, - body, - range: _range, - }) = handler; - - match (name, type_) { - (Some(_), Some(type_)) => { - let contents = &locator.contents()[TextRange::new(type_.end(), body[0].start())]; - - lexer::lex_starts_at(contents, Mode::Module, type_.end()) - .flatten() - .tuple_windows() - .find(|(tok, next_tok)| { - matches!(tok.0, Tok::As) && matches!(next_tok.0, Tok::Name { .. }) - }) - .map(|((..), (_, range))| range) - } - _ => None, - } -} - -/// Return the `Range` of `except` in `Excepthandler`. -pub fn except_range(handler: &Excepthandler, locator: &Locator) -> TextRange { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, type_, .. }) = handler; - let end = if let Some(type_) = type_ { - type_.end() - } else { - body.first().expect("Expected body to be non-empty").start() - }; - let contents = &locator.contents()[TextRange::new(handler.start(), end)]; - - lexer::lex_starts_at(contents, Mode::Module, handler.start()) - .flatten() - .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 { - match stmt { - Stmt::For(ast::StmtFor { body, orelse, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) - | Stmt::While(ast::StmtWhile { body, orelse, .. }) - if !orelse.is_empty() => - { - 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_starts_at(contents, Mode::Module, body_end) - .flatten() - .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: TextRange, locator: &Locator) -> Option { let contents = &locator.contents()[range]; @@ -1482,7 +1302,101 @@ pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool { } } -#[derive(Clone, PartialEq, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, is_macro::Is)] +pub enum Truthiness { + // An expression evaluates to `False`. + Falsey, + // An expression evaluates to `True`. + Truthy, + // An expression evaluates to an unknown value (e.g., a variable `x` of unknown type). + Unknown, +} + +impl From> for Truthiness { + fn from(value: Option) -> Self { + match value { + Some(true) => Truthiness::Truthy, + Some(false) => Truthiness::Falsey, + None => Truthiness::Unknown, + } + } +} + +impl From for Option { + fn from(truthiness: Truthiness) -> Self { + match truthiness { + Truthiness::Truthy => Some(true), + Truthiness::Falsey => Some(false), + Truthiness::Unknown => None, + } + } +} + +impl Truthiness { + pub fn from_expr(expr: &Expr, is_builtin: F) -> Self + where + F: Fn(&str) -> bool, + { + match expr { + Expr::Constant(ast::ExprConstant { value, .. }) => match value { + Constant::Bool(value) => Some(*value), + Constant::None => Some(false), + Constant::Str(string) => Some(!string.is_empty()), + Constant::Bytes(bytes) => Some(!bytes.is_empty()), + Constant::Int(int) => Some(!int.is_zero()), + Constant::Float(float) => Some(*float != 0.0), + Constant::Complex { real, imag } => Some(*real != 0.0 || *imag != 0.0), + Constant::Ellipsis => Some(true), + Constant::Tuple(elts) => Some(!elts.is_empty()), + }, + Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range }) => { + if values.is_empty() { + Some(false) + } else if values.iter().any(|value| { + let Expr::Constant(ast::ExprConstant { value: Constant::Str(string), .. } )= &value else { + return false; + }; + !string.is_empty() + }) { + Some(true) + } else { + None + } + } + Expr::List(ast::ExprList { elts, range: _range, .. }) + | Expr::Set(ast::ExprSet { elts, range: _range }) + | Expr::Tuple(ast::ExprTuple { elts, range: _range,.. }) => Some(!elts.is_empty()), + Expr::Dict(ast::ExprDict { keys, range: _range, .. }) => Some(!keys.is_empty()), + Expr::Call(ast::ExprCall { + func, + args, + keywords, range: _range, + }) => { + if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { + if is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { + if args.is_empty() && keywords.is_empty() { + // Ex) `list()` + Some(false) + } else if args.len() == 1 && keywords.is_empty() { + // Ex) `list([1, 2, 3])` + Self::from_expr(&args[0], is_builtin).into() + } else { + None + } + } else { + None + } + } else { + None + } + } + _ => None, + } + .into() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct LocatedCmpop { pub range: TextRange, pub op: Cmpop, @@ -1581,111 +1495,19 @@ pub fn locate_cmpops(expr: &Expr, locator: &Locator) -> Vec { ops } -#[derive(Copy, Clone, Debug, PartialEq, is_macro::Is)] -pub enum Truthiness { - // An expression evaluates to `False`. - Falsey, - // An expression evaluates to `True`. - Truthy, - // An expression evaluates to an unknown value (e.g., a variable `x` of unknown type). - Unknown, -} - -impl From> for Truthiness { - fn from(value: Option) -> Self { - match value { - Some(true) => Truthiness::Truthy, - Some(false) => Truthiness::Falsey, - None => Truthiness::Unknown, - } - } -} - -impl From for Option { - fn from(truthiness: Truthiness) -> Self { - match truthiness { - Truthiness::Truthy => Some(true), - Truthiness::Falsey => Some(false), - Truthiness::Unknown => None, - } - } -} - -impl Truthiness { - pub fn from_expr(expr: &Expr, is_builtin: F) -> Self - where - F: Fn(&str) -> bool, - { - match expr { - Expr::Constant(ast::ExprConstant { value, .. }) => match value { - Constant::Bool(value) => Some(*value), - Constant::None => Some(false), - Constant::Str(string) => Some(!string.is_empty()), - Constant::Bytes(bytes) => Some(!bytes.is_empty()), - Constant::Int(int) => Some(!int.is_zero()), - Constant::Float(float) => Some(*float != 0.0), - Constant::Complex { real, imag } => Some(*real != 0.0 || *imag != 0.0), - Constant::Ellipsis => Some(true), - Constant::Tuple(elts) => Some(!elts.is_empty()), - }, - Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range }) => { - if values.is_empty() { - Some(false) - } else if values.iter().any(|value| { - let Expr::Constant(ast::ExprConstant { value: Constant::Str(string), .. } )= &value else { - return false; - }; - !string.is_empty() - }) { - Some(true) - } else { - None - } - } - Expr::List(ast::ExprList { elts, range: _range, .. }) - | Expr::Set(ast::ExprSet { elts, range: _range }) - | Expr::Tuple(ast::ExprTuple { elts, range: _range,.. }) => Some(!elts.is_empty()), - Expr::Dict(ast::ExprDict { keys, range: _range, .. }) => Some(!keys.is_empty()), - Expr::Call(ast::ExprCall { - func, - args, - keywords, range: _range, - }) => { - if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { - if is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { - if args.is_empty() && keywords.is_empty() { - Some(false) - } else if args.len() == 1 && keywords.is_empty() { - Self::from_expr(&args[0], is_builtin).into() - } else { - None - } - } else { - None - } - } else { - None - } - } - _ => None, - } - .into() - } -} - #[cfg(test)] mod tests { use std::borrow::Cow; use anyhow::Result; use ruff_text_size::{TextLen, TextRange, TextSize}; - use rustpython_ast::{Expr, Stmt, Suite}; - use rustpython_parser::ast::Cmpop; + use rustpython_ast::{Cmpop, Expr, Stmt}; + use rustpython_parser::ast::Suite; use rustpython_parser::Parse; use crate::helpers::{ - elif_else_range, else_range, first_colon_range, has_trailing_content, identifier_range, - locate_cmpops, resolve_imported_module_path, LocatedCmpop, + elif_else_range, first_colon_range, has_trailing_content, locate_cmpops, + resolve_imported_module_path, LocatedCmpop, }; use crate::source_code::Locator; @@ -1728,90 +1550,6 @@ y = 2 Ok(()) } - #[test] - fn extract_identifier_range() -> Result<()> { - let contents = "def f(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(4), TextSize::from(5)) - ); - - let contents = "async def f(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(10), TextSize::from(11)) - ); - - let contents = r#" -def \ - f(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(8), TextSize::from(9)) - ); - - let contents = "class Class(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(6), TextSize::from(11)) - ); - - let contents = "class Class: pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(6), TextSize::from(11)) - ); - - let contents = r#" -@decorator() -class Class(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(19), TextSize::from(24)) - ); - - let contents = r#" -@decorator() # Comment -class Class(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(30), TextSize::from(35)) - ); - - let contents = r#"x = y + 1"#.trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(0), TextSize::from(9)) - ); - - Ok(()) - } - #[test] fn resolve_import() { // Return the module directly. @@ -1849,26 +1587,6 @@ class Class(): ); } - #[test] - fn extract_else_range() -> Result<()> { - let contents = r#" -for x in y: - pass -else: - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - let range = else_range(&stmt, &locator).unwrap(); - assert_eq!(&contents[range], "else"); - assert_eq!( - range, - TextRange::new(TextSize::from(21), TextSize::from(25)) - ); - Ok(()) - } - #[test] fn extract_first_colon_range() { let contents = "with a: pass"; diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs new file mode 100644 index 0000000000..c1cb6c4a32 --- /dev/null +++ b/crates/ruff_python_ast/src/identifier.rs @@ -0,0 +1,621 @@ +//! Extract [`TextRange`] information from AST nodes. +//! +//! In the `RustPython` AST, each node has a `range` field that contains the +//! start and end byte offsets of the node. However, attributes on those +//! nodes may not have their own ranges. In particular, identifiers are +//! not given their own ranges, unless they're part of a name expression. +//! +//! For example, given: +//! ```python +//! def f(): +//! ... +//! ``` +//! +//! The statement defining `f` has a range, but the identifier `f` does not. +//! +//! This module assists with extracting [`TextRange`] ranges from AST nodes +//! via manual lexical analysis. + +use std::ops::{Add, Sub}; +use std::str::Chars; + +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_ast::{Alias, Arg, Pattern}; +use rustpython_parser::ast::{self, Excepthandler, Ranged, Stmt}; + +use ruff_python_whitespace::is_python_whitespace; + +use crate::source_code::Locator; + +pub trait Identifier { + /// Return the [`TextRange`] of the identifier in the given AST node. + fn identifier(&self, locator: &Locator) -> TextRange; +} + +pub trait TryIdentifier { + /// Return the [`TextRange`] of the identifier in the given AST node, or `None` if + /// the node does not have an identifier. + fn try_identifier(&self, locator: &Locator) -> Option; +} + +impl Identifier for Stmt { + /// Return the [`TextRange`] of the identifier in the given statement. + /// + /// For example, return the range of `f` in: + /// ```python + /// def f(): + /// ... + /// ``` + fn identifier(&self, locator: &Locator) -> TextRange { + match self { + Stmt::ClassDef(ast::StmtClassDef { + decorator_list, + range, + .. + }) + | Stmt::FunctionDef(ast::StmtFunctionDef { + decorator_list, + range, + .. + }) => { + let range = decorator_list.last().map_or(*range, |last_decorator| { + TextRange::new(last_decorator.end(), range.end()) + }); + + // The first "identifier" is the `def` or `class` keyword. + // The second "identifier" is the function or class name. + IdentifierTokenizer::starts_at(range.start(), locator.contents()) + .nth(1) + .expect("Unable to identify identifier in function or class definition") + } + Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + decorator_list, + range, + .. + }) => { + let range = decorator_list.last().map_or(*range, |last_decorator| { + TextRange::new(last_decorator.end(), range.end()) + }); + + // The first "identifier" is the `async` keyword. + // The second "identifier" is the `def` or `class` keyword. + // The third "identifier" is the function or class name. + IdentifierTokenizer::starts_at(range.start(), locator.contents()) + .nth(2) + .expect("Unable to identify identifier in function or class definition") + } + _ => self.range(), + } + } +} + +impl Identifier for Arg { + /// Return the [`TextRange`] for the identifier defining an [`Arg`]. + /// + /// For example, return the range of `x` in: + /// ```python + /// def f(x: int = 0): + /// ... + /// ``` + fn identifier(&self, locator: &Locator) -> TextRange { + IdentifierTokenizer::new(locator.contents(), self.range()) + .next() + .expect("Failed to find argument identifier") + } +} + +impl Identifier for Alias { + /// Return the [`TextRange`] for the identifier defining an [`Alias`]. + /// + /// For example, return the range of `x` in: + /// ```python + /// from foo import bar as x + /// ``` + fn identifier(&self, locator: &Locator) -> TextRange { + if matches!(self.name.as_str(), "*") { + self.range() + } else if self.asname.is_none() { + // The first identifier is the module name. + IdentifierTokenizer::new(locator.contents(), self.range()) + .next() + .expect("Failed to find alias identifier") + } else { + // The first identifier is the module name. + // The second identifier is the "as" keyword. + // The third identifier is the alias name. + IdentifierTokenizer::new(locator.contents(), self.range()) + .last() + .expect("Failed to find alias identifier") + } + } +} + +impl TryIdentifier for Pattern { + /// Return the [`TextRange`] of the identifier in the given pattern. + /// + /// For example, return the range of `z` in: + /// ```python + /// match x: + /// # Pattern::MatchAs + /// case z: + /// ... + /// ``` + /// + /// Or: + /// ```python + /// match x: + /// # Pattern::MatchAs + /// case y as z: + /// ... + /// ``` + /// + /// Or : + /// ```python + /// match x: + /// # Pattern::MatchMapping + /// case {"a": 1, **z} + /// ... + /// ``` + /// + /// Or : + /// ```python + /// match x: + /// # Pattern::MatchStar + /// case *z: + /// ... + /// ``` + fn try_identifier(&self, locator: &Locator) -> Option { + match self { + Pattern::MatchAs(ast::PatternMatchAs { + name: Some(_), + pattern, + range, + }) => { + Some(if let Some(pattern) = pattern { + // Identify `z` in: + // ```python + // match x: + // case Foo(bar) as z: + // ... + // ``` + IdentifierTokenizer::starts_at(pattern.end(), locator.contents()) + .nth(1) + .expect("Unable to identify identifier in pattern") + } else { + // Identify `z` in: + // ```python + // match x: + // case z: + // ... + // ``` + *range + }) + } + Pattern::MatchMapping(ast::PatternMatchMapping { + patterns, + rest: Some(_), + .. + }) => { + Some(if let Some(pattern) = patterns.last() { + // Identify `z` in: + // ```python + // match x: + // case {"a": 1, **z} + // ... + // ``` + // + // A mapping pattern can contain at most one double-star pattern, + // and it must be the last pattern in the mapping. + IdentifierTokenizer::starts_at(pattern.end(), locator.contents()) + .next() + .expect("Unable to identify identifier in pattern") + } else { + // Identify `z` in: + // ```python + // match x: + // case {**z} + // ... + // ``` + IdentifierTokenizer::starts_at(self.start(), locator.contents()) + .next() + .expect("Unable to identify identifier in pattern") + }) + } + Pattern::MatchStar(ast::PatternMatchStar { name: Some(_), .. }) => { + // Identify `z` in: + // ```python + // match x: + // case *z: + // ... + // ``` + Some( + IdentifierTokenizer::starts_at(self.start(), locator.contents()) + .next() + .expect("Unable to identify identifier in pattern"), + ) + } + _ => None, + } + } +} + +impl TryIdentifier for Excepthandler { + /// Return the [`TextRange`] of a named exception in an [`Excepthandler`]. + /// + /// For example, return the range of `e` in: + /// ```python + /// try: + /// ... + /// except ValueError as e: + /// ... + /// ``` + fn try_identifier(&self, locator: &Locator) -> Option { + let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, name, .. }) = + self; + + if name.is_none() { + return None; + } + + let Some(type_) = type_ else { + return None; + }; + + // The exception name is the first identifier token after the `as` keyword. + Some( + IdentifierTokenizer::starts_at(type_.end(), locator.contents()) + .nth(1) + .expect("Failed to find exception identifier in exception handler"), + ) + } +} + +/// Return the [`TextRange`] for every name in a [`Stmt`]. +/// +/// Intended to be used for `global` and `nonlocal` statements. +/// +/// For example, return the ranges of `x` and `y` in: +/// ```python +/// global x, y +/// ``` +pub fn names<'a>(stmt: &Stmt, locator: &'a Locator<'a>) -> impl Iterator + 'a { + // Given `global x, y`, the first identifier is `global`, and the remaining identifiers are + // the names. + IdentifierTokenizer::new(locator.contents(), stmt.range()).skip(1) +} + +/// Return the [`TextRange`] of the `except` token in an [`Excepthandler`]. +pub fn except(handler: &Excepthandler, locator: &Locator) -> TextRange { + IdentifierTokenizer::new(locator.contents(), handler.range()) + .next() + .expect("Failed to find `except` token in `Excepthandler`") +} + +/// Return the [`TextRange`] of the `else` token in a `For`, `AsyncFor`, or `While` statement. +pub fn else_(stmt: &Stmt, locator: &Locator) -> Option { + let (Stmt::For(ast::StmtFor { body, orelse, .. }) + | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) + | Stmt::While(ast::StmtWhile { body, orelse, .. })) = stmt else { + return None; + }; + + if orelse.is_empty() { + return None; + } + + IdentifierTokenizer::starts_at( + body.last().expect("Expected body to be non-empty").end(), + locator.contents(), + ) + .next() +} + +/// Return `true` if the given character starts a valid Python identifier. +/// +/// Python identifiers must start with an alphabetic character or an underscore. +fn is_python_identifier_start(c: char) -> bool { + c.is_alphabetic() || c == '_' +} + +/// Return `true` if the given character is a valid Python identifier continuation character. +/// +/// Python identifiers can contain alphanumeric characters and underscores, but cannot start with a +/// number. +fn is_python_identifier_continue(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +/// Simple zero allocation tokenizer for Python identifiers. +/// +/// The tokenizer must operate over a range that can only contain identifiers, keywords, and +/// comments (along with whitespace and continuation characters). It does not support other tokens, +/// like operators, literals, or delimiters. It also does not differentiate between keywords and +/// identifiers, treating every valid token as an "identifier". +/// +/// This is useful for cases like, e.g., identifying the alias name in an aliased import (`bar` in +/// `import foo as bar`), where we're guaranteed to only have identifiers and keywords in the +/// relevant range. +pub(crate) struct IdentifierTokenizer<'a> { + cursor: Cursor<'a>, + offset: TextSize, +} + +impl<'a> IdentifierTokenizer<'a> { + pub(crate) fn new(source: &'a str, range: TextRange) -> Self { + Self { + cursor: Cursor::new(&source[range]), + offset: range.start(), + } + } + + pub(crate) fn starts_at(offset: TextSize, source: &'a str) -> Self { + let range = TextRange::new(offset, source.text_len()); + Self::new(source, range) + } + + fn next_token(&mut self) -> Option { + while let Some(c) = self.cursor.bump() { + match c { + c if is_python_identifier_start(c) => { + let start = self.offset.add(self.cursor.offset()).sub(c.text_len()); + self.cursor.eat_while(is_python_identifier_continue); + let end = self.offset.add(self.cursor.offset()); + return Some(TextRange::new(start, end)); + } + + c if is_python_whitespace(c) => { + self.cursor.eat_while(is_python_whitespace); + } + + '#' => { + self.cursor.eat_while(|c| !matches!(c, '\n' | '\r')); + } + + '\r' => { + self.cursor.eat_char('\n'); + } + + '\n' => { + // Nothing to do. + } + + '\\' => { + // Nothing to do. + } + + _ => { + // Nothing to do. + } + }; + } + + None + } +} + +impl Iterator for IdentifierTokenizer<'_> { + type Item = TextRange; + + fn next(&mut self) -> Option { + self.next_token() + } +} + +const EOF_CHAR: char = '\0'; + +#[derive(Debug, Clone)] +struct Cursor<'a> { + chars: Chars<'a>, + offset: TextSize, +} + +impl<'a> Cursor<'a> { + fn new(source: &'a str) -> Self { + Self { + chars: source.chars(), + offset: TextSize::from(0), + } + } + + const fn offset(&self) -> TextSize { + self.offset + } + + /// Peeks the next character from the input stream without consuming it. + /// Returns [`EOF_CHAR`] if the file is at the end of the file. + fn first(&self) -> char { + self.chars.clone().next().unwrap_or(EOF_CHAR) + } + + /// Returns `true` if the file is at the end of the file. + fn is_eof(&self) -> bool { + self.chars.as_str().is_empty() + } + + /// Consumes the next character. + fn bump(&mut self) -> Option { + if let Some(char) = self.chars.next() { + self.offset += char.text_len(); + Some(char) + } else { + None + } + } + + /// Eats the next character if it matches the given character. + fn eat_char(&mut self, c: char) -> bool { + if self.first() == c { + self.bump(); + true + } else { + false + } + } + + /// Eats symbols while predicate returns true or until the end of file is reached. + fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + while predicate(self.first()) && !self.is_eof() { + self.bump(); + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use ruff_text_size::{TextRange, TextSize}; + use rustpython_ast::Stmt; + use rustpython_parser::Parse; + + use crate::identifier; + use crate::identifier::Identifier; + use crate::source_code::Locator; + + #[test] + fn extract_arg_range() -> Result<()> { + let contents = "def f(x): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let function_def = stmt.as_function_def_stmt().unwrap(); + let args = &function_def.args.args; + let arg = &args[0]; + let locator = Locator::new(contents); + assert_eq!( + arg.identifier(&locator), + TextRange::new(TextSize::from(6), TextSize::from(7)) + ); + + let contents = "def f(x: int): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let function_def = stmt.as_function_def_stmt().unwrap(); + let args = &function_def.args.args; + let arg = &args[0]; + let locator = Locator::new(contents); + assert_eq!( + arg.identifier(&locator), + TextRange::new(TextSize::from(6), TextSize::from(7)) + ); + + let contents = r#" +def f( + x: int, # Comment +): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let function_def = stmt.as_function_def_stmt().unwrap(); + let args = &function_def.args.args; + let arg = &args[0]; + let locator = Locator::new(contents); + assert_eq!( + arg.identifier(&locator), + TextRange::new(TextSize::from(11), TextSize::from(12)) + ); + + Ok(()) + } + + #[test] + fn extract_identifier_range() -> Result<()> { + let contents = "def f(): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(4), TextSize::from(5)) + ); + + let contents = "async def f(): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(10), TextSize::from(11)) + ); + + let contents = r#" +def \ + f(): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(8), TextSize::from(9)) + ); + + let contents = "class Class(): pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(6), TextSize::from(11)) + ); + + let contents = "class Class: pass".trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(6), TextSize::from(11)) + ); + + let contents = r#" +@decorator() +class Class(): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(19), TextSize::from(24)) + ); + + let contents = r#" +@decorator() # Comment +class Class(): + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(30), TextSize::from(35)) + ); + + let contents = r#"x = y + 1"#.trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + assert_eq!( + stmt.identifier(&locator), + TextRange::new(TextSize::from(0), TextSize::from(9)) + ); + + Ok(()) + } + + #[test] + fn extract_else_range() -> Result<()> { + let contents = r#" +for x in y: + pass +else: + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + let range = identifier::else_(&stmt, &locator).unwrap(); + assert_eq!(&contents[range], "else"); + assert_eq!( + range, + TextRange::new(TextSize::from(21), TextSize::from(25)) + ); + Ok(()) + } +} diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index be82bf8233..275e62aeab 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -6,6 +6,7 @@ pub mod docstrings; pub mod function; pub mod hashable; pub mod helpers; +pub mod identifier; pub mod imports; pub mod node; pub mod prelude; diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index c1f5bcfdf6..01415e89b1 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -477,6 +477,7 @@ impl<'a> Cursor<'a> { self.source_length = self.text_len(); } + /// Returns `true` if the file is at the end of the file. fn is_eof(&self) -> bool { self.chars.as_str().is_empty() } diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index b390362ae8..ea79278389 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -5,8 +5,6 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::Ranged; use ruff_index::{newtype_index, IndexSlice, IndexVec}; -use ruff_python_ast::helpers; -use ruff_python_ast::source_code::Locator; use crate::context::ExecutionContext; use crate::model::SemanticModel; @@ -141,18 +139,6 @@ impl<'a> Binding<'a> { } } - /// Returns the appropriate visual range for highlighting this binding. - pub fn trimmed_range(&self, semantic: &SemanticModel, locator: &Locator) -> TextRange { - match self.kind { - BindingKind::ClassDefinition | BindingKind::FunctionDefinition => { - self.source.map_or(self.range, |source| { - helpers::identifier_range(semantic.stmts[source], locator) - }) - } - _ => self.range, - } - } - /// Returns the range of the binding's parent. pub fn parent_range(&self, semantic: &SemanticModel) -> Option { self.source