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:
Micha Reiser 2024-03-04 10:06:51 +01:00 committed by GitHub
parent ba4328226d
commit a6d892b1f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
181 changed files with 1692 additions and 1412 deletions

View file

@ -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

View file

@ -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;

View file

@ -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(())
}