diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index efd91ca4b9..144e4d56f5 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -325,7 +325,7 @@ where self.add_binding( name, alias.identifier(), - BindingKind::SubmoduleImport(SubmoduleImport { call_path }), + BindingKind::SubmoduleImport(SubmoduleImport { path: call_path }), BindingFlags::EXTERNAL, ); } else { @@ -346,7 +346,7 @@ where self.add_binding( name, alias.identifier(), - BindingKind::Import(Import { call_path }), + BindingKind::Import(Import { path: call_path }), flags, ); } @@ -399,7 +399,7 @@ where self.add_binding( name, alias.identifier(), - BindingKind::FromImport(FromImport { call_path }), + BindingKind::FromImport(FromImport { path: call_path }), flags, ); } diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs index d3f3e6fc92..79596ef3f2 100644 --- a/crates/ruff/src/renamer.rs +++ b/crates/ruff/src/renamer.rs @@ -231,7 +231,7 @@ impl Renamer { } BindingKind::SubmoduleImport(import) => { // Ex) Rename `import pandas.core` to `import pandas as pd`. - let module_name = import.call_path.first().unwrap(); + let module_name = import.path.first().unwrap(); Some(Edit::range_replacement( format!("{module_name} as {target}"), binding.range, diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs b/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs index 90f6481255..8ee066ae13 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs @@ -54,7 +54,7 @@ pub(crate) fn unaliased_collections_abc_set_import( let BindingKind::FromImport(import) = &binding.kind else { return None; }; - if !matches!(import.call_path(), ["collections", "abc", "Set"]) { + if !matches!(import.path(), ["collections", "abc", "Set"]) { return None; } diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index 1136bfa1c7..b448cd8e5b 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -37,7 +37,7 @@ pub(super) fn test_expression(expr: &Expr, semantic: &SemanticModel) -> Resoluti | BindingKind::LoopVar | BindingKind::Global | BindingKind::Nonlocal(_) => Resolution::RelevantLocal, - BindingKind::Import(import) if matches!(import.call_path(), ["pandas"]) => { + BindingKind::Import(import) if matches!(import.path(), ["pandas"]) => { Resolution::PandasModule } _ => Resolution::IrrelevantBinding, diff --git a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs index 7383eecf2d..1cbe650a18 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -65,7 +65,7 @@ pub(crate) fn inplace_argument( .and_then(|module| checker.semantic().find_binding(module)) .map_or(false, |binding| { if let BindingKind::Import(import) = &binding.kind { - matches!(import.call_path(), ["pandas"]) + matches!(import.path(), ["pandas"]) } else { false } diff --git a/crates/ruff_python_ast/src/call_path.rs b/crates/ruff_python_ast/src/call_path.rs index fe8043a106..59f4598e4b 100644 --- a/crates/ruff_python_ast/src/call_path.rs +++ b/crates/ruff_python_ast/src/call_path.rs @@ -1,8 +1,40 @@ use smallvec::{smallvec, SmallVec}; +use std::ops::{Deref, Index}; use crate::{nodes, Expr}; -/// A representation of a qualified name, like `typing.List`. +/// A generic path to a dot-separated symbol. +/// +/// For example: +/// - Given `foo.bar.baz()`, the symbol path for the function call would be `["foo", "bar", "baz"]`. +/// - Given `foo.bar.baz`, the symbol path for the attribute access would be `["foo", "bar", "baz"]`. +/// - Given `import foo.bar.baz`, the symbol path for the import would be `["foo", "bar", "baz"]`. +pub struct SymbolPath<'a>(&'a [&'a str]); + +impl<'a> From> for SymbolPath<'a> { + fn from(path: CallPath<'a>) -> Self { + Self(path.as_slice()) + } +} + +// impl Index for SymbolPath<'_> { +// type Output = str; +// +// fn index(&self, index: usize) -> &Self::Output { +// self.0[index] +// } +// } + +impl<'a> Deref for SymbolPath<'a> { + type Target = [&'a str]; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +/// A representation of dot-separated path, like `foo.bar.baz`. +/// The owned version of [`SymbolPath`]. pub type CallPath<'a> = SmallVec<[&'a str; 8]>; /// Convert an `Expr` to its [`CallPath`] segments (like `["typing", "List"]`). @@ -137,15 +169,15 @@ pub fn collect_call_path(expr: &Expr) -> Option { /// 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)) + collect_call_path(expr).map(|call_path| format_call_path(SymbolPath::from(call_path))) } /// Format a call path for display. -pub fn format_call_path(call_path: &[&str]) -> String { +pub fn format_call_path(call_path: SymbolPath) -> String { if call_path.first().map_or(false, |first| first.is_empty()) { // If the first segment is empty, the `CallPath` is that of a builtin. // Ex) `["", "bool"]` -> `"bool"` - call_path[1..].join(".") + call_path.iter().skip(1).join(".") } else if call_path .first() .map_or(false, |first| matches!(*first, ".")) diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 9c48d0e052..0a79ab5fcd 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -1,10 +1,11 @@ use std::borrow::Cow; -use std::ops::{Deref, DerefMut}; +use std::ops::{Deref, DerefMut, Index, Range}; +use std::slice::SliceIndex; use bitflags::bitflags; use ruff_index::{newtype_index, IndexSlice, IndexVec}; -use ruff_python_ast::call_path::format_call_path; +use ruff_python_ast::call_path::{format_call_path, CallPath}; use ruff_python_ast::Ranged; use ruff_source_file::Locator; use ruff_text_size::TextRange; @@ -118,44 +119,32 @@ impl<'a> Binding<'a> { // import foo.bar // import foo.baz // ``` - BindingKind::Import(Import { - call_path: redefinition, - }) => { - if let BindingKind::SubmoduleImport(SubmoduleImport { - call_path: definition, - }) = &existing.kind + BindingKind::Import(Import { path: redefinition }) => { + if let BindingKind::SubmoduleImport(SubmoduleImport { path: definition }) = + &existing.kind { return redefinition == definition; } } - BindingKind::FromImport(FromImport { - call_path: redefinition, - }) => { - if let BindingKind::SubmoduleImport(SubmoduleImport { - call_path: definition, - }) = &existing.kind + BindingKind::FromImport(FromImport { path: redefinition }) => { + if let BindingKind::SubmoduleImport(SubmoduleImport { path: definition }) = + &existing.kind { return redefinition == definition; } } - BindingKind::SubmoduleImport(SubmoduleImport { - call_path: redefinition, - }) => match &existing.kind { - BindingKind::Import(Import { - call_path: definition, - }) - | BindingKind::SubmoduleImport(SubmoduleImport { - call_path: definition, - }) => { - return redefinition == definition; + BindingKind::SubmoduleImport(SubmoduleImport { path: redefinition }) => { + match &existing.kind { + BindingKind::Import(Import { path: definition }) + | BindingKind::SubmoduleImport(SubmoduleImport { path: definition }) => { + return redefinition == definition; + } + BindingKind::FromImport(FromImport { path: definition }) => { + return redefinition == definition; + } + _ => {} } - BindingKind::FromImport(FromImport { - call_path: definition, - }) => { - return redefinition == definition; - } - _ => {} - }, + } // Deletions, annotations, `__future__` imports, and builtins are never considered // redefinitions. BindingKind::Deletion @@ -338,7 +327,7 @@ pub struct Import<'a> { /// The full name of the module being imported. /// Ex) Given `import foo`, `qualified_name` would be "foo". /// Ex) Given `import foo as bar`, `qualified_name` would be "foo". - pub call_path: Box<[&'a str]>, + pub path: Box<[&'a str]>, } /// A binding for a member imported from a module, keyed on the name to which the member is bound. @@ -349,7 +338,7 @@ pub struct FromImport<'a> { /// The full name of the member being imported. /// Ex) Given `from foo import bar`, `qualified_name` would be "foo.bar". /// Ex) Given `from foo import bar as baz`, `qualified_name` would be "foo.bar". - pub call_path: Box<[&'a str]>, + pub path: Box<[&'a str]>, } /// A binding for a submodule imported from a module, keyed on the name of the parent module. @@ -358,7 +347,7 @@ pub struct FromImport<'a> { pub struct SubmoduleImport<'a> { /// The full name of the submodule being imported. /// Ex) Given `import foo.bar`, `qualified_name` would be "foo.bar". - pub call_path: Box<[&'a str]>, + pub path: Box<[&'a str]>, } #[derive(Debug, Clone, is_macro::Is)] @@ -515,13 +504,40 @@ bitflags! { } } +/// The path to the symbol imported by an import statement. +/// +/// For example: +/// - Given `import foo`, the import path would be `["foo"]`. +/// - Given `import foo.bar`, the import path would be `["foo", "bar"]`. +/// - Given `from foo import bar`, the import path would be `["foo", "bar"]`. +/// - Given `from foo.bar import baz`, the import path would be `["foo", "bar", "baz]`. +pub struct ImportPath<'a>(SymbolPath<'a>); + +/// The path to the module imported by an import statement. +/// +/// For example: +/// - Given `import foo`, the module path would be `["foo"]`. +/// - Given `import foo.bar`, the import path would be `["foo"]`. +/// - Given `from foo import bar`, the module path would be `["foo"]`. +/// - Given `from foo.bar import baz`, the import path would be `["foo", "bar"]`. +pub struct ModulePath<'a>(SymbolPath<'a>); + +/// The name of the member imported by an import statement. +/// +/// For example: +/// - Given `import foo`, the module path would be `"foo"`. +/// - Given `import foo.bar`, the import path would be `"foo.bar"`. +/// - Given `from foo import bar`, the module path would be `"bar"`. +/// - Given `from foo.bar import baz`, the import path would be `"baz"`. +pub struct MemberName<'a>(Cow<'a, str>); + /// A trait for imported symbols. pub trait Imported<'a> { - /// Returns the call path to the imported symbol. - fn call_path(&self) -> &[&str]; + /// Returns the qualified path to the imported symbol. + fn path(&self) -> ImportPath<'a>; /// Returns the module name of the imported symbol. - fn module_name(&self) -> &[&str]; + fn module_name(&self) -> ModulePath<'a>; /// Returns the member name of the imported symbol. For a straight import, this is equivalent /// to [`qualified_name`]; for a `from` import, this is the name of the imported symbol. @@ -529,19 +545,19 @@ pub trait Imported<'a> { /// Returns the fully-qualified name of the imported symbol. fn qualified_name(&self) -> String { - format_call_path(self.call_path()) + format_call_path(self.path().0) } } impl<'a> Imported<'a> for Import<'a> { /// For example, given `import foo`, returns `["foo"]`. - fn call_path(&self) -> &[&str] { - self.call_path.as_ref() + fn path(&self) -> ImportPath<'a> { + ImportPath(&self.path) } /// For example, given `import foo`, returns `["foo"]`. - fn module_name(&self) -> &[&str] { - &self.call_path[..1] + fn module_name(&self) -> ModulePath<'a> { + ModulePath(&self.path[..1]) } /// For example, given `import foo`, returns `"foo"`. @@ -552,13 +568,13 @@ impl<'a> Imported<'a> for Import<'a> { impl<'a> Imported<'a> for SubmoduleImport<'a> { /// For example, given `import foo.bar`, returns `["foo", "bar"]`. - fn call_path(&self) -> &[&str] { - self.call_path.as_ref() + fn path(&self) -> ImportPath<'a> { + ImportPath(&self.path) } /// For example, given `import foo.bar`, returns `["foo"]`. - fn module_name(&self) -> &[&str] { - &self.call_path[..1] + fn module_name(&self) -> ModulePath<'a> { + ModulePath(&self.path[..1]) } /// For example, given `import foo.bar`, returns `"foo.bar"`. @@ -569,18 +585,18 @@ impl<'a> Imported<'a> for SubmoduleImport<'a> { impl<'a> Imported<'a> for FromImport<'a> { /// For example, given `from foo import bar`, returns `["foo", "bar"]`. - fn call_path(&self) -> &[&str] { - self.call_path.as_ref() + fn path(&self) -> ImportPath<'a> { + ImportPath(&self.path) } /// For example, given `from foo import bar`, returns `["foo"]`. - fn module_name(&self) -> &[&str] { - &self.call_path[..self.call_path.len() - 1] + fn module_name(&self) -> ModulePath<'a> { + ModulePath(&self.path[..self.path.len() - 1]) } /// For example, given `from foo import bar`, returns `"bar"`. fn member_name(&self) -> Cow<'a, str> { - Cow::Borrowed(self.call_path[self.call_path.len() - 1]) + Cow::Borrowed(self.path[self.path.len() - 1]) } } @@ -593,15 +609,15 @@ pub enum AnyImport<'a> { } impl<'a> Imported<'a> for AnyImport<'a> { - fn call_path(&self) -> &[&str] { + fn path(&self) -> ImportPath<'a> { match self { - Self::Import(import) => import.call_path(), - Self::SubmoduleImport(import) => import.call_path(), - Self::FromImport(import) => import.call_path(), + Self::Import(import) => import.path(), + Self::SubmoduleImport(import) => import.path(), + Self::FromImport(import) => import.path(), } } - fn module_name(&self) -> &[&str] { + fn module_name(&self) -> ModulePath<'a> { match self { Self::Import(import) => import.module_name(), Self::SubmoduleImport(import) => import.module_name(), diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 77c44f0759..3c50e0d50d 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -585,7 +585,7 @@ impl<'a> SemanticModel<'a> { // print(pa.csv.read_csv("test.csv")) // ``` let import = self.bindings[binding_id].as_any_import()?; - let call_path = import.call_path(); + let call_path = import.path(); let segment = call_path.last()?; if *segment == symbol { return None; @@ -630,13 +630,13 @@ impl<'a> SemanticModel<'a> { }; match &binding.kind { - BindingKind::Import(Import { call_path }) => { + BindingKind::Import(Import { path: call_path }) => { let value_path = collect_call_path(value)?; let (_, tail) = value_path.split_first()?; let resolved: CallPath = call_path.iter().chain(tail.iter()).copied().collect(); Some(resolved) } - BindingKind::SubmoduleImport(SubmoduleImport { call_path }) => { + BindingKind::SubmoduleImport(SubmoduleImport { path: call_path }) => { let value_path = collect_call_path(value)?; let (_, tail) = value_path.split_first()?; let resolved: CallPath = call_path @@ -647,10 +647,9 @@ impl<'a> SemanticModel<'a> { .collect(); Some(resolved) } - BindingKind::FromImport(FromImport { call_path }) => { + BindingKind::FromImport(FromImport { path: call_path }) => { let value_path = collect_call_path(value)?; let (_, tail) = value_path.split_first()?; - let resolved: CallPath = if call_path.first().map_or(false, |segment| *segment == ".") { from_relative_import(self.module_path?, call_path, tail)? @@ -690,7 +689,7 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="sys"` and `object="exit"`: // `import sys` -> `sys.exit` // `import sys as sys2` -> `sys2.exit` - BindingKind::Import(Import { call_path }) => { + BindingKind::Import(Import { path: call_path }) => { if call_path.as_ref() == module_path.as_slice() { if let Some(source) = binding.source { // Verify that `sys` isn't bound in an inner scope. @@ -711,7 +710,7 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="os.path"` and `object="join"`: // `from os.path import join` -> `join` // `from os.path import join as join2` -> `join2` - BindingKind::FromImport(FromImport { call_path }) => { + BindingKind::FromImport(FromImport { path: call_path }) => { if let Some((target_member, target_module)) = call_path.split_last() { if target_module == module_path.as_slice() && target_member == &member { if let Some(source) = binding.source {