mirror of
https://github.com/rectorphp/rector.git
synced 2024-06-07 11:50:51 +00:00
f01725a084
Co-authored-by: GitHub Action <action@github.com>
188 lines
4.9 KiB
PHP
188 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Rector\Php74\Rector\LNumber;
|
|
|
|
use Nette\Utils\Strings;
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Scalar\DNumber;
|
|
use PhpParser\Node\Scalar\LNumber;
|
|
use Rector\Core\Contract\Rector\AllowEmptyConfigurableRectorInterface;
|
|
use Rector\Core\Rector\AbstractRector;
|
|
use Rector\Core\Util\StringUtils;
|
|
use Rector\Core\ValueObject\PhpVersionFeature;
|
|
use Rector\NodeTypeResolver\Node\AttributeKey;
|
|
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
|
|
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
|
|
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
|
|
use Webmozart\Assert\Assert;
|
|
|
|
/**
|
|
* @changelog https://wiki.php.net/rfc/numeric_literal_separator
|
|
* @changelog https://github.com/nikic/PHP-Parser/pull/615
|
|
* @see \Rector\Tests\Php74\Rector\LNumber\AddLiteralSeparatorToNumberRector\AddLiteralSeparatorToNumberRectorTest
|
|
* @changelog https://twitter.com/seldaek/status/1329064983120982022
|
|
*
|
|
* Taking the most generic use case to the account: https://wiki.php.net/rfc/numeric_literal_separator#should_it_be_the_role_of_an_ide_to_group_digits
|
|
* The final check should be done manually
|
|
*/
|
|
final class AddLiteralSeparatorToNumberRector extends AbstractRector implements AllowEmptyConfigurableRectorInterface, MinPhpVersionInterface
|
|
{
|
|
/**
|
|
* @api
|
|
* @var string
|
|
*/
|
|
public const LIMIT_VALUE = 'limit_value';
|
|
|
|
/**
|
|
* @var int
|
|
*/
|
|
private const GROUP_SIZE = 3;
|
|
|
|
/**
|
|
* @var int
|
|
*/
|
|
private const DEFAULT_LIMIT_VALUE = 1_000_000;
|
|
|
|
private int $limitValue = self::DEFAULT_LIMIT_VALUE;
|
|
|
|
/**
|
|
* @param mixed[] $configuration
|
|
*/
|
|
public function configure(array $configuration): void
|
|
{
|
|
$limitValue = $configuration[self::LIMIT_VALUE] ?? self::DEFAULT_LIMIT_VALUE;
|
|
Assert::integer($limitValue);
|
|
|
|
$this->limitValue = $limitValue;
|
|
}
|
|
|
|
public function getRuleDefinition(): RuleDefinition
|
|
{
|
|
return new RuleDefinition(
|
|
'Add "_" as thousands separator in numbers for higher or equals to limitValue config',
|
|
[
|
|
new ConfiguredCodeSample(
|
|
<<<'CODE_SAMPLE'
|
|
class SomeClass
|
|
{
|
|
public function run()
|
|
{
|
|
$int = 500000;
|
|
$float = 1000500.001;
|
|
}
|
|
}
|
|
CODE_SAMPLE
|
|
,
|
|
<<<'CODE_SAMPLE'
|
|
class SomeClass
|
|
{
|
|
public function run()
|
|
{
|
|
$int = 500_000;
|
|
$float = 1_000_500.001;
|
|
}
|
|
}
|
|
CODE_SAMPLE
|
|
,
|
|
[
|
|
self::LIMIT_VALUE => 1_000_000,
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<class-string<Node>>
|
|
*/
|
|
public function getNodeTypes(): array
|
|
{
|
|
return [LNumber::class, DNumber::class];
|
|
}
|
|
|
|
/**
|
|
* @param LNumber|DNumber $node
|
|
*/
|
|
public function refactor(Node $node): ?Node
|
|
{
|
|
$rawValue = $node->getAttribute(AttributeKey::RAW_VALUE);
|
|
|
|
if ($this->shouldSkip($node, $rawValue)) {
|
|
return null;
|
|
}
|
|
|
|
if (\str_contains((string) $rawValue, '.')) {
|
|
[$mainPart, $decimalPart] = explode('.', (string) $rawValue);
|
|
|
|
$chunks = $this->strSplitNegative($mainPart, self::GROUP_SIZE);
|
|
$literalSeparatedNumber = implode('_', $chunks) . '.' . $decimalPart;
|
|
} else {
|
|
$chunks = $this->strSplitNegative($rawValue, self::GROUP_SIZE);
|
|
$literalSeparatedNumber = implode('_', $chunks);
|
|
|
|
// PHP converts: (string) 1000.0 -> "1000"!
|
|
if (is_float($node->value)) {
|
|
$literalSeparatedNumber .= '.0';
|
|
}
|
|
}
|
|
|
|
$node->value = $literalSeparatedNumber;
|
|
|
|
return $node;
|
|
}
|
|
|
|
public function provideMinPhpVersion(): int
|
|
{
|
|
return PhpVersionFeature::LITERAL_SEPARATOR;
|
|
}
|
|
|
|
private function shouldSkip(LNumber | DNumber $node, mixed $rawValue): bool
|
|
{
|
|
if (! is_string($rawValue)) {
|
|
return true;
|
|
}
|
|
|
|
// already contains separator
|
|
if (str_contains($rawValue, '_')) {
|
|
return true;
|
|
}
|
|
|
|
if ($node->value < $this->limitValue) {
|
|
return true;
|
|
}
|
|
|
|
$kind = $node->getAttribute(AttributeKey::KIND);
|
|
if (in_array($kind, [LNumber::KIND_BIN, LNumber::KIND_OCT, LNumber::KIND_HEX], true)) {
|
|
return true;
|
|
}
|
|
|
|
// e+/e-
|
|
if (StringUtils::isMatch($rawValue, '#e#i')) {
|
|
return true;
|
|
}
|
|
|
|
// too short
|
|
return Strings::length($rawValue) <= self::GROUP_SIZE;
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
private function strSplitNegative(string $string, int $length): array
|
|
{
|
|
$inversed = strrev($string);
|
|
|
|
/** @var string[] $chunks */
|
|
$chunks = str_split($inversed, $length);
|
|
|
|
$chunks = array_reverse($chunks);
|
|
foreach ($chunks as $key => $chunk) {
|
|
$chunks[$key] = strrev($chunk);
|
|
}
|
|
|
|
return $chunks;
|
|
}
|
|
}
|