Skip to content

Commit 983594e

Browse files
authored
Merge pull request #20 from jackd248/attribute-fix
fix: enhance DocBlock handling for attributes in DocBlockHeaderFixer
2 parents 639fd98 + 4be76f5 commit 983594e

File tree

2 files changed

+130
-2
lines changed

2 files changed

+130
-2
lines changed

src/Rules/DocBlockHeaderFixer.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,19 +211,38 @@ private function getStructureName(Tokens $tokens, int $structureIndex): string
211211

212212
private function findExistingDocBlock(Tokens $tokens, int $structureIndex): ?int
213213
{
214+
$insideAttribute = false;
215+
214216
for ($i = $structureIndex - 1; $i >= 0; --$i) {
215217
$token = $tokens[$i];
216218

217219
if ($token->isWhitespace()) {
218220
continue;
219221
}
220222

223+
// When going backwards, ']' marks the end of an attribute (we enter it)
224+
if (']' === $token->getContent()) {
225+
$insideAttribute = true;
226+
continue;
227+
}
228+
229+
// T_ATTRIBUTE '#[' marks the start of an attribute (we exit it when going backwards)
230+
if ($token->isGivenKind(\T_ATTRIBUTE)) {
231+
$insideAttribute = false;
232+
continue;
233+
}
234+
235+
// Skip everything inside attributes
236+
if ($insideAttribute) {
237+
continue;
238+
}
239+
221240
if ($token->isGivenKind(\T_DOC_COMMENT)) {
222241
return $i;
223242
}
224243

225244
// If we hit any other meaningful token (except modifiers), stop looking
226-
if (!$token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_READONLY, \T_ATTRIBUTE])) {
245+
if (!$token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_READONLY])) {
227246
break;
228247
}
229248
}
@@ -306,6 +325,7 @@ private function insertNewDocBlock(Tokens $tokens, int $structureIndex, array $a
306325
private function findInsertPosition(Tokens $tokens, int $structureIndex): int
307326
{
308327
$insertIndex = $structureIndex;
328+
$insideAttribute = false;
309329

310330
// Look backwards for attributes, final, abstract keywords
311331
for ($i = $structureIndex - 1; $i >= 0; --$i) {
@@ -315,7 +335,25 @@ private function findInsertPosition(Tokens $tokens, int $structureIndex): int
315335
continue;
316336
}
317337

318-
if ($token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_READONLY, \T_ATTRIBUTE])) {
338+
// When going backwards, ']' marks the end of an attribute (we enter it)
339+
if (']' === $token->getContent()) {
340+
$insideAttribute = true;
341+
continue;
342+
}
343+
344+
// T_ATTRIBUTE '#[' marks the start of an attribute (we exit it when going backwards)
345+
if ($token->isGivenKind(\T_ATTRIBUTE)) {
346+
$insideAttribute = false;
347+
$insertIndex = $i;
348+
continue;
349+
}
350+
351+
// Skip everything inside attributes
352+
if ($insideAttribute) {
353+
continue;
354+
}
355+
356+
if ($token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_READONLY])) {
319357
$insertIndex = $i;
320358
continue;
321359
}

tests/src/Rules/DocBlockHeaderFixerTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,96 @@ public function testFindInsertPositionWithAttribute(): void
534534
self::assertSame(1, $result);
535535
}
536536

537+
public function testFindInsertPositionWithAttributeWithParameters(): void
538+
{
539+
$code = "<?php #[AsEventListener(identifier: 'my-identifier')] class Foo {}";
540+
$tokens = Tokens::fromCode($code);
541+
542+
$method = new ReflectionMethod($this->fixer, 'findInsertPosition');
543+
544+
// Find the class token index
545+
$classIndex = null;
546+
for ($i = 0; $i < $tokens->count(); ++$i) {
547+
if ($tokens[$i]->isGivenKind(\T_CLASS)) {
548+
$classIndex = $i;
549+
break;
550+
}
551+
}
552+
553+
$result = $method->invoke($this->fixer, $tokens, $classIndex);
554+
555+
// Should return index 1 which is the #[ token
556+
self::assertSame(1, $result);
557+
}
558+
559+
public function testApplyFixAddsDocBlockBeforeAttribute(): void
560+
{
561+
$code = "<?php\n#[AsEventListener(identifier: 'my-identifier')]\nclass Foo {}";
562+
$tokens = Tokens::fromCode($code);
563+
$file = new SplFileInfo(__FILE__);
564+
565+
$method = new ReflectionMethod($this->fixer, 'applyFix');
566+
567+
$this->fixer->configure([
568+
'annotations' => ['author' => 'John Doe'],
569+
'separate' => 'none',
570+
'ensure_spacing' => false,
571+
]);
572+
$method->invoke($this->fixer, $file, $tokens);
573+
574+
$result = $tokens->generateCode();
575+
576+
// DocBlock should be BEFORE the attribute, not between attribute and class
577+
self::assertMatchesRegularExpression('/@author John Doe.*#\[AsEventListener/s', $result);
578+
}
579+
580+
public function testFindInsertPositionWithMultipleAttributes(): void
581+
{
582+
$code = "<?php\n#[Attribute1]\n#[Attribute2(param: 'value')]\nclass Foo {}";
583+
$tokens = Tokens::fromCode($code);
584+
585+
$method = new ReflectionMethod($this->fixer, 'findInsertPosition');
586+
587+
// Find the class token index
588+
$classIndex = null;
589+
for ($i = 0; $i < $tokens->count(); ++$i) {
590+
if ($tokens[$i]->isGivenKind(\T_CLASS)) {
591+
$classIndex = $i;
592+
break;
593+
}
594+
}
595+
596+
$result = $method->invoke($this->fixer, $tokens, $classIndex);
597+
598+
// Should return the index of the first attribute (#[Attribute1])
599+
// Token structure: [0]=T_OPEN_TAG, [1]=T_WHITESPACE, [2]=T_ATTRIBUTE(#[), ...
600+
// The first #[Attribute1] token is at index 2
601+
self::assertTrue($tokens[$result]->isGivenKind(\T_ATTRIBUTE));
602+
}
603+
604+
public function testFindExistingDocBlockWithAttributesBetween(): void
605+
{
606+
$code = "<?php\n/**\n * @author John Doe\n */\n#[SomeAttribute(param: 'value')]\nclass Foo {}";
607+
$tokens = Tokens::fromCode($code);
608+
609+
$method = new ReflectionMethod($this->fixer, 'findExistingDocBlock');
610+
611+
// Find the class token index
612+
$classIndex = null;
613+
for ($i = 0; $i < $tokens->count(); ++$i) {
614+
if ($tokens[$i]->isGivenKind(\T_CLASS)) {
615+
$classIndex = $i;
616+
break;
617+
}
618+
}
619+
620+
$result = $method->invoke($this->fixer, $tokens, $classIndex);
621+
622+
// Should find the DocBlock even with attribute in between
623+
self::assertNotNull($result);
624+
self::assertStringContainsString('@author John Doe', $tokens[$result]->getContent());
625+
}
626+
537627
#[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.2')]
538628
public function testFindInsertPositionWithReadonlyModifier(): void
539629
{

0 commit comments

Comments
 (0)