mirror of https://github.com/astral-sh/ruff
[ty] Avoid double-analyzing tuple in `Final` subscript (#21828)
## Summary
As-is, a single-element tuple gets destructured via:
```rust
let arguments = if let ast::Expr::Tuple(tuple) = slice {
&*tuple.elts
} else {
std::slice::from_ref(slice)
};
```
But then, because it's a single element, we call
`infer_annotation_expression_impl`, passing in the tuple, rather than
the first element.
Closes https://github.com/astral-sh/ty/issues/1793.
Closes https://github.com/astral-sh/ty/issues/1768.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cbff09b9af
commit
285d6410d3
|
|
@ -101,6 +101,37 @@ class C:
|
||||||
x: ClassVar[int, str] = 1
|
x: ClassVar[int, str] = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Trailing comma creates a tuple
|
||||||
|
|
||||||
|
A trailing comma in a subscript creates a single-element tuple. We need to handle this gracefully
|
||||||
|
and emit a proper error rather than crashing (see
|
||||||
|
[ty#1793](https://github.com/astral-sh/ty/issues/1793)).
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
class C:
|
||||||
|
# error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?"
|
||||||
|
x: ClassVar[(),]
|
||||||
|
|
||||||
|
# error: [invalid-attribute-access] "Cannot assign to ClassVar `x` from an instance of type `C`"
|
||||||
|
C().x = 42
|
||||||
|
reveal_type(C.x) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
This also applies when the trailing comma is inside the brackets (see
|
||||||
|
[ty#1768](https://github.com/astral-sh/ty/issues/1768)):
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
class D:
|
||||||
|
# A trailing comma here doesn't change the meaning; it's still one argument.
|
||||||
|
a: ClassVar[int,] = 1
|
||||||
|
|
||||||
|
reveal_type(D.a) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
## Illegal `ClassVar` in type expression
|
## Illegal `ClassVar` in type expression
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,22 @@ class C:
|
||||||
x: Final[int, str] = 1
|
x: Final[int, str] = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Trailing comma creates a tuple
|
||||||
|
|
||||||
|
A trailing comma in a subscript creates a single-element tuple. We need to handle this gracefully
|
||||||
|
and emit a proper error rather than crashing (see
|
||||||
|
[ty#1793](https://github.com/astral-sh/ty/issues/1793)).
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
# error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?"
|
||||||
|
x: Final[(),] = 42
|
||||||
|
|
||||||
|
# error: [invalid-assignment] "Reassignment of `Final` symbol `x` is not allowed"
|
||||||
|
x = 56
|
||||||
|
```
|
||||||
|
|
||||||
### Illegal `Final` in type expression
|
### Illegal `Final` in type expression
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,25 @@ class Wrong:
|
||||||
x: InitVar[int, str] # error: [invalid-type-form] "Type qualifier `InitVar` expected exactly 1 argument, got 2"
|
x: InitVar[int, str] # error: [invalid-type-form] "Type qualifier `InitVar` expected exactly 1 argument, got 2"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A trailing comma in a subscript creates a single-element tuple. We need to handle this gracefully
|
||||||
|
and emit a proper error rather than crashing (see
|
||||||
|
[ty#1793](https://github.com/astral-sh/ty/issues/1793)).
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import InitVar, dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AlsoWrong:
|
||||||
|
# error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?"
|
||||||
|
x: InitVar[(),]
|
||||||
|
|
||||||
|
# revealed: (self: AlsoWrong, x: Unknown) -> None
|
||||||
|
reveal_type(AlsoWrong.__init__)
|
||||||
|
|
||||||
|
# error: [unresolved-attribute]
|
||||||
|
reveal_type(AlsoWrong(42).x) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
A bare `InitVar` is not allowed according to the [type annotation grammar]:
|
A bare `InitVar` is not allowed according to the [type annotation grammar]:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
||||||
|
|
@ -273,10 +273,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
} else {
|
} else {
|
||||||
std::slice::from_ref(slice)
|
std::slice::from_ref(slice)
|
||||||
};
|
};
|
||||||
let num_arguments = arguments.len();
|
let type_and_qualifiers = if let [argument] = arguments {
|
||||||
let type_and_qualifiers = if num_arguments == 1 {
|
let mut type_and_qualifiers = self.infer_annotation_expression_impl(
|
||||||
let mut type_and_qualifiers = self
|
argument,
|
||||||
.infer_annotation_expression_impl(slice, PEP613Policy::Disallowed);
|
PEP613Policy::Disallowed,
|
||||||
|
);
|
||||||
|
|
||||||
match type_qualifier {
|
match type_qualifier {
|
||||||
SpecialFormType::ClassVar => {
|
SpecialFormType::ClassVar => {
|
||||||
|
|
@ -307,6 +308,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
if let Some(builder) =
|
if let Some(builder) =
|
||||||
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
||||||
{
|
{
|
||||||
|
let num_arguments = arguments.len();
|
||||||
builder.into_diagnostic(format_args!(
|
builder.into_diagnostic(format_args!(
|
||||||
"Type qualifier `{type_qualifier}` expected exactly 1 argument, \
|
"Type qualifier `{type_qualifier}` expected exactly 1 argument, \
|
||||||
got {num_arguments}",
|
got {num_arguments}",
|
||||||
|
|
@ -325,10 +327,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
} else {
|
} else {
|
||||||
std::slice::from_ref(slice)
|
std::slice::from_ref(slice)
|
||||||
};
|
};
|
||||||
let num_arguments = arguments.len();
|
let type_and_qualifiers = if let [argument] = arguments {
|
||||||
let type_and_qualifiers = if num_arguments == 1 {
|
let mut type_and_qualifiers = self.infer_annotation_expression_impl(
|
||||||
let mut type_and_qualifiers = self
|
argument,
|
||||||
.infer_annotation_expression_impl(slice, PEP613Policy::Disallowed);
|
PEP613Policy::Disallowed,
|
||||||
|
);
|
||||||
type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR);
|
type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR);
|
||||||
type_and_qualifiers
|
type_and_qualifiers
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -341,6 +344,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
if let Some(builder) =
|
if let Some(builder) =
|
||||||
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
||||||
{
|
{
|
||||||
|
let num_arguments = arguments.len();
|
||||||
builder.into_diagnostic(format_args!(
|
builder.into_diagnostic(format_args!(
|
||||||
"Type qualifier `InitVar` expected exactly 1 argument, \
|
"Type qualifier `InitVar` expected exactly 1 argument, \
|
||||||
got {num_arguments}",
|
got {num_arguments}",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue