mirror of https://github.com/astral-sh/ruff
[ty] Improve diagnostics when a submodule is not available as an attribute on a module-literal type (#21561)
This commit is contained in:
parent
f2ce5e561a
commit
3410041b4c
|
|
@ -2683,6 +2683,39 @@ reveal_type(datetime.UTC) # revealed: Unknown
|
|||
reveal_type(datetime.fakenotreal) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unimported submodule incorrectly accessed as attribute
|
||||
|
||||
We give special diagnostics for this common case too:
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`foo/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`foo/bar.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`baz/bar.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import foo
|
||||
import baz
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(foo.bar) # revealed: Unknown
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(baz.bar) # revealed: Unknown
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ Y: int = 47
|
|||
import mypackage
|
||||
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: "has no member `fails`"
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ Y: int = 47
|
|||
import mypackage
|
||||
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: "has no member `fails`"
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ Y: int = 47
|
|||
import mypackage
|
||||
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: "has no member `fails`"
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ Y: int = 47
|
|||
import mypackage
|
||||
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: "has no member `fails`"
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ X: int = 42
|
|||
import mypackage
|
||||
|
||||
# TODO: this could work and would be nice to have?
|
||||
# error: "has no member `imported`"
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ X: int = 42
|
|||
import mypackage
|
||||
|
||||
# TODO: this could work and would be nice to have
|
||||
# error: "has no member `imported`"
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -242,13 +242,13 @@ X: int = 42
|
|||
import mypackage
|
||||
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "has no member `nested`"
|
||||
reveal_type(mypackage.nested) # revealed: Unknown
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "has no member `nested`"
|
||||
reveal_type(mypackage.nested.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -280,9 +280,9 @@ import mypackage
|
|||
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# TODO: this would be nice to support
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
reveal_type(mypackage.nested.X) # revealed: int
|
||||
|
|
@ -318,13 +318,13 @@ X: int = 42
|
|||
import mypackage
|
||||
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "has no member `nested`"
|
||||
reveal_type(mypackage.nested) # revealed: Unknown
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "has no member `nested`"
|
||||
reveal_type(mypackage.nested.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -356,9 +356,9 @@ import mypackage
|
|||
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# TODO: this would be nice to support
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: "has no member `nested`"
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
reveal_type(mypackage.nested.X) # revealed: int
|
||||
|
|
@ -393,11 +393,11 @@ X: int = 42
|
|||
```py
|
||||
import mypackage
|
||||
|
||||
# error: "has no member `submodule`"
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule) # revealed: Unknown
|
||||
# error: "has no member `submodule`"
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: "has no member `submodule`"
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -429,11 +429,11 @@ X: int = 42
|
|||
import mypackage
|
||||
|
||||
# TODO: this would be nice to support
|
||||
# error: "has no member `submodule`"
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule) # revealed: Unknown
|
||||
# error: "has no member `submodule`"
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: "has no member `submodule`"
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -460,9 +460,9 @@ X: int = 42
|
|||
```py
|
||||
import mypackage
|
||||
|
||||
# error: "has no member `imported`"
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
# error: "has no member `imported_m`"
|
||||
# error: [unresolved-attribute] "has no member `imported_m`"
|
||||
reveal_type(mypackage.imported_m.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -486,7 +486,7 @@ X: int = 42
|
|||
import mypackage
|
||||
|
||||
# TODO: this would be nice to support, as it works at runtime
|
||||
# error: "has no member `imported`"
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
reveal_type(mypackage.imported_m.X) # revealed: int
|
||||
```
|
||||
|
|
@ -566,7 +566,7 @@ X: int = 42
|
|||
from mypackage import *
|
||||
|
||||
# TODO: this would be nice to support
|
||||
# error: "`imported` used when not defined"
|
||||
# error: [unresolved-reference] "`imported` used when not defined"
|
||||
reveal_type(imported.X) # revealed: Unknown
|
||||
reveal_type(Z) # revealed: int
|
||||
```
|
||||
|
|
@ -669,10 +669,11 @@ X: int = 42
|
|||
import mypackage
|
||||
from mypackage import imported
|
||||
|
||||
reveal_type(imported.X) # revealed: int
|
||||
|
||||
# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes
|
||||
# for details, see: https://github.com/astral-sh/ty/issues/1488
|
||||
reveal_type(imported.X) # revealed: int
|
||||
# error: "has no member `imported`"
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -695,9 +696,10 @@ X: int = 42
|
|||
import mypackage
|
||||
from mypackage import imported
|
||||
|
||||
# TODO: this would be nice to support, as it works at runtime
|
||||
reveal_type(imported.X) # revealed: int
|
||||
# error: "has no member `imported`"
|
||||
|
||||
# TODO: this would be nice to support, as it works at runtime
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -733,9 +735,9 @@ import mypackage
|
|||
from mypackage import imported
|
||||
|
||||
reveal_type(imported.X) # revealed: int
|
||||
# error: "has no member `fails`"
|
||||
# error: [unresolved-attribute] "has no member `fails`"
|
||||
reveal_type(imported.fails.Y) # revealed: Unknown
|
||||
# error: "has no member `fails`"
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -768,7 +770,7 @@ from mypackage import imported
|
|||
|
||||
reveal_type(imported.X) # revealed: int
|
||||
reveal_type(imported.fails.Y) # revealed: int
|
||||
# error: "has no member `fails`"
|
||||
# error: [unresolved-attribute] "Submodule `fails`"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ X: int = 42
|
|||
from . import foo
|
||||
import package
|
||||
|
||||
# error: [unresolved-attribute] "Module `package` has no member `foo`"
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(package.foo.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: attributes.md - Attributes - Unimported submodule incorrectly accessed as attribute
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## foo/__init__.py
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## foo/bar.py
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## baz/bar.py
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | import foo
|
||||
2 | import baz
|
||||
3 |
|
||||
4 | # error: [unresolved-attribute]
|
||||
5 | reveal_type(foo.bar) # revealed: Unknown
|
||||
6 | # error: [unresolved-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: Unknown
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Submodule `bar` may not be available as an attribute on module `foo`
|
||||
--> src/main.py:5:13
|
||||
|
|
||||
4 | # error: [unresolved-attribute]
|
||||
5 | reveal_type(foo.bar) # revealed: Unknown
|
||||
| ^^^^^^^
|
||||
6 | # error: [unresolved-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: Unknown
|
||||
|
|
||||
help: Consider explicitly importing `foo.bar`
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Submodule `bar` may not be available as an attribute on module `baz`
|
||||
--> src/main.py:7:13
|
||||
|
|
||||
5 | reveal_type(foo.bar) # revealed: Unknown
|
||||
6 | # error: [unresolved-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: Unknown
|
||||
| ^^^^^^^
|
||||
|
|
||||
help: Consider explicitly importing `baz.bar`
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
|
@ -628,7 +628,7 @@ import imported
|
|||
from module2 import imported as other_imported
|
||||
from ty_extensions import TypeOf, static_assert, is_equivalent_to
|
||||
|
||||
# error: [unresolved-attribute] "Module `imported` has no member `abc`"
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(imported.abc) # revealed: Unknown
|
||||
|
||||
reveal_type(other_imported.abc) # revealed: <module 'imported.abc'>
|
||||
|
|
|
|||
|
|
@ -364,10 +364,12 @@ pub(crate) struct ImportFromDefinitionNodeRef<'ast> {
|
|||
pub(crate) alias_index: usize,
|
||||
pub(crate) is_reexported: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ImportFromSubmoduleDefinitionNodeRef<'ast> {
|
||||
pub(crate) node: &'ast ast::StmtImportFrom,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct AssignmentDefinitionNodeRef<'ast, 'db> {
|
||||
pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>,
|
||||
|
|
|
|||
|
|
@ -9082,10 +9082,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
|
||||
let diagnostic = match value_type {
|
||||
Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!(
|
||||
"Module `{}` has no member `{attr_name}`",
|
||||
module.module(db).name(db),
|
||||
)),
|
||||
Type::ModuleLiteral(module) => {
|
||||
let module = module.module(db);
|
||||
let module_name = module.name(db);
|
||||
if module.kind(db).is_package()
|
||||
&& let Some(relative_submodule) = ModuleName::new(attr_name)
|
||||
{
|
||||
let mut maybe_submodule_name = module_name.clone();
|
||||
maybe_submodule_name.extend(&relative_submodule);
|
||||
if resolve_module(db, &maybe_submodule_name).is_some() {
|
||||
let mut diag = builder.into_diagnostic(format_args!(
|
||||
"Submodule `{attr_name}` may not be available as an attribute \
|
||||
on module `{module_name}`"
|
||||
));
|
||||
diag.help(format_args!(
|
||||
"Consider explicitly importing `{maybe_submodule_name}`"
|
||||
));
|
||||
return fallback();
|
||||
}
|
||||
}
|
||||
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Module `{module_name}` has no member `{attr_name}`",
|
||||
))
|
||||
}
|
||||
Type::ClassLiteral(class) => builder.into_diagnostic(format_args!(
|
||||
"Class `{}` has no attribute `{attr_name}`",
|
||||
class.name(db),
|
||||
|
|
|
|||
Loading…
Reference in New Issue