Compare commits

...

2 Commits

Author SHA1 Message Date
Charlie Marsh
2446cd49fa Make CallPath its own struct 2023-04-01 12:06:01 -04:00
Charlie Marsh
5f5e71e81d Make collect_call_path return an Option 2023-04-01 12:05:52 -04:00
22 changed files with 242 additions and 165 deletions

View File

@@ -107,8 +107,7 @@ pub fn check_positional_boolean_in_def(
} }
if decorator_list.iter().any(|expr| { if decorator_list.iter().any(|expr| {
let call_path = collect_call_path(expr); collect_call_path(expr).map_or(false, |call_path| call_path.as_slice() == [name, "setter"])
call_path.as_slice() == [name, "setter"]
}) { }) {
return; return;
} }
@@ -151,8 +150,7 @@ pub fn check_boolean_default_value_in_function_definition(
} }
if decorator_list.iter().any(|expr| { if decorator_list.iter().any(|expr| {
let call_path = collect_call_path(expr); collect_call_path(expr).map_or(false, |call_path| call_path.as_slice() == [name, "setter"])
call_path.as_slice() == [name, "setter"]
}) { }) {
return; return;
} }

View File

@@ -70,8 +70,7 @@ fn duplicate_handler_exceptions<'a>(
let mut duplicates: FxHashSet<CallPath> = FxHashSet::default(); let mut duplicates: FxHashSet<CallPath> = FxHashSet::default();
let mut unique_elts: Vec<&Expr> = Vec::default(); let mut unique_elts: Vec<&Expr> = Vec::default();
for type_ in elts { for type_ in elts {
let call_path = call_path::collect_call_path(type_); if let Some(call_path) = call_path::collect_call_path(type_) {
if !call_path.is_empty() {
if seen.contains_key(&call_path) { if seen.contains_key(&call_path) {
duplicates.insert(call_path); duplicates.insert(call_path);
} else { } else {
@@ -125,8 +124,7 @@ pub fn duplicate_exceptions(checker: &mut Checker, handlers: &[Excepthandler]) {
}; };
match &type_.node { match &type_.node {
ExprKind::Attribute { .. } | ExprKind::Name { .. } => { ExprKind::Attribute { .. } | ExprKind::Name { .. } => {
let call_path = call_path::collect_call_path(type_); if let Some(call_path) = call_path::collect_call_path(type_) {
if !call_path.is_empty() {
if seen.contains(&call_path) { if seen.contains(&call_path) {
duplicates.entry(call_path).or_default().push(type_); duplicates.entry(call_path).or_default().push(type_);
} else { } else {

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast::{Arguments, Constant, Expr, ExprKind};
use ruff_diagnostics::Violation; use ruff_diagnostics::Violation;
use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::to_call_path; use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::call_path::{compose_call_path, CallPath}; use ruff_python_ast::call_path::{compose_call_path, CallPath};
use ruff_python_ast::types::Range; use ruff_python_ast::types::Range;
use ruff_python_ast::visitor; use ruff_python_ast::visitor;
@@ -123,7 +123,7 @@ pub fn function_call_argument_default(checker: &mut Checker, arguments: &Argumen
.flake8_bugbear .flake8_bugbear
.extend_immutable_calls .extend_immutable_calls
.iter() .iter()
.map(|target| to_call_path(target)) .map(|target| from_qualified_name(target))
.collect(); .collect();
let diagnostics = { let diagnostics = {
let mut visitor = ArgumentDefaultVisitor { let mut visitor = ArgumentDefaultVisitor {

View File

@@ -2,14 +2,11 @@ use rustpython_parser::ast::{Expr, Stmt};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::format_call_path; use ruff_python_ast::call_path::{format_call_path, from_unqualified_name, CallPath};
use ruff_python_ast::types::Range; use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::flake8_debugger::types::DebuggerUsingType;
use super::types::DebuggerUsingType;
// flake8-debugger
#[violation] #[violation]
pub struct Debugger { pub struct Debugger {
@@ -70,9 +67,12 @@ pub fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option<
} }
if let Some(module) = module { if let Some(module) = module {
let mut call_path = module.split('.').collect::<Vec<_>>(); let mut call_path: CallPath = from_unqualified_name(module);
call_path.push(name); call_path.push(name);
if DEBUGGERS.iter().any(|target| call_path == **target) { if DEBUGGERS
.iter()
.any(|target| call_path.as_slice() == *target)
{
return Some(Diagnostic::new( return Some(Diagnostic::new(
Debugger { Debugger {
using_type: DebuggerUsingType::Import(format_call_path(&call_path)), using_type: DebuggerUsingType::Import(format_call_path(&call_path)),
@@ -81,10 +81,10 @@ pub fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option<
)); ));
} }
} else { } else {
let parts = name.split('.').collect::<Vec<_>>(); let parts: CallPath = from_unqualified_name(name);
if DEBUGGERS if DEBUGGERS
.iter() .iter()
.any(|call_path| call_path[..call_path.len() - 1] == parts) .any(|call_path| &call_path[..call_path.len() - 1] == parts.as_slice())
{ {
return Some(Diagnostic::new( return Some(Diagnostic::new(
Debugger { Debugger {

View File

@@ -16,8 +16,8 @@ use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule}; use crate::registry::{AsRule, Rule};
use super::helpers::{ use super::helpers::{
get_mark_decorators, get_mark_name, is_abstractmethod_decorator, is_pytest_fixture, get_mark_decorators, is_abstractmethod_decorator, is_pytest_fixture, is_pytest_yield_fixture,
is_pytest_yield_fixture, keyword_is_literal, keyword_is_literal,
}; };
#[violation] #[violation]
@@ -212,7 +212,9 @@ where
} }
} }
ExprKind::Call { func, .. } => { ExprKind::Call { func, .. } => {
if collect_call_path(func).as_slice() == ["request", "addfinalizer"] { if collect_call_path(func).map_or(false, |call_path| {
call_path.as_slice() == ["request", "addfinalizer"]
}) {
self.addfinalizer_call = Some(expr); self.addfinalizer_call = Some(expr);
}; };
visitor::walk_expr(self, expr); visitor::walk_expr(self, expr);
@@ -466,20 +468,19 @@ fn check_fixture_addfinalizer(checker: &mut Checker, args: &Arguments, body: &[S
/// PT024, PT025 /// PT024, PT025
fn check_fixture_marks(checker: &mut Checker, decorators: &[Expr]) { fn check_fixture_marks(checker: &mut Checker, decorators: &[Expr]) {
for mark in get_mark_decorators(decorators) { for (expr, call_path) in get_mark_decorators(decorators) {
let name = get_mark_name(mark); let name = call_path.last().expect("Expected a mark name");
if checker if checker
.settings .settings
.rules .rules
.enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture) .enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture)
{ {
if name == "asyncio" { if *name == "asyncio" {
let mut diagnostic = let mut diagnostic =
Diagnostic::new(PytestUnnecessaryAsyncioMarkOnFixture, Range::from(mark)); Diagnostic::new(PytestUnnecessaryAsyncioMarkOnFixture, Range::from(expr));
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
let start = Location::new(mark.location.row(), 0); let start = Location::new(expr.location.row(), 0);
let end = Location::new(mark.end_location.unwrap().row() + 1, 0); let end = Location::new(expr.end_location.unwrap().row() + 1, 0);
diagnostic.set_fix(Edit::deletion(start, end)); diagnostic.set_fix(Edit::deletion(start, end));
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
@@ -491,12 +492,12 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Expr]) {
.rules .rules
.enabled(Rule::PytestErroneousUseFixturesOnFixture) .enabled(Rule::PytestErroneousUseFixturesOnFixture)
{ {
if name == "usefixtures" { if *name == "usefixtures" {
let mut diagnostic = let mut diagnostic =
Diagnostic::new(PytestErroneousUseFixturesOnFixture, Range::from(mark)); Diagnostic::new(PytestErroneousUseFixturesOnFixture, Range::from(expr));
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
let start = Location::new(mark.location.row(), 0); let start = Location::new(expr.location.row(), 0);
let end = Location::new(mark.end_location.unwrap().row() + 1, 0); let end = Location::new(expr.end_location.unwrap().row() + 1, 0);
diagnostic.set_fix(Edit::deletion(start, end)); diagnostic.set_fix(Edit::deletion(start, end));
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);

View File

@@ -1,5 +1,5 @@
use num_traits::identities::Zero; use num_traits::identities::Zero;
use ruff_python_ast::call_path::collect_call_path; use ruff_python_ast::call_path::{collect_call_path, CallPath};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword}; use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword};
use ruff_python_ast::helpers::map_callable; use ruff_python_ast::helpers::map_callable;
@@ -8,14 +8,17 @@ use crate::checkers::ast::Checker;
const ITERABLE_INITIALIZERS: &[&str] = &["dict", "frozenset", "list", "tuple", "set"]; const ITERABLE_INITIALIZERS: &[&str] = &["dict", "frozenset", "list", "tuple", "set"];
pub fn get_mark_decorators(decorators: &[Expr]) -> impl Iterator<Item = &Expr> { pub fn get_mark_decorators(decorators: &[Expr]) -> impl Iterator<Item = (&Expr, CallPath)> {
decorators decorators.iter().filter_map(|decorator| {
.iter() let Some(call_path) = collect_call_path(map_callable(decorator)) else {
.filter(|decorator| is_pytest_mark(decorator)) return None;
} };
if call_path.len() > 2 && call_path.as_slice()[..2] == ["pytest", "mark"] {
pub fn get_mark_name(decorator: &Expr) -> &str { Some((decorator, call_path))
collect_call_path(map_callable(decorator)).last().unwrap() } else {
None
}
})
} }
pub fn is_pytest_fail(call: &Expr, checker: &Checker) -> bool { pub fn is_pytest_fail(call: &Expr, checker: &Checker) -> bool {
@@ -40,15 +43,6 @@ pub fn is_pytest_fixture(decorator: &Expr, checker: &Checker) -> bool {
}) })
} }
pub fn is_pytest_mark(decorator: &Expr) -> bool {
let segments = collect_call_path(map_callable(decorator));
if segments.len() > 2 {
segments[0] == "pytest" && segments[1] == "mark"
} else {
false
}
}
pub fn is_pytest_yield_fixture(decorator: &Expr, checker: &Checker) -> bool { pub fn is_pytest_yield_fixture(decorator: &Expr, checker: &Checker) -> bool {
checker checker
.ctx .ctx

View File

@@ -2,12 +2,13 @@ use rustpython_parser::ast::{Expr, ExprKind, Location};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::types::Range; use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule}; use crate::registry::{AsRule, Rule};
use super::helpers::{get_mark_decorators, get_mark_name}; use super::helpers::get_mark_decorators;
#[violation] #[violation]
pub struct PytestIncorrectMarkParenthesesStyle { pub struct PytestIncorrectMarkParenthesesStyle {
@@ -52,13 +53,14 @@ impl AlwaysAutofixableViolation for PytestUseFixturesWithoutParameters {
fn pytest_mark_parentheses( fn pytest_mark_parentheses(
checker: &mut Checker, checker: &mut Checker,
decorator: &Expr, decorator: &Expr,
call_path: &CallPath,
fix: Edit, fix: Edit,
preferred: &str, preferred: &str,
actual: &str, actual: &str,
) { ) {
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
PytestIncorrectMarkParenthesesStyle { PytestIncorrectMarkParenthesesStyle {
mark_name: get_mark_name(decorator).to_string(), mark_name: (*call_path.last().unwrap()).to_string(),
expected_parens: preferred.to_string(), expected_parens: preferred.to_string(),
actual_parens: actual.to_string(), actual_parens: actual.to_string(),
}, },
@@ -70,7 +72,7 @@ fn pytest_mark_parentheses(
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
fn check_mark_parentheses(checker: &mut Checker, decorator: &Expr) { fn check_mark_parentheses(checker: &mut Checker, decorator: &Expr, call_path: &CallPath) {
match &decorator.node { match &decorator.node {
ExprKind::Call { ExprKind::Call {
func, func,
@@ -84,20 +86,20 @@ fn check_mark_parentheses(checker: &mut Checker, decorator: &Expr) {
{ {
let fix = let fix =
Edit::deletion(func.end_location.unwrap(), decorator.end_location.unwrap()); Edit::deletion(func.end_location.unwrap(), decorator.end_location.unwrap());
pytest_mark_parentheses(checker, decorator, fix, "", "()"); pytest_mark_parentheses(checker, decorator, call_path, fix, "", "()");
} }
} }
_ => { _ => {
if checker.settings.flake8_pytest_style.mark_parentheses { if checker.settings.flake8_pytest_style.mark_parentheses {
let fix = Edit::insertion("()".to_string(), decorator.end_location.unwrap()); let fix = Edit::insertion("()".to_string(), decorator.end_location.unwrap());
pytest_mark_parentheses(checker, decorator, fix, "()", ""); pytest_mark_parentheses(checker, decorator, call_path, fix, "()", "");
} }
} }
} }
} }
fn check_useless_usefixtures(checker: &mut Checker, decorator: &Expr) { fn check_useless_usefixtures(checker: &mut Checker, decorator: &Expr, call_path: &CallPath) {
if get_mark_name(decorator) != "usefixtures" { if *call_path.last().unwrap() != "usefixtures" {
return; return;
} }
@@ -130,12 +132,12 @@ pub fn marks(checker: &mut Checker, decorators: &[Expr]) {
.rules .rules
.enabled(Rule::PytestUseFixturesWithoutParameters); .enabled(Rule::PytestUseFixturesWithoutParameters);
for mark in get_mark_decorators(decorators) { for (expr, call_path) in get_mark_decorators(decorators) {
if enforce_parentheses { if enforce_parentheses {
check_mark_parentheses(checker, mark); check_mark_parentheses(checker, expr, &call_path);
} }
if enforce_useless_usefixtures { if enforce_useless_usefixtures {
check_useless_usefixtures(checker, mark); check_useless_usefixtures(checker, expr, &call_path);
} }
} }
} }

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, ExprKind, Keyword, Stmt, StmtKind, Withitem};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::format_call_path; use ruff_python_ast::call_path::format_call_path;
use ruff_python_ast::call_path::to_call_path; use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::types::Range; use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@@ -157,7 +157,7 @@ fn exception_needs_match(checker: &mut Checker, exception: &Expr) {
.flake8_pytest_style .flake8_pytest_style
.raises_extend_require_match_for, .raises_extend_require_match_for,
) )
.any(|target| call_path == to_call_path(target)); .any(|target| call_path == from_qualified_name(target));
if is_broad_exception { if is_broad_exception {
Some(format_call_path(&call_path)) Some(format_call_path(&call_path))
} else { } else {

View File

@@ -73,46 +73,48 @@ pub fn private_member_access(checker: &mut Checker, expr: &Expr) {
if let ExprKind::Call { func, .. } = &value.node { if let ExprKind::Call { func, .. } = &value.node {
// Ignore `super()` calls. // Ignore `super()` calls.
let call_path = collect_call_path(func); if let Some(call_path) = collect_call_path(func) {
if call_path.as_slice() == ["super"] { if call_path.as_slice() == ["super"] {
return; return;
}
} }
} else { } else {
// Ignore `self` and `cls` accesses. // Ignore `self` and `cls` accesses.
let call_path = collect_call_path(value); if let Some(call_path) = collect_call_path(value) {
if call_path.as_slice() == ["self"] if call_path.as_slice() == ["self"]
|| call_path.as_slice() == ["cls"] || call_path.as_slice() == ["cls"]
|| call_path.as_slice() == ["mcs"] || call_path.as_slice() == ["mcs"]
{ {
return; return;
} }
// Ignore accesses on class members from _within_ the class. // Ignore accesses on class members from _within_ the class.
if checker if checker
.ctx .ctx
.scopes .scopes
.iter() .iter()
.rev() .rev()
.find_map(|scope| match &scope.kind { .find_map(|scope| match &scope.kind {
ScopeKind::Class(class_def) => Some(class_def), ScopeKind::Class(class_def) => Some(class_def),
_ => None, _ => None,
}) })
.map_or(false, |class_def| { .map_or(false, |class_def| {
if call_path.as_slice() == [class_def.name] { if call_path.as_slice() == [class_def.name] {
checker checker
.ctx .ctx
.find_binding(class_def.name) .find_binding(class_def.name)
.map_or(false, |binding| { .map_or(false, |binding| {
// TODO(charlie): Could the name ever be bound to a _different_ // TODO(charlie): Could the name ever be bound to a
// class here? // _different_ class here?
binding.kind.is_class_definition() binding.kind.is_class_definition()
}) })
} else { } else {
false false
} }
}) })
{ {
return; return;
}
} }
} }

View File

@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation, CacheKey}; use ruff_macros::{derive_message_formats, violation, CacheKey};
use ruff_python_ast::call_path::CallPath; use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::types::Range; use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@@ -103,7 +103,7 @@ pub fn banned_attribute_access(checker: &mut Checker, expr: &Expr) {
.flake8_tidy_imports .flake8_tidy_imports
.banned_api .banned_api
.iter() .iter()
.find(|(banned_path, ..)| call_path == banned_path.split('.').collect::<CallPath>()) .find(|(banned_path, ..)| call_path == from_qualified_name(banned_path))
}) { }) {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
BannedApi { BannedApi {

View File

@@ -2,7 +2,7 @@ use num_traits::Zero;
use rustpython_parser::ast::{Constant, Expr, ExprKind}; use rustpython_parser::ast::{Constant, Expr, ExprKind};
use ruff_python_ast::binding::{Binding, BindingKind, ExecutionContext}; use ruff_python_ast::binding::{Binding, BindingKind, ExecutionContext};
use ruff_python_ast::call_path::to_call_path; use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::context::Context; use ruff_python_ast::context::Context;
use ruff_python_ast::helpers::map_callable; use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::scope::ScopeKind; use ruff_python_ast::scope::ScopeKind;
@@ -78,7 +78,7 @@ fn runtime_evaluated_base_class(context: &Context, base_classes: &[String]) -> b
if let Some(call_path) = context.resolve_call_path(base) { if let Some(call_path) = context.resolve_call_path(base) {
if base_classes if base_classes
.iter() .iter()
.any(|base_class| to_call_path(base_class) == call_path) .any(|base_class| from_qualified_name(base_class) == call_path)
{ {
return true; return true;
} }
@@ -94,7 +94,7 @@ fn runtime_evaluated_decorators(context: &Context, decorators: &[String]) -> boo
if let Some(call_path) = context.resolve_call_path(map_callable(decorator)) { if let Some(call_path) = context.resolve_call_path(map_callable(decorator)) {
if decorators if decorators
.iter() .iter()
.any(|decorator| to_call_path(decorator) == call_path) .any(|decorator| from_qualified_name(decorator) == call_path)
{ {
return true; return true;
} }

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::call_path::to_call_path; use ruff_python_ast::call_path::from_qualified_name;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use ruff_python_ast::cast; use ruff_python_ast::cast;
@@ -53,7 +53,7 @@ pub(crate) fn should_ignore_definition(
if let Some(call_path) = checker.ctx.resolve_call_path(map_callable(decorator)) { if let Some(call_path) = checker.ctx.resolve_call_path(map_callable(decorator)) {
if ignore_decorators if ignore_decorators
.iter() .iter()
.any(|decorator| to_call_path(decorator) == call_path) .any(|decorator| from_qualified_name(decorator) == call_path)
{ {
return true; return true;
} }

View File

@@ -5,7 +5,7 @@ use once_cell::sync::Lazy;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::{to_call_path, CallPath}; use ruff_python_ast::call_path::{from_qualified_name, CallPath};
use ruff_python_ast::cast; use ruff_python_ast::cast;
use ruff_python_ast::newlines::StrExt; use ruff_python_ast::newlines::StrExt;
use ruff_python_ast::types::Range; use ruff_python_ast::types::Range;
@@ -33,7 +33,7 @@ pub fn non_imperative_mood(
let property_decorators = property_decorators let property_decorators = property_decorators
.iter() .iter()
.map(|decorator| to_call_path(decorator)) .map(|decorator| from_qualified_name(decorator))
.collect::<Vec<CallPath>>(); .collect::<Vec<CallPath>>();
if is_test(cast::name(parent)) if is_test(cast::name(parent))

View File

@@ -39,7 +39,9 @@ pub fn datetime_utc_alias(checker: &mut Checker, expr: &Expr) {
call_path.as_slice() == ["datetime", "timezone", "utc"] call_path.as_slice() == ["datetime", "timezone", "utc"]
}) })
{ {
let straight_import = collect_call_path(expr).as_slice() == ["datetime", "timezone", "utc"]; let straight_import = collect_call_path(expr).map_or(false, |call_path| {
call_path.as_slice() == ["datetime", "timezone", "utc"]
});
let mut diagnostic = let mut diagnostic =
Diagnostic::new(DatetimeTimezoneUTC { straight_import }, Range::from(expr)); Diagnostic::new(DatetimeTimezoneUTC { straight_import }, Range::from(expr));
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {

View File

@@ -248,7 +248,9 @@ fn format_import_from(
/// UP026 /// UP026
pub fn deprecated_mock_attribute(checker: &mut Checker, expr: &Expr) { pub fn deprecated_mock_attribute(checker: &mut Checker, expr: &Expr) {
if let ExprKind::Attribute { value, .. } = &expr.node { if let ExprKind::Attribute { value, .. } = &expr.node {
if collect_call_path(value).as_slice() == ["mock", "mock"] { if collect_call_path(value)
.map_or(false, |call_path| call_path.as_slice() == ["mock", "mock"])
{
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
DeprecatedMockImport { DeprecatedMockImport {
reference_type: MockReference::Attribute, reference_type: MockReference::Attribute,

View File

@@ -1,13 +1,112 @@
use rustpython_parser::ast::{Expr, ExprKind}; use rustpython_parser::ast::{Expr, ExprKind};
use smallvec::smallvec; use smallvec::smallvec;
use std::fmt::Display;
/// A representation of a qualified name, like `typing.List`. /// A representation of a qualified name, like `typing.List`.
pub type CallPath<'a> = smallvec::SmallVec<[&'a str; 8]>; #[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallPath<'a>(smallvec::SmallVec<[&'a str; 8]>);
fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut CallPath<'a>) -> bool { impl<'a> CallPath<'a> {
/// Create a new, empty [`CallPath`].
pub fn new() -> Self {
Self(smallvec![])
}
/// Create a new, empty [`CallPath`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self(smallvec::SmallVec::with_capacity(capacity))
}
/// Create a [`CallPath`] from an expression.
pub fn try_from_expr(expr: &'a Expr) -> Option<Self> {
let mut segments = CallPath::new();
collect_call_path(expr, &mut segments).then_some(segments)
}
/// Create a [`CallPath`] from a fully-qualified name.
///
/// ```rust
/// # use smallvec::smallvec;
/// # use ruff_python_ast::call_path::{CallPath, from_qualified_name};
///
/// assert_eq!(CallPath::from_qualified_name("typing.List").as_slice(), ["typing", "List"]);
/// assert_eq!(CallPath::from_qualified_name("list").as_slice(), ["", "list"]);
/// ```
pub fn from_qualified_name(name: &'a str) -> Self {
Self(if name.contains('.') {
name.split('.').collect()
} else {
// Special-case: for builtins, return `["", "int"]` instead of `["int"]`.
smallvec!["", name]
})
}
/// Create a [`CallPath`] from an unqualified name.
///
/// ```rust
/// # use smallvec::smallvec;
/// # use ruff_python_ast::call_path::{CallPath, from_unqualified_name};
///
/// assert_eq!(CallPath::from_unqualified_name("typing.List").as_slice(), ["typing", "List"]);
/// assert_eq!(CallPath::from_unqualified_name("list").as_slice(), ["list"]);
/// ```
pub fn from_unqualified_name(name: &'a str) -> Self {
Self(name.split('.').collect())
}
pub fn push(&mut self, segment: &'a str) {
self.0.push(segment)
}
pub fn pop(&mut self) -> Option<&'a str> {
self.0.pop()
}
pub fn extend<I: IntoIterator<Item = &'a str>>(&mut self, iter: I) {
self.0.extend(iter)
}
pub fn first(&self) -> Option<&&'a str> {
self.0.first()
}
pub fn last(&self) -> Option<&&'a str> {
self.0.last()
}
pub fn as_slice(&self) -> &[&str] {
self.0.as_slice()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn starts_with(&self, other: &Self) -> bool {
self.0.starts_with(&other.0)
}
}
impl<'a> IntoIterator for CallPath<'a> {
type Item = &'a str;
type IntoIter = smallvec::IntoIter<[&'a str; 8]>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Display for CallPath<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", format_call_path(self.as_slice()))
}
}
/// Collect a [`CallPath`] from an [`Expr`].
fn collect_call_path<'a>(expr: &'a Expr, parts: &mut CallPath<'a>) -> bool {
match &expr.node { match &expr.node {
ExprKind::Attribute { value, attr, .. } => { ExprKind::Attribute { value, attr, .. } => {
if collect_call_path_inner(value, parts) { if collect_call_path(value, parts) {
parts.push(attr); parts.push(attr);
true true
} else { } else {
@@ -22,25 +121,8 @@ fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut CallPath<'a>) -> bool
} }
} }
/// Convert an `Expr` to its [`CallPath`] segments (like `["typing", "List"]`). /// Format a [`CallPath`] for display.
pub fn collect_call_path(expr: &Expr) -> CallPath { fn format_call_path(call_path: &[&str]) -> String {
let mut segments = smallvec![];
collect_call_path_inner(expr, &mut segments);
segments
}
/// Convert an `Expr` to its call path (like `List`, or `typing.List`).
pub fn compose_call_path(expr: &Expr) -> Option<String> {
let call_path = collect_call_path(expr);
if call_path.is_empty() {
None
} else {
Some(format_call_path(&call_path))
}
}
/// Format a call path for display.
pub fn format_call_path(call_path: &[&str]) -> String {
if call_path if call_path
.first() .first()
.expect("Unable to format empty call path") .expect("Unable to format empty call path")
@@ -51,13 +133,3 @@ pub fn format_call_path(call_path: &[&str]) -> String {
call_path.join(".") call_path.join(".")
} }
} }
/// Split a fully-qualified name (like `typing.List`) into (`typing`, `List`).
pub fn to_call_path(target: &str) -> CallPath {
if target.contains('.') {
target.split('.').collect()
} else {
// Special-case: for builtins, return `["", "int"]` instead of `["int"]`.
smallvec!["", target]
}
}

View File

@@ -3,7 +3,6 @@ use std::path::Path;
use nohash_hasher::{BuildNoHashHasher, IntMap}; use nohash_hasher::{BuildNoHashHasher, IntMap};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use rustpython_parser::ast::{Expr, Stmt}; use rustpython_parser::ast::{Expr, Stmt};
use smallvec::smallvec;
use ruff_python_stdlib::path::is_python_stub_file; use ruff_python_stdlib::path::is_python_stub_file;
use ruff_python_stdlib::typing::TYPING_EXTENSIONS; use ruff_python_stdlib::typing::TYPING_EXTENSIONS;
@@ -12,7 +11,7 @@ use crate::binding::{
Binding, BindingId, BindingKind, Bindings, Exceptions, ExecutionContext, FromImportation, Binding, BindingId, BindingKind, Bindings, Exceptions, ExecutionContext, FromImportation,
Importation, SubmoduleImportation, Importation, SubmoduleImportation,
}; };
use crate::call_path::{collect_call_path, CallPath}; use crate::call_path::CallPath;
use crate::helpers::from_relative_import; use crate::helpers::from_relative_import;
use crate::scope::{Scope, ScopeId, ScopeKind, ScopeStack, Scopes}; use crate::scope::{Scope, ScopeId, ScopeKind, ScopeStack, Scopes};
use crate::types::RefEquality; use crate::types::RefEquality;
@@ -105,7 +104,7 @@ impl<'a> Context<'a> {
} }
/// Return `true` if the call path is a reference to `typing.${target}`. /// Return `true` if the call path is a reference to `typing.${target}`.
pub fn match_typing_call_path(&self, call_path: &CallPath, target: &str) -> bool { pub fn match_typing_call_path(&self, call_path: &CallPath<'a>, target: &'a str) -> bool {
if call_path.as_slice() == ["typing", target] { if call_path.as_slice() == ["typing", target] {
return true; return true;
} }
@@ -117,7 +116,7 @@ impl<'a> Context<'a> {
} }
if self.typing_modules.iter().any(|module| { if self.typing_modules.iter().any(|module| {
let mut module: CallPath = module.split('.').collect(); let mut module = CallPath::from_unqualified_name(module);
module.push(target); module.push(target);
*call_path == module *call_path == module
}) { }) {
@@ -156,7 +155,9 @@ impl<'a> Context<'a> {
where where
'b: 'a, 'b: 'a,
{ {
let call_path = collect_call_path(value); let Some(call_path) = CallPath::try_from_expr(value) else {
return None;
};
let Some(head) = call_path.first() else { let Some(head) = call_path.first() else {
return None; return None;
}; };
@@ -177,7 +178,7 @@ impl<'a> Context<'a> {
None None
} }
} else { } else {
let mut source_path: CallPath = name.split('.').collect(); let mut source_path = CallPath::from_unqualified_name(name);
source_path.extend(call_path.into_iter().skip(1)); source_path.extend(call_path.into_iter().skip(1));
Some(source_path) Some(source_path)
} }
@@ -194,13 +195,13 @@ impl<'a> Context<'a> {
None None
} }
} else { } else {
let mut source_path: CallPath = name.split('.').collect(); let mut source_path = CallPath::from_unqualified_name(name);
source_path.extend(call_path.into_iter().skip(1)); source_path.extend(call_path.into_iter().skip(1));
Some(source_path) Some(source_path)
} }
} }
BindingKind::Builtin => { BindingKind::Builtin => {
let mut source_path: CallPath = smallvec![]; let mut source_path = CallPath::with_capacity(call_path.len() + 1);
source_path.push(""); source_path.push("");
source_path.extend(call_path); source_path.extend(call_path);
Some(source_path) Some(source_path)

View File

@@ -1,6 +1,6 @@
use rustpython_parser::ast::Expr; use rustpython_parser::ast::Expr;
use crate::call_path::to_call_path; use crate::call_path::CallPath;
use crate::context::Context; use crate::context::Context;
use crate::helpers::map_callable; use crate::helpers::map_callable;
use crate::scope::{Scope, ScopeKind}; use crate::scope::{Scope, ScopeKind};
@@ -36,7 +36,7 @@ pub fn classify(
call_path.as_slice() == ["", "staticmethod"] call_path.as_slice() == ["", "staticmethod"]
|| staticmethod_decorators || staticmethod_decorators
.iter() .iter()
.any(|decorator| call_path == to_call_path(decorator)) .any(|decorator| call_path == CallPath::from_qualified_name(decorator))
}) })
}) { }) {
FunctionType::StaticMethod FunctionType::StaticMethod
@@ -56,7 +56,7 @@ pub fn classify(
call_path.as_slice() == ["", "classmethod"] || call_path.as_slice() == ["", "classmethod"] ||
classmethod_decorators classmethod_decorators
.iter() .iter()
.any(|decorator| call_path == to_call_path(decorator)) .any(|decorator| call_path == CallPath::from_qualified_name(decorator))
}) })
}) })
{ {

View File

@@ -717,7 +717,7 @@ pub fn to_module_path(package: &Path, path: &Path) -> Option<Vec<String>> {
/// Create a call path from a relative import. /// Create a call path from a relative import.
pub fn from_relative_import<'a>(module: &'a [String], name: &'a str) -> CallPath<'a> { pub fn from_relative_import<'a>(module: &'a [String], name: &'a str) -> CallPath<'a> {
let mut call_path: CallPath = SmallVec::with_capacity(module.len() + 1); let mut call_path: CallPath = CallPath::with_capacity(module.len() + 1);
// Start with the module path. // Start with the module path.
call_path.extend(module.iter().map(String::as_str)); call_path.extend(module.iter().map(String::as_str));

View File

@@ -1,6 +1,6 @@
use crate::call_path::CallPath;
use rustpython_parser::ast::{Expr, ExprKind}; use rustpython_parser::ast::{Expr, ExprKind};
use crate::call_path::collect_call_path;
use crate::context::Context; use crate::context::Context;
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@@ -43,12 +43,16 @@ impl LoggingLevel {
/// ``` /// ```
pub fn is_logger_candidate(context: &Context, func: &Expr) -> bool { pub fn is_logger_candidate(context: &Context, func: &Expr) -> bool {
if let ExprKind::Attribute { value, .. } = &func.node { if let ExprKind::Attribute { value, .. } = &func.node {
let call_path = context if let Some(call_path) = context
.resolve_call_path(value) .resolve_call_path(value)
.unwrap_or_else(|| collect_call_path(value)); .or_else(|| CallPath::try_from_expr(value))
if let Some(tail) = call_path.last() { {
if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging") { let tail = call_path.last();
return true; if let Some(tail) = call_path.last() {
if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging")
{
return true;
}
} }
} }
} }

View File

@@ -4,6 +4,7 @@ use rustpython_parser::ast::{Expr, ExprKind, Location};
use ruff_python_stdlib::typing::{PEP_585_BUILTINS_ELIGIBLE, PEP_593_SUBSCRIPTS, SUBSCRIPTS}; use ruff_python_stdlib::typing::{PEP_585_BUILTINS_ELIGIBLE, PEP_593_SUBSCRIPTS, SUBSCRIPTS};
use crate::call_path::CallPath;
use crate::context::Context; use crate::context::Context;
use crate::relocate::relocate_expr; use crate::relocate::relocate_expr;
use crate::source_code::Locator; use crate::source_code::Locator;
@@ -47,7 +48,7 @@ pub fn match_annotated_subscript<'a>(
} }
for module in typing_modules { for module in typing_modules {
let module_call_path = module.split('.').collect::<Vec<_>>(); let module_call_path = CallPath::from_qualified_name(module);
if call_path.starts_with(&module_call_path) { if call_path.starts_with(&module_call_path) {
for subscript in SUBSCRIPTS.iter() { for subscript in SUBSCRIPTS.iter() {
if call_path.last() == subscript.last() { if call_path.last() == subscript.last() {

View File

@@ -2,7 +2,6 @@ use std::path::Path;
use rustpython_parser::ast::{Expr, Stmt, StmtKind}; use rustpython_parser::ast::{Expr, Stmt, StmtKind};
use crate::call_path::collect_call_path;
use crate::call_path::CallPath; use crate::call_path::CallPath;
use crate::context::Context; use crate::context::Context;
use crate::helpers::map_callable; use crate::helpers::map_callable;
@@ -183,9 +182,10 @@ pub fn method_visibility(stmt: &Stmt) -> Visibility {
} => { } => {
// Is this a setter or deleter? // Is this a setter or deleter?
if decorator_list.iter().any(|expr| { if decorator_list.iter().any(|expr| {
let call_path = collect_call_path(expr); CallPath::try_from_expr(expr).map_or(false, |call_path| {
call_path.as_slice() == [name, "setter"] call_path.as_slice() == [name, "setter"]
|| call_path.as_slice() == [name, "deleter"] || call_path.as_slice() == [name, "deleter"]
})
}) { }) {
return Visibility::Private; return Visibility::Private;
} }