diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 3382bc5a4f..def709f656 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -26,7 +26,6 @@ //! represents the lint-rule analysis phase. In the future, these steps may be separated into //! distinct passes over the AST. -use std::iter::once; use std::path::Path; use itertools::Itertools; @@ -40,7 +39,7 @@ use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, IsolationLevel}; use ruff_python_ast::all::{extract_all_names, DunderAllFlags}; use ruff_python_ast::helpers::{ - extract_handled_exceptions, from_relative_import, from_relative_import_parts, to_module_path, + extract_handled_exceptions, from_relative_import_parts, to_module_path, }; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::str::trailing_quote; @@ -322,15 +321,11 @@ where // Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be // "foo.bar". let name = alias.name.split('.').next().unwrap(); - let qualified_name = &alias.name; - let call_path: Box<[&str]> = qualified_name.split('.').collect(); + let call_path: Box<[&str]> = alias.name.split('.').collect(); self.add_binding( name, alias.identifier(), - BindingKind::SubmoduleImport(SubmoduleImport { - qualified_name, - call_path, - }), + BindingKind::SubmoduleImport(SubmoduleImport { call_path }), BindingFlags::EXTERNAL, ); } else { @@ -347,15 +342,11 @@ where } let name = alias.asname.as_ref().unwrap_or(&alias.name); - let qualified_name = &alias.name; - let call_path: Box<[&str]> = qualified_name.split('.').collect(); + let call_path: Box<[&str]> = alias.name.split('.').collect(); self.add_binding( name, alias.identifier(), - BindingKind::Import(Import { - qualified_name, - call_path, - }), + BindingKind::Import(Import { call_path }), flags, ); } @@ -399,25 +390,20 @@ where // be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz" // and `qualified_name` would be "foo.bar". let name = alias.asname.as_ref().unwrap_or(&alias.name); - - let qualified_name = - helpers::format_import_from_member(level, module, &alias.name); - let call_path = from_relative_import_parts( + if let Some(call_path) = from_relative_import_parts( self.module_path.unwrap_or_default(), level, module, &alias.name, - ); - let call_path: Box<[&str]> = call_path.into_boxed_slice(); - self.add_binding( - name, - alias.identifier(), - BindingKind::FromImport(FromImport { - qualified_name, - call_path, - }), - flags, - ); + ) { + let call_path: Box<[&str]> = call_path.into_boxed_slice(); + self.add_binding( + name, + alias.identifier(), + BindingKind::FromImport(FromImport { call_path }), + flags, + ); + } } } } diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs index 84f4cfbe76..d3f3e6fc92 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.qualified_name.split('.').next().unwrap(); + let module_name = import.call_path.first().unwrap(); Some(Edit::range_replacement( format!("{module_name} as {target}"), binding.range, diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs index 628e7b167c..528166c9d0 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs @@ -60,7 +60,7 @@ pub(crate) fn unconventional_import_alias( return None; }; - let Some(expected_alias) = conventions.get(qualified_name) else { + let Some(expected_alias) = conventions.get(qualified_name.as_str()) else { return None; }; 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 dc91c5330b..581baefac2 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 @@ -50,10 +50,10 @@ pub(crate) fn unaliased_collections_abc_set_import( checker: &Checker, binding: &Binding, ) -> Option { - let BindingKind::FromImport(FromImport { qualified_name, .. }) = &binding.kind else { + let BindingKind::FromImport(FromImport { call_path }) = &binding.kind else { return None; }; - if qualified_name.as_str() != "collections.abc.Set" { + if !matches!(call_path.as_ref(), ["collections", "abc", "Set"]) { return None; } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index 08f2c91d39..260c6989b3 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -75,7 +75,7 @@ pub(crate) fn runtime_import_in_type_checking_block( for binding_id in scope.binding_ids() { let binding = checker.semantic().binding(binding_id); - let Some(qualified_name) = binding.qualified_name() else { + let Some(call_path) = binding.call_path() else { continue; }; @@ -97,7 +97,7 @@ pub(crate) fn runtime_import_in_type_checking_block( }; let import = Import { - qualified_name, + call_path, reference_id, range: binding.range, parent_range: binding.parent_range(checker.semantic()), @@ -131,7 +131,7 @@ pub(crate) fn runtime_import_in_type_checking_block( }; for Import { - qualified_name, + call_path, range, parent_range, .. @@ -139,7 +139,7 @@ pub(crate) fn runtime_import_in_type_checking_block( { let mut diagnostic = Diagnostic::new( RuntimeImportInTypeCheckingBlock { - qualified_name: qualified_name.to_string(), + qualified_name: call_path.join("."), }, range, ); @@ -156,7 +156,7 @@ pub(crate) fn runtime_import_in_type_checking_block( // Separately, generate a diagnostic for every _ignored_ import, to ensure that the // suppression comments aren't marked as unused. for Import { - qualified_name, + call_path, range, parent_range, .. @@ -164,7 +164,7 @@ pub(crate) fn runtime_import_in_type_checking_block( { let mut diagnostic = Diagnostic::new( RuntimeImportInTypeCheckingBlock { - qualified_name: qualified_name.to_string(), + qualified_name: call_path.join("."), }, range, ); @@ -178,7 +178,7 @@ pub(crate) fn runtime_import_in_type_checking_block( /// A runtime-required import with its surrounding context. struct Import<'a> { /// The qualified name of the import (e.g., `typing.List` for `from typing import List`). - qualified_name: &'a str, + call_path: &'a [&'a str], /// The first reference to the imported symbol. reference_id: ResolvedReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). @@ -191,9 +191,9 @@ struct Import<'a> { fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result { let stmt = checker.semantic().stmts[stmt_id]; let parent = checker.semantic().stmts.parent(stmt); - let qualified_names: Vec<&str> = imports + let qualified_names: Vec = imports .iter() - .map(|Import { qualified_name, .. }| *qualified_name) + .map(|Import { call_path, .. }| call_path.join(".")) .collect(); // Find the first reference across all imports. @@ -207,7 +207,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result // Step 1) Remove the import. let remove_import_edit = autofix::edits::remove_unused_imports( - qualified_names.iter().copied(), + qualified_names.iter().map(|name| name.as_str()), stmt, parent, checker.locator(), @@ -219,7 +219,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result let add_import_edit = checker.importer().runtime_import_edit( &StmtImports { stmt, - qualified_names, + qualified_names: qualified_names.iter().map(|name| name.as_str()).collect(), }, at, )?; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 4695395b41..b38e0e0635 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -211,7 +211,7 @@ pub(crate) fn typing_only_runtime_import( }; if is_exempt( - qualified_name, + qualified_name.as_str(), &checker .settings .flake8_type_checking @@ -248,7 +248,7 @@ pub(crate) fn typing_only_runtime_import( // Categorize the import, using coarse-grained categorization. let import_type = match categorize( - qualified_name, + qualified_name.as_str(), Some(level), &checker.settings.src, checker.package(), @@ -353,9 +353,9 @@ pub(crate) fn typing_only_runtime_import( } /// A runtime-required import with its surrounding context. -struct Import<'a> { +struct Import { /// The qualified name of the import (e.g., `typing.List` for `from typing import List`). - qualified_name: &'a str, + qualified_name: String, /// The first reference to the imported symbol. reference_id: ResolvedReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). @@ -417,7 +417,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result let parent = checker.semantic().stmts.parent(stmt); let qualified_names: Vec<&str> = imports .iter() - .map(|Import { qualified_name, .. }| *qualified_name) + .map(|Import { qualified_name, .. }| qualified_name.as_str()) .collect(); // Find the first reference across all imports. diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-first-party-import_TCH001.py.snap.new b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-first-party-import_TCH001.py.snap.new new file mode 100644 index 0000000000..e5d8c04739 --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-first-party-import_TCH001.py.snap.new @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +assertion_line: 43 +--- + diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index 797dfa6901..1aa1f0ab13 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -26,28 +26,25 @@ pub(super) fn test_expression(expr: &Expr, semantic: &SemanticModel) -> Resoluti | Expr::ListComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => Resolution::IrrelevantExpression, - Expr::Name(ast::ExprName { id, .. }) => { - semantic - .find_binding(id) - .map_or(Resolution::IrrelevantBinding, |binding| { - match binding.kind { - BindingKind::Annotation - | BindingKind::Argument - | BindingKind::Assignment - | BindingKind::NamedExprAssignment - | BindingKind::UnpackedAssignment - | BindingKind::LoopVar - | BindingKind::Global - | BindingKind::Nonlocal(_) => Resolution::RelevantLocal, - BindingKind::Import(Import { qualified_name, .. }) - if qualified_name == "pandas" => - { - Resolution::PandasModule - } - _ => Resolution::IrrelevantBinding, - } - }) - } + Expr::Name(ast::ExprName { id, .. }) => semantic.find_binding(id).map_or( + Resolution::IrrelevantBinding, + |binding| match &binding.kind { + BindingKind::Annotation + | BindingKind::Argument + | BindingKind::Assignment + | BindingKind::NamedExprAssignment + | BindingKind::UnpackedAssignment + | BindingKind::LoopVar + | BindingKind::Global + | BindingKind::Nonlocal(_) => Resolution::RelevantLocal, + BindingKind::Import(Import { call_path }) + if matches!(call_path.as_ref(), ["pandas"]) => + { + Resolution::PandasModule + } + _ => Resolution::IrrelevantBinding, + }, + ), _ => Resolution::RelevantLocal, } } 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 c3a01fb65c..af3814e1db 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -65,13 +65,11 @@ pub(crate) fn inplace_argument( .first() .and_then(|module| checker.semantic().find_binding(module)) .map_or(false, |binding| { - matches!( - binding.kind, - BindingKind::Import(Import { - qualified_name: "pandas", - .. - }) - ) + if let BindingKind::Import(Import { call_path, .. }) = &binding.kind { + matches!(call_path.as_ref(), ["pandas"]) + } else { + false + } }) { return; diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index 5ad4f41655..0b7c7f144b 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -214,9 +214,9 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut } /// An unused import with its surrounding context. -struct Import<'a> { +struct Import { /// The qualified name of the import (e.g., `typing.List` for `from typing import List`). - qualified_name: &'a str, + qualified_name: String, /// The trimmed range of the import (e.g., `List` in `from typing import List`). range: TextRange, /// The range of the import's parent statement. @@ -230,7 +230,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result let edit = autofix::edits::remove_unused_imports( imports .iter() - .map(|Import { qualified_name, .. }| *qualified_name), + .map(|Import { qualified_name, .. }| qualified_name.as_str()), stmt, parent, checker.locator(), diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap.new b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap.new new file mode 100644 index 0000000000..4c578d39a8 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap.new @@ -0,0 +1,62 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +assertion_line: 147 +--- +F401_6.py:7:25: F401 [*] `pyflakes.background.BackgroundTasks` imported but unused + | +6 | # F401 `background.BackgroundTasks` imported but unused +7 | from .background import BackgroundTasks + | ^^^^^^^^^^^^^^^ F401 +8 | +9 | # F401 `datastructures.UploadFile` imported but unused + | + = help: Remove unused import: `pyflakes.background.BackgroundTasks` + +ℹ Fix + +F401_6.py:10:43: F401 [*] `pyflakes.datastructures.UploadFile` imported but unused + | + 9 | # F401 `datastructures.UploadFile` imported but unused +10 | from .datastructures import UploadFile as FileUpload + | ^^^^^^^^^^ F401 +11 | +12 | # OK + | + = help: Remove unused import: `pyflakes.datastructures.UploadFile` + +ℹ Fix + +F401_6.py:16:8: F401 [*] `background` imported but unused + | +15 | # F401 `background` imported but unused +16 | import background + | ^^^^^^^^^^ F401 +17 | +18 | # F401 `datastructures` imported but unused + | + = help: Remove unused import: `background` + +ℹ Fix +13 13 | import applications as applications +14 14 | +15 15 | # F401 `background` imported but unused +16 |-import background +17 16 | +18 17 | # F401 `datastructures` imported but unused +19 18 | import datastructures as structures + +F401_6.py:19:26: F401 [*] `datastructures` imported but unused + | +18 | # F401 `datastructures` imported but unused +19 | import datastructures as structures + | ^^^^^^^^^^ F401 + | + = help: Remove unused import: `datastructures` + +ℹ Fix +16 16 | import background +17 17 | +18 18 | # F401 `datastructures` imported but unused +19 |-import datastructures as structures + + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap.new b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap.new new file mode 100644 index 0000000000..5893fcba3e --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap.new @@ -0,0 +1,268 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +assertion_line: 88 +--- +UP024_0.py:6:8: UP024 [*] Replace aliased errors with `OSError` + | +4 | try: +5 | pass +6 | except EnvironmentError: + | ^^^^^^^^^^^^^^^^ UP024 +7 | pass + | + = help: Replace `EnvironmentError` with builtin `OSError` + +ℹ Fix +3 3 | # These should be fixed +4 4 | try: +5 5 | pass +6 |-except EnvironmentError: + 6 |+except OSError: +7 7 | pass +8 8 | +9 9 | try: + +UP024_0.py:11:8: UP024 [*] Replace aliased errors with `OSError` + | + 9 | try: +10 | pass +11 | except IOError: + | ^^^^^^^ UP024 +12 | pass + | + = help: Replace `IOError` with builtin `OSError` + +ℹ Fix +8 8 | +9 9 | try: +10 10 | pass +11 |-except IOError: + 11 |+except OSError: +12 12 | pass +13 13 | +14 14 | try: + +UP024_0.py:16:8: UP024 [*] Replace aliased errors with `OSError` + | +14 | try: +15 | pass +16 | except WindowsError: + | ^^^^^^^^^^^^ UP024 +17 | pass + | + = help: Replace `WindowsError` with builtin `OSError` + +ℹ Fix +13 13 | +14 14 | try: +15 15 | pass +16 |-except WindowsError: + 16 |+except OSError: +17 17 | pass +18 18 | +19 19 | try: + +UP024_0.py:21:8: UP024 [*] Replace aliased errors with `OSError` + | +19 | try: +20 | pass +21 | except mmap.error: + | ^^^^^^^^^^ UP024 +22 | pass + | + = help: Replace `mmap.error` with builtin `OSError` + +ℹ Fix +18 18 | +19 19 | try: +20 20 | pass +21 |-except mmap.error: + 21 |+except OSError: +22 22 | pass +23 23 | +24 24 | try: + +UP024_0.py:26:8: UP024 [*] Replace aliased errors with `OSError` + | +24 | try: +25 | pass +26 | except select.error: + | ^^^^^^^^^^^^ UP024 +27 | pass + | + = help: Replace `select.error` with builtin `OSError` + +ℹ Fix +23 23 | +24 24 | try: +25 25 | pass +26 |-except select.error: + 26 |+except OSError: +27 27 | pass +28 28 | +29 29 | try: + +UP024_0.py:31:8: UP024 [*] Replace aliased errors with `OSError` + | +29 | try: +30 | pass +31 | except socket.error: + | ^^^^^^^^^^^^ UP024 +32 | pass + | + = help: Replace `socket.error` with builtin `OSError` + +ℹ Fix +28 28 | +29 29 | try: +30 30 | pass +31 |-except socket.error: + 31 |+except OSError: +32 32 | pass +33 33 | +34 34 | try: + +UP024_0.py:36:8: UP024 [*] Replace aliased errors with `OSError` + | +34 | try: +35 | pass +36 | except error: + | ^^^^^ UP024 +37 | pass + | + = help: Replace `error` with builtin `OSError` + +ℹ Fix +33 33 | +34 34 | try: +35 35 | pass +36 |-except error: + 36 |+except OSError: +37 37 | pass +38 38 | +39 39 | # Should NOT be in parentheses when replaced + +UP024_0.py:43:8: UP024 [*] Replace aliased errors with `OSError` + | +41 | try: +42 | pass +43 | except (IOError,): + | ^^^^^^^^^^ UP024 +44 | pass +45 | try: + | + = help: Replace with builtin `OSError` + +ℹ Fix +40 40 | +41 41 | try: +42 42 | pass +43 |-except (IOError,): + 43 |+except OSError: +44 44 | pass +45 45 | try: +46 46 | pass + +UP024_0.py:47:8: UP024 [*] Replace aliased errors with `OSError` + | +45 | try: +46 | pass +47 | except (mmap.error,): + | ^^^^^^^^^^^^^ UP024 +48 | pass +49 | try: + | + = help: Replace with builtin `OSError` + +ℹ Fix +44 44 | pass +45 45 | try: +46 46 | pass +47 |-except (mmap.error,): + 47 |+except OSError: +48 48 | pass +49 49 | try: +50 50 | pass + +UP024_0.py:51:8: UP024 [*] Replace aliased errors with `OSError` + | +49 | try: +50 | pass +51 | except (EnvironmentError, IOError, OSError, select.error): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP024 +52 | pass + | + = help: Replace with builtin `OSError` + +ℹ Fix +48 48 | pass +49 49 | try: +50 50 | pass +51 |-except (EnvironmentError, IOError, OSError, select.error): + 51 |+except OSError: +52 52 | pass +53 53 | +54 54 | # Should be kept in parentheses (because multiple) + +UP024_0.py:58:8: UP024 [*] Replace aliased errors with `OSError` + | +56 | try: +57 | pass +58 | except (IOError, KeyError, OSError): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP024 +59 | pass + | + = help: Replace with builtin `OSError` + +ℹ Fix +55 55 | +56 56 | try: +57 57 | pass +58 |-except (IOError, KeyError, OSError): + 58 |+except (KeyError, OSError): +59 59 | pass +60 60 | +61 61 | # First should change, second should not + +UP024_0.py:65:8: UP024 [*] Replace aliased errors with `OSError` + | +63 | try: +64 | pass +65 | except (IOError, error): + | ^^^^^^^^^^^^^^^^ UP024 +66 | pass +67 | # These should not change + | + = help: Replace with builtin `OSError` + +ℹ Fix +62 62 | from .mmap import error +63 63 | try: +64 64 | pass +65 |-except (IOError, error): + 65 |+except OSError: +66 66 | pass +67 67 | # These should not change +68 68 | + +UP024_0.py:87:8: UP024 [*] Replace aliased errors with `OSError` + | +85 | try: +86 | pass +87 | except (mmap).error: + | ^^^^^^^^^^^^ UP024 +88 | pass + | + = help: Replace `mmap.error` with builtin `OSError` + +ℹ Fix +84 84 | pass +85 85 | try: +86 86 | pass +87 |-except (mmap).error: + 87 |+except OSError: +88 88 | pass +89 89 | +90 90 | try: + + diff --git a/crates/ruff_python_ast/src/call_path.rs b/crates/ruff_python_ast/src/call_path.rs index b9f8bb912d..90f6362152 100644 --- a/crates/ruff_python_ast/src/call_path.rs +++ b/crates/ruff_python_ast/src/call_path.rs @@ -1,11 +1,12 @@ -use crate::{nodes, Expr}; use smallvec::{smallvec, SmallVec}; +use crate::{nodes, Expr}; + /// A representation of a qualified name, like `typing.List`. pub type CallPath<'a> = SmallVec<[&'a str; 8]>; /// Convert an `Expr` to its [`CallPath`] segments (like `["typing", "List"]`). -pub fn collect_head_path(expr: &Expr) -> Option<(&ast::ExprName, CallPath)> { +pub fn collect_head_path(expr: &Expr) -> Option<(&nodes::ExprName, CallPath)> { // 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 { diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 6d048d82cf..1d798b3586 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -899,18 +899,21 @@ pub fn from_relative_import_parts<'a>( level: Option, module: Option<&'a str>, member: &'a str, -) -> CallPath<'a> { +) -> Option> { let mut call_path: CallPath = SmallVec::with_capacity(module_path.len() + 1); - // Start with the module path. - call_path.extend(module_path.iter().map(String::as_str)); - // Remove segments based on the number of dots. - for _ in 0..level.unwrap_or(0) { - if call_path.is_empty() { - return SmallVec::new(); + if let Some(level) = level { + if level > 0 { + call_path.extend(module_path.iter().map(String::as_str)); + + for _ in 0..level { + if call_path.is_empty() { + return None; + } + call_path.pop(); + } } - call_path.pop(); } // Add the remaining segments. @@ -921,7 +924,7 @@ pub fn from_relative_import_parts<'a>( // Add the member. call_path.push(member); - call_path + Some(call_path) } /// Given an imported module (based on its relative import level and module name), return the diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 252219846a..4c20732af0 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -1,12 +1,12 @@ use std::ops::{Deref, DerefMut}; use bitflags::bitflags; -use ruff_python_ast::Ranged; -use ruff_text_size::TextRange; use ruff_index::{newtype_index, IndexSlice, IndexVec}; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::call_path::format_call_path; +use ruff_python_ast::Ranged; use ruff_source_file::Locator; +use ruff_text_size::TextRange; use crate::context::ExecutionContext; use crate::model::SemanticModel; @@ -177,30 +177,29 @@ impl<'a> Binding<'a> { } /// Returns the fully-qualified symbol name, if this symbol was imported from another module. - pub fn qualified_name(&self) -> Option<&str> { + pub fn call_path(&self) -> Option<&[&str]> { match &self.kind { - BindingKind::Import(Import { qualified_name, .. }) => Some(qualified_name), - BindingKind::FromImport(FromImport { qualified_name, .. }) => Some(qualified_name), - BindingKind::SubmoduleImport(SubmoduleImport { qualified_name, .. }) => { - Some(qualified_name) - } + BindingKind::Import(Import { call_path }) => Some(call_path), + BindingKind::FromImport(FromImport { call_path }) => Some(call_path), + BindingKind::SubmoduleImport(SubmoduleImport { call_path }) => Some(call_path), _ => None, } } + /// Returns the fully-qualified symbol name, if this symbol was imported from another module. + pub fn qualified_name(&self) -> Option { + self.call_path().map(format_call_path) + } + /// Returns the fully-qualified name of the module from which this symbol was imported, if this /// symbol was imported from another module. - pub fn module_name(&self) -> Option<&str> { + pub fn module_name(&self) -> Option<&[&str]> { match &self.kind { - BindingKind::Import(Import { qualified_name, .. }) - | BindingKind::SubmoduleImport(SubmoduleImport { qualified_name, .. }) => { - Some(qualified_name.split('.').next().unwrap_or(qualified_name)) + BindingKind::Import(Import { call_path }) + | BindingKind::SubmoduleImport(SubmoduleImport { call_path }) => Some(&call_path[..1]), + BindingKind::FromImport(FromImport { call_path }) => { + Some(&call_path[..call_path.len() - 1]) } - BindingKind::FromImport(FromImport { qualified_name, .. }) => Some( - qualified_name - .rsplit_once('.') - .map_or(qualified_name, |(module, _)| module), - ), _ => None, } } @@ -357,7 +356,6 @@ 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 qualified_name: &'a str, pub call_path: Box<[&'a str]>, } @@ -369,7 +367,6 @@ 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 qualified_name: String, pub call_path: Box<[&'a str]>, } @@ -379,7 +376,6 @@ 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 qualified_name: &'a str, pub call_path: Box<[&'a str]>, } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 1f70f87721..e006a63927 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -29,7 +29,6 @@ use crate::{UnresolvedReference, UnresolvedReferenceFlags}; /// A semantic model for a Python module, to enable querying the module's semantic information. pub struct SemanticModel<'a> { typing_modules: &'a [String], - module_path: Option<&'a [String]>, /// Stack of all visited statements. pub stmts: Nodes<'a>, @@ -129,7 +128,6 @@ impl<'a> SemanticModel<'a> { pub fn new(typing_modules: &'a [String], path: &'a Path, module: Module<'a>) -> Self { Self { typing_modules, - module_path: module.path(), stmts: Nodes::default(), stmt_id: None, exprs: Vec::default(), @@ -585,17 +583,13 @@ impl<'a> SemanticModel<'a> { // import pyarrow.csv // print(pa.csv.read_csv("test.csv")) // ``` - let qualified_name = self.bindings[binding_id].qualified_name()?; - let has_alias = qualified_name - .split('.') - .last() - .map(|segment| segment != symbol) - .unwrap_or_default(); - if !has_alias { + let call_path = self.bindings[binding_id].call_path()?; + let segment = call_path.last()?; + if *segment == symbol { return None; } - let binding_id = self.scopes[scope_id].get(qualified_name)?; + let binding_id = self.scopes[scope_id].get(segment)?; if !self.bindings[binding_id].kind.is_submodule_import() { return None; } @@ -635,23 +629,17 @@ impl<'a> SemanticModel<'a> { }; match &binding.kind { - BindingKind::Import(Import { call_path, .. }) => { + BindingKind::Import(Import { call_path }) => { let resolved: CallPath = call_path.iter().chain(tail.iter()).copied().collect(); - // resolved.extend_from_slice(call_path); - // resolved.extend_from_slice(tail); Some(resolved) } - BindingKind::SubmoduleImport(SubmoduleImport { qualified_name, .. }) => { - let name = qualified_name.split('.').next().unwrap_or(qualified_name); - let mut source_path: CallPath = from_unqualified_name(name); + BindingKind::SubmoduleImport(SubmoduleImport { call_path }) => { + let mut source_path: CallPath = CallPath::from_slice(&call_path[..1]); source_path.extend_from_slice(tail.as_slice()); Some(source_path) } - BindingKind::FromImport(FromImport { call_path, .. }) => { + BindingKind::FromImport(FromImport { call_path }) => { let resolved: CallPath = call_path.iter().chain(tail.iter()).copied().collect(); - // let mut resolved = SmallVec::with_capacity(call_path.len() + tail.len()); - // resolved.extend_from_slice(call_path); - // resolved.extend_from_slice(tail); Some(resolved) } BindingKind::Builtin => Some(smallvec!["", head.id.as_str()]), @@ -676,6 +664,7 @@ impl<'a> SemanticModel<'a> { module: &str, member: &str, ) -> Option { + let module_path: Vec<&str> = module.split('.').collect(); self.scopes().enumerate().find_map(|(scope_index, scope)| { scope.bindings().find_map(|(name, binding_id)| { let binding = &self.bindings[binding_id]; @@ -683,8 +672,8 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="sys"` and `object="exit"`: // `import sys` -> `sys.exit` // `import sys as sys2` -> `sys2.exit` - BindingKind::Import(Import { qualified_name, .. }) => { - if qualified_name == &module { + BindingKind::Import(Import { 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. if self @@ -704,10 +693,9 @@ 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 { qualified_name, .. }) => { - if let Some((target_module, target_member)) = qualified_name.split_once('.') - { - if target_module == module && target_member == member { + BindingKind::FromImport(FromImport { 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 { // Verify that `join` isn't bound in an inner scope. if self diff --git a/foo.py b/foo.py deleted file mode 100644 index e441803c4d..0000000000 --- a/foo.py +++ /dev/null @@ -1,6 +0,0 @@ - - -print( - "Unexpected changes:\n" - "\n".join(["1", "2", "3"]) -)