| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- <?php
- declare(strict_types=1);
- /*
- * 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;
- use PhpCsFixer\AbstractFixer;
- use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
- use PhpCsFixer\Tokenizer\Analyzer\RangeAnalyzer;
- use PhpCsFixer\Tokenizer\CT;
- use PhpCsFixer\Tokenizer\Token;
- use PhpCsFixer\Tokenizer\Tokens;
- /**
- * @internal
- */
- abstract class AbstractShortOperatorFixer extends AbstractFixer
- {
- protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
- {
- $alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
- for ($index = \count($tokens) - 1; $index > 3; --$index) {
- if (!$this->isOperatorTokenCandidate($tokens, $index)) {
- continue;
- }
- // get what is before the operator
- $beforeRange = $this->getBeforeOperatorRange($tokens, $index);
- $equalsIndex = $tokens->getPrevMeaningfulToken($beforeRange['start']);
- // make sure that before that is '='
- if (!$tokens[$equalsIndex]->equals('=')) {
- continue;
- }
- // get and check what is before '='
- $assignRange = $this->getBeforeOperatorRange($tokens, $equalsIndex);
- $beforeAssignmentIndex = $tokens->getPrevMeaningfulToken($assignRange['start']);
- if ($tokens[$beforeAssignmentIndex]->equals(':')) {
- if (!$this->belongsToSwitchOrAlternativeSyntax($alternativeSyntaxAnalyzer, $tokens, $beforeAssignmentIndex)) {
- continue;
- }
- } elseif (!$tokens[$beforeAssignmentIndex]->equalsAny([';', '{', '}', '(', ')', ',', [T_OPEN_TAG], [T_RETURN]])) {
- continue;
- }
- // check if "assign" and "before" the operator are (functionally) the same
- if (RangeAnalyzer::rangeEqualsRange($tokens, $assignRange, $beforeRange)) {
- $this->shortenOperation($tokens, $equalsIndex, $index, $assignRange, $beforeRange);
- continue;
- }
- if (!$this->isOperatorCommutative($tokens[$index])) {
- continue;
- }
- $afterRange = $this->getAfterOperatorRange($tokens, $index);
- // check if "assign" and "after" the operator are (functionally) the same
- if (!RangeAnalyzer::rangeEqualsRange($tokens, $assignRange, $afterRange)) {
- continue;
- }
- $this->shortenOperation($tokens, $equalsIndex, $index, $assignRange, $afterRange);
- }
- }
- abstract protected function getReplacementToken(Token $token): Token;
- abstract protected function isOperatorTokenCandidate(Tokens $tokens, int $index): bool;
- /**
- * @param array{start: int, end: int} $assignRange
- * @param array{start: int, end: int} $operatorRange
- */
- private function shortenOperation(
- Tokens $tokens,
- int $equalsIndex,
- int $operatorIndex,
- array $assignRange,
- array $operatorRange
- ): void {
- $tokens[$equalsIndex] = $this->getReplacementToken($tokens[$operatorIndex]);
- $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndex);
- $this->clearMeaningfulFromRange($tokens, $operatorRange);
- foreach ([$equalsIndex, $assignRange['end']] as $i) {
- $i = $tokens->getNonEmptySibling($i, 1);
- if ($tokens[$i]->isWhitespace(" \t")) {
- $tokens[$i] = new Token([T_WHITESPACE, ' ']);
- } elseif (!$tokens[$i]->isWhitespace()) {
- $tokens->insertAt($i, new Token([T_WHITESPACE, ' ']));
- }
- }
- }
- /**
- * @return array{start: int, end: int}
- */
- private function getAfterOperatorRange(Tokens $tokens, int $index): array
- {
- $index = $tokens->getNextMeaningfulToken($index);
- $range = ['start' => $index];
- while (true) {
- $nextIndex = $tokens->getNextMeaningfulToken($index);
- if (null === $nextIndex || $tokens[$nextIndex]->equalsAny([';', ',', [T_CLOSE_TAG]])) {
- break;
- }
- $blockType = Tokens::detectBlockType($tokens[$nextIndex]);
- if (null === $blockType) {
- $index = $nextIndex;
- continue;
- }
- if (false === $blockType['isStart']) {
- break;
- }
- $index = $tokens->findBlockEnd($blockType['type'], $nextIndex);
- }
- $range['end'] = $index;
- return $range;
- }
- /**
- * @return array{start: int, end: int}
- */
- private function getBeforeOperatorRange(Tokens $tokens, int $index): array
- {
- static $blockOpenTypes;
- if (null === $blockOpenTypes) {
- $blockOpenTypes = [',']; // not a true "block type", but speeds up things
- foreach (Tokens::getBlockEdgeDefinitions() as $definition) {
- $blockOpenTypes[] = $definition['start'];
- }
- }
- $controlStructureWithoutBracesTypes = [T_IF, T_ELSE, T_ELSEIF, T_FOR, T_FOREACH, T_WHILE];
- $previousIndex = $tokens->getPrevMeaningfulToken($index);
- $previousToken = $tokens[$previousIndex];
- if ($tokens[$previousIndex]->equalsAny($blockOpenTypes)) {
- return ['start' => $index, 'end' => $index];
- }
- $range = ['end' => $previousIndex];
- $index = $previousIndex;
- while ($previousToken->equalsAny([
- '$',
- ']',
- ')',
- [CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
- [CT::T_DYNAMIC_PROP_BRACE_CLOSE],
- [CT::T_DYNAMIC_VAR_BRACE_CLOSE],
- [T_NS_SEPARATOR],
- [T_STRING],
- [T_VARIABLE],
- ])) {
- $blockType = Tokens::detectBlockType($previousToken);
- if (null !== $blockType) {
- $blockStart = $tokens->findBlockStart($blockType['type'], $previousIndex);
- if ($tokens[$previousIndex]->equals(')') && $tokens[$tokens->getPrevMeaningfulToken($blockStart)]->isGivenKind($controlStructureWithoutBracesTypes)) {
- break; // we went too far back
- }
- $previousIndex = $blockStart;
- }
- $index = $previousIndex;
- $previousIndex = $tokens->getPrevMeaningfulToken($previousIndex);
- $previousToken = $tokens[$previousIndex];
- }
- if ($previousToken->isGivenKind(T_OBJECT_OPERATOR)) {
- $index = $this->getBeforeOperatorRange($tokens, $previousIndex)['start'];
- } elseif ($previousToken->isGivenKind(T_PAAMAYIM_NEKUDOTAYIM)) {
- $index = $this->getBeforeOperatorRange($tokens, $tokens->getPrevMeaningfulToken($previousIndex))['start'];
- }
- $range['start'] = $index;
- return $range;
- }
- /**
- * @param array{start: int, end: int} $range
- */
- private function clearMeaningfulFromRange(Tokens $tokens, array $range): void
- {
- // $range['end'] must be meaningful!
- for ($i = $range['end']; $i >= $range['start']; $i = $tokens->getPrevMeaningfulToken($i)) {
- $tokens->clearTokenAndMergeSurroundingWhitespace($i);
- }
- }
- private function isOperatorCommutative(Token $operatorToken): bool
- {
- static $commutativeKinds = ['*', '|', '&', '^']; // note that for arrays in PHP `+` is not commutative
- static $nonCommutativeKinds = ['-', '/', '.', '%', '+'];
- if ($operatorToken->isGivenKind(T_COALESCE)) {
- return false;
- }
- if ($operatorToken->equalsAny($commutativeKinds)) {
- return true;
- }
- if ($operatorToken->equalsAny($nonCommutativeKinds)) {
- return false;
- }
- throw new \InvalidArgumentException(\sprintf('Not supported operator "%s".', $operatorToken->toJson()));
- }
- private function belongsToSwitchOrAlternativeSyntax(AlternativeSyntaxAnalyzer $alternativeSyntaxAnalyzer, Tokens $tokens, int $index): bool
- {
- $candidate = $index;
- $index = $tokens->getPrevMeaningfulToken($candidate);
- if ($tokens[$index]->isGivenKind(T_DEFAULT)) {
- return true;
- }
- $index = $tokens->getPrevMeaningfulToken($index);
- if ($tokens[$index]->isGivenKind(T_CASE)) {
- return true;
- }
- return $alternativeSyntaxAnalyzer->belongsToAlternativeSyntax($tokens, $candidate);
- }
- }
|