rector/rules/Php74/Rector/LNumber/AddLiteralSeparatorToNumberRector.php
Tomas Votruba f01725a084
Use php-parser to work with literal _ number separator (#2321)
Co-authored-by: GitHub Action <action@github.com>
2022-05-31 21:09:06 +00:00

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;
}
}