NumericLiteralSeparatorFixer.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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\Basic;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  15. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
  16. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
  17. use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
  18. use PhpCsFixer\FixerDefinition\CodeSample;
  19. use PhpCsFixer\FixerDefinition\FixerDefinition;
  20. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  21. use PhpCsFixer\Preg;
  22. use PhpCsFixer\Tokenizer\Token;
  23. use PhpCsFixer\Tokenizer\Tokens;
  24. /**
  25. * Let's you add underscores to numeric literals.
  26. *
  27. * Inspired by:
  28. * - {@link https://github.com/kubawerlos/php-cs-fixer-custom-fixers/blob/main/src/Fixer/NumericLiteralSeparatorFixer.php}
  29. * - {@link https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/rules/numeric-separators-style.js}
  30. *
  31. * @author Marvin Heilemann <marvin.heilemann+github@googlemail.com>
  32. * @author Greg Korba <greg@codito.dev>
  33. */
  34. final class NumericLiteralSeparatorFixer extends AbstractFixer implements ConfigurableFixerInterface
  35. {
  36. public const STRATEGY_USE_SEPARATOR = 'use_separator';
  37. public const STRATEGY_NO_SEPARATOR = 'no_separator';
  38. public function getDefinition(): FixerDefinitionInterface
  39. {
  40. return new FixerDefinition(
  41. 'Adds separators to numeric literals of any kind.',
  42. [
  43. new CodeSample(
  44. <<<'PHP'
  45. <?php
  46. $integer = 1234_5678;
  47. $octal = 01_234_56;
  48. $binary = 0b00_10_01_00;
  49. $hexadecimal = 0x3D45_8F4F;
  50. PHP
  51. ),
  52. new CodeSample(
  53. <<<'PHP'
  54. <?php
  55. $integer = 12345678;
  56. $octal = 0123456;
  57. $binary = 0b0010010011011010;
  58. $hexadecimal = 0x3D458F4F;
  59. PHP
  60. ,
  61. ['strategy' => self::STRATEGY_USE_SEPARATOR],
  62. ),
  63. new CodeSample(
  64. "<?php \$var = 24_40_21;\n",
  65. ['override_existing' => true]
  66. ),
  67. ]
  68. );
  69. }
  70. public function isCandidate(Tokens $tokens): bool
  71. {
  72. return $tokens->isAnyTokenKindsFound([T_DNUMBER, T_LNUMBER]);
  73. }
  74. protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
  75. {
  76. return new FixerConfigurationResolver([
  77. (new FixerOptionBuilder(
  78. 'override_existing',
  79. 'Whether literals already containing underscores should be reformatted.'
  80. ))
  81. ->setAllowedTypes(['bool'])
  82. ->setDefault(false)
  83. ->getOption(),
  84. (new FixerOptionBuilder(
  85. 'strategy',
  86. 'Whether numeric literal should be separated by underscores or not.'
  87. ))
  88. ->setAllowedValues([self::STRATEGY_USE_SEPARATOR, self::STRATEGY_NO_SEPARATOR])
  89. ->setDefault(self::STRATEGY_NO_SEPARATOR)
  90. ->getOption(),
  91. ]);
  92. }
  93. protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  94. {
  95. foreach ($tokens as $index => $token) {
  96. if (!$token->isGivenKind([T_DNUMBER, T_LNUMBER])) {
  97. continue;
  98. }
  99. $content = $token->getContent();
  100. $newContent = $this->formatValue($content);
  101. if ($content === $newContent) {
  102. // Skip Token override if its the same content, like when it
  103. // already got a valid literal separator structure.
  104. continue;
  105. }
  106. $tokens[$index] = new Token([$token->getId(), $newContent]);
  107. }
  108. }
  109. private function formatValue(string $value): string
  110. {
  111. if (self::STRATEGY_NO_SEPARATOR === $this->configuration['strategy']) {
  112. return str_contains($value, '_') ? str_replace('_', '', $value) : $value;
  113. }
  114. if (true === $this->configuration['override_existing']) {
  115. $value = str_replace('_', '', $value);
  116. } elseif (str_contains($value, '_')) {
  117. // Keep already underscored literals untouched.
  118. return $value;
  119. }
  120. $lowerValue = strtolower($value);
  121. if (str_starts_with($lowerValue, '0b')) {
  122. // Binary
  123. return $this->insertEveryRight($value, 8, 2);
  124. }
  125. if (str_starts_with($lowerValue, '0x')) {
  126. // Hexadecimal
  127. return $this->insertEveryRight($value, 2, 2);
  128. }
  129. if (str_starts_with($lowerValue, '0o')) {
  130. // Octal
  131. return $this->insertEveryRight($value, 3, 2);
  132. }
  133. if (str_starts_with($lowerValue, '0')) {
  134. // Octal prior PHP 8.1
  135. return $this->insertEveryRight($value, 3, 1);
  136. }
  137. // All other types
  138. /** If its a negative value we need an offset */
  139. $negativeOffset = static fn ($v) => str_contains($v, '-') ? 1 : 0;
  140. Preg::matchAll('/([0-9-_]+)((\.)([0-9_]+))?((e)([0-9-_]+))?/i', $value, $result);
  141. $integer = $result[1][0];
  142. $joinedValue = $this->insertEveryRight($integer, 3, $negativeOffset($integer));
  143. $dot = $result[3][0];
  144. if ('' !== $dot) {
  145. $integer = $result[4][0];
  146. $decimal = $this->insertEveryLeft($integer, 3, $negativeOffset($integer));
  147. $joinedValue = $joinedValue.$dot.$decimal;
  148. }
  149. $tim = $result[6][0];
  150. if ('' !== $tim) {
  151. $integer = $result[7][0];
  152. $times = $this->insertEveryRight($integer, 3, $negativeOffset($integer));
  153. $joinedValue = $joinedValue.$tim.$times;
  154. }
  155. return $joinedValue;
  156. }
  157. private function insertEveryRight(string $value, int $length, int $offset = 0): string
  158. {
  159. $position = $length * -1;
  160. while ($position > -(\strlen($value) - $offset)) {
  161. $value = substr_replace($value, '_', $position, 0);
  162. $position -= $length + 1;
  163. }
  164. return $value;
  165. }
  166. private function insertEveryLeft(string $value, int $length, int $offset = 0): string
  167. {
  168. $position = $length;
  169. while ($position < \strlen($value)) {
  170. $value = substr_replace($value, '_', $position, $offset);
  171. $position += $length + 1;
  172. }
  173. return $value;
  174. }
  175. }