diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md index f697543c1b..5578332677 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -101,6 +101,37 @@ class C: 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 ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index 1c42ded723..af468f6132 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -340,6 +340,22 @@ class C: 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 ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md index 9282782ce4..7d6c14b1c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md @@ -112,6 +112,25 @@ class Wrong: 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]: ```py diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index ce3b2a4a92..374f65878b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -273,10 +273,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { std::slice::from_ref(slice) }; - let num_arguments = arguments.len(); - let type_and_qualifiers = if num_arguments == 1 { - let mut type_and_qualifiers = self - .infer_annotation_expression_impl(slice, PEP613Policy::Disallowed); + let type_and_qualifiers = if let [argument] = arguments { + let mut type_and_qualifiers = self.infer_annotation_expression_impl( + argument, + PEP613Policy::Disallowed, + ); match type_qualifier { SpecialFormType::ClassVar => { @@ -307,6 +308,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let num_arguments = arguments.len(); builder.into_diagnostic(format_args!( "Type qualifier `{type_qualifier}` expected exactly 1 argument, \ got {num_arguments}", @@ -325,10 +327,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { std::slice::from_ref(slice) }; - let num_arguments = arguments.len(); - let type_and_qualifiers = if num_arguments == 1 { - let mut type_and_qualifiers = self - .infer_annotation_expression_impl(slice, PEP613Policy::Disallowed); + let type_and_qualifiers = if let [argument] = arguments { + let mut type_and_qualifiers = self.infer_annotation_expression_impl( + argument, + PEP613Policy::Disallowed, + ); type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR); type_and_qualifiers } else { @@ -341,6 +344,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let num_arguments = arguments.len(); builder.into_diagnostic(format_args!( "Type qualifier `InitVar` expected exactly 1 argument, \ got {num_arguments}",