@@ -196,6 +196,54 @@ public function testParseExistingAnnotations(): void
196196 self ::assertSame ($ expected , $ result );
197197 }
198198
199+ public function testParseExistingAnnotationsWithDuplicates (): void
200+ {
201+ $ method = new ReflectionMethod ($ this ->fixer , 'parseExistingAnnotations ' );
202+
203+ $ docBlock = "/** \n * @implements ArrayAccess<int, string> \n * @implements IteratorAggregate<int, string> \n */ " ;
204+ $ result = $ method ->invoke ($ this ->fixer , $ docBlock );
205+
206+ self ::assertIsArray ($ result );
207+ self ::assertArrayHasKey ('implements ' , $ result );
208+ self ::assertIsArray ($ result ['implements ' ]);
209+ self ::assertCount (2 , $ result ['implements ' ]);
210+ self ::assertSame ('ArrayAccess<int, string> ' , $ result ['implements ' ][0 ]);
211+ self ::assertSame ('IteratorAggregate<int, string> ' , $ result ['implements ' ][1 ]);
212+ }
213+
214+ public function testParseExistingAnnotationsWithMultipleDuplicateTags (): void
215+ {
216+ $ method = new ReflectionMethod ($ this ->fixer , 'parseExistingAnnotations ' );
217+
218+ $ docBlock = "/** \n * @author First Author \n * @license MIT \n * @author Second Author \n * @author Third Author \n */ " ;
219+ $ result = $ method ->invoke ($ this ->fixer , $ docBlock );
220+
221+ self ::assertIsArray ($ result );
222+ self ::assertArrayHasKey ('author ' , $ result );
223+ self ::assertIsArray ($ result ['author ' ]);
224+ self ::assertCount (3 , $ result ['author ' ]);
225+ self ::assertSame ('First Author ' , $ result ['author ' ][0 ]);
226+ self ::assertSame ('Second Author ' , $ result ['author ' ][1 ]);
227+ self ::assertSame ('Third Author ' , $ result ['author ' ][2 ]);
228+ self ::assertSame ('MIT ' , $ result ['license ' ]);
229+ }
230+
231+ public function testParseExistingAnnotationsWithDuplicateEmptyValues (): void
232+ {
233+ $ method = new ReflectionMethod ($ this ->fixer , 'parseExistingAnnotations ' );
234+
235+ $ docBlock = "/** \n * @internal \n * @api \n * @internal \n */ " ;
236+ $ result = $ method ->invoke ($ this ->fixer , $ docBlock );
237+
238+ self ::assertIsArray ($ result );
239+ self ::assertArrayHasKey ('internal ' , $ result );
240+ self ::assertIsArray ($ result ['internal ' ]);
241+ self ::assertCount (2 , $ result ['internal ' ]);
242+ self ::assertSame ('' , $ result ['internal ' ][0 ]);
243+ self ::assertSame ('' , $ result ['internal ' ][1 ]);
244+ self ::assertSame ('' , $ result ['api ' ]);
245+ }
246+
199247 public function testMergeAnnotations (): void
200248 {
201249 $ method = new ReflectionMethod ($ this ->fixer , 'mergeAnnotations ' );
@@ -994,4 +1042,70 @@ public function testSkipsAnonymousClassWithReadonlyModifier(): void
9941042 // Anonymous class with readonly modifier should NOT have DocBlock added
9951043 self ::assertSame ($ code , $ tokens ->generateCode ());
9961044 }
1045+
1046+ public function testFullRoundTripWithDuplicateImplementsAnnotations (): void
1047+ {
1048+ $ parseMethod = new ReflectionMethod ($ this ->fixer , 'parseExistingAnnotations ' );
1049+ $ buildMethod = new ReflectionMethod ($ this ->fixer , 'buildDocBlock ' );
1050+
1051+ // Original DocBlock with duplicate @implements
1052+ $ originalDocBlock = "/** \n * @author Konrad Michalik \n * @license GPL-3.0 \n * @implements ArrayAccess<int|null, IconImage> \n * @implements IteratorAggregate<int, IconImage> \n */ " ;
1053+
1054+ // Parse existing annotations
1055+ $ parsed = $ parseMethod ->invoke ($ this ->fixer , $ originalDocBlock );
1056+
1057+ // Verify parsing preserved both @implements
1058+ self ::assertIsArray ($ parsed ['implements ' ]);
1059+ self ::assertCount (2 , $ parsed ['implements ' ]);
1060+
1061+ // Build DocBlock from parsed annotations
1062+ $ rebuilt = $ buildMethod ->invoke ($ this ->fixer , $ parsed , '' );
1063+
1064+ // Verify both @implements are present in rebuilt DocBlock
1065+ self ::assertStringContainsString ('@author Konrad Michalik ' , $ rebuilt );
1066+ self ::assertStringContainsString ('@license GPL-3.0 ' , $ rebuilt );
1067+ self ::assertStringContainsString ('@implements ArrayAccess<int|null, IconImage> ' , $ rebuilt );
1068+ self ::assertStringContainsString ('@implements IteratorAggregate<int, IconImage> ' , $ rebuilt );
1069+ }
1070+
1071+ public function testMergeWithExistingDocBlockPreservesDuplicateImplements (): void
1072+ {
1073+ $ code = "<?php /** \n * @implements ArrayAccess<int, string> \n * @implements IteratorAggregate<int, string> \n */ class TestClass {} " ;
1074+ $ tokens = Tokens::fromCode ($ code );
1075+ $ annotations = ['author ' => 'John Doe ' ];
1076+
1077+ $ method = new ReflectionMethod ($ this ->fixer , 'mergeWithExistingDocBlock ' );
1078+
1079+ $ this ->fixer ->configure (['preserve_existing ' => true ]);
1080+ $ method ->invoke ($ this ->fixer , $ tokens , 1 , $ annotations , 'TestClass ' );
1081+
1082+ $ result = $ tokens ->generateCode ();
1083+
1084+ // Both @implements should be preserved
1085+ self ::assertStringContainsString ('@implements ArrayAccess<int, string> ' , $ result );
1086+ self ::assertStringContainsString ('@implements IteratorAggregate<int, string> ' , $ result );
1087+ self ::assertStringContainsString ('@author John Doe ' , $ result );
1088+ }
1089+
1090+ public function testApplyFixPreservesDuplicateImplementsInExistingDocBlock (): void
1091+ {
1092+ $ code = "<?php \n/** \n * @implements ArrayAccess<int, string> \n * @implements IteratorAggregate<int, string> \n */ \nclass TestClass {} " ;
1093+ $ tokens = Tokens::fromCode ($ code );
1094+ $ file = new SplFileInfo (__FILE__ );
1095+
1096+ $ method = new ReflectionMethod ($ this ->fixer , 'applyFix ' );
1097+
1098+ $ this ->fixer ->configure ([
1099+ 'annotations ' => ['author ' => 'John Doe ' ],
1100+ 'preserve_existing ' => true ,
1101+ ]);
1102+ $ method ->invoke ($ this ->fixer , $ file , $ tokens );
1103+
1104+ $ result = $ tokens ->generateCode ();
1105+
1106+ // Verify both @implements annotations are preserved after merge
1107+ self ::assertStringContainsString ('@implements ArrayAccess<int, string> ' , $ result );
1108+ self ::assertStringContainsString ('@implements IteratorAggregate<int, string> ' , $ result );
1109+ self ::assertStringContainsString ('@author John Doe ' , $ result );
1110+ }
9971111}
0 commit comments