Skip to content

Commit

Permalink
feat: Introduce AttributeAnalysis (#7909)
Browse files Browse the repository at this point in the history
Co-authored-by: Greg Korba <greg@codito.dev>

The concept implemented in this PR was proposed and discussed here:
#7395 (comment)
#7395 (comment)
#7395 (comment)
  • Loading branch information
HypeMC committed Apr 5, 2024
1 parent 0e69940 commit 7eb5ec3
Show file tree
Hide file tree
Showing 4 changed files with 517 additions and 0 deletions.
73 changes: 73 additions & 0 deletions src/Tokenizer/Analyzer/Analysis/AttributeAnalysis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Tokenizer\Analyzer\Analysis;

/**
* @internal
*
* @phpstan-type _AttributeItems list<array{start: int, end: int, name: string}>
*/
final class AttributeAnalysis
{
private int $startIndex;
private int $endIndex;
private int $openingBracketIndex;
private int $closingBracketIndex;

/**
* @var _AttributeItems
*/
private array $attributes;

/**
* @param _AttributeItems $attributes
*/
public function __construct(int $startIndex, int $endIndex, int $openingBracketIndex, int $closingBracketIndex, array $attributes)
{
$this->startIndex = $startIndex;
$this->endIndex = $endIndex;
$this->openingBracketIndex = $openingBracketIndex;
$this->closingBracketIndex = $closingBracketIndex;
$this->attributes = $attributes;
}

public function getStartIndex(): int
{
return $this->startIndex;
}

public function getEndIndex(): int
{
return $this->endIndex;
}

public function getOpeningBracketIndex(): int
{
return $this->openingBracketIndex;
}

public function getClosingBracketIndex(): int
{
return $this->closingBracketIndex;
}

/**
* @return _AttributeItems
*/
public function getAttributes(): array
{
return $this->attributes;
}
}
109 changes: 109 additions & 0 deletions src/Tokenizer/Analyzer/AttributeAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@

namespace PhpCsFixer\Tokenizer\Analyzer;

use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;

/**
* @internal
*
* @phpstan-import-type _AttributeItems from AttributeAnalysis
*/
final class AttributeAnalyzer
{
Expand Down Expand Up @@ -67,4 +71,109 @@ public static function isAttribute(Tokens $tokens, int $index): bool

return 0 === $count;
}

/**
* Find all consecutive elements that start with #[ and end with ] and the attributes inside.
*
* @return list<AttributeAnalysis>
*/
public static function collect(Tokens $tokens, int $index): array
{
if (!$tokens[$index]->isGivenKind(T_ATTRIBUTE)) {
throw new \InvalidArgumentException('Given index must point to an attribute.');
}

// Rewind to first attribute in group
while ($tokens[$prevIndex = $tokens->getPrevMeaningfulToken($index)]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
$index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $prevIndex);
}

/** @var list<AttributeAnalysis> $elements */
$elements = [];

$openingIndex = $index;
do {
$elements[] = $element = self::collectOne($tokens, $openingIndex);
$openingIndex = $tokens->getNextMeaningfulToken($element->getEndIndex());
} while ($tokens[$openingIndex]->isGivenKind(T_ATTRIBUTE));

return $elements;
}

/**
* Find one element that starts with #[ and ends with ] and the attributes inside.
*/
public static function collectOne(Tokens $tokens, int $index): AttributeAnalysis
{
if (!$tokens[$index]->isGivenKind(T_ATTRIBUTE)) {
throw new \InvalidArgumentException('Given index must point to an attribute.');
}

$startIndex = $index;
if ($tokens[$prevIndex = $tokens->getPrevMeaningfulToken($index)]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
// Include comments/PHPDoc if they are present
$startIndex = $tokens->getNextNonWhitespace($prevIndex);
}

$closingIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
$endIndex = $tokens->getNextNonWhitespace($closingIndex);

return new AttributeAnalysis(
$startIndex,
$endIndex - 1,
$index,
$closingIndex,
self::collectAttributes($tokens, $index, $closingIndex),
);
}

/**
* @return _AttributeItems
*/
private static function collectAttributes(Tokens $tokens, int $index, int $closingIndex): array
{
/** @var _AttributeItems $elements */
$elements = [];

do {
$attributeStartIndex = $index + 1;

$nameStartIndex = $tokens->getNextTokenOfKind($index, [[T_STRING], [T_NS_SEPARATOR]]);
$index = $tokens->getNextTokenOfKind($attributeStartIndex, ['(', ',', [CT::T_ATTRIBUTE_CLOSE]]);
$attributeName = $tokens->generatePartialCode($nameStartIndex, $tokens->getPrevMeaningfulToken($index));

// Find closing parentheses, we need to do this in case there's a comma inside the parentheses
if ($tokens[$index]->equals('(')) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$index = $tokens->getNextTokenOfKind($index, [',', [CT::T_ATTRIBUTE_CLOSE]]);
}

$elements[] = [
'start' => $attributeStartIndex,
'end' => $index - 1,
'name' => $attributeName,
];

$nextIndex = $index;

// In case there's a comma right before T_ATTRIBUTE_CLOSE
if ($nextIndex < $closingIndex) {
$nextIndex = $tokens->getNextMeaningfulToken($index);
}
} while ($nextIndex < $closingIndex);

// End last element at newline if it exists and there's no trailing comma
--$index;
while ($tokens[$index]->isWhitespace()) {
if (Preg::match('/\R/', $tokens[$index]->getContent())) {
$lastElementKey = array_key_last($elements);
$elements[$lastElementKey]['end'] = $index - 1;

break;
}
--$index;
}

return $elements;
}
}
41 changes: 41 additions & 0 deletions tests/Tokenizer/Analyzer/Analysis/AttributeAnalysisTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Tests\Tokenizer\Analyzer\Analysis;

use PhpCsFixer\Tests\TestCase;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;

/**
* @covers \PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis
*
* @internal
*/
final class AttributeAnalysisTest extends TestCase
{
public function testAttribute(): void
{
$attributes = [
['start' => 3, 'end' => 12, 'name' => 'AB\\Baz'],
['start' => 14, 'end' => 32, 'name' => '\\A\\B\\Qux'],
];
$analysis = new AttributeAnalysis(2, 34, 3, 34, $attributes);

self::assertSame(2, $analysis->getStartIndex());
self::assertSame(34, $analysis->getEndIndex());
self::assertSame(3, $analysis->getOpeningBracketIndex());
self::assertSame(34, $analysis->getClosingBracketIndex());
self::assertSame($attributes, $analysis->getAttributes());
}
}

0 comments on commit 7eb5ec3

Please sign in to comment.