Skip to content

Commit fd3dbbb

Browse files
authored
Add NoUselessDirnameCallFixer (#661)
1 parent 3a1041b commit fd3dbbb

File tree

5 files changed

+286
-1
lines changed

5 files changed

+286
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG for PHP CS Fixer: custom fixers
22

3+
## v3.5.0
4+
- Add NoUselessDirnameCallFixer
5+
36
## v3.4.0
47
- Add DeclareAfterOpeningTagFixer
58

README.md

Lines changed: 9 additions & 1 deletion
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-3064-brightgreen.svg)
6+
![Tests](https://img.shields.io/badge/tests-3101-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)
@@ -296,6 +296,14 @@ There must be no useless comments.
296296
}
297297
```
298298

299+
#### NoUselessDirnameCallFixer
300+
Function `dirname` call must be removed if not needed.
301+
```diff
302+
<?php
303+
-require dirname(__DIR__) . "/vendor/autoload.php";
304+
+require __DIR__ . "/../vendor/autoload.php";
305+
```
306+
299307
#### NoUselessDoctrineRepositoryCommentFixer
300308
There can be no comments generated by Doctrine ORM.
301309
```diff
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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\FunctionsAnalyzer;
18+
use PhpCsFixer\Tokenizer\Token;
19+
use PhpCsFixer\Tokenizer\Tokens;
20+
21+
final class NoUselessDirnameCallFixer extends AbstractFixer
22+
{
23+
public function getDefinition(): FixerDefinitionInterface
24+
{
25+
return new FixerDefinition(
26+
'Function `dirname` call must be removed if not needed.',
27+
[new CodeSample('<?php
28+
require dirname(__DIR__) . "/vendor/autoload.php";
29+
')]
30+
);
31+
}
32+
33+
/**
34+
* Must run before ConcatSpaceFixer.
35+
*/
36+
public function getPriority(): int
37+
{
38+
return 1;
39+
}
40+
41+
public function isCandidate(Tokens $tokens): bool
42+
{
43+
return $tokens->isTokenKindFound(\T_DIR);
44+
}
45+
46+
public function isRisky(): bool
47+
{
48+
return false;
49+
}
50+
51+
public function fix(\SplFileInfo $file, Tokens $tokens): void
52+
{
53+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
54+
if (!$tokens[$index]->isGivenKind(\T_DIR)) {
55+
continue;
56+
}
57+
58+
$prevInserts = $this->getPrevTokensUpdates($tokens, $index);
59+
if ($prevInserts === null) {
60+
continue;
61+
}
62+
63+
$nextInserts = $this->getNextTokensUpdates($tokens, $index);
64+
if ($nextInserts === null) {
65+
continue;
66+
}
67+
68+
foreach ($prevInserts + $nextInserts as $i => $content) {
69+
if ($content === '') {
70+
$tokens->clearTokenAndMergeSurroundingWhitespace($i);
71+
} else {
72+
$tokens[$i] = new Token([\T_CONSTANT_ENCAPSED_STRING, $content]);
73+
}
74+
}
75+
}
76+
}
77+
78+
/**
79+
* @return null|array<int, string>
80+
*/
81+
private function getPrevTokensUpdates(Tokens $tokens, int $index): ?array
82+
{
83+
$updates = [];
84+
85+
/** @var int $openParenthesisIndex */
86+
$openParenthesisIndex = $tokens->getPrevMeaningfulToken($index);
87+
if (!$tokens[$openParenthesisIndex]->equals('(')) {
88+
return null;
89+
}
90+
$updates[$openParenthesisIndex] = '';
91+
92+
/** @var int $dirnameCallIndex */
93+
$dirnameCallIndex = $tokens->getPrevMeaningfulToken($openParenthesisIndex);
94+
if (!$tokens[$dirnameCallIndex]->equals([\T_STRING, 'dirname'], false)) {
95+
return null;
96+
}
97+
if (!(new FunctionsAnalyzer())->isGlobalFunctionCall($tokens, $dirnameCallIndex)) {
98+
return null;
99+
}
100+
$updates[$dirnameCallIndex] = '';
101+
102+
/** @var int $namespaceSeparatorIndex */
103+
$namespaceSeparatorIndex = $tokens->getPrevMeaningfulToken($dirnameCallIndex);
104+
if ($tokens[$namespaceSeparatorIndex]->isGivenKind(\T_NS_SEPARATOR)) {
105+
$updates[$namespaceSeparatorIndex] = '';
106+
}
107+
108+
return $updates;
109+
}
110+
111+
/**
112+
* @return null|array<int, string>
113+
*/
114+
private function getNextTokensUpdates(Tokens $tokens, int $index): ?array
115+
{
116+
$depthLevel = 1;
117+
$updates = [];
118+
119+
/** @var int $commaOrClosingParenthesisIndex */
120+
$commaOrClosingParenthesisIndex = $tokens->getNextMeaningfulToken($index);
121+
if ($tokens[$commaOrClosingParenthesisIndex]->equals(',')) {
122+
$updates[$commaOrClosingParenthesisIndex] = '';
123+
/** @var int $afterCommaIndex */
124+
$afterCommaIndex = $tokens->getNextMeaningfulToken($commaOrClosingParenthesisIndex);
125+
if ($tokens[$afterCommaIndex]->isGivenKind(\T_LNUMBER)) {
126+
$depthLevel = (int) $tokens[$afterCommaIndex]->getContent();
127+
$updates[$afterCommaIndex] = '';
128+
/** @var int $commaOrClosingParenthesisIndex */
129+
$commaOrClosingParenthesisIndex = $tokens->getNextMeaningfulToken($afterCommaIndex);
130+
}
131+
}
132+
133+
if ($tokens[$commaOrClosingParenthesisIndex]->equals(',')) {
134+
$updates[$commaOrClosingParenthesisIndex] = '';
135+
/** @var int $commaOrClosingParenthesisIndex */
136+
$commaOrClosingParenthesisIndex = $tokens->getNextMeaningfulToken($commaOrClosingParenthesisIndex);
137+
}
138+
$closingParenthesisIndex = $commaOrClosingParenthesisIndex;
139+
140+
if (!$tokens[$closingParenthesisIndex]->equals(')')) {
141+
return null;
142+
}
143+
$updates[$closingParenthesisIndex] = '';
144+
145+
/** @var int $concatenationIndex */
146+
$concatenationIndex = $tokens->getNextMeaningfulToken($closingParenthesisIndex);
147+
if (!$tokens[$concatenationIndex]->equals('.')) {
148+
return null;
149+
}
150+
151+
/** @var int $stringIndex */
152+
$stringIndex = $tokens->getNextMeaningfulToken($concatenationIndex);
153+
if (!$tokens[$stringIndex]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
154+
return null;
155+
}
156+
157+
$stringContent = $tokens[$stringIndex]->getContent();
158+
$updates[$stringIndex] = \substr($stringContent, 0, 1) . \str_repeat('/..', $depthLevel) . \substr($stringContent, 1);
159+
160+
return $updates;
161+
}
162+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 Tests\Fixer;
13+
14+
/**
15+
* @internal
16+
*
17+
* @covers \PhpCsFixerCustomFixers\Fixer\NoUselessDirnameCallFixer
18+
*/
19+
final class NoUselessDirnameCallFixerTest extends AbstractFixerTestCase
20+
{
21+
public function testIsRisky(): void
22+
{
23+
self::assertFalse($this->fixer->isRisky());
24+
}
25+
26+
/**
27+
* @dataProvider provideFixCases
28+
*/
29+
public function testFix(string $expected, ?string $input = null): void
30+
{
31+
$this->doTest($expected, $input);
32+
}
33+
34+
/**
35+
* @return iterable<array{0: string, 1?: string}>
36+
*/
37+
public static function provideFixCases(): iterable
38+
{
39+
yield ['<?php Vendor\dirname(__DIR__) . "/path";'];
40+
yield ['<?php dearname(__DIR__) . "/path";'];
41+
yield ['<?php dirname(__DIR__, LEVEL) . "/path";'];
42+
yield ['<?php dirname(__DIR__) . $path;'];
43+
yield ['<?php dirname(__DIR__); "/path";'];
44+
45+
yield [
46+
"<?php __DIR__ . '/../path';",
47+
"<?php dirname(__DIR__) . '/path';",
48+
];
49+
50+
yield [
51+
'<?php __DIR__ . "/../path";',
52+
'<?php dirname(__DIR__) . "/path";',
53+
];
54+
55+
yield [
56+
'<?php __DIR__ . "/../path";',
57+
'<?php \dirname(__DIR__) . "/path";',
58+
];
59+
60+
yield [
61+
'<?php __DIR__ . "/../../../path";',
62+
'<?php \dirname(__DIR__,3) . "/path";',
63+
];
64+
65+
yield [
66+
'<?php __DIR__ . "/../path";',
67+
'<?php DIRNAME(__DIR__) . "/path";',
68+
];
69+
70+
yield [
71+
"<?php __DIR__ . '/../path';",
72+
"<?php dirname ( __DIR__ ) . '/path';",
73+
];
74+
75+
yield [
76+
'<?php
77+
__DIR__ . "/../path1";
78+
foo(__DIR__) . "/path2";
79+
dirname(__DIR__, $level) . "/path3";
80+
__DIR__ . "/../path4";
81+
',
82+
'<?php
83+
dirname(__DIR__) . "/path1";
84+
foo(__DIR__) . "/path2";
85+
dirname(__DIR__, $level) . "/path3";
86+
dirname(__DIR__) . "/path4";
87+
',
88+
];
89+
90+
// test with trailing comma
91+
if (\PHP_VERSION_ID >= 70300) {
92+
yield [
93+
'<?php __DIR__ . "/../path";',
94+
'<?php dirname(__DIR__,) . "/path";',
95+
];
96+
97+
yield [
98+
'<?php __DIR__ . "/../../../path";',
99+
'<?php \dirname(__DIR__,3,) . "/path";',
100+
];
101+
}
102+
}
103+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
--CONFIGURATION--
2+
{ "PhpCsFixerCustomFixers/no_useless_dirname_call": true, "concat_space": true }
3+
--EXPECTED--
4+
<?php
5+
__DIR__.'/../path';
6+
7+
--INPUT--
8+
<?php
9+
dirname( __DIR__ ).'/path';

0 commit comments

Comments
 (0)