Skip to content

Commit b1cbe9b

Browse files
authored
Add NoDuplicatedArrayKeyFixer (#196)
1 parent cf4b50e commit b1cbe9b

File tree

9 files changed

+686
-1
lines changed

9 files changed

+686
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44
- Add CommentedOutFunctionFixer
5+
- Add NoDuplicatedArrayKeyFixer
56

67
## v2.0.0 - *2020-03-01*
78
- Drop PHP 7.1 support

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[![Travis CI build status](https://img.shields.io/travis/kubawerlos/php-cs-fixer-custom-fixers/master.svg?label=Travis+CI)](https://travis-ci.org/kubawerlos/php-cs-fixer-custom-fixers)
1010
[![AppVeyor build status](https://img.shields.io/appveyor/ci/kubawerlos/php-cs-fixer-custom-fixers/master?label=AppVeyor)](https://ci.appveyor.com/project/kubawerlos/php-cs-fixer-custom-fixers)
1111
[![Code coverage](https://img.shields.io/coveralls/github/kubawerlos/php-cs-fixer-custom-fixers/master.svg)](https://coveralls.io/github/kubawerlos/php-cs-fixer-custom-fixers?branch=master)
12-
![Tests](https://img.shields.io/badge/tests-1993-brightgreen.svg)
12+
![Tests](https://img.shields.io/badge/tests-2054-brightgreen.svg)
1313
[![Mutation testing badge](https://badge.stryker-mutator.io/github.com/kubawerlos/php-cs-fixer-custom-fixers/master)](https://stryker-mutator.github.io)
1414
[![Psalm type coverage](https://shepherd.dev/github/kubawerlos/php-cs-fixer-custom-fixers/coverage.svg)](https://shepherd.dev/github/kubawerlos/php-cs-fixer-custom-fixers)
1515

@@ -153,6 +153,17 @@ There must be no comment generated by Doctrine Migrations.
153153
}
154154
```
155155

156+
#### NoDuplicatedArrayKeyFixer
157+
Duplicated array keys must be removed.
158+
```diff
159+
<?php
160+
$x = [
161+
- "foo" => 1,
162+
"bar" => 2,
163+
"foo" => 3,
164+
];
165+
```
166+
156167
#### NoDuplicatedImportsFixer
157168
Duplicated `use` statements must be removed.
158169
```diff

dev-tools/psalm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<PossiblyUnusedMethod>
3838
<errorLevel type='suppress'>
3939
<file name='./src/Fixer/OrderedClassElementsInternalFixer.php' />
40+
<file name='../src/Analyzer/Analysis/ArrayElementAnalysis.php' />
4041
<file name='../src/Analyzer/Analysis/SwitchAnalysis.php' />
4142
<file name='../src/Fixer/DeprecatingFixerInterface.php' />
4243
</errorLevel>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PhpCsFixerCustomFixers\Analyzer\Analysis;
6+
7+
/**
8+
* @internal
9+
*/
10+
final class ArrayElementAnalysis
11+
{
12+
/** @var ?int */
13+
private $keyStartIndex;
14+
15+
/** @var ?int */
16+
private $keyEndIndex;
17+
18+
/** @var int */
19+
private $valueStartIndex;
20+
21+
/** @var int */
22+
private $valueEndIndex;
23+
24+
public function __construct(?int $keyStartIndex, ?int $keyEndIndex, int $valueStartIndex, int $valueEndIndex)
25+
{
26+
$this->keyStartIndex = $keyStartIndex;
27+
$this->keyEndIndex = $keyEndIndex;
28+
$this->valueStartIndex = $valueStartIndex;
29+
$this->valueEndIndex = $valueEndIndex;
30+
}
31+
32+
public function getKeyStartIndex(): ?int
33+
{
34+
return $this->keyStartIndex;
35+
}
36+
37+
public function getKeyEndIndex(): ?int
38+
{
39+
return $this->keyEndIndex;
40+
}
41+
42+
public function getValueStartIndex(): int
43+
{
44+
return $this->valueStartIndex;
45+
}
46+
47+
public function getValueEndIndex(): int
48+
{
49+
return $this->valueEndIndex;
50+
}
51+
}

src/Analyzer/ArrayAnalyzer.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PhpCsFixerCustomFixers\Analyzer;
6+
7+
use PhpCsFixer\Tokenizer\CT;
8+
use PhpCsFixer\Tokenizer\Tokens;
9+
use PhpCsFixerCustomFixers\Analyzer\Analysis\ArrayElementAnalysis;
10+
11+
/**
12+
* @internal
13+
*/
14+
final class ArrayAnalyzer
15+
{
16+
/**
17+
* @return ArrayElementAnalysis[]
18+
*/
19+
public function getElements(Tokens $tokens, int $index): array
20+
{
21+
if ($tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
22+
/** @var int $arrayContentStartIndex */
23+
$arrayContentStartIndex = $tokens->getNextMeaningfulToken($index);
24+
25+
/** @var int $arrayContentEndIndex */
26+
$arrayContentEndIndex = $tokens->getPrevMeaningfulToken($tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index));
27+
28+
return $this->getElementsForArrayContent($tokens, $arrayContentStartIndex, $arrayContentEndIndex);
29+
}
30+
31+
if ($tokens[$index]->isGivenKind(T_ARRAY)) {
32+
/** @var int $arrayOpenBraceIndex */
33+
$arrayOpenBraceIndex = $tokens->getNextTokenOfKind($index, ['(']);
34+
35+
/** @var int $arrayContentStartIndex */
36+
$arrayContentStartIndex = $tokens->getNextMeaningfulToken($arrayOpenBraceIndex);
37+
38+
/** @var int $arrayContentEndIndex */
39+
$arrayContentEndIndex = $tokens->getPrevMeaningfulToken($tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $arrayOpenBraceIndex));
40+
41+
return $this->getElementsForArrayContent($tokens, $arrayContentStartIndex, $arrayContentEndIndex);
42+
}
43+
44+
throw new \InvalidArgumentException(\sprintf('Index %d is not an array.', $index));
45+
}
46+
47+
/**
48+
* @return ArrayElementAnalysis[]
49+
*/
50+
private function getElementsForArrayContent(Tokens $tokens, int $startIndex, int $endIndex): array
51+
{
52+
$elements = [];
53+
54+
$index = $startIndex;
55+
while ($endIndex >= $index = $this->nextCandidateIndex($tokens, $index)) {
56+
if (!$tokens[$index]->equals(',')) {
57+
continue;
58+
}
59+
60+
/** @var int $elementEndIndex */
61+
$elementEndIndex = $tokens->getPrevMeaningfulToken($index);
62+
63+
$elements[] = $this->createArrayElementAnalysis($tokens, $startIndex, $elementEndIndex);
64+
65+
/** @var int $startIndex */
66+
$startIndex = $tokens->getNextMeaningfulToken($index);
67+
}
68+
69+
if ($startIndex <= $endIndex) {
70+
$elements[] = $this->createArrayElementAnalysis($tokens, $startIndex, $endIndex);
71+
}
72+
73+
return $elements;
74+
}
75+
76+
private function createArrayElementAnalysis(Tokens $tokens, int $startIndex, int $endIndex): ArrayElementAnalysis
77+
{
78+
$index = $startIndex;
79+
while ($endIndex > $index = $this->nextCandidateIndex($tokens, $index)) {
80+
if (!$tokens[$index]->isGivenKind(T_DOUBLE_ARROW)) {
81+
continue;
82+
}
83+
84+
/** @var int $keyEndIndex */
85+
$keyEndIndex = $tokens->getPrevMeaningfulToken($index);
86+
87+
/** @var int $valueStartIndex */
88+
$valueStartIndex = $tokens->getNextMeaningfulToken($index);
89+
90+
return new ArrayElementAnalysis($startIndex, $keyEndIndex, $valueStartIndex, $endIndex);
91+
}
92+
93+
return new ArrayElementAnalysis(null, null, $startIndex, $endIndex);
94+
}
95+
96+
private function nextCandidateIndex(Tokens $tokens, int $index): int
97+
{
98+
if ($tokens[$index]->equals('{')) {
99+
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
100+
}
101+
102+
if ($tokens[$index]->equals('(')) {
103+
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
104+
}
105+
106+
if ($tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
107+
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
108+
}
109+
110+
if ($tokens[$index]->isGivenKind(T_ARRAY)) {
111+
/** @var int $arrayOpenBraceIndex */
112+
$arrayOpenBraceIndex = $tokens->getNextTokenOfKind($index, ['(']);
113+
114+
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $arrayOpenBraceIndex);
115+
}
116+
117+
return $index + 1;
118+
}
119+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PhpCsFixerCustomFixers\Fixer;
6+
7+
use PhpCsFixer\FixerDefinition\CodeSample;
8+
use PhpCsFixer\FixerDefinition\FixerDefinition;
9+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
10+
use PhpCsFixer\Preg;
11+
use PhpCsFixer\Tokenizer\CT;
12+
use PhpCsFixer\Tokenizer\Tokens;
13+
use PhpCsFixerCustomFixers\Analyzer\Analysis\ArrayElementAnalysis;
14+
use PhpCsFixerCustomFixers\Analyzer\ArrayAnalyzer;
15+
use PhpCsFixerCustomFixers\TokenRemover;
16+
17+
final class NoDuplicatedArrayKeyFixer extends AbstractFixer
18+
{
19+
public function getDefinition(): FixerDefinitionInterface
20+
{
21+
return new FixerDefinition(
22+
'Duplicated array keys must be removed.',
23+
[new CodeSample('<?php
24+
$x = [
25+
"foo" => 1,
26+
"bar" => 2,
27+
"foo" => 3,
28+
];
29+
')]
30+
);
31+
}
32+
33+
public function getPriority(): int
34+
{
35+
return 0;
36+
}
37+
38+
public function isCandidate(Tokens $tokens): bool
39+
{
40+
return $tokens->isAnyTokenKindsFound([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN]);
41+
}
42+
43+
public function isRisky(): bool
44+
{
45+
return false;
46+
}
47+
48+
public function fix(\SplFileInfo $file, Tokens $tokens): void
49+
{
50+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
51+
if (!$tokens[$index]->isGivenKind([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
52+
continue;
53+
}
54+
55+
$this->fixArray($tokens, $index);
56+
}
57+
}
58+
59+
private function fixArray(Tokens $tokens, int $index): void
60+
{
61+
$arrayAnalyzer = new ArrayAnalyzer();
62+
63+
$keys = [];
64+
foreach (\array_reverse($arrayAnalyzer->getElements($tokens, $index)) as $arrayElementAnalysis) {
65+
$key = $this->getKeyContentIfPossible($tokens, $arrayElementAnalysis);
66+
if ($key === null) {
67+
continue;
68+
}
69+
if (isset($keys[$key])) {
70+
/** @var int $startIndex */
71+
$startIndex = $arrayElementAnalysis->getKeyStartIndex();
72+
73+
/** @var int $endIndex */
74+
$endIndex = $tokens->getNextMeaningfulToken($arrayElementAnalysis->getValueEndIndex());
75+
if ($tokens[$endIndex + 1]->isWhitespace() && Preg::match('/^\h+$/', $tokens[$endIndex + 1]->getContent()) === 1) {
76+
$endIndex++;
77+
}
78+
79+
$tokens->clearRange($startIndex + 1, $endIndex);
80+
TokenRemover::removeWithLinesIfPossible($tokens, $startIndex);
81+
}
82+
$keys[$key] = true;
83+
}
84+
}
85+
86+
private function getKeyContentIfPossible(Tokens $tokens, ArrayElementAnalysis $arrayElementAnalysis): ?string
87+
{
88+
if ($arrayElementAnalysis->getKeyStartIndex() === null || $arrayElementAnalysis->getKeyEndIndex() === null) {
89+
return null;
90+
}
91+
92+
$content = '';
93+
for ($index = $arrayElementAnalysis->getKeyEndIndex(); $index >= $arrayElementAnalysis->getKeyStartIndex(); $index--) {
94+
if ($tokens[$index]->isWhitespace() || $tokens[$index]->isComment()) {
95+
continue;
96+
}
97+
if ($tokens[$index]->equalsAny([[T_VARIABLE], '('])) {
98+
return null;
99+
}
100+
$content .= $tokens[$index]->getContent();
101+
}
102+
103+
return $content;
104+
}
105+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Tests\Analyzer\Analysis;
6+
7+
use PhpCsFixerCustomFixers\Analyzer\Analysis\ArrayElementAnalysis;
8+
use PHPUnit\Framework\TestCase;
9+
10+
/**
11+
* @internal
12+
*
13+
* @covers \PhpCsFixerCustomFixers\Analyzer\Analysis\ArrayElementAnalysis
14+
*/
15+
final class ArrayElementAnalysisTest extends TestCase
16+
{
17+
public function testGetKeyStartIndex(): void
18+
{
19+
$analysis = new ArrayElementAnalysis(1, 2, 3, 4);
20+
self::assertSame(1, $analysis->getKeyStartIndex());
21+
}
22+
23+
public function testGetKeyEndIndex(): void
24+
{
25+
$analysis = new ArrayElementAnalysis(1, 2, 3, 4);
26+
self::assertSame(2, $analysis->getKeyEndIndex());
27+
}
28+
29+
public function testGetValueStartIndex(): void
30+
{
31+
$analysis = new ArrayElementAnalysis(1, 2, 3, 4);
32+
self::assertSame(3, $analysis->getValueStartIndex());
33+
}
34+
35+
public function testGetValueEndIndex(): void
36+
{
37+
$analysis = new ArrayElementAnalysis(1, 2, 3, 4);
38+
self::assertSame(4, $analysis->getValueEndIndex());
39+
}
40+
}

0 commit comments

Comments
 (0)