Skip to content

Commit 798dfd6

Browse files
authored
NoUselessParenthesisFixer - fix expressions
1 parent b664ac4 commit 798dfd6

File tree

6 files changed

+167
-22
lines changed

6 files changed

+167
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Add StringableInterfaceFixer
66
- NoCommentedOutCodeFixer - do not remove URLs
77
- NoDuplicatedArrayKeyFixer - add option "ignore_expressions"
8+
- NoUselessParenthesisFixer - fix expressions
89
- PhpdocNoIncorrectVarAnnotationFixer - handle class properties when variable names are different and constants with visibility
910

1011
## v2.5.0

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![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)
44
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
55
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
6-
![Tests](https://img.shields.io/badge/tests-2837-brightgreen.svg)
6+
![Tests](https://img.shields.io/badge/tests-2869-brightgreen.svg)
77
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
88

99
[![CI Status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions)
@@ -273,7 +273,7 @@ There can be no comments generated by Doctrine ORM.
273273
```
274274

275275
#### NoUselessParenthesisFixer
276-
There can be no useless parentheses.
276+
There must be no useless parentheses.
277277
```diff
278278
<?php
279279
-foo(($bar));

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"php": "^7.2 || ^8.0",
1414
"ext-filter": "*",
1515
"ext-tokenizer": "*",
16-
"friendsofphp/php-cs-fixer": "^3.0.0",
16+
"friendsofphp/php-cs-fixer": "^3.1.0",
1717
"symfony/finder": "^3.0 || ^4.0 || ^5.0"
1818
},
1919
"require-dev": {

src/Fixer/NoUselessParenthesisFixer.php

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ final class NoUselessParenthesisFixer extends AbstractFixer
2626
public function getDefinition(): FixerDefinitionInterface
2727
{
2828
return new FixerDefinition(
29-
'There can be no useless parentheses.',
29+
'There must be no useless parentheses.',
3030
[
3131
new CodeSample('<?php
3232
foo(($bar));
@@ -79,47 +79,127 @@ public function fix(\SplFileInfo $file, Tokens $tokens): void
7979
/** @var Token $prevToken */
8080
$prevToken = $tokens[$prevIndex];
8181

82-
if ($prevToken->isGivenKind(\T_RETURN)) {
82+
if ($prevToken->isGivenKind([\T_RETURN, \T_THROW])) {
8383
$tokens->ensureWhitespaceAtIndex($prevIndex + 1, 0, ' ');
8484
}
8585
}
8686
}
8787

8888
private function isBlockToRemove(Tokens $tokens, int $startIndex, int $endIndex): bool
8989
{
90-
$blocksAnalyzer = new BlocksAnalyzer();
91-
92-
// is there a block of parenthesis inside?
93-
/** @var int $nextStartIndex */
94-
$nextStartIndex = $tokens->getNextMeaningfulToken($startIndex);
95-
/** @var Token $nextStartToken */
96-
$nextStartToken = $tokens[$nextStartIndex];
97-
if ($nextStartToken->equalsAny(['(', [CT::T_BRACE_CLASS_INSTANTIATION_OPEN]])) {
98-
/** @var int $prevEndIndex */
99-
$prevEndIndex = $tokens->getPrevMeaningfulToken($endIndex);
100-
if ($blocksAnalyzer->isBlock($tokens, $nextStartIndex, $prevEndIndex)) {
101-
return true;
102-
}
90+
if ($this->isParenthesisBlockInside($tokens, $startIndex, $endIndex)) {
91+
return true;
10392
}
10493

105-
// is there a block of parenthesis outside?
10694
/** @var int $prevStartIndex */
10795
$prevStartIndex = $tokens->getPrevMeaningfulToken($startIndex);
10896
/** @var int $nextEndIndex */
10997
$nextEndIndex = $tokens->getNextMeaningfulToken($endIndex);
110-
if ($blocksAnalyzer->isBlock($tokens, $prevStartIndex, $nextEndIndex)) {
98+
99+
if ((new BlocksAnalyzer())->isBlock($tokens, $prevStartIndex, $nextEndIndex)) {
111100
return true;
112101
}
113102

114-
// is there assignment, return or throw before?
115103
/** @var Token $prevStartToken */
116104
$prevStartToken = $tokens[$prevStartIndex];
117105
/** @var Token $nextEndToken */
118106
$nextEndToken = $tokens[$nextEndIndex];
119107

108+
if ($this->isForbiddenBeforeOpenParenthesis($tokens, $prevStartIndex)) {
109+
return false;
110+
}
111+
112+
if ($this->isExpressionInside($tokens, $startIndex, $endIndex)) {
113+
return true;
114+
}
115+
120116
return $prevStartToken->equalsAny(['=', [\T_RETURN], [\T_THROW]]) && $nextEndToken->equals(';');
121117
}
122118

119+
private function isForbiddenBeforeOpenParenthesis(Tokens $tokens, int $index): bool
120+
{
121+
/** @var Token $token */
122+
$token = $tokens[$index];
123+
124+
if (
125+
$token->isGivenKind([
126+
\T_ARRAY,
127+
\T_CATCH,
128+
\T_CLASS,
129+
\T_ELSEIF,
130+
\T_EMPTY,
131+
\T_EVAL,
132+
\T_EXIT,
133+
\T_FUNCTION,
134+
\T_IF,
135+
\T_ISSET,
136+
\T_LIST,
137+
\T_STATIC,
138+
\T_STRING,
139+
\T_SWITCH,
140+
\T_UNSET,
141+
\T_VARIABLE,
142+
\T_WHILE,
143+
CT::T_CLASS_CONSTANT,
144+
CT::T_USE_LAMBDA,
145+
])
146+
|| \defined('T_FN') && $token->isGivenKind(\T_FN)
147+
|| \defined('T_MATCH') && $token->isGivenKind(\T_MATCH)
148+
) {
149+
return true;
150+
}
151+
152+
/** @var null|array{isStart: bool, type: int} $blockType */
153+
$blockType = Tokens::detectBlockType($token);
154+
155+
return $blockType !== null && !$blockType['isStart'];
156+
}
157+
158+
private function isParenthesisBlockInside(Tokens $tokens, int $startIndex, int $endIndex): bool
159+
{
160+
/** @var int $nextStartIndex */
161+
$nextStartIndex = $tokens->getNextMeaningfulToken($startIndex);
162+
/** @var Token $nextStartToken */
163+
$nextStartToken = $tokens[$nextStartIndex];
164+
165+
return $nextStartToken->equalsAny(['(', [CT::T_BRACE_CLASS_INSTANTIATION_OPEN]])
166+
&& (new BlocksAnalyzer())->isBlock($tokens, $nextStartIndex, $tokens->getPrevMeaningfulToken($endIndex));
167+
}
168+
169+
private function isExpressionInside(Tokens $tokens, int $startIndex, int $endIndex): bool
170+
{
171+
$expression = false;
172+
173+
/** @var int $index */
174+
$index = $tokens->getNextMeaningfulToken($startIndex);
175+
176+
while ($index < $endIndex) {
177+
$expression = true;
178+
179+
/** @var Token $token */
180+
$token = $tokens[$index];
181+
182+
if (
183+
!$token->isGivenKind([
184+
\T_CONSTANT_ENCAPSED_STRING,
185+
\T_DNUMBER,
186+
\T_DOUBLE_COLON,
187+
\T_LNUMBER,
188+
\T_OBJECT_OPERATOR,
189+
\T_STRING,
190+
\T_VARIABLE,
191+
]) && !$token->isMagicConstant()
192+
) {
193+
return false;
194+
}
195+
196+
/** @var int $index */
197+
$index = $tokens->getNextMeaningfulToken($index);
198+
}
199+
200+
return $expression;
201+
}
202+
123203
private function clearWhitespace(Tokens $tokens, int $index): void
124204
{
125205
/** @var Token $token */

tests/Fixer/NoUselessParenthesisFixerTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,36 @@ public static function provideFixCases(): iterable
4242
yield ['<?php $foo([1, 2]);'];
4343
yield ['<?php foo(($a || $b) && ($c || $d));'];
4444
yield ['<?php $a = array([]);'];
45+
yield ['<?php $f($x);'];
46+
yield ['<?php if ($x) {} elseif ($y) {}'];
47+
yield ['<?php array($x);'];
48+
yield ['<?php empty($x);'];
49+
yield ['<?php isset($x);'];
50+
yield ['<?php unset($x);'];
51+
yield ['<?php exit(2);'];
52+
yield ['<?php eval("<?php echo 3;");'];
53+
yield ['<?php list($x) = [1];'];
54+
yield ['<?php switch ($x) { default: return true; }'];
55+
yield ['<?php try {} catch (Exception $x) {}'];
4556
yield ['<?php $c = new class([]) {};'];
57+
yield ['<?php $f = function ($x) { return $x + 2; };'];
58+
yield ['<?php $f = function ($x): int { return $x + 2; };'];
59+
yield ['<?php $f = function ($x) use ($y) { return $x + $y; };'];
60+
yield ['<?php $f = function ($x) use ($y): int { return $x + $y; };'];
61+
yield ['<?php $arrayOfCallbacks["foo"]($bar);'];
62+
yield ['<?php do {} while($x);'];
63+
yield ['<?php Obj::class($x);'];
64+
yield ['<?php class Foo { public static function create($x) { return new static($x); } }'];
65+
yield ['<?php class Foo { public static function create($x) { return $this->{$prop}; } }'];
66+
yield ['<?php $c = new class($x) {};'];
67+
yield ['<?php $object->{"set_value"}($x);'];
68+
yield ['<?php $object->{"set_{$name}"}($x);'];
69+
if (\defined('T_FN')) {
70+
yield ['<?php $f = fn ($x) => $x + 2;'];
71+
}
72+
if (\defined('T_MATCH')) {
73+
yield ['<?php return match ($x) { default => 0 };'];
74+
}
4675
yield ['<?php class Foo {
4776
public function createSelf() {
4877
return new self([1, 2]);
@@ -67,6 +96,11 @@ public function createStatic() {
6796
'<?php throw (new Exception("message"));',
6897
];
6998

99+
yield [
100+
'<?php throw new Exception("message");',
101+
'<?php throw(new Exception("message"));',
102+
];
103+
70104
yield [
71105
'<?php return array();',
72106
'<?php return(array());',
@@ -147,6 +181,36 @@ public function createStatic() {
147181
'<?php foo( /* one */ ( /* two */ ( /* three */ $bar /* four */ ) /* five */ ) /* six */ );',
148182
];
149183

184+
yield [
185+
'<?php echo 1 + 2 + 3;',
186+
'<?php echo 1 + (2) + 3;',
187+
];
188+
189+
yield [
190+
'<?php echo 1.5 + 2.5 + 3.5;',
191+
'<?php echo 1.5 + (2.5) + 3.5;',
192+
];
193+
194+
yield [
195+
'<?php $s = "a" . "b" . "c";',
196+
'<?php $s = "a" . ("b") . "c";',
197+
];
198+
199+
yield [
200+
'<?php echo 1 + $obj->value + 3;',
201+
'<?php echo 1 + ($obj->value) + 3;',
202+
];
203+
204+
yield [
205+
'<?php echo 1 + Obj::VALUE + 3;',
206+
'<?php echo 1 + (Obj::VALUE) + 3;',
207+
];
208+
209+
yield [
210+
'<?php $s = "a" . "b" . "c";',
211+
'<?php $s = "a" . ( "b" ) . "c";',
212+
];
213+
150214
yield [
151215
'<?php return // foo
152216
1 // bar

tests/Readme/ReadmeCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
*
2323
* @covers \PhpCsFixerCustomFixersDev\Readme\ReadmeCommand
2424
*
25-
* @requires PHP >=7.4
25+
* @requires PHP >=8.0
2626
*/
2727
final class ReadmeCommandTest extends TestCase
2828
{

0 commit comments

Comments
 (0)