* Dariusz RumiƄski * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Fixer\StringNotation; use PhpCsFixer\AbstractFunctionReferenceFixer; use PhpCsFixer\FixerDefinition\CodeSample; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; final class StringLengthToEmptyFixer extends AbstractFunctionReferenceFixer { public function getDefinition(): FixerDefinitionInterface { return new FixerDefinition( 'String tests for empty must be done against `\'\'`, not with `strlen`.', [new CodeSample("findStrLengthCalls($tokens) as $candidate) { [$functionNameIndex, $openParenthesisIndex, $closeParenthesisIndex] = $candidate; $arguments = $argumentsAnalyzer->getArguments($tokens, $openParenthesisIndex, $closeParenthesisIndex); if (1 !== \count($arguments)) { continue; // must be one argument } // test for leading `\` before `strlen` call $nextIndex = $tokens->getNextMeaningfulToken($closeParenthesisIndex); $previousIndex = $tokens->getPrevMeaningfulToken($functionNameIndex); if ($tokens[$previousIndex]->isGivenKind(T_NS_SEPARATOR)) { $namespaceSeparatorIndex = $previousIndex; $previousIndex = $tokens->getPrevMeaningfulToken($previousIndex); } else { $namespaceSeparatorIndex = null; } // test for yoda vs non-yoda fix case if ($this->isOperatorOfInterest($tokens[$previousIndex])) { // test if valid yoda case to fix $operatorIndex = $previousIndex; $operandIndex = $tokens->getPrevMeaningfulToken($previousIndex); if (!$this->isOperandOfInterest($tokens[$operandIndex])) { // test if operand is `0` or `1` continue; } $replacement = $this->getReplacementYoda($tokens[$operatorIndex], $tokens[$operandIndex]); if (null === $replacement) { continue; } if ($this->isOfHigherPrecedence($tokens[$nextIndex])) { // is of higher precedence right; continue continue; } if ($this->isOfHigherPrecedence($tokens[$tokens->getPrevMeaningfulToken($operandIndex)])) { // is of higher precedence left; continue continue; } } elseif ($this->isOperatorOfInterest($tokens[$nextIndex])) { // test if valid !yoda case to fix $operatorIndex = $nextIndex; $operandIndex = $tokens->getNextMeaningfulToken($nextIndex); if (!$this->isOperandOfInterest($tokens[$operandIndex])) { // test if operand is `0` or `1` continue; } $replacement = $this->getReplacementNotYoda($tokens[$operatorIndex], $tokens[$operandIndex]); if (null === $replacement) { continue; } if ($this->isOfHigherPrecedence($tokens[$tokens->getNextMeaningfulToken($operandIndex)])) { // is of higher precedence right; continue continue; } if ($this->isOfHigherPrecedence($tokens[$previousIndex])) { // is of higher precedence left; continue continue; } } else { continue; } // prepare for fixing $keepParentheses = $this->keepParentheses($tokens, $openParenthesisIndex, $closeParenthesisIndex); if (T_IS_IDENTICAL === $replacement) { $operandContent = '==='; } else { // T_IS_NOT_IDENTICAL === $replacement $operandContent = '!=='; } // apply fixing $tokens[$operandIndex] = new Token([T_CONSTANT_ENCAPSED_STRING, "''"]); $tokens[$operatorIndex] = new Token([$replacement, $operandContent]); if (!$keepParentheses) { $tokens->clearTokenAndMergeSurroundingWhitespace($closeParenthesisIndex); $tokens->clearTokenAndMergeSurroundingWhitespace($openParenthesisIndex); } $tokens->clearTokenAndMergeSurroundingWhitespace($functionNameIndex); if (null !== $namespaceSeparatorIndex) { $tokens->clearTokenAndMergeSurroundingWhitespace($namespaceSeparatorIndex); } } } private function getReplacementYoda(Token $operator, Token $operand): ?int { /* Yoda 0 0 === strlen($b) | '' === $b 0 !== strlen($b) | '' !== $b 0 <= strlen($b) | X makes no sense, assume overridden 0 >= strlen($b) | '' === $b 0 < strlen($b) | '' !== $b 0 > strlen($b) | X makes no sense, assume overridden */ if ('0' === $operand->getContent()) { if ($operator->isGivenKind([T_IS_IDENTICAL, T_IS_GREATER_OR_EQUAL])) { return T_IS_IDENTICAL; } if ($operator->isGivenKind(T_IS_NOT_IDENTICAL) || $operator->equals('<')) { return T_IS_NOT_IDENTICAL; } return null; } /* Yoda 1 1 === strlen($b) | X cannot simplify 1 !== strlen($b) | X cannot simplify 1 <= strlen($b) | '' !== $b 1 >= strlen($b) | cannot simplify 1 < strlen($b) | cannot simplify 1 > strlen($b) | '' === $b */ if ($operator->isGivenKind(T_IS_SMALLER_OR_EQUAL)) { return T_IS_NOT_IDENTICAL; } if ($operator->equals('>')) { return T_IS_IDENTICAL; } return null; } private function getReplacementNotYoda(Token $operator, Token $operand): ?int { /* Not Yoda 0 strlen($b) === 0 | $b === '' strlen($b) !== 0 | $b !== '' strlen($b) <= 0 | $b === '' strlen($b) >= 0 | X makes no sense, assume overridden strlen($b) < 0 | X makes no sense, assume overridden strlen($b) > 0 | $b !== '' */ if ('0' === $operand->getContent()) { if ($operator->isGivenKind([T_IS_IDENTICAL, T_IS_SMALLER_OR_EQUAL])) { return T_IS_IDENTICAL; } if ($operator->isGivenKind(T_IS_NOT_IDENTICAL) || $operator->equals('>')) { return T_IS_NOT_IDENTICAL; } return null; } /* Not Yoda 1 strlen($b) === 1 | X cannot simplify strlen($b) !== 1 | X cannot simplify strlen($b) <= 1 | X cannot simplify strlen($b) >= 1 | $b !== '' strlen($b) < 1 | $b === '' strlen($b) > 1 | X cannot simplify */ if ($operator->isGivenKind(T_IS_GREATER_OR_EQUAL)) { return T_IS_NOT_IDENTICAL; } if ($operator->equals('<')) { return T_IS_IDENTICAL; } return null; } private function isOperandOfInterest(Token $token): bool { if (!$token->isGivenKind(T_LNUMBER)) { return false; } $content = $token->getContent(); return '0' === $content || '1' === $content; } private function isOperatorOfInterest(Token $token): bool { return $token->isGivenKind([T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_IS_SMALLER_OR_EQUAL, T_IS_GREATER_OR_EQUAL]) || $token->equals('<') || $token->equals('>'); } private function isOfHigherPrecedence(Token $token): bool { static $operatorsPerContent = [ '!', '%', '*', '+', '-', '.', '/', '~', '?', ]; return $token->isGivenKind([T_INSTANCEOF, T_POW, T_SL, T_SR]) || $token->equalsAny($operatorsPerContent); } private function keepParentheses(Tokens $tokens, int $openParenthesisIndex, int $closeParenthesisIndex): bool { $i = $tokens->getNextMeaningfulToken($openParenthesisIndex); if ($tokens[$i]->isCast()) { $i = $tokens->getNextMeaningfulToken($i); } for (; $i < $closeParenthesisIndex; ++$i) { $token = $tokens[$i]; if ($token->isGivenKind([T_VARIABLE, T_STRING]) || $token->isObjectOperator() || $token->isWhitespace() || $token->isComment()) { continue; } $blockType = Tokens::detectBlockType($token); if (null !== $blockType && $blockType['isStart']) { $i = $tokens->findBlockEnd($blockType['type'], $i); continue; } return true; } return false; } private function findStrLengthCalls(Tokens $tokens): \Generator { $candidates = []; $count = \count($tokens); for ($i = 0; $i < $count; ++$i) { $candidate = $this->find('strlen', $tokens, $i, $count); if (null === $candidate) { break; } $i = $candidate[1]; // proceed to openParenthesisIndex $candidates[] = $candidate; } foreach (array_reverse($candidates) as $candidate) { yield $candidate; } } }