Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add NoDuplicatedArrayKeyFixer (#196)
- Loading branch information
1 parent
cf4b50e
commit b1cbe9b
Showing
9 changed files
with
686 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace PhpCsFixerCustomFixers\Analyzer\Analysis; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
final class ArrayElementAnalysis | ||
{ | ||
/** @var ?int */ | ||
private $keyStartIndex; | ||
|
||
/** @var ?int */ | ||
private $keyEndIndex; | ||
|
||
/** @var int */ | ||
private $valueStartIndex; | ||
|
||
/** @var int */ | ||
private $valueEndIndex; | ||
|
||
public function __construct(?int $keyStartIndex, ?int $keyEndIndex, int $valueStartIndex, int $valueEndIndex) | ||
{ | ||
$this->keyStartIndex = $keyStartIndex; | ||
$this->keyEndIndex = $keyEndIndex; | ||
$this->valueStartIndex = $valueStartIndex; | ||
$this->valueEndIndex = $valueEndIndex; | ||
} | ||
|
||
public function getKeyStartIndex(): ?int | ||
{ | ||
return $this->keyStartIndex; | ||
} | ||
|
||
public function getKeyEndIndex(): ?int | ||
{ | ||
return $this->keyEndIndex; | ||
} | ||
|
||
public function getValueStartIndex(): int | ||
{ | ||
return $this->valueStartIndex; | ||
} | ||
|
||
public function getValueEndIndex(): int | ||
{ | ||
return $this->valueEndIndex; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace PhpCsFixerCustomFixers\Analyzer; | ||
|
||
use PhpCsFixer\Tokenizer\CT; | ||
use PhpCsFixer\Tokenizer\Tokens; | ||
use PhpCsFixerCustomFixers\Analyzer\Analysis\ArrayElementAnalysis; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
final class ArrayAnalyzer | ||
{ | ||
/** | ||
* @return ArrayElementAnalysis[] | ||
*/ | ||
public function getElements(Tokens $tokens, int $index): array | ||
{ | ||
if ($tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) { | ||
/** @var int $arrayContentStartIndex */ | ||
$arrayContentStartIndex = $tokens->getNextMeaningfulToken($index); | ||
|
||
/** @var int $arrayContentEndIndex */ | ||
$arrayContentEndIndex = $tokens->getPrevMeaningfulToken($tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index)); | ||
|
||
return $this->getElementsForArrayContent($tokens, $arrayContentStartIndex, $arrayContentEndIndex); | ||
} | ||
|
||
if ($tokens[$index]->isGivenKind(T_ARRAY)) { | ||
/** @var int $arrayOpenBraceIndex */ | ||
$arrayOpenBraceIndex = $tokens->getNextTokenOfKind($index, ['(']); | ||
|
||
/** @var int $arrayContentStartIndex */ | ||
$arrayContentStartIndex = $tokens->getNextMeaningfulToken($arrayOpenBraceIndex); | ||
|
||
/** @var int $arrayContentEndIndex */ | ||
$arrayContentEndIndex = $tokens->getPrevMeaningfulToken($tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $arrayOpenBraceIndex)); | ||
|
||
return $this->getElementsForArrayContent($tokens, $arrayContentStartIndex, $arrayContentEndIndex); | ||
} | ||
|
||
throw new \InvalidArgumentException(\sprintf('Index %d is not an array.', $index)); | ||
} | ||
|
||
/** | ||
* @return ArrayElementAnalysis[] | ||
*/ | ||
private function getElementsForArrayContent(Tokens $tokens, int $startIndex, int $endIndex): array | ||
{ | ||
$elements = []; | ||
|
||
$index = $startIndex; | ||
while ($endIndex >= $index = $this->nextCandidateIndex($tokens, $index)) { | ||
if (!$tokens[$index]->equals(',')) { | ||
continue; | ||
} | ||
|
||
/** @var int $elementEndIndex */ | ||
$elementEndIndex = $tokens->getPrevMeaningfulToken($index); | ||
|
||
$elements[] = $this->createArrayElementAnalysis($tokens, $startIndex, $elementEndIndex); | ||
|
||
/** @var int $startIndex */ | ||
$startIndex = $tokens->getNextMeaningfulToken($index); | ||
} | ||
|
||
if ($startIndex <= $endIndex) { | ||
$elements[] = $this->createArrayElementAnalysis($tokens, $startIndex, $endIndex); | ||
} | ||
|
||
return $elements; | ||
} | ||
|
||
private function createArrayElementAnalysis(Tokens $tokens, int $startIndex, int $endIndex): ArrayElementAnalysis | ||
{ | ||
$index = $startIndex; | ||
while ($endIndex > $index = $this->nextCandidateIndex($tokens, $index)) { | ||
if (!$tokens[$index]->isGivenKind(T_DOUBLE_ARROW)) { | ||
continue; | ||
} | ||
|
||
/** @var int $keyEndIndex */ | ||
$keyEndIndex = $tokens->getPrevMeaningfulToken($index); | ||
|
||
/** @var int $valueStartIndex */ | ||
$valueStartIndex = $tokens->getNextMeaningfulToken($index); | ||
|
||
return new ArrayElementAnalysis($startIndex, $keyEndIndex, $valueStartIndex, $endIndex); | ||
} | ||
|
||
return new ArrayElementAnalysis(null, null, $startIndex, $endIndex); | ||
} | ||
|
||
private function nextCandidateIndex(Tokens $tokens, int $index): int | ||
{ | ||
if ($tokens[$index]->equals('{')) { | ||
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index); | ||
} | ||
|
||
if ($tokens[$index]->equals('(')) { | ||
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index); | ||
} | ||
|
||
if ($tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) { | ||
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index); | ||
} | ||
|
||
if ($tokens[$index]->isGivenKind(T_ARRAY)) { | ||
/** @var int $arrayOpenBraceIndex */ | ||
$arrayOpenBraceIndex = $tokens->getNextTokenOfKind($index, ['(']); | ||
|
||
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $arrayOpenBraceIndex); | ||
} | ||
|
||
return $index + 1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace PhpCsFixerCustomFixers\Fixer; | ||
|
||
use PhpCsFixer\FixerDefinition\CodeSample; | ||
use PhpCsFixer\FixerDefinition\FixerDefinition; | ||
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; | ||
use PhpCsFixer\Preg; | ||
use PhpCsFixer\Tokenizer\CT; | ||
use PhpCsFixer\Tokenizer\Tokens; | ||
use PhpCsFixerCustomFixers\Analyzer\Analysis\ArrayElementAnalysis; | ||
use PhpCsFixerCustomFixers\Analyzer\ArrayAnalyzer; | ||
use PhpCsFixerCustomFixers\TokenRemover; | ||
|
||
final class NoDuplicatedArrayKeyFixer extends AbstractFixer | ||
{ | ||
public function getDefinition(): FixerDefinitionInterface | ||
{ | ||
return new FixerDefinition( | ||
'Duplicated array keys must be removed.', | ||
[new CodeSample('<?php | ||
$x = [ | ||
"foo" => 1, | ||
"bar" => 2, | ||
"foo" => 3, | ||
]; | ||
')] | ||
); | ||
} | ||
|
||
public function getPriority(): int | ||
{ | ||
return 0; | ||
} | ||
|
||
public function isCandidate(Tokens $tokens): bool | ||
{ | ||
return $tokens->isAnyTokenKindsFound([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN]); | ||
} | ||
|
||
public function isRisky(): bool | ||
{ | ||
return false; | ||
} | ||
|
||
public function fix(\SplFileInfo $file, Tokens $tokens): void | ||
{ | ||
for ($index = $tokens->count() - 1; $index > 0; $index--) { | ||
if (!$tokens[$index]->isGivenKind([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) { | ||
continue; | ||
} | ||
|
||
$this->fixArray($tokens, $index); | ||
} | ||
} | ||
|
||
private function fixArray(Tokens $tokens, int $index): void | ||
{ | ||
$arrayAnalyzer = new ArrayAnalyzer(); | ||
|
||
$keys = []; | ||
foreach (\array_reverse($arrayAnalyzer->getElements($tokens, $index)) as $arrayElementAnalysis) { | ||
$key = $this->getKeyContentIfPossible($tokens, $arrayElementAnalysis); | ||
if ($key === null) { | ||
continue; | ||
} | ||
if (isset($keys[$key])) { | ||
/** @var int $startIndex */ | ||
$startIndex = $arrayElementAnalysis->getKeyStartIndex(); | ||
|
||
/** @var int $endIndex */ | ||
$endIndex = $tokens->getNextMeaningfulToken($arrayElementAnalysis->getValueEndIndex()); | ||
if ($tokens[$endIndex + 1]->isWhitespace() && Preg::match('/^\h+$/', $tokens[$endIndex + 1]->getContent()) === 1) { | ||
$endIndex++; | ||
} | ||
|
||
$tokens->clearRange($startIndex + 1, $endIndex); | ||
TokenRemover::removeWithLinesIfPossible($tokens, $startIndex); | ||
} | ||
$keys[$key] = true; | ||
} | ||
} | ||
|
||
private function getKeyContentIfPossible(Tokens $tokens, ArrayElementAnalysis $arrayElementAnalysis): ?string | ||
{ | ||
if ($arrayElementAnalysis->getKeyStartIndex() === null || $arrayElementAnalysis->getKeyEndIndex() === null) { | ||
return null; | ||
} | ||
|
||
$content = ''; | ||
for ($index = $arrayElementAnalysis->getKeyEndIndex(); $index >= $arrayElementAnalysis->getKeyStartIndex(); $index--) { | ||
if ($tokens[$index]->isWhitespace() || $tokens[$index]->isComment()) { | ||
continue; | ||
} | ||
if ($tokens[$index]->equalsAny([[T_VARIABLE], '('])) { | ||
return null; | ||
} | ||
$content .= $tokens[$index]->getContent(); | ||
} | ||
|
||
return $content; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace Tests\Analyzer\Analysis; | ||
|
||
use PhpCsFixerCustomFixers\Analyzer\Analysis\ArrayElementAnalysis; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
/** | ||
* @internal | ||
* | ||
* @covers \PhpCsFixerCustomFixers\Analyzer\Analysis\ArrayElementAnalysis | ||
*/ | ||
final class ArrayElementAnalysisTest extends TestCase | ||
{ | ||
public function testGetKeyStartIndex(): void | ||
{ | ||
$analysis = new ArrayElementAnalysis(1, 2, 3, 4); | ||
self::assertSame(1, $analysis->getKeyStartIndex()); | ||
} | ||
|
||
public function testGetKeyEndIndex(): void | ||
{ | ||
$analysis = new ArrayElementAnalysis(1, 2, 3, 4); | ||
self::assertSame(2, $analysis->getKeyEndIndex()); | ||
} | ||
|
||
public function testGetValueStartIndex(): void | ||
{ | ||
$analysis = new ArrayElementAnalysis(1, 2, 3, 4); | ||
self::assertSame(3, $analysis->getValueStartIndex()); | ||
} | ||
|
||
public function testGetValueEndIndex(): void | ||
{ | ||
$analysis = new ArrayElementAnalysis(1, 2, 3, 4); | ||
self::assertSame(4, $analysis->getValueEndIndex()); | ||
} | ||
} |
Oops, something went wrong.