ria/vendor/friendsofphp/php-cs-fixer/src/Fixer/Operator/TernaryToNullCoalescingFixer.php
2024-06-04 11:41:50 +00:00

225 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class TernaryToNullCoalescingFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Use `null` coalescing operator `??` where possible. Requires PHP >= 7.0.',
[
new CodeSample(
"<?php\n\$sample = isset(\$a) ? \$a : \$b;\n"
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before AssignNullCoalescingToCoalesceEqualFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_ISSET);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$issetIndices = array_keys($tokens->findGivenKind(T_ISSET));
while ($issetIndex = array_pop($issetIndices)) {
$this->fixIsset($tokens, $issetIndex);
}
}
/**
* @param int $index of `T_ISSET` token
*/
private function fixIsset(Tokens $tokens, int $index): void
{
$prevTokenIndex = $tokens->getPrevMeaningfulToken($index);
if ($this->isHigherPrecedenceAssociativityOperator($tokens[$prevTokenIndex])) {
return;
}
$startBraceIndex = $tokens->getNextTokenOfKind($index, ['(']);
$endBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startBraceIndex);
$ternaryQuestionMarkIndex = $tokens->getNextMeaningfulToken($endBraceIndex);
if (!$tokens[$ternaryQuestionMarkIndex]->equals('?')) {
return; // we are not in a ternary operator
}
// search what is inside the isset()
$issetTokens = $this->getMeaningfulSequence($tokens, $startBraceIndex, $endBraceIndex);
if ($this->hasChangingContent($issetTokens)) {
return; // some weird stuff inside the isset
}
$issetCode = $issetTokens->generateCode();
if ('$this' === $issetCode) {
return; // null coalescing operator does not with $this
}
// search what is inside the middle argument of ternary operator
$ternaryColonIndex = $tokens->getNextTokenOfKind($ternaryQuestionMarkIndex, [':']);
$ternaryFirstOperandTokens = $this->getMeaningfulSequence($tokens, $ternaryQuestionMarkIndex, $ternaryColonIndex);
if ($issetCode !== $ternaryFirstOperandTokens->generateCode()) {
return; // regardless of non-meaningful tokens, the operands are different
}
$ternaryFirstOperandIndex = $tokens->getNextMeaningfulToken($ternaryQuestionMarkIndex);
// preserve comments and spaces
$comments = [];
$commentStarted = false;
for ($loopIndex = $index; $loopIndex < $ternaryFirstOperandIndex; ++$loopIndex) {
if ($tokens[$loopIndex]->isComment()) {
$comments[] = $tokens[$loopIndex];
$commentStarted = true;
} elseif ($commentStarted) {
if ($tokens[$loopIndex]->isWhitespace()) {
$comments[] = $tokens[$loopIndex];
}
$commentStarted = false;
}
}
$tokens[$ternaryColonIndex] = new Token([T_COALESCE, '??']);
$tokens->overrideRange($index, $ternaryFirstOperandIndex - 1, $comments);
}
/**
* Get the sequence of meaningful tokens and returns a new Tokens instance.
*
* @param int $start start index
* @param int $end end index
*/
private function getMeaningfulSequence(Tokens $tokens, int $start, int $end): Tokens
{
$sequence = [];
$index = $start;
while ($index < $end) {
$index = $tokens->getNextMeaningfulToken($index);
if ($index >= $end || null === $index) {
break;
}
$sequence[] = $tokens[$index];
}
return Tokens::fromArray($sequence);
}
/**
* Check if the requested token is an operator computed
* before the ternary operator along with the `isset()`.
*/
private function isHigherPrecedenceAssociativityOperator(Token $token): bool
{
static $operatorsPerId = [
T_ARRAY_CAST => true,
T_BOOLEAN_AND => true,
T_BOOLEAN_OR => true,
T_BOOL_CAST => true,
T_COALESCE => true,
T_DEC => true,
T_DOUBLE_CAST => true,
T_INC => true,
T_INT_CAST => true,
T_IS_EQUAL => true,
T_IS_GREATER_OR_EQUAL => true,
T_IS_IDENTICAL => true,
T_IS_NOT_EQUAL => true,
T_IS_NOT_IDENTICAL => true,
T_IS_SMALLER_OR_EQUAL => true,
T_OBJECT_CAST => true,
T_POW => true,
T_SL => true,
T_SPACESHIP => true,
T_SR => true,
T_STRING_CAST => true,
T_UNSET_CAST => true,
];
static $operatorsPerContent = [
'!',
'%',
'&',
'*',
'+',
'-',
'/',
':',
'^',
'|',
'~',
'.',
];
return isset($operatorsPerId[$token->getId()]) || $token->equalsAny($operatorsPerContent);
}
/**
* Check if the `isset()` content may change if called multiple times.
*
* @param Tokens $tokens The original token list
*/
private function hasChangingContent(Tokens $tokens): bool
{
static $operatorsPerId = [
T_DEC,
T_INC,
T_YIELD,
T_YIELD_FROM,
];
foreach ($tokens as $token) {
if ($token->isGivenKind($operatorsPerId) || $token->equals('(')) {
return true;
}
}
return false;
}
}