diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index 6b7e30e146..840f9cb237 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -718,6 +718,18 @@ impl ClassField { matches!(&self.0, ClassFieldInner::InstanceAttribute { .. }) } + /// Returns true if this field can have the `@override` decorator applied to it. + pub fn can_have_override_decorator(&self) -> bool { + match &self.0 { + ClassFieldInner::InstanceAttribute { .. } + | ClassFieldInner::NestedClass { .. } + | ClassFieldInner::ClassAttribute { .. } => false, + ClassFieldInner::Property { .. } + | ClassFieldInner::Descriptor { .. } + | ClassFieldInner::Method { .. } => true, + } + } + pub fn ty(&self) -> Type { match &self.0 { ClassFieldInner::Property { ty, .. } => ty.clone(), @@ -862,7 +874,7 @@ impl ClassField { } } - fn is_class_var(&self) -> bool { + pub fn is_class_var(&self) -> bool { match &self.0 { ClassFieldInner::Property { .. } => false, ClassFieldInner::Descriptor { annotation, .. } => { @@ -3067,7 +3079,11 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { // Check for missing @override decorator when overriding a parent attribute. // This error is emitted when a method overrides a parent but doesn't have @override. // Since this error has Severity::Ignore by default, it won't be shown unless enabled. - if !is_override && parent_attr_found && !parent_has_any { + if !is_override + && parent_attr_found + && !parent_has_any + && class_field.can_have_override_decorator() + { self.error( errors, range, diff --git a/pyrefly/lib/test/class_overrides.rs b/pyrefly/lib/test/class_overrides.rs index fb78e4c171..1fc53e16f1 100644 --- a/pyrefly/lib/test/class_overrides.rs +++ b/pyrefly/lib/test/class_overrides.rs @@ -912,7 +912,7 @@ def f(d: D): // Tests for MissingOverrideDecorator (strict override enforcement) testcase!( - test_missing_override_decorator, + test_missing_override_decorator_method, TestEnv::new().enable_missing_override_decorator_error(), r#" class Base: @@ -1000,7 +1000,7 @@ class Base: x: int class Derived(Base): - x: int # E: Class member `Derived.x` overrides a member in a parent class but is missing an `@override` decorator + x: int "#, ); @@ -1043,3 +1043,45 @@ class Derived(Base): def __str__(self) -> str: ... # E: overrides a member in a parent class but is missing an `@override` decorator "#, ); + +testcase!( + test_missing_override_decorator_instance_attribute, + TestEnv::new().enable_missing_override_decorator_error(), + r#" +class A: + def __init__(self): + self.a = 1 + +class B(A): + def __init__(self): + self.a = 2 # OK - instance attribute, not a method override + "#, +); + +testcase!( + test_missing_override_decorator_nested_class, + TestEnv::new().enable_missing_override_decorator_error(), + r#" +# Nested classes cannot have the @override decorator applied to them. +class A: + class C: pass + +class B(A): + class C(A.C): pass # OK - nested class, @override cannot be applied + "#, +); + +testcase!( + test_missing_override_decorator_classvar, + TestEnv::new().enable_missing_override_decorator_error(), + r#" +from typing import ClassVar + +# ClassVars cannot have the @override decorator applied to them. +class A: + x: ClassVar[int] + +class B(A): + x: ClassVar[int] # OK - ClassVar, @override cannot be applied + "#, +);