@@ -66,11 +66,17 @@ final class AddOverrideAttributeToOverriddenMethodsRector extends AbstractRector
6666 * @var string
6767 */
6868 public const ALLOW_OVERRIDE_EMPTY_METHOD = 'allow_override_empty_method ' ;
69+ /**
70+ * @api
71+ * @var string
72+ */
73+ public const ADD_TO_INTERFACE_METHODS = 'add_to_interface_methods ' ;
6974 /**
7075 * @var string
7176 */
7277 private const OVERRIDE_CLASS = 'Override ' ;
7378 private bool $ allowOverrideEmptyMethod = \false;
79+ private bool $ addToInterfaceMethods = \false;
7480 private bool $ hasChanged = \false;
7581 public function __construct (ReflectionProvider $ reflectionProvider , ClassAnalyzer $ classAnalyzer , PhpAttributeAnalyzer $ phpAttributeAnalyzer , AstResolver $ astResolver , ValueResolver $ valueResolver , ParentClassAnalyzer $ parentClassAnalyzer )
7682 {
@@ -118,7 +124,36 @@ public function foo()
118124 }
119125}
120126CODE_SAMPLE
121- , [self ::ALLOW_OVERRIDE_EMPTY_METHOD => \false])]);
127+ , [self ::ALLOW_OVERRIDE_EMPTY_METHOD => \false]), new ConfiguredCodeSample (<<<'CODE_SAMPLE'
128+ interface ParentInterface
129+ {
130+ public function foo();
131+ }
132+
133+ final class ChildClass implements ParentInterface
134+ {
135+ public function foo()
136+ {
137+ echo 'implements interface';
138+ }
139+ }
140+ CODE_SAMPLE
141+ , <<<'CODE_SAMPLE'
142+ interface ParentInterface
143+ {
144+ public function foo();
145+ }
146+
147+ final class ChildClass implements ParentInterface
148+ {
149+ #[\Override]
150+ public function foo()
151+ {
152+ echo 'implements interface';
153+ }
154+ }
155+ CODE_SAMPLE
156+ , [self ::ADD_TO_INTERFACE_METHODS => \true])]);
122157 }
123158 /**
124159 * @return array<class-string<Node>>
@@ -133,6 +168,7 @@ public function getNodeTypes(): array
133168 public function configure (array $ configuration ): void
134169 {
135170 $ this ->allowOverrideEmptyMethod = $ configuration [self ::ALLOW_OVERRIDE_EMPTY_METHOD ] ?? \false;
171+ $ this ->addToInterfaceMethods = $ configuration [self ::ADD_TO_INTERFACE_METHODS ] ?? \false;
136172 }
137173 /**
138174 * @param Class_ $node
@@ -143,8 +179,7 @@ public function refactor(Node $node): ?Node
143179 if ($ this ->classAnalyzer ->isAnonymousClass ($ node )) {
144180 return null ;
145181 }
146- // skip if no parents, nor traits, nor strinables are involved
147- if ($ node ->extends === null && $ node ->getTraitUses () === [] && !$ this ->implementsStringable ($ node )) {
182+ if ($ this ->shouldSkipNode ($ node )) {
148183 return null ;
149184 }
150185 $ className = (string ) $ this ->getName ($ node );
@@ -153,14 +188,14 @@ public function refactor(Node $node): ?Node
153188 }
154189 $ classReflection = $ this ->reflectionProvider ->getClass ($ className );
155190 $ parentClassReflections = $ classReflection ->getParents ();
191+ // interfaces are added for Stringable
192+ if ($ this ->addToInterfaceMethods || $ this ->allowOverrideEmptyMethod ) {
193+ $ parentClassReflections = array_merge ($ parentClassReflections , $ classReflection ->getInterfaces ());
194+ }
156195 if ($ this ->allowOverrideEmptyMethod ) {
157- $ parentClassReflections = array_merge (
158- $ parentClassReflections ,
159- $ classReflection ->getInterfaces (),
160- // place on last to ensure verify method exists on parent early
161- // for non abstract method from trait
162- $ classReflection ->getTraits ()
163- );
196+ // place on last to ensure verify method exists on parent early
197+ // for non abstract method from trait
198+ $ parentClassReflections = array_merge ($ parentClassReflections , $ classReflection ->getTraits ());
164199 }
165200 if ($ parentClassReflections === []) {
166201 return null ;
@@ -233,9 +268,14 @@ private function shouldSkipClassMethod(ClassMethod $classMethod): bool
233268 }
234269 private function shouldSkipParentClassMethod (ClassReflection $ parentClassReflection , ClassMethod $ classMethod ): bool
235270 {
236- if ($ this ->allowOverrideEmptyMethod && $ parentClassReflection ->isBuiltIn ()) {
271+ // special case for Stringable interface
272+ if ($ this ->allowOverrideEmptyMethod && $ parentClassReflection ->getName () === 'Stringable ' ) {
237273 return \false;
238274 }
275+ // if the method is on interface, skip based on the config flag
276+ if ($ parentClassReflection ->isInterface ()) {
277+ return !$ this ->addToInterfaceMethods ;
278+ }
239279 // parse parent method, if it has some contents or not
240280 $ parentClass = $ this ->astResolver ->resolveClassFromClassReflection ($ parentClassReflection );
241281 if (!$ parentClass instanceof ClassLike) {
@@ -289,13 +329,26 @@ private function resolveClassMethodFromTraitUse(ClassLike $classLike, string $me
289329 }
290330 return null ;
291331 }
292- private function implementsStringable (Class_ $ class ): bool
332+ // early return for the class if it does not extend anything
333+ private function shouldSkipNode (Class_ $ class ): bool
293334 {
294- foreach ($ class ->implements as $ implement ) {
295- if ($ this ->isName ($ implement , 'Stringable ' )) {
296- return \true;
335+ if ($ class ->extends !== null ) {
336+ return \false;
337+ }
338+ if ($ class ->getTraitUses () !== []) {
339+ return \false;
340+ }
341+ if ($ this ->addToInterfaceMethods && $ class ->implements !== []) {
342+ return \false;
343+ }
344+ // add override to Stringable if flag is set
345+ if ($ this ->allowOverrideEmptyMethod ) {
346+ foreach ($ class ->implements as $ implement ) {
347+ if ($ this ->isName ($ implement , 'Stringable ' )) {
348+ return \false;
349+ }
297350 }
298351 }
299- return \false ;
352+ return \true ;
300353 }
301354}
0 commit comments