diff --git a/PhpCollective/Sniffs/Internal/DisallowFunctionsSniff.php b/PhpCollective/Sniffs/Internal/DisallowFunctionsSniff.php index 1fcdae3..9ce4b4d 100644 --- a/PhpCollective/Sniffs/Internal/DisallowFunctionsSniff.php +++ b/PhpCollective/Sniffs/Internal/DisallowFunctionsSniff.php @@ -96,7 +96,7 @@ protected function checkForbiddenFunctions(File $phpcsFile, int $stackPtr): void } $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if (!$previous || in_array($tokens[$previous]['code'], static::$wrongTokens, true)) { + if ($previous === false || in_array($tokens[$previous]['code'], static::$wrongTokens, true)) { return; } diff --git a/PhpCollective/Sniffs/PHP/DisallowFunctionsSniff.php b/PhpCollective/Sniffs/PHP/DisallowFunctionsSniff.php index e0248b5..cb0906b 100644 --- a/PhpCollective/Sniffs/PHP/DisallowFunctionsSniff.php +++ b/PhpCollective/Sniffs/PHP/DisallowFunctionsSniff.php @@ -59,7 +59,7 @@ protected function checkForbiddenFunctions(File $phpcsFile, int $stackPtr): void $tokenContent = $tokens[$stackPtr]['content']; $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if (!$previous || in_array($tokens[$previous]['code'], static::$wrongTokens)) { + if ($previous === false || in_array($tokens[$previous]['code'], static::$wrongTokens)) { return; } @@ -88,7 +88,7 @@ protected function checkImplodeUsage(File $phpcsFile, int $stackPtr): void } $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if (!$previous || in_array($tokens[$previous]['code'], static::$wrongTokens)) { + if ($previous === false || in_array($tokens[$previous]['code'], static::$wrongTokens)) { return; } diff --git a/PhpCollective/Sniffs/PHP/NoIsNullSniff.php b/PhpCollective/Sniffs/PHP/NoIsNullSniff.php index 618548e..8e1afbc 100644 --- a/PhpCollective/Sniffs/PHP/NoIsNullSniff.php +++ b/PhpCollective/Sniffs/PHP/NoIsNullSniff.php @@ -40,7 +40,7 @@ public function process(File $phpcsFile, $stackPtr): void $tokenContent = $tokens[$stackPtr]['content']; $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if (!$previous || in_array($tokens[$previous]['code'], $wrongTokens)) { + if ($previous === false || in_array($tokens[$previous]['code'], $wrongTokens)) { return; } diff --git a/PhpCollective/Sniffs/PHP/RemoveFunctionAliasSniff.php b/PhpCollective/Sniffs/PHP/RemoveFunctionAliasSniff.php index 8a14415..5cdc493 100644 --- a/PhpCollective/Sniffs/PHP/RemoveFunctionAliasSniff.php +++ b/PhpCollective/Sniffs/PHP/RemoveFunctionAliasSniff.php @@ -74,7 +74,7 @@ protected function checkFixableAliases(File $phpcsFile, int $stackPtr): void $tokenContent = $tokens[$stackPtr]['content']; $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if (!$previous || in_array($tokens[$previous]['code'], $wrongTokens)) { + if ($previous === false || in_array($tokens[$previous]['code'], $wrongTokens)) { return; } diff --git a/PhpCollective/Sniffs/PHP/ShortCastSniff.php b/PhpCollective/Sniffs/PHP/ShortCastSniff.php index 628a6fe..c414ece 100644 --- a/PhpCollective/Sniffs/PHP/ShortCastSniff.php +++ b/PhpCollective/Sniffs/PHP/ShortCastSniff.php @@ -40,7 +40,7 @@ public function process(File $phpcsFile, $stackPtr): void if ($tokens[$stackPtr]['content'] === '!') { $prevIndex = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if (!$prevIndex || $tokens[$prevIndex]['content'] !== '!') { + if ($prevIndex === false || $tokens[$prevIndex]['content'] !== '!') { return; } diff --git a/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php b/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php index d3541d6..32d1b74 100644 --- a/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php +++ b/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php @@ -9,6 +9,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; /** * No whitespace should be between implicit cast and variable, the same as with other casts. @@ -21,7 +22,7 @@ class ImplicitCastSpacingSniff implements Sniff */ public function register(): array { - return [T_BOOLEAN_NOT, T_NONE, T_ASPERAND, T_INC, T_DEC]; + return [T_BOOLEAN_NOT, T_NONE, T_ASPERAND, T_INC, T_DEC, T_MINUS]; } /** @@ -37,6 +38,19 @@ public function process(File $phpcsFile, $stackPtr): void return; } + // A minus is only an implicit cast when it negates; as a subtraction it wants its spaces. + if ($tokens[$stackPtr]['code'] === T_MINUS) { + if (!$this->isUnaryOperator($phpcsFile, $stackPtr)) { + return; + } + + // `- -$i` must keep its space: closing it up would produce `--$i`, a decrement. + $followingIndex = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + if ($followingIndex !== false && in_array($tokens[$followingIndex]['code'], [T_MINUS, T_DEC], true)) { + return; + } + } + $nextIndex = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); if ($nextIndex === false || $nextIndex - $stackPtr === 1) { @@ -51,6 +65,62 @@ public function process(File $phpcsFile, $stackPtr): void } } + /** + * A minus is unary only when what precedes it cannot end a value - an operator, an opening + * bracket, a comma, `return` and so on. + * + * The test is deliberately this way round. Listing what may PRECEDE a subtraction instead + * would have to enumerate every value-producing token, and anything forgotten (`__LINE__`, + * `true`, `null`, a qualified constant) would be read as a negation and "fixed" into + * `__LINE__ -1`. Defaulting to binary keeps an unknown predecessor harmless. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $stackPtr + * + * @return bool + */ + protected function isUnaryOperator(File $phpcsFile, int $stackPtr): bool + { + $tokens = $phpcsFile->getTokens(); + + $previousIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + if ($previousIndex === false) { + return true; + } + + $unaryPrefixes = Tokens::$operators + + Tokens::$assignmentTokens + + Tokens::$comparisonTokens + + Tokens::$booleanOperators + + Tokens::$castTokens + + [ + T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS, + T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET, + T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY, + T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET, + T_COMMA => T_COMMA, + T_SEMICOLON => T_SEMICOLON, + T_COLON => T_COLON, + T_DOUBLE_ARROW => T_DOUBLE_ARROW, + T_INLINE_THEN => T_INLINE_THEN, + T_INLINE_ELSE => T_INLINE_ELSE, + T_RETURN => T_RETURN, + T_ECHO => T_ECHO, + T_PRINT => T_PRINT, + T_CASE => T_CASE, + T_BOOLEAN_NOT => T_BOOLEAN_NOT, + T_YIELD => T_YIELD, + T_YIELD_FROM => T_YIELD_FROM, + T_THROW => T_THROW, + T_FN_ARROW => T_FN_ARROW, + T_MATCH_ARROW => T_MATCH_ARROW, + T_OPEN_TAG => T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO => T_OPEN_TAG_WITH_ECHO, + ]; + + return isset($unaryPrefixes[$tokens[$previousIndex]['code']]); + } + /** * @param \PHP_CodeSniffer\Files\File $phpcsFile * @param int $stackPtr diff --git a/PhpCollective/ruleset.xml b/PhpCollective/ruleset.xml index a6d9f9b..e5a84ee 100644 --- a/PhpCollective/ruleset.xml +++ b/PhpCollective/ruleset.xml @@ -207,15 +207,16 @@ - @@ -234,6 +235,30 @@ + + + + + + + + + + + + + diff --git a/docs/sniffs.md b/docs/sniffs.md index 3f29935..d9dd83c 100644 --- a/docs/sniffs.md +++ b/docs/sniffs.md @@ -1,7 +1,7 @@ # PhpCollective Code Sniffer -The PhpCollectiveStrict standard contains 251 sniffs +The PhpCollectiveStrict standard contains 250 sniffs Generic (27 sniffs) ------------------- @@ -163,7 +163,7 @@ PSR2 (12 sniffs) - PSR2.Namespaces.NamespaceDeclaration - PSR2.Namespaces.UseDeclaration -PSR12 (14 sniffs) +PSR12 (13 sniffs) ----------------- - PSR12.Classes.AnonClassDeclaration - PSR12.Classes.ClassInstantiation @@ -174,7 +174,6 @@ PSR12 (14 sniffs) - PSR12.Files.ImportStatement - PSR12.Functions.NullableTypeDeclaration - PSR12.Functions.ReturnTypeDeclaration -- PSR12.Keywords.ShortFormTypeKeywords - PSR12.Namespaces.CompoundNamespaceDepth - PSR12.Operators.OperatorSpacing - PSR12.Properties.ConstantVisibility diff --git a/tests/_data/ImplicitCastSpacing/after.php b/tests/_data/ImplicitCastSpacing/after.php index c7448d6..f8432c9 100644 --- a/tests/_data/ImplicitCastSpacing/after.php +++ b/tests/_data/ImplicitCastSpacing/after.php @@ -10,15 +10,22 @@ public function run(bool $ready, int $count, int $mask, callable $callable): int { $not = !$ready; $silenced = @$callable(); - $flipped = ~ $mask; - ++$count; - $count--; + $negated = -$count; + $flipped = ~ $mask; $validNot = !$ready; $validSilenced = @$callable(); - ++$count; - $count++; + $validNegated = -$count; + $validSubtraction = $count - $mask; + $constantMinus = __LINE__ - 1; + $boolMinus = (int)true - 1; + $nullMinus = (int)null - 1; + $fqcnMinus = \PHP_INT_MAX - 1; + $doubleNegated = - -$count; + $arrow = fn (): int => -$count; - return (int)$not + (int)$silenced + $flipped + $count + (int)$validNot + (int)$validSilenced; + return (int)$not + (int)$silenced + $negated + $flipped + (int)$validNot + + (int)$validSilenced + $validNegated + $validSubtraction + + $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow(); } } diff --git a/tests/_data/ImplicitCastSpacing/before.php b/tests/_data/ImplicitCastSpacing/before.php index d2f31d9..89be656 100644 --- a/tests/_data/ImplicitCastSpacing/before.php +++ b/tests/_data/ImplicitCastSpacing/before.php @@ -10,15 +10,22 @@ public function run(bool $ready, int $count, int $mask, callable $callable): int { $not = ! $ready; $silenced = @ $callable(); - $flipped = ~ $mask; - ++ $count; - $count --; + $negated = - $count; + $flipped = ~ $mask; $validNot = !$ready; $validSilenced = @$callable(); - ++$count; - $count++; + $validNegated = -$count; + $validSubtraction = $count - $mask; + $constantMinus = __LINE__ - 1; + $boolMinus = (int)true - 1; + $nullMinus = (int)null - 1; + $fqcnMinus = \PHP_INT_MAX - 1; + $doubleNegated = - -$count; + $arrow = fn (): int => - $count; - return (int)$not + (int)$silenced + $flipped + $count + (int)$validNot + (int)$validSilenced; + return (int)$not + (int)$silenced + $negated + $flipped + (int)$validNot + + (int)$validSilenced + $validNegated + $validSubtraction + + $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow(); } } diff --git a/tests/_data/ImplicitCastSpacing/before.tokens.php b/tests/_data/ImplicitCastSpacing/before.tokens.php new file mode 100644 index 0000000..88efc6e --- /dev/null +++ b/tests/_data/ImplicitCastSpacing/before.tokens.php @@ -0,0 +1,62 @@ + - $count; +// T_WHITESPACE T_VARIABLE T_WHITESPACE T_EQUAL T_WHITESPACE T_FN T_WHITESPACE T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE T_FN_ARROW T_WHITESPACE T_MINUS T_WHITESPACE T_VARIABLE T_SEMICOLON T_WHITESPACE + +// T_WHITESPACE + return (int)$not + (int)$silenced + $negated + $flipped + (int)$validNot +// T_WHITESPACE T_RETURN T_WHITESPACE T_INT_CAST T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_INT_CAST T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_INT_CAST T_VARIABLE T_WHITESPACE + + (int)$validSilenced + $validNegated + $validSubtraction +// T_WHITESPACE T_PLUS T_WHITESPACE T_INT_CAST T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE + + $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow(); +// T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_WHITESPACE T_PLUS T_WHITESPACE T_VARIABLE T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE + } +// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE +} +// T_CLOSE_CURLY_BRACKET T_WHITESPACE \ No newline at end of file