diff --git a/doc/list.rst b/doc/list.rst index d798dbd7b52..7c5524b81d4 100644 --- a/doc/list.rst +++ b/doc/list.rst @@ -2728,6 +2728,16 @@ List of Available Rules Part of rule set `@PhpCsFixer:risky <./ruleSets/PhpCsFixerRisky.rst>`_ `Source PhpCsFixer\\Fixer\\Strict\\StrictParamFixer <./../src/Fixer/Strict/StrictParamFixer.php>`_ +- `string_length_to_empty <./rules/string_notation/string_length_to_empty.rst>`_ + + String tests for empty must be done against ``''``, not with ``strlen``. + + *warning risky* Risky when ``strlen`` is overridden, when called using a ``stringable`` + object, also no longer triggers warning when called using non-string(able). + + Part of rule sets `@PhpCsFixer:risky <./ruleSets/PhpCsFixerRisky.rst>`_ `@Symfony:risky <./ruleSets/SymfonyRisky.rst>`_ + + `Source PhpCsFixer\\Fixer\\StringNotation\\StringLengthToEmptyFixer <./../src/Fixer/StringNotation/StringLengthToEmptyFixer.php>`_ - `string_line_ending <./rules/string_notation/string_line_ending.rst>`_ All multi-line strings must use correct line ending. diff --git a/doc/ruleSets/SymfonyRisky.rst b/doc/ruleSets/SymfonyRisky.rst index 9063a319d43..5a63122ce17 100644 --- a/doc/ruleSets/SymfonyRisky.rst +++ b/doc/ruleSets/SymfonyRisky.rst @@ -42,5 +42,6 @@ Rules - `psr_autoloading <./../rules/basic/psr_autoloading.rst>`_ - `self_accessor <./../rules/class_notation/self_accessor.rst>`_ - `set_type_to_cast <./../rules/alias/set_type_to_cast.rst>`_ +- `string_length_to_empty <./../rules/string_notation/string_length_to_empty.rst>`_ - `string_line_ending <./../rules/string_notation/string_line_ending.rst>`_ - `ternary_to_elvis_operator <./../rules/operator/ternary_to_elvis_operator.rst>`_ diff --git a/doc/rules/index.rst b/doc/rules/index.rst index c54feb3db99..8d1ad6618a3 100644 --- a/doc/rules/index.rst +++ b/doc/rules/index.rst @@ -776,6 +776,9 @@ String Notation - `single_quote <./string_notation/single_quote.rst>`_ Convert double quotes to single quotes for simple strings. +- `string_length_to_empty <./string_notation/string_length_to_empty.rst>`_ *(risky)* + + String tests for empty must be done against ``''``, not with ``strlen``. - `string_line_ending <./string_notation/string_line_ending.rst>`_ *(risky)* All multi-line strings must use correct line ending. diff --git a/doc/rules/string_notation/string_length_to_empty.rst b/doc/rules/string_notation/string_length_to_empty.rst new file mode 100644 index 00000000000..61c6e07d915 --- /dev/null +++ b/doc/rules/string_notation/string_length_to_empty.rst @@ -0,0 +1,34 @@ +=============================== +Rule ``string_length_to_empty`` +=============================== + +String tests for empty must be done against ``''``, not with ``strlen``. + +.. warning:: Using this rule is risky. + + Risky when ``strlen`` is overridden, when called using a ``stringable`` + object, also no longer triggers warning when called using non-string(able). + +Examples +-------- + +Example #1 +~~~~~~~~~~ + +.. code-block:: diff + + --- Original + +++ New + -`_ rule set will enable the ``string_length_to_empty`` rule. + +@Symfony:risky + Using the `@Symfony:risky <./../../ruleSets/SymfonyRisky.rst>`_ rule set will enable the ``string_length_to_empty`` rule. diff --git a/src/AbstractFunctionReferenceFixer.php b/src/AbstractFunctionReferenceFixer.php index a1c57d9541d..1f36fce04fd 100644 --- a/src/AbstractFunctionReferenceFixer.php +++ b/src/AbstractFunctionReferenceFixer.php @@ -24,6 +24,19 @@ */ abstract class AbstractFunctionReferenceFixer extends AbstractFixer { + /** + * @var null|FunctionsAnalyzer + */ + private $functionsAnalyzer; + + /** + * {@inheritdoc} + */ + public function isCandidate(Tokens $tokens): bool + { + return $tokens->isTokenKindFound(T_STRING); + } + /** * {@inheritdoc} */ @@ -40,6 +53,10 @@ public function isRisky(): bool */ protected function find(string $functionNameToSearch, Tokens $tokens, int $start = 0, ?int $end = null): ?array { + if (null === $this->functionsAnalyzer) { + $this->functionsAnalyzer = new FunctionsAnalyzer(); + } + // make interface consistent with findSequence $end = $end ?? $tokens->count(); @@ -54,9 +71,7 @@ protected function find(string $functionNameToSearch, Tokens $tokens, int $start // translate results for humans [$functionName, $openParenthesis] = array_keys($matches); - $functionsAnalyzer = new FunctionsAnalyzer(); - - if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $functionName)) { + if (!$this->functionsAnalyzer->isGlobalFunctionCall($tokens, $functionName)) { return $this->find($functionNameToSearch, $tokens, $openParenthesis, $end); } diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php index 9e51ec2a9bf..2dea02cc37c 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -78,7 +78,7 @@ public function toJson(): string if (JSON_ERROR_NONE !== json_last_error()) { throw new \UnexpectedValueException(sprintf( - 'Can not encode cache signature to JSON, error: "%s". If you have non-UTF8 chars in your signature, like in license for `header_comment`, consider enabling `ext-mbstring` or install `symfony/polyfill-mbstring`.', + 'Cannot encode cache signature to JSON, error: "%s". If you have non-UTF8 chars in your signature, like in license for `header_comment`, consider enabling `ext-mbstring` or install `symfony/polyfill-mbstring`.', json_last_error_msg() )); } diff --git a/src/Console/Command/DescribeCommand.php b/src/Console/Command/DescribeCommand.php index 78d40a6a32b..23e6c561fc7 100644 --- a/src/Console/Command/DescribeCommand.php +++ b/src/Console/Command/DescribeCommand.php @@ -256,7 +256,7 @@ static function (string $type): string { if (0 === \count($codeSamples)) { $output->writeln([ - 'Fixing examples can not be demonstrated on the current PHP version.', + 'Fixing examples cannot be demonstrated on the current PHP version.', '', ]); } else { diff --git a/src/DocBlock/DocBlock.php b/src/DocBlock/DocBlock.php index 6e873014a2a..b62c9caeeda 100644 --- a/src/DocBlock/DocBlock.php +++ b/src/DocBlock/DocBlock.php @@ -244,7 +244,7 @@ private function getSingleLineDocBlockEntry(Line $line): string { $lineString = $line->getContent(); - if (0 === \strlen($lineString)) { + if ('' === $lineString) { return $lineString; } diff --git a/src/FileReader.php b/src/FileReader.php index 6f81e40b95e..d71f5f76e00 100644 --- a/src/FileReader.php +++ b/src/FileReader.php @@ -17,7 +17,7 @@ /** * File reader that unify access to regular file and stdin-alike file. * - * Regular file could be read multiple times with `file_get_contents`, but file provided on stdin can not. + * Regular file could be read multiple times with `file_get_contents`, but file provided on stdin cannot. * Consecutive try will provide empty content for stdin-alike file. * This reader unifies access to them. * diff --git a/src/Fixer/Alias/MbStrFunctionsFixer.php b/src/Fixer/Alias/MbStrFunctionsFixer.php index 7932338ed06..cfa8c48c7a8 100644 --- a/src/Fixer/Alias/MbStrFunctionsFixer.php +++ b/src/Fixer/Alias/MbStrFunctionsFixer.php @@ -93,14 +93,6 @@ public function getDefinition(): FixerDefinitionInterface ); } - /** - * {@inheritdoc} - */ - public function isCandidate(Tokens $tokens): bool - { - return $tokens->isTokenKindFound(T_STRING); - } - /** * {@inheritdoc} */ diff --git a/src/Fixer/Alias/ModernizeStrposFixer.php b/src/Fixer/Alias/ModernizeStrposFixer.php index eee0bb51cc1..1fddad243dd 100644 --- a/src/Fixer/Alias/ModernizeStrposFixer.php +++ b/src/Fixer/Alias/ModernizeStrposFixer.php @@ -188,15 +188,15 @@ private function getCompareTokens(Tokens $tokens, int $offsetIndex, int $directi private function isOfHigherPrecedence(Token $token): bool { - static $operatorsPerId = [ - T_DEC => true, // -- - T_INC => true, // ++ - T_INSTANCEOF => true, // instanceof - T_IS_GREATER_OR_EQUAL => true, // >= - T_IS_SMALLER_OR_EQUAL => true, // <= - T_POW => true, // ** - T_SL => true, // << - T_SR => true, // >> + static $operatorsKinds = [ + T_DEC, // -- + T_INC, // ++ + T_INSTANCEOF, // instanceof + T_IS_GREATER_OR_EQUAL, // >= + T_IS_SMALLER_OR_EQUAL, // <= + T_POW, // ** + T_SL, // << + T_SR, // >> ]; static $operatorsPerContent = [ @@ -212,6 +212,6 @@ private function isOfHigherPrecedence(Token $token): bool '~', ]; - return isset($operatorsPerId[$token->getId()]) || $token->equalsAny($operatorsPerContent); + return $token->isGivenKind($operatorsKinds) || $token->equalsAny($operatorsPerContent); } } diff --git a/src/Fixer/Alias/PowToExponentiationFixer.php b/src/Fixer/Alias/PowToExponentiationFixer.php index f71020e6b15..44f35a00bf6 100644 --- a/src/Fixer/Alias/PowToExponentiationFixer.php +++ b/src/Fixer/Alias/PowToExponentiationFixer.php @@ -106,7 +106,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void } /** - * @return array[] + * @return array */ private function findPowCalls(Tokens $tokens): array { diff --git a/src/Fixer/Alias/RandomApiMigrationFixer.php b/src/Fixer/Alias/RandomApiMigrationFixer.php index a4772bcae37..282d878888b 100644 --- a/src/Fixer/Alias/RandomApiMigrationFixer.php +++ b/src/Fixer/Alias/RandomApiMigrationFixer.php @@ -81,14 +81,6 @@ public function getDefinition(): FixerDefinitionInterface ); } - /** - * {@inheritdoc} - */ - public function isCandidate(Tokens $tokens): bool - { - return $tokens->isTokenKindFound(T_STRING); - } - /** * {@inheritdoc} */ diff --git a/src/Fixer/Basic/PsrAutoloadingFixer.php b/src/Fixer/Basic/PsrAutoloadingFixer.php index 0b3ebeb3af1..3d22c4e05cc 100644 --- a/src/Fixer/Basic/PsrAutoloadingFixer.php +++ b/src/Fixer/Basic/PsrAutoloadingFixer.php @@ -124,11 +124,11 @@ public function supports(\SplFileInfo $file): bool $tokens = Tokens::fromCode(sprintf('getBasename('.php'))); if ($tokens[3]->isKeyword() || $tokens[3]->isMagicConstant()) { - // name can not be a class name - detected by PHP 5.x + // name cannot be a class name - detected by PHP 5.x return false; } } catch (\ParseError $e) { - // name can not be a class name - detected by PHP 7.x + // name cannot be a class name - detected by PHP 7.x return false; } diff --git a/src/Fixer/CastNotation/ModernizeTypesCastingFixer.php b/src/Fixer/CastNotation/ModernizeTypesCastingFixer.php index e645149c59d..ac7d8b00525 100644 --- a/src/Fixer/CastNotation/ModernizeTypesCastingFixer.php +++ b/src/Fixer/CastNotation/ModernizeTypesCastingFixer.php @@ -50,14 +50,6 @@ public function getDefinition(): FixerDefinitionInterface ); } - /** - * {@inheritdoc} - */ - public function isCandidate(Tokens $tokens): bool - { - return $tokens->isTokenKindFound(T_STRING); - } - /** * {@inheritdoc} */ diff --git a/src/Fixer/FunctionNotation/FunctionDeclarationFixer.php b/src/Fixer/FunctionNotation/FunctionDeclarationFixer.php index 47e591bce08..f5008d3ea39 100644 --- a/src/Fixer/FunctionNotation/FunctionDeclarationFixer.php +++ b/src/Fixer/FunctionNotation/FunctionDeclarationFixer.php @@ -57,11 +57,7 @@ final class FunctionDeclarationFixer extends AbstractFixer implements Configurab */ public function isCandidate(Tokens $tokens): bool { - if (\PHP_VERSION_ID >= 70400 && $tokens->isTokenKindFound(T_FN)) { - return true; - } - - return $tokens->isTokenKindFound(T_FUNCTION); + return $tokens->isTokenKindFound(T_FUNCTION) || (\PHP_VERSION_ID >= 70400 && $tokens->isTokenKindFound(T_FN)); } /** diff --git a/src/Fixer/LanguageConstruct/DirConstantFixer.php b/src/Fixer/LanguageConstruct/DirConstantFixer.php index be692bc2663..112101a324e 100644 --- a/src/Fixer/LanguageConstruct/DirConstantFixer.php +++ b/src/Fixer/LanguageConstruct/DirConstantFixer.php @@ -44,7 +44,7 @@ public function getDefinition(): FixerDefinitionInterface */ public function isCandidate(Tokens $tokens): bool { - return $tokens->isTokenKindFound(T_FILE); + return $tokens->isAllTokenKindsFound([T_STRING, T_FILE]); } /** diff --git a/src/Fixer/Operator/BinaryOperatorSpacesFixer.php b/src/Fixer/Operator/BinaryOperatorSpacesFixer.php index 3a9ca5e5f1d..a88d2b49069 100644 --- a/src/Fixer/Operator/BinaryOperatorSpacesFixer.php +++ b/src/Fixer/Operator/BinaryOperatorSpacesFixer.php @@ -720,7 +720,7 @@ private function replacePlaceholders(Tokens $tokens, string $alignStrategy): str $before = substr($lines[$index], 0, $currentPosition); if (self::ALIGN_SINGLE_SPACE === $alignStrategy) { - if (1 > \strlen($before) || ' ' !== substr($before, -1)) { // if last char of before-content is not ' '; add it + if (!str_ends_with($before, ' ')) { // if last char of before-content is not ' '; add it $before .= ' '; } } elseif (self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy) { diff --git a/src/Fixer/Phpdoc/PhpdocLineSpanFixer.php b/src/Fixer/Phpdoc/PhpdocLineSpanFixer.php index 298e4f092c5..00f7badd41a 100644 --- a/src/Fixer/Phpdoc/PhpdocLineSpanFixer.php +++ b/src/Fixer/Phpdoc/PhpdocLineSpanFixer.php @@ -102,6 +102,11 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void } $type = $element['type']; + + if (!isset($this->configuration[$type])) { + continue; + } + $docIndex = $this->getDocBlockIndex($tokens, $index); $doc = new DocBlock($tokens[$docIndex]->getContent()); diff --git a/src/Fixer/StringNotation/StringLengthToEmptyFixer.php b/src/Fixer/StringNotation/StringLengthToEmptyFixer.php new file mode 100644 index 00000000000..35d5603103e --- /dev/null +++ b/src/Fixer/StringNotation/StringLengthToEmptyFixer.php @@ -0,0 +1,330 @@ + + * 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 +{ + /** + * {@inheritdoc} + */ + 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; + } + + /** @var null|array{isStart: bool, type: int} $blockType */ + $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; + } + } +} diff --git a/src/Fixer/Whitespace/NoExtraBlankLinesFixer.php b/src/Fixer/Whitespace/NoExtraBlankLinesFixer.php index fb183f6514a..fcab4e981ae 100644 --- a/src/Fixer/Whitespace/NoExtraBlankLinesFixer.php +++ b/src/Fixer/Whitespace/NoExtraBlankLinesFixer.php @@ -271,7 +271,7 @@ class Bar * {@inheritdoc} * * Must run before BlankLineBeforeStatementFixer. - * Must run after ClassAttributesSeparationFixer, CombineConsecutiveUnsetsFixer, EmptyLoopBodyFixer, EmptyLoopConditionFixer, FunctionToConstantFixer, ModernizeStrposFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUnusedImportsFixer, NoUselessElseFixer, NoUselessReturnFixer, NoUselessSprintfFixer. + * Must run after ClassAttributesSeparationFixer, CombineConsecutiveUnsetsFixer, EmptyLoopBodyFixer, EmptyLoopConditionFixer, FunctionToConstantFixer, ModernizeStrposFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUnusedImportsFixer, NoUselessElseFixer, NoUselessReturnFixer, NoUselessSprintfFixer, StringLengthToEmptyFixer. */ public function getPriority(): int { diff --git a/src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php b/src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php index d709a305539..0fb421a67b2 100644 --- a/src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php +++ b/src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php @@ -50,7 +50,7 @@ function foo( \$bar, \$baz ) /** * {@inheritdoc} * - * Must run before FunctionToConstantFixer, GetClassToClassKeywordFixer. + * Must run before FunctionToConstantFixer, GetClassToClassKeywordFixer, StringLengthToEmptyFixer. * Must run after CombineConsecutiveIssetsFixer, CombineNestedDirnameFixer, LambdaNotUsedImportFixer, ModernizeStrposFixer, NoUselessSprintfFixer, PowToExponentiationFixer. */ public function getPriority(): int diff --git a/src/Fixer/Whitespace/NoTrailingWhitespaceFixer.php b/src/Fixer/Whitespace/NoTrailingWhitespaceFixer.php index 05a4f429690..1c3c87dd2e1 100644 --- a/src/Fixer/Whitespace/NoTrailingWhitespaceFixer.php +++ b/src/Fixer/Whitespace/NoTrailingWhitespaceFixer.php @@ -46,7 +46,7 @@ public function getDefinition(): FixerDefinitionInterface /** * {@inheritdoc} * - * Must run after CombineConsecutiveIssetsFixer, CombineConsecutiveUnsetsFixer, EmptyLoopBodyFixer, EmptyLoopConditionFixer, FunctionToConstantFixer, ModernizeStrposFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUnneededControlParenthesesFixer, NoUselessElseFixer, TernaryToElvisOperatorFixer. + * Must run after CombineConsecutiveIssetsFixer, CombineConsecutiveUnsetsFixer, EmptyLoopBodyFixer, EmptyLoopConditionFixer, FunctionToConstantFixer, ModernizeStrposFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUnneededControlParenthesesFixer, NoUselessElseFixer, StringLengthToEmptyFixer, TernaryToElvisOperatorFixer. */ public function getPriority(): int { diff --git a/src/RuleSet/Sets/SymfonyRiskySet.php b/src/RuleSet/Sets/SymfonyRiskySet.php index 271815ecbee..562ca3b6cd5 100644 --- a/src/RuleSet/Sets/SymfonyRiskySet.php +++ b/src/RuleSet/Sets/SymfonyRiskySet.php @@ -63,6 +63,7 @@ public function getRules(): array 'psr_autoloading' => true, 'self_accessor' => true, 'set_type_to_cast' => true, + 'string_length_to_empty' => true, 'string_line_ending' => true, 'ternary_to_elvis_operator' => true, ]; diff --git a/src/Tokenizer/Token.php b/src/Tokenizer/Token.php index e1099825a9e..d5850e2d24a 100644 --- a/src/Tokenizer/Token.php +++ b/src/Tokenizer/Token.php @@ -464,7 +464,7 @@ public function toJson(): string if (JSON_ERROR_NONE !== json_last_error()) { $jsonResult = json_encode( [ - 'errorDescription' => 'Can not encode Tokens to JSON.', + 'errorDescription' => 'Cannot encode Tokens to JSON.', 'rawErrorMessage' => json_last_error_msg(), ], JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK diff --git a/tests/AutoReview/FixerFactoryTest.php b/tests/AutoReview/FixerFactoryTest.php index 1c47ba2bb6d..6df08d595a1 100644 --- a/tests/AutoReview/FixerFactoryTest.php +++ b/tests/AutoReview/FixerFactoryTest.php @@ -190,6 +190,7 @@ public function provideFixersPriorityCases(): array [$fixers['no_spaces_after_function_name'], $fixers['get_class_to_class_keyword']], [$fixers['no_spaces_inside_parenthesis'], $fixers['function_to_constant']], [$fixers['no_spaces_inside_parenthesis'], $fixers['get_class_to_class_keyword']], + [$fixers['no_spaces_inside_parenthesis'], $fixers['string_length_to_empty']], [$fixers['no_superfluous_elseif'], $fixers['simplified_if_return']], [$fixers['no_superfluous_phpdoc_tags'], $fixers['no_empty_phpdoc']], [$fixers['no_superfluous_phpdoc_tags'], $fixers['void_return']], @@ -297,6 +298,8 @@ public function provideFixersPriorityCases(): array [$fixers['standardize_not_equals'], $fixers['binary_operator_spaces']], [$fixers['strict_comparison'], $fixers['binary_operator_spaces']], [$fixers['strict_param'], $fixers['native_function_invocation']], + [$fixers['string_length_to_empty'], $fixers['no_extra_blank_lines']], + [$fixers['string_length_to_empty'], $fixers['no_trailing_whitespace']], [$fixers['ternary_to_elvis_operator'], $fixers['no_trailing_whitespace']], [$fixers['ternary_to_elvis_operator'], $fixers['ternary_operator_spaces']], [$fixers['ternary_to_null_coalescing'], $fixers['assign_null_coalescing_to_coalesce_equal']], diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php index a7af5e04f79..b49a55ad742 100644 --- a/tests/Cache/CacheTest.php +++ b/tests/Cache/CacheTest.php @@ -204,7 +204,7 @@ public function testToJsonThrowsExceptionOnInvalid(): void ); $this->expectExceptionMessage( - 'Can not encode cache signature to JSON, error: "Malformed UTF-8 characters, possibly incorrectly encoded". If you have non-UTF8 chars in your signature, like in license for `header_comment`, consider enabling `ext-mbstring` or install `symfony/polyfill-mbstring`.' + 'Cannot encode cache signature to JSON, error: "Malformed UTF-8 characters, possibly incorrectly encoded". If you have non-UTF8 chars in your signature, like in license for `header_comment`, consider enabling `ext-mbstring` or install `symfony/polyfill-mbstring`.' ); $cache->toJson(); diff --git a/tests/Fixer/Phpdoc/PhpdocLineSpanFixerTest.php b/tests/Fixer/Phpdoc/PhpdocLineSpanFixerTest.php index b7a4a5a4096..46b605b9ca8 100644 --- a/tests/Fixer/Phpdoc/PhpdocLineSpanFixerTest.php +++ b/tests/Fixer/Phpdoc/PhpdocLineSpanFixerTest.php @@ -473,11 +473,16 @@ class Foo 'const' => null, ], ], - 'It can handle constants with visibility' => [ + 'It can handle constants with visibility, does not crash on trait imports' => [ ' + * 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\Tests\Fixer\StringNotation; + +use PhpCsFixer\Tests\Test\AbstractFixerTestCase; + +/** + * @internal + * + * @covers \PhpCsFixer\Fixer\StringNotation\StringLengthToEmptyFixer + */ +final class StringLengthToEmptyFixerTest extends AbstractFixerTestCase +{ + /** + * @dataProvider provideTestFixCases + */ + public function testFix(string $expected, ?string $input = null): void + { + $this->doTest($expected, $input); + } + + public function provideTestFixCases(): \Generator + { + yield [ + ' [ + '', + '', + ]; + + yield 'nested' => [ + '= strlen($b);', + ]; + + yield [ + ' strlen($b);', + ]; + + yield [ + '= strlen($b);', + ]; + + yield [ + ' strlen($b);', + ]; + + yield [ + '= 0;', + ]; + + yield [ + ' 0;', + ]; + + yield [ + '= 1;', + ]; + + yield [ + ' 1;', + ]; + + yield [ + 'a !== \'\';', + 'a) >= 1;', + ]; + + yield [ + '$a[$a++](1) /* 1 */ === \'\';', + '$a[$a++](1) /* 1 */ ) < 1;', + ]; + + yield [ + 'foo(++$i, static function () use ($z){ return $z + 1;});', + 'foo(++$i, static function () use ($z){ return $z + 1;}));', + ]; + + yield [ + ' 0) { echo 1; }', + ]; + + yield 'do not fix' => [ + '> $c; +$a10 = 0 === strlen($b) << $c; + +$a01n = strlen($b) === 0 ** $c; +$a03n = strlen($b) === 0 % $c; +$a04n = strlen($b) === 0 / $c; +$a05n = strlen($b) === 0 * $c; +$a06n = strlen($b) === 0 + $c; +$a07n = strlen($b) === 0 - $c; +$a08n = strlen($b) === 0 . $c; +$a09n = strlen($b) === 0 >> $c; +$a10n = strlen($b) === 0 << $c; + +$b = "a"; + +$c = 0 === strlen($b) - 1; +var_dump($c); + +$c = "" === $b - 1; +var_dump($c); + +//----------------------------------- +// type juggle + +$d = false; + +$e = 0 === strlen($d) ? -1 : 0; +var_dump($e); + +$e = "" === $d ? -1 : 0; +var_dump($e); + +//----------------------------------- +// wrong argument count + +$f = strlen(1,2); +$g = \strlen(1,2,3); + +//----------------------------------- +// others + +$h = 0 === (string) strlen($b); +$i = 0 === @strlen($b); +$j = 0 === !strlen($b); + +$jj = 2 === strlen($b); +$jk = __DIR__ === strlen($b); +$jl = \'X\' !== strlen($b); + +$jj = strlen($b) === 2; +$jk = strlen($b) === __DIR__; +$jl = strlen($b) !== \'X\'; + +//----------------------------------- +// not global calls + +$k = 0 === $a->strlen($b); +$l = 0 === Foo::strlen($b); + +//----------------------------------- +// comments + +// $a = 0 === strlen($b); +# $a = 0 === strlen($b); +/* $a = 0 === strlen($b); */ +/** $a = 0 === strlen($b); */ +', + ]; + + // cases where `(` and `)` must be kept + + yield [ + ' 0;', + ]; + + yield [ + 'doTest( + '