Skip to content

Commit ea85a36

Browse files
authored
Add NumericLiteralSeparatorFixer (#162)
1 parent 30a0d30 commit ea85a36

File tree

7 files changed

+423
-2
lines changed

7 files changed

+423
-2
lines changed

.php_cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ return PhpCsFixer\Config::create()
137137
return $carry;
138138
}
139139

140+
if ($fixer instanceof PhpCsFixerCustomFixers\Fixer\NoNullableBooleanTypeFixer) {
141+
return $carry;
142+
}
143+
140144
if ($fixer instanceof PhpCsFixerCustomFixers\Fixer\NoReferenceInFunctionDefinitionFixer) {
141145
return $carry;
142146
}
@@ -156,6 +160,7 @@ return PhpCsFixer\Config::create()
156160
'implements',
157161
'internal',
158162
'param',
163+
'requires',
159164
'return',
160165
'var',
161166
]];

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# CHANGELOG for PHP CS Fixer: custom fixers
22

3-
## [Unreleased]
3+
## v2.1.0 - [Unreleased]
44
- Add CommentedOutFunctionFixer
55
- Add NoDuplicatedArrayKeyFixer
6+
- Add NumericLiteralSeparatorFixer
67

78
## v2.0.0 - *2020-03-01*
89
- Drop PHP 7.1 support

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[![Travis CI build status](https://img.shields.io/travis/kubawerlos/php-cs-fixer-custom-fixers/master.svg?label=Travis+CI)](https://travis-ci.org/kubawerlos/php-cs-fixer-custom-fixers)
1010
[![AppVeyor build status](https://img.shields.io/appveyor/ci/kubawerlos/php-cs-fixer-custom-fixers/master?label=AppVeyor)](https://ci.appveyor.com/project/kubawerlos/php-cs-fixer-custom-fixers)
1111
[![Code coverage](https://img.shields.io/coveralls/github/kubawerlos/php-cs-fixer-custom-fixers/master.svg)](https://coveralls.io/github/kubawerlos/php-cs-fixer-custom-fixers?branch=master)
12-
![Tests](https://img.shields.io/badge/tests-2054-brightgreen.svg)
12+
![Tests](https://img.shields.io/badge/tests-2168-brightgreen.svg)
1313
[![Mutation testing badge](https://badge.stryker-mutator.io/github.com/kubawerlos/php-cs-fixer-custom-fixers/master)](https://stryker-mutator.github.io)
1414
[![Psalm type coverage](https://shepherd.dev/github/kubawerlos/php-cs-fixer-custom-fixers/coverage.svg)](https://shepherd.dev/github/kubawerlos/php-cs-fixer-custom-fixers)
1515

@@ -278,6 +278,28 @@ Function `sprintf` without parameters should not be used.
278278
+$foo = 'Foo';
279279
```
280280

281+
#### NumericLiteralSeparatorFixer
282+
Numeric literals must have configured separators.
283+
Configuration options:
284+
- `binary` (`bool`, `null`): whether add, remove or ignore separators in binary numbers.; defaults to `false`
285+
- `decimal` (`bool`, `null`): whether add, remove or ignore separators in decimal numbers.; defaults to `false`
286+
- `float` (`bool`, `null`): whether add, remove or ignore separators in float numbers.; defaults to `false`
287+
- `hexadecimal` (`bool`, `null`): whether add, remove or ignore separators in hexadecimal numbers.; defaults to `false`
288+
- `octal` (`bool`, `null`): whether add, remove or ignore separators in octal numbers.; defaults to `false`
289+
```diff
290+
<?php
291+
-echo 0b01010100_01101000; // binary
292+
-echo 135_798_642; // decimal
293+
-echo 1_234.456_78e-4_321; // float
294+
-echo 0xAE_B0_42_FC; // hexadecimal
295+
-echo 0123_4567; // octal
296+
+echo 0b0101010001101000; // binary
297+
+echo 135798642; // decimal
298+
+echo 1234.45678e-4321; // float
299+
+echo 0xAEB042FC; // hexadecimal
300+
+echo 01234567; // octal
301+
```
302+
281303
#### OperatorLinebreakFixer
282304
Operators must always be at the beginning or at the end of the line.
283305
*To be deprecated after [this](https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/4021) is merged and released.*
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PhpCsFixerCustomFixers\Fixer;
6+
7+
use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
8+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
9+
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
10+
use PhpCsFixer\FixerDefinition\FixerDefinition;
11+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
12+
use PhpCsFixer\FixerDefinition\VersionSpecification;
13+
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
14+
use PhpCsFixer\Preg;
15+
use PhpCsFixer\Tokenizer\Token;
16+
use PhpCsFixer\Tokenizer\Tokens;
17+
18+
final class NumericLiteralSeparatorFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
19+
{
20+
/** @var null|bool */
21+
private $binarySeparator = false;
22+
23+
/** @var null|bool */
24+
private $decimalSeparator = false;
25+
26+
/** @var null|bool */
27+
private $floatSeparator = false;
28+
29+
/** @var null|bool */
30+
private $hexadecimalSeparator = false;
31+
32+
/** @var null|bool */
33+
private $octalSeparator = false;
34+
35+
public function getDefinition(): FixerDefinitionInterface
36+
{
37+
return new FixerDefinition(
38+
'Numeric literals must have configured separators.',
39+
[new VersionSpecificCodeSample(
40+
'<?php
41+
echo 0b01010100_01101000; // binary
42+
echo 135_798_642; // decimal
43+
echo 1_234.456_78e-4_321; // float
44+
echo 0xAE_B0_42_FC; // hexadecimal
45+
echo 0123_4567; // octal
46+
',
47+
new VersionSpecification(70400)
48+
)]
49+
);
50+
}
51+
52+
public function getConfigurationDefinition(): FixerConfigurationResolver
53+
{
54+
return new FixerConfigurationResolver([
55+
(new FixerOptionBuilder('binary', 'whether add, remove or ignore separators in binary numbers.'))
56+
->setAllowedTypes(['bool', 'null'])
57+
->setDefault($this->binarySeparator)
58+
->getOption(),
59+
(new FixerOptionBuilder('decimal', 'whether add, remove or ignore separators in decimal numbers.'))
60+
->setAllowedTypes(['bool', 'null'])
61+
->setDefault($this->decimalSeparator)
62+
->getOption(),
63+
(new FixerOptionBuilder('float', 'whether add, remove or ignore separators in float numbers.'))
64+
->setAllowedTypes(['bool', 'null'])
65+
->setDefault($this->floatSeparator)
66+
->getOption(),
67+
(new FixerOptionBuilder('hexadecimal', 'whether add, remove or ignore separators in hexadecimal numbers.'))
68+
->setAllowedTypes(['bool', 'null'])
69+
->setDefault($this->hexadecimalSeparator)
70+
->getOption(),
71+
(new FixerOptionBuilder('octal', 'whether add, remove or ignore separators in octal numbers.'))
72+
->setAllowedTypes(['bool', 'null'])
73+
->setDefault($this->octalSeparator)
74+
->getOption(),
75+
]);
76+
}
77+
78+
public function configure(?array $configuration = null): void
79+
{
80+
/** @var array<null|bool> $configuration */
81+
$configuration = $configuration ?? [];
82+
83+
$this->binarySeparator = \array_key_exists('binary', $configuration) ? $configuration['binary'] : $this->binarySeparator;
84+
$this->decimalSeparator = \array_key_exists('decimal', $configuration) ? $configuration['decimal'] : $this->decimalSeparator;
85+
$this->floatSeparator = \array_key_exists('float', $configuration) ? $configuration['float'] : $this->floatSeparator;
86+
$this->hexadecimalSeparator = \array_key_exists('hexadecimal', $configuration) ? $configuration['hexadecimal'] : $this->hexadecimalSeparator;
87+
$this->octalSeparator = \array_key_exists('octal', $configuration) ? $configuration['octal'] : $this->octalSeparator;
88+
}
89+
90+
public function getPriority(): int
91+
{
92+
return 0;
93+
}
94+
95+
public function isCandidate(Tokens $tokens): bool
96+
{
97+
return \PHP_VERSION_ID >= 70400 && $tokens->isAnyTokenKindsFound([T_DNUMBER, T_LNUMBER]);
98+
}
99+
100+
public function isRisky(): bool
101+
{
102+
return false;
103+
}
104+
105+
public function fix(\SplFileInfo $file, Tokens $tokens): void
106+
{
107+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
108+
if (!$tokens[$index]->isGivenKind([T_DNUMBER, T_LNUMBER])) {
109+
continue;
110+
}
111+
112+
$content = $tokens[$index]->getContent();
113+
$newContent = $this->getNewContent($content);
114+
115+
if ($content !== $newContent) {
116+
$tokens[$index] = new Token([$tokens[$index]->getId(), $newContent]);
117+
}
118+
}
119+
}
120+
121+
private function getNewContent(string $content): string
122+
{
123+
if (\strpos($content, '.') !== false) {
124+
$content = $this->updateContent($content, null, '.', 3, $this->floatSeparator);
125+
$content = $this->updateContent($content, '.', 'e', 3, $this->floatSeparator, false);
126+
127+
return $this->updateContent($content, 'e', null, 3, $this->floatSeparator);
128+
}
129+
130+
if (\stripos($content, '0b') === 0) {
131+
return $this->updateContent($content, 'b', null, 8, $this->binarySeparator);
132+
}
133+
134+
if (\stripos($content, '0x') === 0) {
135+
return $this->updateContent($content, 'x', null, 2, $this->hexadecimalSeparator);
136+
}
137+
138+
if (Preg::match('/e-?[\d_]+$/i', $content) === 1) {
139+
$content = $this->updateContent($content, null, 'e', 3, $this->floatSeparator);
140+
141+
return $this->updateContent($content, 'e', null, 3, $this->floatSeparator);
142+
}
143+
144+
if (\strpos($content, '0') === 0) {
145+
return $this->updateContent($content, '0', null, 4, $this->octalSeparator);
146+
}
147+
148+
return $this->updateContent($content, null, null, 3, $this->decimalSeparator);
149+
}
150+
151+
private function updateContent(string $content, ?string $startCharacter, ?string $endCharacter, int $groupSize, ?bool $addSeparators, bool $fromRight = true): string
152+
{
153+
if ($addSeparators === null) {
154+
return $content;
155+
}
156+
157+
$startPosition = $this->getStartPosition($content, $startCharacter);
158+
if ($startPosition === null) {
159+
return $content;
160+
}
161+
$endPosition = $this->getEndPosition($content, $endCharacter);
162+
163+
$substringToUpdate = \substr($content, $startPosition, $endPosition - $startPosition);
164+
$substringToUpdate = \str_replace('_', '', $substringToUpdate);
165+
166+
if ($addSeparators) {
167+
if ($fromRight) {
168+
$substringToUpdate = \strrev($substringToUpdate);
169+
}
170+
171+
/** @var string $substringToUpdate */
172+
$substringToUpdate = Preg::replace(\sprintf('/[\da-fA-F]{%d}(?!-)(?!$)/', $groupSize), '$0_', $substringToUpdate);
173+
174+
if ($fromRight) {
175+
$substringToUpdate = \strrev($substringToUpdate);
176+
}
177+
}
178+
179+
return \substr($content, 0, $startPosition) . $substringToUpdate . \substr($content, $endPosition);
180+
}
181+
182+
private function getStartPosition(string $content, ?string $startCharacter): ?int
183+
{
184+
if ($startCharacter === null) {
185+
return 0;
186+
}
187+
188+
$startPosition = \stripos($content, $startCharacter);
189+
190+
if ($startPosition === false) {
191+
return null;
192+
}
193+
194+
return $startPosition + 1;
195+
}
196+
197+
private function getEndPosition(string $content, ?string $endCharacter): int
198+
{
199+
if ($endCharacter === null) {
200+
return \strlen($content);
201+
}
202+
203+
$endPosition = \stripos($content, $endCharacter);
204+
205+
if ($endPosition === false) {
206+
return \strlen($content);
207+
}
208+
209+
return $endPosition;
210+
}
211+
}

tests/Fixer/AbstractFixerTestCase.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ final public function testCodeSampleEndsWithNewLine(): void
6969
static::assertRegExp('/\n$/', $codeSample->getCode());
7070
}
7171

72+
/**
73+
* @coversNothing
74+
*/
7275
final public function testCodeSampleIsChangedDuringFixing(): void
7376
{
7477
$codeSample = $this->fixer->getDefinition()->getCodeSamples()[0];

0 commit comments

Comments
 (0)