mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-15 16:10:38 +00:00
Split CallPath
into QualifiedName
and UnqualifiedName
(#10210)
## Summary Charlie can probably explain this better than I but it turns out, `CallPath` is used for two different things: * To represent unqualified names like `version` where `version` can be a local variable or imported (e.g. `from sys import version` where the full qualified name is `sys.version`) * To represent resolved, full qualified names This PR splits `CallPath` into two types to make this destinction clear. > Note: I haven't renamed all `call_path` variables to `qualified_name` or `unqualified_name`. I can do that if that's welcomed but I first want to get feedback on the approach and naming overall. ## Test Plan `cargo test`
This commit is contained in:
parent
ba4328226d
commit
a6d892b1f4
181 changed files with 1692 additions and 1412 deletions
|
@ -7,7 +7,7 @@ use ruff_python_trivia::{indentation_at_offset, CommentRanges, SimpleTokenKind,
|
|||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::call_path::{CallPath, CallPathBuilder};
|
||||
use crate::name::{QualifiedName, QualifiedNameBuilder};
|
||||
use crate::parenthesize::parenthesized_range;
|
||||
use crate::statement_visitor::StatementVisitor;
|
||||
use crate::visitor::Visitor;
|
||||
|
@ -800,8 +800,8 @@ pub fn collect_import_from_member<'a>(
|
|||
level: Option<u32>,
|
||||
module: Option<&'a str>,
|
||||
member: &'a str,
|
||||
) -> CallPath<'a> {
|
||||
let mut call_path_builder = CallPathBuilder::with_capacity(
|
||||
) -> QualifiedName<'a> {
|
||||
let mut qualified_name_builder = QualifiedNameBuilder::with_capacity(
|
||||
level.unwrap_or_default() as usize
|
||||
+ module
|
||||
.map(|module| module.split('.').count())
|
||||
|
@ -813,20 +813,20 @@ pub fn collect_import_from_member<'a>(
|
|||
if let Some(level) = level {
|
||||
if level > 0 {
|
||||
for _ in 0..level {
|
||||
call_path_builder.push(".");
|
||||
qualified_name_builder.push(".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the remaining segments.
|
||||
if let Some(module) = module {
|
||||
call_path_builder.extend(module.split('.'));
|
||||
qualified_name_builder.extend(module.split('.'));
|
||||
}
|
||||
|
||||
// Add the member.
|
||||
call_path_builder.push(member);
|
||||
qualified_name_builder.push(member);
|
||||
|
||||
call_path_builder.build()
|
||||
qualified_name_builder.build()
|
||||
}
|
||||
|
||||
/// Format the call path for a relative import, or `None` if the relative import extends beyond
|
||||
|
@ -838,29 +838,29 @@ pub fn from_relative_import<'a>(
|
|||
import: &[&'a str],
|
||||
// The remaining segments to the call path (e.g., given `bar.baz`, `["baz"]`).
|
||||
tail: &[&'a str],
|
||||
) -> Option<CallPath<'a>> {
|
||||
let mut call_path_builder =
|
||||
CallPathBuilder::with_capacity(module.len() + import.len() + tail.len());
|
||||
) -> Option<QualifiedName<'a>> {
|
||||
let mut qualified_name_builder =
|
||||
QualifiedNameBuilder::with_capacity(module.len() + import.len() + tail.len());
|
||||
|
||||
// Start with the module path.
|
||||
call_path_builder.extend(module.iter().map(String::as_str));
|
||||
qualified_name_builder.extend(module.iter().map(String::as_str));
|
||||
|
||||
// Remove segments based on the number of dots.
|
||||
for segment in import {
|
||||
if *segment == "." {
|
||||
if call_path_builder.is_empty() {
|
||||
if qualified_name_builder.is_empty() {
|
||||
return None;
|
||||
}
|
||||
call_path_builder.pop();
|
||||
qualified_name_builder.pop();
|
||||
} else {
|
||||
call_path_builder.push(segment);
|
||||
qualified_name_builder.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the remaining segments.
|
||||
call_path_builder.extend_from_slice(tail);
|
||||
qualified_name_builder.extend_from_slice(tail);
|
||||
|
||||
Some(call_path_builder.build())
|
||||
Some(qualified_name_builder.build())
|
||||
}
|
||||
|
||||
/// Given an imported module (based on its relative import level and module name), return the
|
||||
|
|
|
@ -6,7 +6,6 @@ pub use node::{AnyNode, AnyNodeRef, AstNode, NodeKind};
|
|||
pub use nodes::*;
|
||||
|
||||
pub mod all;
|
||||
pub mod call_path;
|
||||
pub mod comparable;
|
||||
pub mod docstrings;
|
||||
mod expression;
|
||||
|
@ -15,6 +14,7 @@ pub mod helpers;
|
|||
pub mod identifier;
|
||||
pub mod imports;
|
||||
mod int;
|
||||
pub mod name;
|
||||
mod node;
|
||||
mod nodes;
|
||||
pub mod parenthesize;
|
||||
|
|
|
@ -5,49 +5,44 @@ use crate::{nodes, Expr};
|
|||
|
||||
/// A representation of a qualified name, like `typing.List`.
|
||||
#[derive(Debug, Clone, Eq, Hash)]
|
||||
pub struct CallPath<'a> {
|
||||
pub struct QualifiedName<'a> {
|
||||
segments: SmallVec<[&'a str; 8]>,
|
||||
}
|
||||
|
||||
impl<'a> CallPath<'a> {
|
||||
pub fn from_expr(expr: &'a Expr) -> Option<Self> {
|
||||
let segments = collect_call_path(expr)?;
|
||||
Some(Self { segments })
|
||||
}
|
||||
|
||||
/// Create a [`CallPath`] from an unqualified name.
|
||||
impl<'a> QualifiedName<'a> {
|
||||
/// Create a [`QualifiedName`] from a dotted name.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use smallvec::smallvec;
|
||||
/// # use ruff_python_ast::call_path::CallPath;
|
||||
/// # use ruff_python_ast::name::QualifiedName;
|
||||
///
|
||||
/// assert_eq!(CallPath::from_unqualified_name("typing.List").segments(), ["typing", "List"]);
|
||||
/// assert_eq!(CallPath::from_unqualified_name("list").segments(), ["list"]);
|
||||
/// assert_eq!(QualifiedName::from_dotted_name("typing.List").segments(), ["typing", "List"]);
|
||||
/// assert_eq!(QualifiedName::from_dotted_name("list").segments(), ["", "list"]);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn from_unqualified_name(name: &'a str) -> Self {
|
||||
name.split('.').collect()
|
||||
}
|
||||
|
||||
/// Create a [`CallPath`] from a fully-qualified name.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use smallvec::smallvec;
|
||||
/// # use ruff_python_ast::call_path::CallPath;
|
||||
///
|
||||
/// assert_eq!(CallPath::from_qualified_name("typing.List").segments(), ["typing", "List"]);
|
||||
/// assert_eq!(CallPath::from_qualified_name("list").segments(), ["", "list"]);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn from_qualified_name(name: &'a str) -> Self {
|
||||
pub fn from_dotted_name(name: &'a str) -> Self {
|
||||
if let Some(dot) = name.find('.') {
|
||||
let mut segments = SmallVec::new();
|
||||
segments.push(&name[..dot]);
|
||||
segments.extend(name[dot + 1..].split('.'));
|
||||
Self { segments }
|
||||
} else {
|
||||
// Special-case: for builtins, return `["", "int"]` instead of `["int"]`.
|
||||
Self::from_slice(&["", name])
|
||||
Self::builtin(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a name that's guaranteed not be a built in
|
||||
#[inline]
|
||||
pub fn imported(name: &'a str) -> Self {
|
||||
name.split('.').collect()
|
||||
}
|
||||
|
||||
/// Creates a qualified name for a built in
|
||||
#[inline]
|
||||
pub fn builtin(name: &'a str) -> Self {
|
||||
debug_assert!(!name.contains('.'));
|
||||
Self {
|
||||
segments: ["", name].into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,7 +53,7 @@ impl<'a> CallPath<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn starts_with(&self, other: &CallPath) -> bool {
|
||||
pub fn starts_with(&self, other: &QualifiedName) -> bool {
|
||||
self.segments().starts_with(other.segments())
|
||||
}
|
||||
|
||||
|
@ -73,7 +68,7 @@ impl<'a> CallPath<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<&'a str> for CallPath<'a> {
|
||||
impl<'a> FromIterator<&'a str> for QualifiedName<'a> {
|
||||
fn from_iter<I: IntoIterator<Item = &'a str>>(iter: I) -> Self {
|
||||
Self {
|
||||
segments: iter.into_iter().collect(),
|
||||
|
@ -81,32 +76,28 @@ impl<'a> FromIterator<&'a str> for CallPath<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> PartialEq<CallPath<'b>> for CallPath<'a> {
|
||||
impl<'a, 'b> PartialEq<QualifiedName<'b>> for QualifiedName<'a> {
|
||||
#[inline]
|
||||
fn eq(&self, other: &CallPath<'b>) -> bool {
|
||||
fn eq(&self, other: &QualifiedName<'b>) -> bool {
|
||||
self.segments == other.segments
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CallPathBuilder<'a> {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QualifiedNameBuilder<'a> {
|
||||
segments: SmallVec<[&'a str; 8]>,
|
||||
}
|
||||
|
||||
impl<'a> CallPathBuilder<'a> {
|
||||
impl<'a> QualifiedNameBuilder<'a> {
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
segments: SmallVec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn from_path(call_path: CallPath<'a>) -> Self {
|
||||
pub fn from_qualified_name(qualified_name: QualifiedName<'a>) -> Self {
|
||||
Self {
|
||||
segments: call_path.segments,
|
||||
segments: qualified_name.segments,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,15 +124,115 @@ impl<'a> CallPathBuilder<'a> {
|
|||
self.segments.extend_from_slice(segments);
|
||||
}
|
||||
|
||||
pub fn build(self) -> CallPath<'a> {
|
||||
CallPath {
|
||||
pub fn build(self) -> QualifiedName<'a> {
|
||||
QualifiedName {
|
||||
segments: self.segments,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an `Expr` to its [`CallPath`] segments (like `["typing", "List"]`).
|
||||
fn collect_call_path(expr: &Expr) -> Option<SmallVec<[&str; 8]>> {
|
||||
impl Display for QualifiedName<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
format_qualified_name_segments(self.segments(), f)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_qualified_name_segments(segments: &[&str], w: &mut dyn Write) -> std::fmt::Result {
|
||||
if segments.first().is_some_and(|first| first.is_empty()) {
|
||||
// If the first segment is empty, the `CallPath` is that of a builtin.
|
||||
// Ex) `["", "bool"]` -> `"bool"`
|
||||
let mut first = true;
|
||||
|
||||
for segment in segments.iter().skip(1) {
|
||||
if !first {
|
||||
w.write_char('.')?;
|
||||
}
|
||||
|
||||
w.write_str(segment)?;
|
||||
first = false;
|
||||
}
|
||||
} else if segments.first().is_some_and(|first| matches!(*first, ".")) {
|
||||
// If the call path is dot-prefixed, it's an unresolved relative import.
|
||||
// Ex) `[".foo", "bar"]` -> `".foo.bar"`
|
||||
|
||||
let mut iter = segments.iter();
|
||||
for segment in iter.by_ref() {
|
||||
if *segment == "." {
|
||||
w.write_char('.')?;
|
||||
} else {
|
||||
w.write_str(segment)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for segment in iter {
|
||||
w.write_char('.')?;
|
||||
w.write_str(segment)?;
|
||||
}
|
||||
} else {
|
||||
let mut first = true;
|
||||
for segment in segments {
|
||||
if !first {
|
||||
w.write_char('.')?;
|
||||
}
|
||||
|
||||
w.write_str(segment)?;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash)]
|
||||
pub struct UnqualifiedName<'a> {
|
||||
segments: SmallVec<[&'a str; 8]>,
|
||||
}
|
||||
|
||||
impl<'a> UnqualifiedName<'a> {
|
||||
pub fn from_expr(expr: &'a Expr) -> Option<Self> {
|
||||
let segments = collect_segments(expr)?;
|
||||
Some(Self { segments })
|
||||
}
|
||||
|
||||
pub fn segments(&self) -> &[&'a str] {
|
||||
&self.segments
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> PartialEq<UnqualifiedName<'b>> for UnqualifiedName<'a> {
|
||||
#[inline]
|
||||
fn eq(&self, other: &UnqualifiedName<'b>) -> bool {
|
||||
self.segments == other.segments
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UnqualifiedName<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut first = true;
|
||||
for segment in &self.segments {
|
||||
if !first {
|
||||
f.write_char('.')?;
|
||||
}
|
||||
|
||||
f.write_str(segment)?;
|
||||
first = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<&'a str> for UnqualifiedName<'a> {
|
||||
#[inline]
|
||||
fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
|
||||
Self {
|
||||
segments: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an `Expr` to its [`QualifiedName`] segments (like `["typing", "List"]`).
|
||||
fn collect_segments(expr: &Expr) -> Option<SmallVec<[&str; 8]>> {
|
||||
// Unroll the loop up to eight times, to match the maximum number of expected attributes.
|
||||
// In practice, unrolling appears to give about a 4x speed-up on this hot path.
|
||||
let attr1 = match expr {
|
||||
|
@ -255,7 +346,7 @@ fn collect_call_path(expr: &Expr) -> Option<SmallVec<[&str; 8]>> {
|
|||
_ => return None,
|
||||
};
|
||||
|
||||
collect_call_path(&attr8.value).map(|mut segments| {
|
||||
collect_segments(&attr8.value).map(|mut segments| {
|
||||
segments.extend([
|
||||
attr8.attr.as_str(),
|
||||
attr7.attr.as_str(),
|
||||
|
@ -269,60 +360,3 @@ fn collect_call_path(expr: &Expr) -> Option<SmallVec<[&str; 8]>> {
|
|||
segments
|
||||
})
|
||||
}
|
||||
|
||||
impl Display for CallPath<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
format_call_path_segments(self.segments(), f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an `Expr` to its call path (like `List`, or `typing.List`).
|
||||
pub fn compose_call_path(expr: &Expr) -> Option<String> {
|
||||
CallPath::from_expr(expr).map(|call_path| call_path.to_string())
|
||||
}
|
||||
|
||||
pub fn format_call_path_segments(segments: &[&str], w: &mut dyn Write) -> std::fmt::Result {
|
||||
if segments.first().is_some_and(|first| first.is_empty()) {
|
||||
// If the first segment is empty, the `CallPath` is that of a builtin.
|
||||
// Ex) `["", "bool"]` -> `"bool"`
|
||||
let mut first = true;
|
||||
|
||||
for segment in segments.iter().skip(1) {
|
||||
if !first {
|
||||
w.write_char('.')?;
|
||||
}
|
||||
|
||||
w.write_str(segment)?;
|
||||
first = false;
|
||||
}
|
||||
} else if segments.first().is_some_and(|first| matches!(*first, ".")) {
|
||||
// If the call path is dot-prefixed, it's an unresolved relative import.
|
||||
// Ex) `[".foo", "bar"]` -> `".foo.bar"`
|
||||
|
||||
let mut iter = segments.iter();
|
||||
for segment in iter.by_ref() {
|
||||
if *segment == "." {
|
||||
w.write_char('.')?;
|
||||
} else {
|
||||
w.write_str(segment)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for segment in iter {
|
||||
w.write_char('.')?;
|
||||
w.write_str(segment)?;
|
||||
}
|
||||
} else {
|
||||
let mut first = true;
|
||||
for segment in segments {
|
||||
if !first {
|
||||
w.write_char('.')?;
|
||||
}
|
||||
|
||||
w.write_str(segment)?;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue