Skip to content

Commit fc25fbf

Browse files
committed
Add ForeachUseValueFixer
1 parent 8e5c168 commit fc25fbf

File tree

4 files changed

+444
-1
lines changed

4 files changed

+444
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# CHANGELOG for PHP CS Fixer: custom fixers
22

33
## v3.25.0
4+
- Add ForeachUseValueFixer
45
- Add NoUselessWriteVisibilityFixer
56
- ReadonlyPromotedPropertiesFixer - support asymmetric visibility
67

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Latest stable version](https://img.shields.io/packagist/v/kubawerlos/php-cs-fixer-custom-fixers.svg?label=current%20version)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
66
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
77
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
8-
![Tests](https://img.shields.io/badge/tests-3632-brightgreen.svg)
8+
![Tests](https://img.shields.io/badge/tests-3666-brightgreen.svg)
99
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
1010

1111
[![CI status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml/badge.svg)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml)
@@ -166,6 +166,16 @@ Empty function body must be abbreviated as `{}` and placed on the same line as t
166166
+) {}
167167
```
168168

169+
#### ForeachUseValueFixer
170+
Value from `foreach` must not be used if possible.
171+
```diff
172+
<?php
173+
foreach ($elements as $key => $value) {
174+
- $product *= $elements[$key];
175+
+ $product *= $value;
176+
}
177+
```
178+
169179
#### InternalClassCasingFixer
170180
Classes defined internally by an extension or the core must be referenced with the correct case.
171181
DEPRECATED: use `class_reference_name_casing` instead.

src/Fixer/ForeachUseValueFixer.php

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace PhpCsFixerCustomFixers\Fixer;
13+
14+
use PhpCsFixer\FixerDefinition\CodeSample;
15+
use PhpCsFixer\FixerDefinition\FixerDefinition;
16+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
17+
use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
18+
use PhpCsFixer\Tokenizer\Token;
19+
use PhpCsFixer\Tokenizer\Tokens;
20+
21+
final class ForeachUseValueFixer extends AbstractFixer
22+
{
23+
private const NOT_ALLOWED_NEXT_TOKENS = [
24+
'[',
25+
'=',
26+
[\T_INC, '++'],
27+
[\T_DEC, '--'],
28+
// arithmetic assignments
29+
[\T_PLUS_EQUAL, '+='],
30+
[\T_MINUS_EQUAL, '-='],
31+
[\T_MUL_EQUAL, '*='],
32+
[\T_DIV_EQUAL, '/='],
33+
[\T_MOD_EQUAL, '%='],
34+
[\T_POW_EQUAL, '**='],
35+
// bitwise assignments
36+
[\T_AND_EQUAL, '&='],
37+
[\T_OR_EQUAL, '|='],
38+
[\T_XOR_EQUAL, '^='],
39+
[\T_SL_EQUAL, '<<='],
40+
[\T_SR_EQUAL, '>>='],
41+
// other assignments
42+
[\T_COALESCE_EQUAL, '??='],
43+
[\T_CONCAT_EQUAL, '.='],
44+
];
45+
46+
public function getDefinition(): FixerDefinitionInterface
47+
{
48+
return new FixerDefinition(
49+
'Value from `foreach` must not be used if possible.',
50+
[new CodeSample(
51+
<<<'PHP'
52+
<?php
53+
foreach ($elements as $key => $value) {
54+
$product *= $elements[$key];
55+
}
56+
57+
PHP,
58+
)],
59+
'',
60+
);
61+
}
62+
63+
public function getPriority(): int
64+
{
65+
return 0;
66+
}
67+
68+
public function isCandidate(Tokens $tokens): bool
69+
{
70+
return $tokens->isAllTokenKindsFound([\T_FOREACH, \T_VARIABLE]);
71+
}
72+
73+
public function isRisky(): bool
74+
{
75+
return false;
76+
}
77+
78+
public function fix(\SplFileInfo $file, Tokens $tokens): void
79+
{
80+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
81+
if (!$tokens[$index]->isGivenKind(\T_FOREACH)) {
82+
continue;
83+
}
84+
85+
$openParenthesisIndex = $tokens->getNextMeaningfulToken($index);
86+
\assert(\is_int($openParenthesisIndex));
87+
88+
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesisIndex);
89+
90+
$variables = $this->getForeachVariableNames($tokens, $openParenthesisIndex);
91+
if ($variables === null) {
92+
continue;
93+
}
94+
95+
$blockStartIndex = $tokens->getNextMeaningfulToken($closeParenthesisIndex);
96+
\assert(\is_int($blockStartIndex));
97+
98+
if ($tokens[$blockStartIndex]->equals('{')) {
99+
$blockEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $blockStartIndex);
100+
} elseif ($tokens[$blockStartIndex]->equals(':')) {
101+
$blockEndIndex = (new AlternativeSyntaxAnalyzer())->findAlternativeSyntaxBlockEnd($tokens, $index);
102+
} else {
103+
continue;
104+
}
105+
106+
$this->fixForeachBody($tokens, $blockStartIndex, $blockEndIndex, ...$variables);
107+
}
108+
}
109+
110+
/**
111+
* @return null|array{Token, string, string}
112+
*/
113+
private function getForeachVariableNames(Tokens $tokens, int $openParenthesisIndex): ?array
114+
{
115+
$arrayIndex = $tokens->getNextMeaningfulToken($openParenthesisIndex);
116+
\assert(\is_int($arrayIndex));
117+
118+
$asIndex = $tokens->getNextMeaningfulToken($arrayIndex);
119+
\assert(\is_int($asIndex));
120+
if (!$tokens[$asIndex]->isGivenKind(\T_AS)) {
121+
return null;
122+
}
123+
124+
$keyIndex = $tokens->getNextMeaningfulToken($asIndex);
125+
\assert(\is_int($keyIndex));
126+
if (!$tokens[$keyIndex]->isGivenKind(\T_VARIABLE)) {
127+
return null;
128+
}
129+
130+
$doubleArrayIndex = $tokens->getNextMeaningfulToken($keyIndex);
131+
\assert(\is_int($doubleArrayIndex));
132+
if (!$tokens[$doubleArrayIndex]->isGivenKind(\T_DOUBLE_ARROW)) {
133+
return null;
134+
}
135+
136+
$variableIndex = $tokens->getNextMeaningfulToken($doubleArrayIndex);
137+
\assert(\is_int($variableIndex));
138+
if (!$tokens[$variableIndex]->isGivenKind(\T_VARIABLE)) {
139+
return null;
140+
}
141+
142+
return [
143+
$tokens[$arrayIndex],
144+
$tokens[$keyIndex]->getContent(),
145+
$tokens[$variableIndex]->getContent(),
146+
];
147+
}
148+
149+
private function fixForeachBody(
150+
Tokens $tokens,
151+
int $openBraceIndex,
152+
int $closeBraceIndex,
153+
Token $arrayToken,
154+
string $keyName,
155+
string $variableName,
156+
): void {
157+
$sequence = [
158+
$arrayToken,
159+
'[',
160+
[\T_VARIABLE, $keyName],
161+
']',
162+
];
163+
164+
$index = $openBraceIndex;
165+
while (($found = $tokens->findSequence($sequence, $index, $closeBraceIndex)) !== null) {
166+
$startIndex = \array_key_first($found);
167+
$endIndex = \array_key_last($found);
168+
169+
$index = $endIndex;
170+
171+
if ($this->isInUnset($tokens, $startIndex)) {
172+
continue;
173+
}
174+
175+
$nextIndex = $tokens->getNextMeaningfulToken($endIndex);
176+
\assert(\is_int($nextIndex));
177+
if ($tokens[$nextIndex]->equalsAny(self::NOT_ALLOWED_NEXT_TOKENS)) {
178+
continue;
179+
}
180+
181+
$tokens->overrideRange($startIndex, $endIndex, [new Token([\T_VARIABLE, $variableName])]);
182+
}
183+
}
184+
185+
private function isInUnset(Tokens $tokens, int $startIndex): bool
186+
{
187+
$openParenthesisIndex = $tokens->getPrevMeaningfulToken($startIndex);
188+
\assert(\is_int($openParenthesisIndex));
189+
if (!$tokens[$openParenthesisIndex]->equals('(')) {
190+
return false;
191+
}
192+
193+
$unsetIndex = $tokens->getPrevMeaningfulToken($openParenthesisIndex);
194+
\assert(\is_int($unsetIndex));
195+
196+
return $tokens[$unsetIndex]->isGivenKind(\T_UNSET);
197+
}
198+
}

0 commit comments

Comments
 (0)