diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md new file mode 100644 index 0000000000..d19d2a8c12 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md @@ -0,0 +1,26 @@ +# Diagnostics for invalid attribute access on special forms + + + +```py +from typing_extensions import Any, Final, LiteralString, Self + +X = Any + +class Foo: + X: Final = LiteralString + a: int + b: Self + + class Bar: + def __init__(self): + self.y: Final = LiteralString + +X.foo # error: [unresolved-attribute] +X.aaaaooooooo # error: [unresolved-attribute] +Foo.X.startswith # error: [unresolved-attribute] +Foo.Bar().y.startswith # error: [unresolved-attribute] + +# TODO: false positive (just testing the diagnostic in the meantime) +Foo().b.a # error: [unresolved-attribute] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu…_-_Diagnostics_for_inva…_(249d635e74a41c9e).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu…_-_Diagnostics_for_inva…_(249d635e74a41c9e).snap new file mode 100644 index 0000000000..8672225ce0 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu…_-_Diagnostics_for_inva…_(249d635e74a41c9e).snap @@ -0,0 +1,114 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: special_form_attributes.md - Diagnostics for invalid attribute access on special forms +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/special_form_attributes.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import Any, Final, LiteralString, Self + 2 | + 3 | X = Any + 4 | + 5 | class Foo: + 6 | X: Final = LiteralString + 7 | a: int + 8 | b: Self + 9 | +10 | class Bar: +11 | def __init__(self): +12 | self.y: Final = LiteralString +13 | +14 | X.foo # error: [unresolved-attribute] +15 | X.aaaaooooooo # error: [unresolved-attribute] +16 | Foo.X.startswith # error: [unresolved-attribute] +17 | Foo.Bar().y.startswith # error: [unresolved-attribute] +18 | +19 | # TODO: false positive (just testing the diagnostic in the meantime) +20 | Foo().b.a # error: [unresolved-attribute] +``` + +# Diagnostics + +``` +error[unresolved-attribute]: Special form `typing.Any` has no attribute `foo` + --> src/mdtest_snippet.py:14:1 + | +12 | self.y: Final = LiteralString +13 | +14 | X.foo # error: [unresolved-attribute] + | ^^^^^ +15 | X.aaaaooooooo # error: [unresolved-attribute] +16 | Foo.X.startswith # error: [unresolved-attribute] + | +help: Objects with type `Any` have a `foo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any` +help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Special form `typing.Any` has no attribute `aaaaooooooo` + --> src/mdtest_snippet.py:15:1 + | +14 | X.foo # error: [unresolved-attribute] +15 | X.aaaaooooooo # error: [unresolved-attribute] + | ^^^^^^^^^^^^^ +16 | Foo.X.startswith # error: [unresolved-attribute] +17 | Foo.Bar().y.startswith # error: [unresolved-attribute] + | +help: Objects with type `Any` have an `aaaaooooooo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any` +help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Special form `typing.LiteralString` has no attribute `startswith` + --> src/mdtest_snippet.py:16:1 + | +14 | X.foo # error: [unresolved-attribute] +15 | X.aaaaooooooo # error: [unresolved-attribute] +16 | Foo.X.startswith # error: [unresolved-attribute] + | ^^^^^^^^^^^^^^^^ +17 | Foo.Bar().y.startswith # error: [unresolved-attribute] + | +help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString` +help: This error may indicate that `Foo.X` was defined as `Foo.X = typing.LiteralString` when `Foo.X: typing.LiteralString` was intended +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Special form `typing.LiteralString` has no attribute `startswith` + --> src/mdtest_snippet.py:17:1 + | +15 | X.aaaaooooooo # error: [unresolved-attribute] +16 | Foo.X.startswith # error: [unresolved-attribute] +17 | Foo.Bar().y.startswith # error: [unresolved-attribute] + | ^^^^^^^^^^^^^^^^^^^^^^ +18 | +19 | # TODO: false positive (just testing the diagnostic in the meantime) + | +help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString` +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Special form `typing.Self` has no attribute `a` + --> src/mdtest_snippet.py:20:1 + | +19 | # TODO: false positive (just testing the diagnostic in the meantime) +20 | Foo().b.a # error: [unresolved-attribute] + | ^^^^^^^^^ + | +info: rule `unresolved-attribute` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index d45b35298d..9488d2270d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4,6 +4,7 @@ use itertools::{Either, EitherOrBoth, Itertools}; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; use ruff_db::files::File; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_db::source::source_text; use ruff_python_ast::visitor::{Visitor, walk_expr}; use ruff_python_ast::{ self as ast, AnyNodeRef, ExprContext, HasNodeIndex, NodeIndex, PythonVersion, @@ -9111,6 +9112,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context. fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { + fn is_dotted_name(attribute: &ast::Expr) -> bool { + match attribute { + ast::Expr::Name(_) => true, + ast::Expr::Attribute(ast::ExprAttribute { value, .. }) => is_dotted_name(value), + _ => false, + } + } + let ast::ExprAttribute { value, attr, .. } = attribute; let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); @@ -9204,6 +9213,42 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + if let Type::SpecialForm(special_form) = value_type { + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute) + { + let mut diag = builder.into_diagnostic(format_args!( + "Special form `{special_form}` has no attribute `{attr_name}`", + )); + if let Ok(defined_type) = value_type.in_type_expression( + db, + self.scope(), + self.typevar_binding_context, + ) && !defined_type.member(db, attr_name).place.is_undefined() + { + diag.help(format_args!( + "Objects with type `{ty}` have a{maybe_n} `{attr_name}` attribute, but the symbol \ + `{special_form}` does not itself inhabit the type `{ty}`", + maybe_n = if attr_name.starts_with(['a', 'e', 'i', 'o', 'u']) { + "n" + } else { + "" + }, + ty = defined_type.display(self.db()) + )); + if is_dotted_name(value) { + let source = &source_text(self.db(), self.file())[value.range()]; + diag.help(format_args!( + "This error may indicate that `{source}` was defined as \ + `{source} = {special_form}` when `{source}: {special_form}` \ + was intended" + )); + } + } + } + return fallback(); + } + let Some(builder) = self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute) else { return fallback();