Skip to content

Commit

Permalink
ArrayPushFixer - introduction
Browse files Browse the repository at this point in the history
  • Loading branch information
SpacePossum committed Apr 6, 2020
1 parent 98e354f commit 4400ea5
Show file tree
Hide file tree
Showing 5 changed files with 501 additions and 1 deletion.
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ Choose from the list of available rules:

Each element of an array must be indented exactly once.

* **array_push** [@Symfony:risky, @PhpCsFixer:risky]

Converts simple usages of ``array_push($x, $y);`` to ``$x[] = $y;``.

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

* **array_syntax** [@Symfony, @PhpCsFixer]

PHP arrays should be declared using the configured syntax.
Expand Down
227 changes: 227 additions & 0 deletions src/Fixer/Alias/ArrayPushFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <[email protected]>
* Dariusz Rumiński <[email protected]>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Fixer\Alias;

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

/**
* @author SpacePossum
*/
final class ArrayPushFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition()
{
return new FixerDefinition(
'Converts simple usages of `array_push($x, $y);` to `$x[] = $y;`.',
[new VersionSpecificCodeSample("<?php\narray_push(\$x, \$y);\n", new VersionSpecification(70000))],
null,
'Risky when the function `array_push` is overridden.'
);
}

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

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

protected function applyFix(\SplFileInfo $file, Tokens $tokens)
{
$functionsAnalyzer = new FunctionsAnalyzer();

for ($index = $tokens->count() - 7; $index > 0; --$index) {
if (!$tokens[$index]->equals([T_STRING, 'array_push'], false)) {
continue;
}

if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
continue; // redeclare/override
}

// meaningful before must be `<?php`, `{`, `}` or `;`

$callIndex = $index;
$index = $tokens->getPrevMeaningfulToken($index);
$namespaceSeparatorIndex = null;

if ($tokens[$index]->isGivenKind(T_NS_SEPARATOR)) {
$namespaceSeparatorIndex = $index;
$index = $tokens->getPrevMeaningfulToken($index);
}

if (!$tokens[$index]->equalsAny([';', '{', '}', ')', [T_OPEN_TAG]])) {
continue;
}

// figure out where the arguments list opens

$openBraceIndex = $tokens->getNextMeaningfulToken($callIndex);
$blockType = Tokens::detectBlockType($tokens[$openBraceIndex]);

if (null === $blockType || Tokens::BLOCK_TYPE_PARENTHESIS_BRACE !== $blockType['type']) {
continue;
}

// figure out where the arguments list closes

$closeBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openBraceIndex);

// meaningful after `)` must be `;`, `? >` or nothing

$afterCloseBraceIndex = $tokens->getNextMeaningfulToken($closeBraceIndex);

if (null !== $afterCloseBraceIndex && !$tokens[$afterCloseBraceIndex]->equalsAny([';', [T_CLOSE_TAG]])) {
continue;
}

// must have 2 arguments

// first argument must be a variable (with possibly array indexing etc.),
// after that nothing meaningful should be there till the next `,` or `)`
// if `)` than we cannot fix it (it is a single argument call)

$firstArgumentStop = $this->getFirstArgumentEnd($tokens, $openBraceIndex);
$firstArgumentStop = $tokens->getNextMeaningfulToken($firstArgumentStop);

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

// second argument can be about anything but ellipsis, we must make sure there is not
// a third argument (or more) passed to `array_push`

$secondArgumentStart = $tokens->getNextMeaningfulToken($firstArgumentStop);
$secondArgumentStop = $this->getSecondArgumentEnd($tokens, $secondArgumentStart, $closeBraceIndex);

if (null === $secondArgumentStop) {
continue;
}

// candidate is valid, replace tokens

$tokens->clearTokenAndMergeSurroundingWhitespace($closeBraceIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($firstArgumentStop);
$tokens->insertAt(
$firstArgumentStop,
[
new Token('['),
new Token(']'),
new Token([T_WHITESPACE, ' ']),
new Token('='),
]
);
$tokens->clearTokenAndMergeSurroundingWhitespace($openBraceIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($callIndex);

if (null !== $namespaceSeparatorIndex) {
$tokens->clearTokenAndMergeSurroundingWhitespace($namespaceSeparatorIndex);
}
}
}

/**
* @param int $index
*
* @return int
*/
private function getFirstArgumentEnd(Tokens $tokens, $index)
{
$nextIndex = $tokens->getNextMeaningfulToken($index);
$nextToken = $tokens[$nextIndex];

while ($nextToken->equalsAny([
'$',
'[',
'(',
[CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN],
[CT::T_DYNAMIC_PROP_BRACE_OPEN],
[CT::T_DYNAMIC_VAR_BRACE_OPEN],
[CT::T_NAMESPACE_OPERATOR],
[T_NS_SEPARATOR],
[T_STATIC],
[T_STRING],
[T_VARIABLE],
])) {
$blockType = Tokens::detectBlockType($nextToken);

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

$index = $nextIndex;
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
$nextToken = $tokens[$nextIndex];
}

if ($nextToken->isGivenKind(T_OBJECT_OPERATOR)) {
return $this->getFirstArgumentEnd($tokens, $nextIndex);
}

if ($nextToken->isGivenKind(T_PAAMAYIM_NEKUDOTAYIM)) {
return $this->getFirstArgumentEnd($tokens, $tokens->getNextMeaningfulToken($nextIndex));
}

return $index;
}

/**
* @param int $index
* @param int $endIndex boundary, i.e. tokens index of `)`
*
* @return null|int
*/
private function getSecondArgumentEnd(Tokens $tokens, $index, $endIndex)
{
if ($tokens[$index]->isGivenKind(T_ELLIPSIS)) {
return null;
}

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

for (; $index <= $endIndex; ++$index) {
$blockType = Tokens::detectBlockType($tokens[$index]);

while (null !== $blockType && $blockType['isStart']) {
$index = $tokens->findBlockEnd($blockType['type'], $index);
$index = $tokens->getNextMeaningfulToken($index);
$blockType = Tokens::detectBlockType($tokens[$index]);
}

if ($tokens[$index]->equals(',') || $tokens[$index]->isGivenKind([T_YIELD, T_YIELD_FROM, T_LOGICAL_AND, T_LOGICAL_OR, T_LOGICAL_XOR])) {
return null;
}
}

return $endIndex;
}
}
2 changes: 1 addition & 1 deletion src/Fixer/PhpUnit/PhpUnitExpectationFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ private function fixExpectation(Tokens $tokens, $startIndex, $endIndex)
];

if ($isMultilineWhitespace) {
array_push($tokensOverrideArgStart, new Token([T_WHITESPACE, $indent.$this->whitespacesConfig->getIndent()]));
$tokensOverrideArgStart[] = new Token([T_WHITESPACE, $indent.$this->whitespacesConfig->getIndent()]);
array_unshift($tokensOverrideArgBefore, new Token([T_WHITESPACE, $indent]));
}

Expand Down
1 change: 1 addition & 0 deletions src/RuleSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ final class RuleSet implements RuleSetInterface
],
'@Symfony:risky' => [
'@PHP56Migration:risky' => true,
'array_push' => true,
'combine_nested_dirname' => true,
'dir_constant' => true,
'ereg_to_preg' => true,
Expand Down
Loading

0 comments on commit 4400ea5

Please sign in to comment.