diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md new file mode 100644 index 0000000000..83db02c2be --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md @@ -0,0 +1,149 @@ +# Attribute assignment + + + +This test suite demonstrates various kinds of diagnostics that can be emitted in a +`obj.attr = value` assignment. + +## Instance attributes with class-level defaults + +These can be set on instances and on class objects. + +```py +class C: + attr: int = 0 + +instance = C() +instance.attr = 1 # fine +instance.attr = "wrong" # error: [invalid-assignment] + +C.attr = 1 # fine +C.attr = "wrong" # error: [invalid-assignment] +``` + +## Pure instance attributes + +These can only be set on instances. When trying to set them on class objects, we generate a useful +diagnostic that mentions that the attribute is only available on instances. + +```py +class C: + def __init__(self): + self.attr: int = 0 + +instance = C() +instance.attr = 1 # fine +instance.attr = "wrong" # error: [invalid-assignment] + +C.attr = 1 # error: [invalid-attribute-access] +``` + +## `ClassVar`s + +These can only be set on class objects. When trying to set them on instances, we generate a useful +diagnostic that mentions that the attribute is only available on class objects. + +```py +from typing import ClassVar + +class C: + attr: ClassVar[int] = 0 + +C.attr = 1 # fine +C.attr = "wrong" # error: [invalid-assignment] + +instance = C() +instance.attr = 1 # error: [invalid-attribute-access] +``` + +## Unknown attributes + +When trying to set an attribute that is not defined, we also emit errors: + +```py +class C: ... + +C.non_existent = 1 # error: [unresolved-attribute] + +instance = C() +instance.non_existent = 1 # error: [unresolved-attribute] +``` + +## Possibly-unbound attributes + +When trying to set an attribute that is not defined in all branches, we emit errors: + +```py +def _(flag: bool) -> None: + class C: + if flag: + attr: int = 0 + + C.attr = 1 # error: [possibly-unbound-attribute] + + instance = C() + instance.attr = 1 # error: [possibly-unbound-attribute] +``` + +## Data descriptors + +When assigning to a data descriptor attribute, we implicitly call the descriptor's `__set__` method. +This can lead to various kinds of diagnostics. + +### Invalid argument type + +```py +class Descriptor: + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + attr: Descriptor = Descriptor() + +instance = C() +instance.attr = 1 # fine + +# TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) +instance.attr = "wrong" # error: [invalid-assignment] +``` + +### Invalid `__set__` method signature + +```py +class WrongDescriptor: + def __set__(self, instance: object, value: int, extra: int) -> None: + pass + +class C: + attr: WrongDescriptor = WrongDescriptor() + +instance = C() + +# TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) +instance.attr = 1 # error: [invalid-assignment] +``` + +## Setting attributes on union types + +```py +def _(flag: bool) -> None: + if flag: + class C1: + attr: int = 0 + + else: + class C1: + attr: str = "" + + # TODO: The error message here could be improved to explain why the assignment fails. + C1.attr = 1 # error: [invalid-assignment] + + class C2: + if flag: + attr: int = 0 + else: + attr: str = "" + + # TODO: This should be an error + C2.attr = 1 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap new file mode 100644 index 0000000000..2914deba0c --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap @@ -0,0 +1,39 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid `__set__` method signature +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class WrongDescriptor: + 2 | def __set__(self, instance: object, value: int, extra: int) -> None: + 3 | pass + 4 | + 5 | class C: + 6 | attr: WrongDescriptor = WrongDescriptor() + 7 | + 8 | instance = C() + 9 | +10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) +11 | instance.attr = 1 # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error: lint:invalid-assignment + --> /src/mdtest_snippet.py:11:1 + | +10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) +11 | instance.attr = 1 # error: [invalid-assignment] + | ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap new file mode 100644 index 0000000000..90b4a81494 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap @@ -0,0 +1,40 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid argument type +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Descriptor: + 2 | def __set__(self, instance: object, value: int) -> None: + 3 | pass + 4 | + 5 | class C: + 6 | attr: Descriptor = Descriptor() + 7 | + 8 | instance = C() + 9 | instance.attr = 1 # fine +10 | +11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) +12 | instance.attr = "wrong" # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error: lint:invalid-assignment + --> /src/mdtest_snippet.py:12:1 + | +11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) +12 | instance.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap new file mode 100644 index 0000000000..eb06b09308 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap @@ -0,0 +1,51 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Instance attributes with class-level defaults +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: +2 | attr: int = 0 +3 | +4 | instance = C() +5 | instance.attr = 1 # fine +6 | instance.attr = "wrong" # error: [invalid-assignment] +7 | +8 | C.attr = 1 # fine +9 | C.attr = "wrong" # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error: lint:invalid-assignment + --> /src/mdtest_snippet.py:6:1 + | +4 | instance = C() +5 | instance.attr = 1 # fine +6 | instance.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` +7 | +8 | C.attr = 1 # fine + | + +``` + +``` +error: lint:invalid-assignment + --> /src/mdtest_snippet.py:9:1 + | +8 | C.attr = 1 # fine +9 | C.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap new file mode 100644 index 0000000000..9e2de15098 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap @@ -0,0 +1,51 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Possibly-unbound attributes +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def _(flag: bool) -> None: +2 | class C: +3 | if flag: +4 | attr: int = 0 +5 | +6 | C.attr = 1 # error: [possibly-unbound-attribute] +7 | +8 | instance = C() +9 | instance.attr = 1 # error: [possibly-unbound-attribute] +``` + +# Diagnostics + +``` +warning: lint:possibly-unbound-attribute + --> /src/mdtest_snippet.py:6:5 + | +4 | attr: int = 0 +5 | +6 | C.attr = 1 # error: [possibly-unbound-attribute] + | ------ Attribute `attr` on type `Literal[C]` is possibly unbound +7 | +8 | instance = C() + | + +``` + +``` +warning: lint:possibly-unbound-attribute + --> /src/mdtest_snippet.py:9:5 + | +8 | instance = C() +9 | instance.attr = 1 # error: [possibly-unbound-attribute] + | ------------- Attribute `attr` on type `C` is possibly unbound + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap new file mode 100644 index 0000000000..78fe6d1da5 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap @@ -0,0 +1,52 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Pure instance attributes +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: +2 | def __init__(self): +3 | self.attr: int = 0 +4 | +5 | instance = C() +6 | instance.attr = 1 # fine +7 | instance.attr = "wrong" # error: [invalid-assignment] +8 | +9 | C.attr = 1 # error: [invalid-attribute-access] +``` + +# Diagnostics + +``` +error: lint:invalid-assignment + --> /src/mdtest_snippet.py:7:1 + | +5 | instance = C() +6 | instance.attr = 1 # fine +7 | instance.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` +8 | +9 | C.attr = 1 # error: [invalid-attribute-access] + | + +``` + +``` +error: lint:invalid-attribute-access + --> /src/mdtest_snippet.py:9:1 + | +7 | instance.attr = "wrong" # error: [invalid-assignment] +8 | +9 | C.attr = 1 # error: [invalid-attribute-access] + | ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap new file mode 100644 index 0000000000..ec479c5896 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap @@ -0,0 +1,50 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Setting attributes on union types +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def _(flag: bool) -> None: + 2 | if flag: + 3 | class C1: + 4 | attr: int = 0 + 5 | + 6 | else: + 7 | class C1: + 8 | attr: str = "" + 9 | +10 | # TODO: The error message here could be improved to explain why the assignment fails. +11 | C1.attr = 1 # error: [invalid-assignment] +12 | +13 | class C2: +14 | if flag: +15 | attr: int = 0 +16 | else: +17 | attr: str = "" +18 | +19 | # TODO: This should be an error +20 | C2.attr = 1 +``` + +# Diagnostics + +``` +error: lint:invalid-assignment + --> /src/mdtest_snippet.py:11:5 + | +10 | # TODO: The error message here could be improved to explain why the assignment fails. +11 | C1.attr = 1 # error: [invalid-assignment] + | ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]` +12 | +13 | class C2: + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap new file mode 100644 index 0000000000..53ef654680 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap @@ -0,0 +1,48 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Unknown attributes +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: ... +2 | +3 | C.non_existent = 1 # error: [unresolved-attribute] +4 | +5 | instance = C() +6 | instance.non_existent = 1 # error: [unresolved-attribute] +``` + +# Diagnostics + +``` +error: lint:unresolved-attribute + --> /src/mdtest_snippet.py:3:1 + | +1 | class C: ... +2 | +3 | C.non_existent = 1 # error: [unresolved-attribute] + | ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`. +4 | +5 | instance = C() + | + +``` + +``` +error: lint:unresolved-attribute + --> /src/mdtest_snippet.py:6:1 + | +5 | instance = C() +6 | instance.non_existent = 1 # error: [unresolved-attribute] + | ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`. + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap new file mode 100644 index 0000000000..59389d2036 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap @@ -0,0 +1,51 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - `ClassVar`s +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import ClassVar + 2 | + 3 | class C: + 4 | attr: ClassVar[int] = 0 + 5 | + 6 | C.attr = 1 # fine + 7 | C.attr = "wrong" # error: [invalid-assignment] + 8 | + 9 | instance = C() +10 | instance.attr = 1 # error: [invalid-attribute-access] +``` + +# Diagnostics + +``` +error: lint:invalid-assignment + --> /src/mdtest_snippet.py:7:1 + | +6 | C.attr = 1 # fine +7 | C.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` +8 | +9 | instance = C() + | + +``` + +``` +error: lint:invalid-attribute-access + --> /src/mdtest_snippet.py:10:1 + | + 9 | instance = C() +10 | instance.attr = 1 # error: [invalid-attribute-access] + | ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C` + | + +```