Make CallPath its own struct

This commit is contained in:
Charlie Marsh 2023-04-01 12:03:50 -04:00
parent 5f5e71e81d
commit 2446cd49fa
7 changed files with 128 additions and 71 deletions

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,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"]`). /// Format a [`CallPath`] for display.
pub fn collect_call_path(expr: &Expr) -> Option<CallPath> { fn format_call_path(call_path: &[&str]) -> String {
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<String> {
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 {
if call_path if call_path
.first() .first()
.expect("Unable to format empty call path") .expect("Unable to format empty call path")
@ -45,34 +133,3 @@ pub fn format_call_path(call_path: &[&str]) -> String {
call_path.join(".") 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]
}
}

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, from_unqualified_name, 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 = from_unqualified_name(module); let mut module = CallPath::from_unqualified_name(module);
module.push(target); module.push(target);
*call_path == module *call_path == module
}) { }) {
@ -156,7 +155,7 @@ impl<'a> Context<'a> {
where where
'b: 'a, 'b: 'a,
{ {
let Some(call_path) = collect_call_path(value) else { let Some(call_path) = CallPath::try_from_expr(value) else {
return None; return None;
}; };
let Some(head) = call_path.first() else { let Some(head) = call_path.first() else {
@ -179,7 +178,7 @@ impl<'a> Context<'a> {
None None
} }
} else { } 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)); source_path.extend(call_path.into_iter().skip(1));
Some(source_path) Some(source_path)
} }
@ -196,13 +195,13 @@ impl<'a> Context<'a> {
None None
} }
} else { } 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)); 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::from_qualified_name; 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 == from_qualified_name(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 == from_qualified_name(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,14 +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 Some(call_path) = context if let Some(call_path) = context
.resolve_call_path(value) .resolve_call_path(value)
.or_else(|| collect_call_path(value)) else { .or_else(|| CallPath::try_from_expr(value))
return false; {
}; let tail = call_path.last();
if let Some(tail) = call_path.last() { if let Some(tail) = call_path.last() {
if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging") { if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging")
return true; {
return true;
}
} }
} }
} }

View File

@ -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 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::context::Context;
use crate::relocate::relocate_expr; use crate::relocate::relocate_expr;
use crate::source_code::Locator; use crate::source_code::Locator;
@ -48,7 +48,7 @@ pub fn match_annotated_subscript<'a>(
} }
for module in typing_modules { 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) { 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,7 +182,7 @@ 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| {
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, "setter"]
|| call_path.as_slice() == [name, "deleter"] || call_path.as_slice() == [name, "deleter"]
}) })