Skip to content

Commit d043e99

Browse files
authored
Add NoUselessStrlenFixer (#335)
1 parent d0ec7bd commit d043e99

File tree

4 files changed

+302
-1
lines changed

4 files changed

+302
-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
## v2.3.0 - [Unreleased]
44
- Add NoUselessParenthesisFixer
5+
- Add NoUselessStrlenFixer
56

67
## v2.2.0 - *2020-04-02*
78
- Feature: DataProviderNameFixer - add options "prefix" and "suffix"

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
[![CI Status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions)
1010
[![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)
11-
![Tests](https://img.shields.io/badge/tests-2248-brightgreen.svg)
11+
![Tests](https://img.shields.io/badge/tests-2291-brightgreen.svg)
1212
[![Mutation testing badge](https://badge.stryker-mutator.io/github.com/kubawerlos/php-cs-fixer-custom-fixers/master)](https://stryker-mutator.github.io)
1313
[![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)
1414

@@ -288,6 +288,17 @@ Function `sprintf` without parameters should not be used.
288288
+$foo = 'Foo';
289289
```
290290

291+
#### NoUselessStrlenFixer
292+
Function `strlen` should not be used when compared to 0.
293+
*Risky: when the function `strlen` is overridden.*
294+
```diff
295+
<?php
296+
-$isEmpty = strlen($string) === 0;
297+
-$isNotEmpty = strlen($string) > 0;
298+
+$isEmpty = $string === '';
299+
+$isNotEmpty = $string !== '';
300+
```
301+
291302
#### NumericLiteralSeparatorFixer
292303
Numeric literals must have configured separators.
293304
Configuration options:

src/Fixer/NoUselessStrlenFixer.php

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of PHP CS Fixer: custom fixers.
7+
*
8+
* (c) 2018-2020 Kuba Werłos
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace PhpCsFixerCustomFixers\Fixer;
15+
16+
use PhpCsFixer\FixerDefinition\CodeSample;
17+
use PhpCsFixer\FixerDefinition\FixerDefinition;
18+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
19+
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
20+
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
21+
use PhpCsFixer\Tokenizer\Token;
22+
use PhpCsFixer\Tokenizer\Tokens;
23+
24+
final class NoUselessStrlenFixer extends AbstractFixer
25+
{
26+
public function getDefinition(): FixerDefinitionInterface
27+
{
28+
return new FixerDefinition(
29+
'Function `strlen` should not be used when compared to 0.',
30+
[
31+
new CodeSample(
32+
'<?php
33+
$isEmpty = strlen($string) === 0;
34+
$isNotEmpty = strlen($string) > 0;
35+
'
36+
),
37+
],
38+
null,
39+
'when the function `strlen` is overridden'
40+
);
41+
}
42+
43+
public function getPriority(): int
44+
{
45+
return 0;
46+
}
47+
48+
public function isCandidate(Tokens $tokens): bool
49+
{
50+
return $tokens->isTokenKindFound(T_LNUMBER) && $tokens->isAnyTokenKindsFound(['>', '<', T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_IS_EQUAL, T_IS_NOT_EQUAL]);
51+
}
52+
53+
public function isRisky(): bool
54+
{
55+
return true;
56+
}
57+
58+
public function fix(\SplFileInfo $file, Tokens $tokens): void
59+
{
60+
$argumentsAnalyzer = new ArgumentsAnalyzer();
61+
$functionsAnalyzer = new FunctionsAnalyzer();
62+
63+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
64+
/** @var Token $token */
65+
$token = $tokens[$index];
66+
67+
if (!$token->equalsAny([[T_STRING, 'strlen'], [T_STRING, 'mb_strlen']], false)) {
68+
continue;
69+
}
70+
71+
if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
72+
continue;
73+
}
74+
75+
/** @var int $openParenthesisIndex */
76+
$openParenthesisIndex = $tokens->getNextTokenOfKind($index, ['(']);
77+
78+
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesisIndex);
79+
80+
if ($argumentsAnalyzer->countArguments($tokens, $openParenthesisIndex, $closeParenthesisIndex) !== 1) {
81+
continue;
82+
}
83+
84+
$tokensToRemove = [
85+
$index => 1,
86+
$openParenthesisIndex => 1,
87+
$closeParenthesisIndex => -1,
88+
];
89+
90+
/** @var int $prevIndex */
91+
$prevIndex = $tokens->getPrevMeaningfulToken($index);
92+
93+
/** @var Token $prevToken */
94+
$prevToken = $tokens[$prevIndex];
95+
96+
$startIndex = $index;
97+
if ($prevToken->isGivenKind(T_NS_SEPARATOR)) {
98+
$startIndex = $prevIndex;
99+
$tokensToRemove[$prevIndex] = 1;
100+
}
101+
102+
if (!$this->transformCondition($tokens, $startIndex, $closeParenthesisIndex)) {
103+
continue;
104+
}
105+
106+
$this->removeTokenAndSiblingWhitespace($tokens, $tokensToRemove);
107+
}
108+
}
109+
110+
private function transformCondition(Tokens $tokens, int $startIndex, int $endIndex): bool
111+
{
112+
if ($this->transformConditionLeft($tokens, $startIndex)) {
113+
return true;
114+
}
115+
116+
if ($this->transformConditionRight($tokens, $endIndex)) {
117+
return true;
118+
}
119+
120+
return false;
121+
}
122+
123+
private function transformConditionLeft(Tokens $tokens, int $index): bool
124+
{
125+
/** @var int $prevIndex */
126+
$prevIndex = $tokens->getPrevMeaningfulToken($index);
127+
128+
/** @var Token $prevToken */
129+
$prevToken = $tokens[$prevIndex];
130+
131+
$changeCondition = false;
132+
if ($prevToken->equals('<')) {
133+
$changeCondition = true;
134+
} elseif (!$prevToken->isGivenKind([T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_IS_EQUAL, T_IS_NOT_EQUAL])) {
135+
return false;
136+
}
137+
138+
/** @var int $prevPrevIndex */
139+
$prevPrevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
140+
141+
/** @var Token $prevPrevToken */
142+
$prevPrevToken = $tokens[$prevPrevIndex];
143+
144+
if (!$prevPrevToken->equals([T_LNUMBER, '0'])) {
145+
return false;
146+
}
147+
148+
if ($changeCondition) {
149+
$tokens[$prevIndex] = new Token([T_IS_NOT_IDENTICAL, '!==']);
150+
}
151+
152+
$tokens[$prevPrevIndex] = new Token([T_CONSTANT_ENCAPSED_STRING, '\'\'']);
153+
154+
return true;
155+
}
156+
157+
private function transformConditionRight(Tokens $tokens, int $index): bool
158+
{
159+
/** @var int $nextIndex */
160+
$nextIndex = $tokens->getNextMeaningfulToken($index);
161+
162+
/** @var Token $nextToken */
163+
$nextToken = $tokens[$nextIndex];
164+
165+
$changeCondition = false;
166+
if ($nextToken->equals('>')) {
167+
$changeCondition = true;
168+
} elseif (!$nextToken->isGivenKind([T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_IS_EQUAL, T_IS_NOT_EQUAL])) {
169+
return false;
170+
}
171+
172+
/** @var int $nextNextIndex */
173+
$nextNextIndex = $tokens->getNextMeaningfulToken($nextIndex);
174+
175+
/** @var Token $nextNextToken */
176+
$nextNextToken = $tokens[$nextNextIndex];
177+
178+
if (!$nextNextToken->equals([T_LNUMBER, '0'])) {
179+
return false;
180+
}
181+
182+
if ($changeCondition) {
183+
$tokens[$nextIndex] = new Token([T_IS_NOT_IDENTICAL, '!==']);
184+
}
185+
186+
$tokens[$nextNextIndex] = new Token([T_CONSTANT_ENCAPSED_STRING, '\'\'']);
187+
188+
return true;
189+
}
190+
191+
/**
192+
* @param array<int, int> $tokensToRemove
193+
*/
194+
private function removeTokenAndSiblingWhitespace(Tokens $tokens, array $tokensToRemove): void
195+
{
196+
foreach ($tokensToRemove as $index => $direction) {
197+
$tokens->clearAt($index);
198+
199+
/** @var Token $siblingToken */
200+
$siblingToken = $tokens[$index + $direction];
201+
202+
if ($siblingToken->isWhitespace()) {
203+
$tokens->clearAt($index + $direction);
204+
}
205+
}
206+
}
207+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of PHP CS Fixer: custom fixers.
7+
*
8+
* (c) 2018-2020 Kuba Werłos
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Tests\Fixer;
15+
16+
/**
17+
* @internal
18+
*
19+
* @covers \PhpCsFixerCustomFixers\Fixer\NoUselessStrlenFixer
20+
*/
21+
final class NoUselessStrlenFixerTest extends AbstractFixerTestCase
22+
{
23+
public function testIsRisky(): void
24+
{
25+
self::assertTrue($this->fixer->isRisky());
26+
}
27+
28+
/**
29+
* @dataProvider provideFixCases
30+
*/
31+
public function testFix(string $expected, ?string $input = null): void
32+
{
33+
$this->doTest($expected, $input);
34+
}
35+
36+
public static function provideFixCases(): iterable
37+
{
38+
yield ['<?php Foo\strlen($s) > 0;'];
39+
yield ['<?php strsize($s) > 0;'];
40+
yield ['<?php strlen($s) > 1;'];
41+
yield ['<?php 1 < strlen($s);'];
42+
yield ['<?php strlen() > 0;'];
43+
yield ['<?php strlen($a, $b) > 0;'];
44+
45+
yield ['<?php $s !== \'\';', '<?php strlen($s) > 0;'];
46+
yield ['<?php $s === \'\';', '<?php strlen($s) === 0;'];
47+
yield ['<?php $s == \'\';', '<?php strlen($s) == 0;'];
48+
yield ['<?php $s !== \'\';', '<?php strlen($s) !== 0;'];
49+
yield ['<?php $s != \'\';', '<?php strlen($s) != 0;'];
50+
51+
yield ['<?php \'\' !== $s;', '<?php 0 < strlen($s);'];
52+
yield ['<?php \'\' === $s;', '<?php 0 === strlen($s);'];
53+
yield ['<?php \'\' == $s;', '<?php 0 == strlen($s);'];
54+
yield ['<?php \'\' !== $s;', '<?php 0 !== \strlen($s);'];
55+
yield ['<?php \'\' != $s;', '<?php 0 != \strlen($s);'];
56+
57+
yield ['<?php $s !== \'\';', '<?php \strlen($s) > 0;'];
58+
yield ['<?php $s !== \'\';', '<?php mb_strlen($s) > 0;'];
59+
yield ['<?php $s !== \'\';', '<?php StrLen($s) > 0;'];
60+
yield ['<?php $s !== \'\';', '<?php MB_strlen($s) > 0;'];
61+
yield ['<?php $s !== \'\';', '<?php \mb_strlen($s) > 0;'];
62+
63+
yield ['<?php $s !== \'\';', '<?php strlen ( $s ) > 0;'];
64+
65+
yield [
66+
'<?php
67+
$a !== \'\';
68+
Foo\strlen($a) > 0;
69+
strlen($a) > 1;
70+
strlen($a, $b) > 0;
71+
$a !== \'\';
72+
',
73+
'<?php
74+
strlen($a) > 0;
75+
Foo\strlen($a) > 0;
76+
strlen($a) > 1;
77+
strlen($a, $b) > 0;
78+
strlen($a) > 0;
79+
',
80+
];
81+
}
82+
}

0 commit comments

Comments
 (0)