diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 51d54fa3ae..3f3bd9bed1 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -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: + + + +`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 diff --git a/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md b/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md index b5217a1ca9..7dc2874515 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md +++ b/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md @@ -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: -# 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: # 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: reveal_type(mypackage.nested.X) # revealed: int @@ -318,13 +318,13 @@ X: int = 42 import mypackage reveal_type(mypackage.submodule) # revealed: -# 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: # 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: 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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/relative.md b/crates/ty_python_semantic/resources/mdtest/import/relative.md index a80735e460..fdfe4b16fc 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/relative.md +++ b/crates/ty_python_semantic/resources/mdtest/import/relative.md @@ -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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Unimported_submodule…_(2b6da09ed380b2).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Unimported_submodule…_(2b6da09ed380b2).snap new file mode 100644 index 0000000000..7ec8f5814e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Unimported_submodule…_(2b6da09ed380b2).snap @@ -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 + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index fe846ee213..4ebb7c6bbd 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -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: diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index ad8b28847f..a4a92b9acd 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -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>)>, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ed1042e04f..f2ae212abe 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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),