Skip to content

Commit

Permalink
feature #3826 Add CombineNestedDirnameFixer (gharlan)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 2.13-dev branch (closes #3826).

Discussion
----------

Add CombineNestedDirnameFixer

Input:

```php
dirname(dirname(dirname($path)));
```

is fixed to:

```php
dirname($path, 3);
```

(Requires PHP >= 7.0)

Commits
-------

398f343 Add CombineNestedDirnameFixer
  • Loading branch information
keradus committed Aug 22, 2018
2 parents ed2f77b + 398f343 commit 791a415
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 0 deletions.
7 changes: 7 additions & 0 deletions README.rst
Expand Up @@ -390,6 +390,13 @@ Choose from the list of available rules:

Calling ``unset`` on multiple items should be done in one call.

* **combine_nested_dirname** [@PHP70Migration:risky, @PHP71Migration:risky]

Replace multiple nested calls of ``dirname`` by only one call with second
``$level`` parameter. Requires PHP >= 7.0.

*Risky rule: risky when the function ``dirname`` is overridden.*

* **comment_to_phpdoc**

Comments with annotation should be docblock when used on structural
Expand Down
226 changes: 226 additions & 0 deletions src/Fixer/FunctionNotation/CombineNestedDirnameFixer.php
@@ -0,0 +1,226 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Fixer\FunctionNotation;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
* @author Gregor Harlan
*/
final class CombineNestedDirnameFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition()
{
return new FixerDefinition(
'Replace multiple nested calls of `dirname` by only one call with second `$level` parameter. Requires PHP >= 7.0.',
[
new VersionSpecificCodeSample(
"<?php\ndirname(dirname(dirname(\$path)));\n",
new VersionSpecification(70000)
),
],
null,
'Risky when the function `dirname` is overridden.'
);
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens)
{
return \PHP_VERSION_ID >= 70000 && $tokens->isTokenKindFound(T_STRING);
}

/**
* {@inheritdoc}
*/
public function isRisky()
{
return true;
}

/**
* {@inheritdoc}
*/
public function getPriority()
{
// should run after DirConstantFixer
// should run before MethodArgumentSpaceFixer, NoSpacesInsideParenthesisFixer, NoTrailingWhitespaceFixer, NoWhitespaceInBlankLineFixer
return 3;
}

/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
{
for ($index = $tokens->count() - 1; 0 <= $index; --$index) {
$token = $tokens[$index];

if (!$token->equals([T_STRING, 'dirname'], false)) {
continue;
}

$dirnameInfo = $this->getDirnameInfo($tokens, $index);

if (!$dirnameInfo) {
continue;
}

$prev = $tokens->getPrevMeaningfulToken($dirnameInfo['indexes'][0]);

if (!$tokens[$prev]->equals('(')) {
continue;
}

$prev = $tokens->getPrevMeaningfulToken($prev);

$firstArgumentEnd = $dirnameInfo['end'];

$dirnameInfoArray = [$dirnameInfo];

while ($dirnameInfo = $this->getDirnameInfo($tokens, $prev, $firstArgumentEnd)) {
$dirnameInfoArray[] = $dirnameInfo;

$prev = $tokens->getPrevMeaningfulToken($dirnameInfo['indexes'][0]);

if (!$tokens[$prev]->equals('(')) {
break;
}

$prev = $tokens->getPrevMeaningfulToken($prev);
$firstArgumentEnd = $dirnameInfo['end'];
}

if (\count($dirnameInfoArray) > 1) {
$this->combineDirnames($tokens, $dirnameInfoArray);
}

$index = $prev;
}
}

/**
* @param Tokens $tokens
* @param int $index Index of `dirname`
* @param null|int $firstArgumentEndIndex Index of last token of first argument of `dirname` call
*
* @return array|bool `false` when it is not a (supported) `dirname` call, an array with info about the dirname call otherwise
*/
private function getDirnameInfo(Tokens $tokens, $index, $firstArgumentEndIndex = null)
{
if (!$tokens[$index]->equals([T_STRING, 'dirname'], false)) {
return false;
}

if (!(new FunctionsAnalyzer())->isGlobalFunctionCall($tokens, $index)) {
return false;
}

$info['indexes'] = [];

$prev = $tokens->getPrevMeaningfulToken($index);

if ($tokens[$prev]->isGivenKind(T_NS_SEPARATOR)) {
$info['indexes'][] = $prev;
}

$info['indexes'][] = $index;

// opening parenthesis "("
$next = $tokens->getNextMeaningfulToken($index);
$info['indexes'][] = $next;

if ($firstArgumentEndIndex) {
$next = $tokens->getNextMeaningfulToken($firstArgumentEndIndex);
} else {
$next = $tokens->getNextMeaningfulToken($next);

if ($tokens[$next]->equals(')')) {
return false;
}

while (!$tokens[$next]->equalsAny([',', ')'])) {
$blockType = Tokens::detectBlockType($tokens[$next]);

if ($blockType) {
$next = $tokens->findBlockEnd($blockType['type'], $next);
}

$next = $tokens->getNextMeaningfulToken($next);
}
}

$info['indexes'][] = $next;

if ($tokens[$next]->equals(')')) {
$info['levels'] = 1;
$info['end'] = $next;

return $info;
}

$next = $tokens->getNextMeaningfulToken($next);

if (!$tokens[$next]->isGivenKind(T_LNUMBER)) {
return false;
}

$info['indexes'][] = $next;
$info['secondArgument'] = $next;
$info['levels'] = (int) $tokens[$next]->getContent();

$next = $tokens->getNextMeaningfulToken($next);

if (!$tokens[$next]->equals(')')) {
return false;
}

$info['indexes'][] = $next;
$info['end'] = $next;

return $info;
}

