diff --git a/crates/ruff_python_ast/src/call_path.rs b/crates/ruff_python_ast/src/call_path.rs index 24cbee3e63..ca938b28ae 100644 --- a/crates/ruff_python_ast/src/call_path.rs +++ b/crates/ruff_python_ast/src/call_path.rs @@ -1,13 +1,112 @@ use rustpython_parser::ast::{Expr, ExprKind}; use smallvec::smallvec; +use std::fmt::Display; /// 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 { + 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>(&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 { ExprKind::Attribute { value, attr, .. } => { - if collect_call_path_inner(value, parts) { + if collect_call_path(value, parts) { parts.push(attr); true } else { @@ -22,19 +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"]`). -pub fn collect_call_path(expr: &Expr) -> Option { - let mut segments = smallvec![]; - collect_call_path_inner(expr, &mut segments).then_some(segments) -} - -/// Convert an `Expr` to its call path (like `List`, or `typing.List`). -pub fn compose_call_path(expr: &Expr) -> Option { - collect_call_path(expr).map(|call_path| format_call_path(&call_path)) -} - -/// Format a call path for display. -pub fn format_call_path(call_path: &[&str]) -> String { +/// Format a [`CallPath`] for display. +fn format_call_path(call_path: &[&str]) -> String { if call_path .first() .expect("Unable to format empty call path") @@ -45,34 +133,3 @@ pub fn format_call_path(call_path: &[&str]) -> String { call_path.join(".") } } - -/// Create a [`CallPath`] from an unqualified name. -/// -/// ```rust -/// # use smallvec::smallvec; -/// # use ruff_python_ast::call_path::from_unqualified_name; -/// -/// assert_eq!(from_unqualified_name("typing.List").as_slice(), ["typing", "List"]); -/// assert_eq!(from_unqualified_name("list").as_slice(), ["list"]); -/// ``` -pub fn from_unqualified_name(name: &str) -> CallPath { - name.split('.').collect() -} - -/// Create a [`CallPath`] from a fully-qualified name. -/// -/// ```rust -/// # use smallvec::smallvec; -/// # use ruff_python_ast::call_path::from_qualified_name; -/// -/// assert_eq!(from_qualified_name("typing.List").as_slice(), ["typing", "List"]); -/// assert_eq!(from_qualified_name("list").as_slice(), ["", "list"]); -/// ``` -pub fn from_qualified_name(name: &str) -> CallPath { - if name.contains('.') { - name.split('.').collect() - } else { - // Special-case: for builtins, return `["", "int"]` instead of `["int"]`. - smallvec!["", name] - } -} diff --git a/crates/ruff_python_ast/src/context.rs b/crates/ruff_python_ast/src/context.rs index c01b65572b..90891affc0 100644 --- a/crates/ruff_python_ast/src/context.rs +++ b/crates/ruff_python_ast/src/context.rs @@ -3,7 +3,6 @@ use std::path::Path; use nohash_hasher::{BuildNoHashHasher, IntMap}; use rustc_hash::FxHashMap; use rustpython_parser::ast::{Expr, Stmt}; -use smallvec::smallvec; use ruff_python_stdlib::path::is_python_stub_file; use ruff_python_stdlib::typing::TYPING_EXTENSIONS; @@ -12,7 +11,7 @@ use crate::binding::{ Binding, BindingId, BindingKind, Bindings, Exceptions, ExecutionContext, FromImportation, Importation, SubmoduleImportation, }; -use crate::call_path::{collect_call_path, from_unqualified_name, CallPath}; +use crate::call_path::CallPath; use crate::helpers::from_relative_import; use crate::scope::{Scope, ScopeId, ScopeKind, ScopeStack, Scopes}; use crate::types::RefEquality; @@ -105,7 +104,7 @@ impl<'a> Context<'a> { } /// 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] { return true; } @@ -117,7 +116,7 @@ impl<'a> Context<'a> { } if self.typing_modules.iter().any(|module| { - let mut module: CallPath = from_unqualified_name(module); + let mut module = CallPath::from_unqualified_name(module); module.push(target); *call_path == module }) { @@ -156,7 +155,7 @@ impl<'a> Context<'a> { where 'b: 'a, { - let Some(call_path) = collect_call_path(value) else { + let Some(call_path) = CallPath::try_from_expr(value) else { return None; }; let Some(head) = call_path.first() else { @@ -179,7 +178,7 @@ impl<'a> Context<'a> { None } } else { - let mut source_path: CallPath = from_unqualified_name(name); + let mut source_path = CallPath::from_unqualified_name(name); source_path.extend(call_path.into_iter().skip(1)); Some(source_path) } @@ -196,13 +195,13 @@ impl<'a> Context<'a> { None } } else { - let mut source_path: CallPath = from_unqualified_name(name); + let mut source_path = CallPath::from_unqualified_name(name); source_path.extend(call_path.into_iter().skip(1)); Some(source_path) } } BindingKind::Builtin => { - let mut source_path: CallPath = smallvec![]; + let mut source_path = CallPath::with_capacity(call_path.len() + 1); source_path.push(""); source_path.extend(call_path); Some(source_path) diff --git a/crates/ruff_python_ast/src/function_type.rs b/crates/ruff_python_ast/src/function_type.rs index 913a66d4cc..374f81d954 100644 --- a/crates/ruff_python_ast/src/function_type.rs +++ b/crates/ruff_python_ast/src/function_type.rs @@ -1,6 +1,6 @@ use rustpython_parser::ast::Expr; -use crate::call_path::from_qualified_name; +use crate::call_path::CallPath; use crate::context::Context; use crate::helpers::map_callable; use crate::scope::{Scope, ScopeKind}; @@ -36,7 +36,7 @@ pub fn classify( call_path.as_slice() == ["", "staticmethod"] || staticmethod_decorators .iter() - .any(|decorator| call_path == from_qualified_name(decorator)) + .any(|decorator| call_path == CallPath::from_qualified_name(decorator)) }) }) { FunctionType::StaticMethod @@ -56,7 +56,7 @@ pub fn classify( call_path.as_slice() == ["", "classmethod"] || classmethod_decorators .iter() - .any(|decorator| call_path == from_qualified_name(decorator)) + .any(|decorator| call_path == CallPath::from_qualified_name(decorator)) }) }) { diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 7c0f301362..ce84167678 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -717,7 +717,7 @@ pub fn to_module_path(package: &Path, path: &Path) -> Option> { /// Create a call path from a relative import. 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. call_path.extend(module.iter().map(String::as_str)); diff --git a/crates/ruff_python_ast/src/logging.rs b/crates/ruff_python_ast/src/logging.rs index d189ac9ea9..7ee98d6f2b 100644 --- a/crates/ruff_python_ast/src/logging.rs +++ b/crates/ruff_python_ast/src/logging.rs @@ -1,6 +1,6 @@ +use crate::call_path::CallPath; use rustpython_parser::ast::{Expr, ExprKind}; -use crate::call_path::collect_call_path; use crate::context::Context; #[derive(Copy, Clone)] @@ -43,14 +43,16 @@ impl LoggingLevel { /// ``` pub fn is_logger_candidate(context: &Context, func: &Expr) -> bool { if let ExprKind::Attribute { value, .. } = &func.node { - let Some(call_path) = context + if let Some(call_path) = context .resolve_call_path(value) - .or_else(|| collect_call_path(value)) else { - return false; - }; - if let Some(tail) = call_path.last() { - if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging") { - return true; + .or_else(|| CallPath::try_from_expr(value)) + { + let tail = call_path.last(); + if let Some(tail) = call_path.last() { + if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging") + { + return true; + } } } } diff --git a/crates/ruff_python_ast/src/typing.rs b/crates/ruff_python_ast/src/typing.rs index 729c904891..72e38ccdf3 100644 --- a/crates/ruff_python_ast/src/typing.rs +++ b/crates/ruff_python_ast/src/typing.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{Expr, ExprKind, Location}; use ruff_python_stdlib::typing::{PEP_585_BUILTINS_ELIGIBLE, PEP_593_SUBSCRIPTS, SUBSCRIPTS}; -use crate::call_path::{from_unqualified_name, CallPath}; +use crate::call_path::CallPath; use crate::context::Context; use crate::relocate::relocate_expr; use crate::source_code::Locator; @@ -48,7 +48,7 @@ pub fn match_annotated_subscript<'a>( } for module in typing_modules { - let module_call_path: CallPath = from_unqualified_name(module); + let module_call_path = CallPath::from_qualified_name(module); if call_path.starts_with(&module_call_path) { for subscript in SUBSCRIPTS.iter() { if call_path.last() == subscript.last() { diff --git a/crates/ruff_python_ast/src/visibility.rs b/crates/ruff_python_ast/src/visibility.rs index f65653872b..8f97d62bac 100644 --- a/crates/ruff_python_ast/src/visibility.rs +++ b/crates/ruff_python_ast/src/visibility.rs @@ -2,7 +2,6 @@ use std::path::Path; use rustpython_parser::ast::{Expr, Stmt, StmtKind}; -use crate::call_path::collect_call_path; use crate::call_path::CallPath; use crate::context::Context; use crate::helpers::map_callable; @@ -183,7 +182,7 @@ pub fn method_visibility(stmt: &Stmt) -> Visibility { } => { // Is this a setter or deleter? if decorator_list.iter().any(|expr| { - collect_call_path(expr).map_or(false, |call_path| { + CallPath::try_from_expr(expr).map_or(false, |call_path| { call_path.as_slice() == [name, "setter"] || call_path.as_slice() == [name, "deleter"] })