33namespace PHPStan \Rules \Properties ;
44
55use PhpParser \Node ;
6+ use PhpParser \Node \Attribute ;
67use PHPStan \Analyser \Scope ;
78use PHPStan \DependencyInjection \AutowiredParameter ;
89use PHPStan \DependencyInjection \RegisteredRule ;
@@ -30,6 +31,8 @@ public function __construct(
3031 private bool $ checkPhpDocMethodSignatures ,
3132 #[AutowiredParameter(ref: '%reportMaybesInPropertyPhpDocTypes% ' )]
3233 private bool $ reportMaybes ,
34+ #[AutowiredParameter]
35+ private bool $ checkMissingOverridePropertyAttribute ,
3336 )
3437 {
3538 }
@@ -44,10 +47,63 @@ public function processNode(Node $node, Scope $scope): array
4447 $ classReflection = $ node ->getClassReflection ();
4548 $ prototype = $ this ->findPrototype ($ classReflection , $ node ->getName ());
4649 if ($ prototype === null ) {
50+ if (
51+ $ this ->phpVersion ->supportsOverrideAttribute ()
52+ && $ this ->hasOverrideAttribute ($ node ->getAttrGroups ())
53+ ) {
54+ $ originalNode = $ node ->getOriginalNode ();
55+ if ($ originalNode instanceof Node \Stmt \Property) {
56+ /** @var Node\Stmt\Property $originalNode */
57+ $ originalNode = $ originalNode ->getAttribute ('originalPropertyStmt ' );
58+ }
59+
60+ return [
61+ RuleErrorBuilder::message (sprintf (
62+ 'Property %s::$%s has #[\Override] attribute but does not override any property. ' ,
63+ $ node ->getClassReflection ()->getDisplayName (),
64+ $ node ->getName (),
65+ ))
66+ ->nonIgnorable ()
67+ ->identifier ('property.override ' )
68+ ->fixNode ($ originalNode , function ($ property ) {
69+ $ property ->attrGroups = $ this ->filterOverrideAttribute ($ property ->attrGroups );
70+ return $ property ;
71+ })
72+ ->build (),
73+ ];
74+ }
4775 return [];
4876 }
4977
5078 $ errors = [];
79+ if (
80+ $ this ->phpVersion ->supportsOverrideAttributeOnProperty ()
81+ && $ this ->checkMissingOverridePropertyAttribute
82+ && !$ scope ->isInTrait ()
83+ && !$ this ->hasOverrideAttribute ($ node ->getAttrGroups ())
84+ ) {
85+ $ originalNode = $ node ->getOriginalNode ();
86+ if ($ originalNode instanceof Node \Stmt \Property) {
87+ /** @var Node\Stmt\Property $originalNode */
88+ $ originalNode = $ originalNode ->getAttribute ('originalPropertyStmt ' );
89+ }
90+ $ errors [] = RuleErrorBuilder::message (sprintf (
91+ 'Property %s::$%s overrides property %s::$%s but is missing the #[\Override] attribute. ' ,
92+ $ node ->getClassReflection ()->getDisplayName (),
93+ $ node ->getName (),
94+ $ prototype ->getDeclaringClass ()->getDisplayName (),
95+ $ prototype ->getName (),
96+ ))
97+ ->identifier ('property.missingOverride ' )
98+ ->fixNode ($ originalNode , static function ($ property ) {
99+ $ property ->attrGroups [] = new Node \AttributeGroup ([
100+ new Attribute (new Node \Name \FullyQualified ('Override ' )),
101+ ]);
102+
103+ return $ property ;
104+ })
105+ ->build ();
106+ }
51107 if ($ prototype ->isStatic ()) {
52108 if (!$ node ->isStatic ()) {
53109 $ errors [] = RuleErrorBuilder::message (sprintf (
@@ -356,4 +412,44 @@ private function findPrototypeInInterfaces(ClassReflection $classReflection, str
356412 return null ;
357413 }
358414
415+ /**
416+ * @param Node\AttributeGroup[] $attrGroups
417+ * @return Node\AttributeGroup[]
418+ */
419+ private function filterOverrideAttribute (array $ attrGroups ): array
420+ {
421+ foreach ($ attrGroups as $ i => $ attrGroup ) {
422+ foreach ($ attrGroup ->attrs as $ j => $ attr ) {
423+ if ($ attr ->name ->toLowerString () !== 'override ' ) {
424+ continue ;
425+ }
426+
427+ unset($ attrGroup ->attrs [$ j ]);
428+ if (count ($ attrGroup ->attrs ) !== 0 ) {
429+ continue ;
430+ }
431+
432+ unset($ attrGroups [$ i ]);
433+ }
434+ }
435+
436+ return $ attrGroups ;
437+ }
438+
439+ /**
440+ * @param Node\AttributeGroup[] $attrGroups
441+ */
442+ private function hasOverrideAttribute (array $ attrGroups ): bool
443+ {
444+ foreach ($ attrGroups as $ attrGroup ) {
445+ foreach ($ attrGroup ->attrs as $ attr ) {
446+ if ($ attr ->name ->toLowerString () === 'override ' ) {
447+ return true ;
448+ }
449+ }
450+ }
451+
452+ return false ;
453+ }
454+
359455}
0 commit comments