diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F401_0.py b/crates/ruff/resources/test/fixtures/pyflakes/F401_0.py index 79c56bc09910af..dcec5beed47b2f 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F401_0.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F401_0.py @@ -92,3 +92,10 @@ def b(self) -> None: case 0,: import x import y + + +# Test: access a sub-importation via an alias. +import foo.bar as bop +import foo.bar.baz + +print(bop.baz.read_csv("test.csv")) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F823.py b/crates/ruff/resources/test/fixtures/pyflakes/F823.py index 343150143e4425..d6c5ee7d444f53 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F823.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F823.py @@ -70,3 +70,13 @@ def main(): def requests_mock(requests_mock: rm.Mocker): print(rm.ANY) + + +import sklearn.base +import mlflow.sklearn + + +def f(): + import sklearn + + mlflow diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap index 67c97bb6ba81cb..4996e8b899d348 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_0.py.snap @@ -151,6 +151,8 @@ F401_0.py:93:16: F401 [*] `x` imported but unused 92 92 | case 0,: 93 |- import x 94 93 | import y +95 94 | +96 95 | F401_0.py:94:16: F401 [*] `y` imported but unused | @@ -166,5 +168,27 @@ F401_0.py:94:16: F401 [*] `y` imported but unused 92 92 | case 0,: 93 93 | import x 94 |- import y +95 94 | +96 95 | +97 96 | # Test: access a sub-importation via an alias. + +F401_0.py:99:8: F401 [*] `foo.bar.baz` imported but unused + | + 97 | # Test: access a sub-importation via an alias. + 98 | import foo.bar as bop + 99 | import foo.bar.baz + | ^^^^^^^^^^^ F401 +100 | +101 | print(bop.baz.read_csv("test.csv")) + | + = help: Remove unused import: `foo.bar.baz` + +ℹ Fix +96 96 | +97 97 | # Test: access a sub-importation via an alias. +98 98 | import foo.bar as bop +99 |-import foo.bar.baz +100 99 | +101 100 | print(bop.baz.read_csv("test.csv")) diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs index 3db1ec67df8b02..4c51960ec9325b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -130,16 +130,18 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { Expr::Name(name) if name.ctx.is_load() => { - let Some(Stmt::Assign(StmtAssign { value, .. })) = - self.semantic.lookup_symbol(name.id.as_str()) - .and_then(|binding_id| { - self.semantic - .binding(binding_id) - .source - .map(|node_id| self.semantic.statement(node_id)) - }) else { - return; - }; + let Some(Stmt::Assign(StmtAssign { value, .. })) = self + .semantic + .lookup_symbol(name.id.as_str()) + .and_then(|binding_id| { + self.semantic + .binding(binding_id) + .source + .map(|node_id| self.semantic.statement(node_id)) + }) + else { + return; + }; match value.as_ref() { Expr::Subscript(ExprSubscript { diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 8194640fff5c60..1d910bbab302fb 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -585,7 +585,7 @@ impl<'a> Imported<'a> for FromImport<'a> { } /// A wrapper around an import [`BindingKind`] that can be any of the three types of imports. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, is_macro::Is)] pub enum AnyImport<'a> { Import(&'a Import<'a>), SubmoduleImport(&'a SubmoduleImport<'a>), diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 262b66aa1c818e..b0bdef409e3cda 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -590,14 +590,24 @@ impl<'a> SemanticModel<'a> { // print(pa.csv.read_csv("test.csv")) // ``` let import = self.bindings[binding_id].as_any_import()?; + if !import.is_import() { + return None; + } + + // Grab, e.g., `pyarrow` from `import pyarrow as pa`. let call_path = import.call_path(); let segment = call_path.last()?; if *segment == symbol { return None; } + // Locate the submodule import (e.g., `pyarrow.csv`) that `pa` aliases. let binding_id = self.scopes[scope_id].get(segment)?; - if !self.bindings[binding_id].kind.is_submodule_import() { + let submodule = &self.bindings[binding_id].as_any_import()?; + if !submodule.is_submodule_import() { + return None; + } + if import.module_name() != submodule.module_name() { return None; }