private function combineDirnames(Tokens $tokens, array $dirnameInfoArray)
{
$outerDirnameInfo = array_pop($dirnameInfoArray);
$levels = $outerDirnameInfo['levels'];

foreach ($dirnameInfoArray as $dirnameInfo) {
$levels += $dirnameInfo['levels'];

foreach ($dirnameInfo['indexes'] as $index) {
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
}
}

$levelsToken = new Token([T_LNUMBER, (string) $levels]);

if (isset($outerDirnameInfo['secondArgument'])) {
$tokens[$outerDirnameInfo['secondArgument']] = $levelsToken;
} else {
$tokens->insertAt($outerDirnameInfo['end'], [new Token(','), $levelsToken]);
}
}
}
9 changes: 9 additions & 0 deletions src/Fixer/LanguageConstruct/DirConstantFixer.php
Expand Up @@ -44,6 +44,15 @@ public function isCandidate(Tokens $tokens)
return $tokens->isTokenKindFound(T_FILE);
}

/**
* {@inheritdoc}
*/
public function getPriority()
{
// should run before CombineNestedDirnameFixer
return 4;
}

/**
* {@inheritdoc}
*/
Expand Down
1 change: 1 addition & 0 deletions src/RuleSet.php
Expand Up @@ -215,6 +215,7 @@ final class RuleSet implements RuleSetInterface
],
'@PHP70Migration:risky' => [
'@PHP56Migration:risky' => true,
'combine_nested_dirname' => true,
'declare_strict_types' => true,
'non_printable_character' => [
'use_escape_sequences_in_strings' => true,
Expand Down
5 changes: 5 additions & 0 deletions tests/AutoReview/FixerFactoryTest.php
Expand Up @@ -77,8 +77,13 @@ public function provideFixersPriorityCases()
[$fixers['combine_consecutive_unsets'], $fixers['no_trailing_whitespace']],
[$fixers['combine_consecutive_unsets'], $fixers['no_whitespace_in_blank_line']],
[$fixers['combine_consecutive_unsets'], $fixers['space_after_semicolon']],
[$fixers['combine_nested_dirname'], $fixers['method_argument_space']],
[$fixers['combine_nested_dirname'], $fixers['no_spaces_inside_parenthesis']],
[$fixers['combine_nested_dirname'], $fixers['no_trailing_whitespace']],
[$fixers['combine_nested_dirname'], $fixers['no_whitespace_in_blank_line']],
[$fixers['declare_strict_types'], $fixers['blank_line_after_opening_tag']],
[$fixers['declare_strict_types'], $fixers['declare_equal_normalize']],
[$fixers['dir_constant'], $fixers['combine_nested_dirname']],
[$fixers['doctrine_annotation_array_assignment'], $fixers['doctrine_annotation_spaces']],
[$fixers['elseif'], $fixers['braces']],
[$fixers['escape_implicit_backslashes'], $fixers['heredoc_to_nowdoc']],
Expand Down
108 changes: 108 additions & 0 deletions tests/Fixer/FunctionNotation/CombineNestedDirnameFixerTest.php
@@ -0,0 +1,108 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Tests\Fixer\FunctionNotation;

use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
* @author Gregor Harlan
*
* @internal
*
* @covers \PhpCsFixer\Fixer\FunctionNotation\CombineNestedDirnameFixer
*/
final class CombineNestedDirnameFixerTest extends AbstractFixerTestCase
{
/**
* @param string $expected
* @param null|string $input
*
* @dataProvider provideFixCases
* @requires PHP 7.0
*/
public function testFix($expected, $input = null)
{
$this->doTest($expected, $input);
}

public function provideFixCases()
{
return [
[
'<?php dirname();',
],
[
'<?php dirname($path);',
],
[
'<?php dirname($path, 3);',
],
[
'<?php dirname($path,2);',
'<?php dirname(dirname($path));',
],
[
'<?php dirname /* a */ ( /* b */ /* c */ $path /* d */,2);',
'<?php dirname /* a */ ( /* b */ dirname( /* c */ $path) /* d */);',
],
[
'<?php dirname($path,3);',
'<?php dirname(\dirname(dirname($path)));',
],
[
'<?php dirname($path ,4);',
'<?php dirname(dirname($path, 3));',
],
[
'<?php dirname($path, 4);',
'<?php dirname(dirname($path), 3);',
],
[
'<?php dirname($path , 5);',
'<?php dirname(dirname($path, 2), 3);',
],
[
'<?php dirname($path ,5);',
'<?php dirname(dirname(dirname($path), 3));',
],
[
'<?php dirname(dirname($path, $level));',
],
[
'<?php dirname("foo/".dirname($path));',
],
[
'<?php dirname(dirname($path).$foo);',
],
[
'<?php foo\dirname(dirname($path));',
],
[
'<?php dirname(foo(dirname($path,2)),2);',
'<?php dirname(dirname(foo(dirname(dirname($path)))));',
],
[
'<?php new dirname(dirname($path,2));',
'<?php new dirname(dirname(dirname($path)));',
],
];
}

/**
* @requires PHP <7.0
*/
public function testDoNotFix()
{
$this->doTest('<?php dirname(dirname($path));');
}
}

0 comments on commit 791a415

Please sign in to comment.