mirror of https://github.com/astral-sh/ruff
[ty] Add subdiagnostic hint if the user wrote `X = Any` rather than `X: Any` (#21777)
This commit is contained in:
parent
45ac30a4d7
commit
8ebecb2a88
|
|
@ -0,0 +1,26 @@
|
|||
# Diagnostics for invalid attribute access on special forms
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```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]
|
||||
```
|
||||
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue