AbstractShortOperatorFixer.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Fixer;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
  15. use PhpCsFixer\Tokenizer\Analyzer\RangeAnalyzer;
  16. use PhpCsFixer\Tokenizer\CT;
  17. use PhpCsFixer\Tokenizer\Token;
  18. use PhpCsFixer\Tokenizer\Tokens;
  19. /**
  20. * @internal
  21. */
  22. abstract class AbstractShortOperatorFixer extends AbstractFixer
  23. {
  24. protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  25. {
  26. $alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
  27. for ($index = \count($tokens) - 1; $index > 3; --$index) {
  28. if (!$this->isOperatorTokenCandidate($tokens, $index)) {
  29. continue;
  30. }
  31. // get what is before the operator
  32. $beforeRange = $this->getBeforeOperatorRange($tokens, $index);
  33. $equalsIndex = $tokens->getPrevMeaningfulToken($beforeRange['start']);
  34. // make sure that before that is '='
  35. if (!$tokens[$equalsIndex]->equals('=')) {
  36. continue;
  37. }
  38. // get and check what is before '='
  39. $assignRange = $this->getBeforeOperatorRange($tokens, $equalsIndex);
  40. $beforeAssignmentIndex = $tokens->getPrevMeaningfulToken($assignRange['start']);
  41. if ($tokens[$beforeAssignmentIndex]->equals(':')) {
  42. if (!$this->belongsToSwitchOrAlternativeSyntax($alternativeSyntaxAnalyzer, $tokens, $beforeAssignmentIndex)) {
  43. continue;
  44. }
  45. } elseif (!$tokens[$beforeAssignmentIndex]->equalsAny([';', '{', '}', '(', ')', ',', [T_OPEN_TAG], [T_RETURN]])) {
  46. continue;
  47. }
  48. // check if "assign" and "before" the operator are (functionally) the same
  49. if (RangeAnalyzer::rangeEqualsRange($tokens, $assignRange, $beforeRange)) {
  50. $this->shortenOperation($tokens, $equalsIndex, $index, $assignRange, $beforeRange);
  51. continue;
  52. }
  53. if (!$this->isOperatorCommutative($tokens[$index])) {
  54. continue;
  55. }
  56. $afterRange = $this->getAfterOperatorRange($tokens, $index);
  57. // check if "assign" and "after" the operator are (functionally) the same
  58. if (!RangeAnalyzer::rangeEqualsRange($tokens, $assignRange, $afterRange)) {
  59. continue;
  60. }
  61. $this->shortenOperation($tokens, $equalsIndex, $index, $assignRange, $afterRange);
  62. }
  63. }
  64. abstract protected function getReplacementToken(Token $token): Token;
  65. abstract protected function isOperatorTokenCandidate(Tokens $tokens, int $index): bool;
  66. /**
  67. * @param array{start: int, end: int} $assignRange
  68. * @param array{start: int, end: int} $operatorRange
  69. */
  70. private function shortenOperation(
  71. Tokens $tokens,
  72. int $equalsIndex,
  73. int $operatorIndex,
  74. array $assignRange,
  75. array $operatorRange
  76. ): void {
  77. $tokens[$equalsIndex] = $this->getReplacementToken($tokens[$operatorIndex]);
  78. $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndex);
  79. $this->clearMeaningfulFromRange($tokens, $operatorRange);
  80. foreach ([$equalsIndex, $assignRange['end']] as $i) {
  81. $i = $tokens->getNonEmptySibling($i, 1);
  82. if ($tokens[$i]->isWhitespace(" \t")) {
  83. $tokens[$i] = new Token([T_WHITESPACE, ' ']);
  84. } elseif (!$tokens[$i]->isWhitespace()) {
  85. $tokens->insertAt($i, new Token([T_WHITESPACE, ' ']));
  86. }
  87. }
  88. }
  89. /**
  90. * @return array{start: int, end: int}
  91. */
  92. private function getAfterOperatorRange(Tokens $tokens, int $index): array
  93. {
  94. $index = $tokens->getNextMeaningfulToken($index);
  95. $range = ['start' => $index];
  96. while (true) {
  97. $nextIndex = $tokens->getNextMeaningfulToken($index);
  98. if (null === $nextIndex || $tokens[$nextIndex]->equalsAny([';', ',', [T_CLOSE_TAG]])) {
  99. break;
  100. }
  101. $blockType = Tokens::detectBlockType($tokens[$nextIndex]);
  102. if (null === $blockType) {
  103. $index = $nextIndex;
  104. continue;
  105. }
  106. if (false === $blockType['isStart']) {
  107. break;
  108. }
  109. $index = $tokens->findBlockEnd($blockType['type'], $nextIndex);
  110. }
  111. $range['end'] = $index;
  112. return $range;
  113. }
  114. /**
  115. * @return array{start: int, end: int}
  116. */
  117. private function getBeforeOperatorRange(Tokens $tokens, int $index): array
  118. {
  119. static $blockOpenTypes;
  120. if (null === $blockOpenTypes) {
  121. $blockOpenTypes = [',']; // not a true "block type", but speeds up things
  122. foreach (Tokens::getBlockEdgeDefinitions() as $definition) {
  123. $blockOpenTypes[] = $definition['start'];
  124. }
  125. }
  126. $controlStructureWithoutBracesTypes = [T_IF, T_ELSE, T_ELSEIF, T_FOR, T_FOREACH, T_WHILE];
  127. $previousIndex = $tokens->getPrevMeaningfulToken($index);
  128. $previousToken = $tokens[$previousIndex];
  129. if ($tokens[$previousIndex]->equalsAny($blockOpenTypes)) {
  130. return ['start' => $index, 'end' => $index];
  131. }
  132. $range = ['end' => $previousIndex];
  133. $index = $previousIndex;
  134. while ($previousToken->equalsAny([
  135. '$',
  136. ']',
  137. ')',
  138. [CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
  139. [CT::T_DYNAMIC_PROP_BRACE_CLOSE],
  140. [CT::T_DYNAMIC_VAR_BRACE_CLOSE],
  141. [T_NS_SEPARATOR],
  142. [T_STRING],
  143. [T_VARIABLE],
  144. ])) {
  145. $blockType = Tokens::detectBlockType($previousToken);
  146. if (null !== $blockType) {
  147. $blockStart = $tokens->findBlockStart($blockType['type'], $previousIndex);
  148. if ($tokens[$previousIndex]->equals(')') && $tokens[$tokens->getPrevMeaningfulToken($blockStart)]->isGivenKind($controlStructureWithoutBracesTypes)) {
  149. break; // we went too far back
  150. }
  151. $previousIndex = $blockStart;
  152. }
  153. $index = $previousIndex;
  154. $previousIndex = $tokens->getPrevMeaningfulToken($previousIndex);
  155. $previousToken = $tokens[$previousIndex];
  156. }
  157. if ($previousToken->isGivenKind(T_OBJECT_OPERATOR)) {
  158. $index = $this->getBeforeOperatorRange($tokens, $previousIndex)['start'];
  159. } elseif ($previousToken->isGivenKind(T_PAAMAYIM_NEKUDOTAYIM)) {
  160. $index = $this->getBeforeOperatorRange($tokens, $tokens->getPrevMeaningfulToken($previousIndex))['start'];
  161. }
  162. $range['start'] = $index;
  163. return $range;
  164. }
  165. /**
  166. * @param array{start: int, end: int} $range
  167. */
  168. private function clearMeaningfulFromRange(Tokens $tokens, array $range): void
  169. {
  170. // $range['end'] must be meaningful!
  171. for ($i = $range['end']; $i >= $range['start']; $i = $tokens->getPrevMeaningfulToken($i)) {
  172. $tokens->clearTokenAndMergeSurroundingWhitespace($i);
  173. }
  174. }
  175. private function isOperatorCommutative(Token $operatorToken): bool
  176. {
  177. static $commutativeKinds = ['*', '|', '&', '^']; // note that for arrays in PHP `+` is not commutative
  178. static $nonCommutativeKinds = ['-', '/', '.', '%', '+'];
  179. if ($operatorToken->isGivenKind(T_COALESCE)) {
  180. return false;
  181. }
  182. if ($operatorToken->equalsAny($commutativeKinds)) {
  183. return true;
  184. }
  185. if ($operatorToken->equalsAny($nonCommutativeKinds)) {
  186. return false;
  187. }
  188. throw new \InvalidArgumentException(\sprintf('Not supported operator "%s".', $operatorToken->toJson()));
  189. }
  190. private function belongsToSwitchOrAlternativeSyntax(AlternativeSyntaxAnalyzer $alternativeSyntaxAnalyzer, Tokens $tokens, int $index): bool
  191. {
  192. $candidate = $index;
  193. $index = $tokens->getPrevMeaningfulToken($candidate);
  194. if ($tokens[$index]->isGivenKind(T_DEFAULT)) {
  195. return true;
  196. }
  197. $index = $tokens->getPrevMeaningfulToken($index);
  198. if ($tokens[$index]->isGivenKind(T_CASE)) {
  199. return true;
  200. }
  201. return $alternativeSyntaxAnalyzer->belongsToAlternativeSyntax($tokens, $candidate);
  202. }
  203. }