From 9f7ac1ba23de22c5850b95dcfe4f01ce842f8213 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 6 Aug 2026 15:59:54 +0200 Subject: [PATCH] Cover the reference operator in ImplicitCastSpacing The sniff already owned this shape for `!`, `@` and unary minus, so the reference marker joins it rather than arriving as a separate sniff. That also lets psr2r-sniffer, which enables this rule already, drop its own UnaryOperatorSpacing: the reference marker was the only thing it still covered that nothing here did. Telling a reference from a bitwise and needs more than the preceding token. A type hint precedes the marker in `function f(array & $items)`, and a type hint reads exactly like the left operand of a bitwise and. So a reference is one that stands in front of a variable, or the ellipsis of a by-reference variadic, and either follows something that cannot end a value or sits in a parameter list. Requiring the variable on the right keeps a default such as `function f(int $x = self::A & self::B)` out of it. Covered by fixtures: plain `$a = & $list`, by-reference foreach in both forms, a typed parameter, a variadic, and the bitwise and boolean operators that must stay untouched. --- .../WhiteSpace/ImplicitCastSpacingSniff.php | 50 ++++++++++++++++++- .../ImplicitCastSpacingSniffTest.php | 4 +- tests/_data/ImplicitCastSpacing/after.php | 35 ++++++++++++- tests/_data/ImplicitCastSpacing/before.php | 35 ++++++++++++- 4 files changed, 117 insertions(+), 7 deletions(-) diff --git a/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php b/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php index 37496d6..3d6c5eb 100644 --- a/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php +++ b/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php @@ -22,7 +22,7 @@ class ImplicitCastSpacingSniff implements Sniff */ public function register(): array { - return [T_BOOLEAN_NOT, T_NONE, T_ASPERAND, T_INC, T_DEC, T_MINUS]; + return [T_BOOLEAN_NOT, T_NONE, T_ASPERAND, T_INC, T_DEC, T_MINUS, T_BITWISE_AND]; } /** @@ -38,6 +38,11 @@ public function process(File $phpcsFile, $stackPtr): void return; } + // `&` marks a reference here; as bitwise and it wants its spaces. + if ($tokens[$stackPtr]['code'] === T_BITWISE_AND && !$this->isReferenceOperator($phpcsFile, $stackPtr)) { + 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)) { @@ -65,6 +70,48 @@ public function process(File $phpcsFile, $stackPtr): void } } + /** + * `&` binds a reference rather than a bitwise and when it sits in front of a variable - or the + * ellipsis of a by-reference variadic - and + * either follows something that cannot end a value, or stands in a parameter list. + * + * The parameter list case needs its own test because a type hint precedes the marker in + * `function f(array & $items)`, and a type hint reads exactly like the left operand of a + * bitwise and. Requiring a variable on the right keeps a default such as + * `function f(int $x = A & B)` out of it. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $stackPtr + * + * @return bool + */ + protected function isReferenceOperator(File $phpcsFile, int $stackPtr): bool + { + $tokens = $phpcsFile->getTokens(); + + $nextIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if ($nextIndex === false || !in_array($tokens[$nextIndex]['code'], [T_VARIABLE, T_ELLIPSIS], true)) { + return false; + } + + if ($this->isUnaryOperator($phpcsFile, $stackPtr)) { + return true; + } + + foreach ($tokens[$stackPtr]['nested_parenthesis'] ?? [] as $opener => $closer) { + if (!isset($tokens[$opener]['parenthesis_owner'])) { + continue; + } + + $owner = $tokens[$opener]['parenthesis_owner']; + if (in_array($tokens[$owner]['code'], [T_FUNCTION, T_CLOSURE, T_FN], true)) { + return true; + } + } + + return false; + } + /** * A minus is unary only when what precedes it cannot end a value - an operator, an opening * bracket, a comma, `return` and so on. @@ -111,6 +158,7 @@ protected function isUnaryOperator(File $phpcsFile, int $stackPtr): bool T_ECHO => T_ECHO, T_PRINT => T_PRINT, T_CASE => T_CASE, + T_AS => T_AS, T_BOOLEAN_NOT => T_BOOLEAN_NOT, T_YIELD => T_YIELD, T_YIELD_FROM => T_YIELD_FROM, diff --git a/tests/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniffTest.php b/tests/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniffTest.php index 0923db9..758ca52 100644 --- a/tests/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniffTest.php +++ b/tests/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniffTest.php @@ -17,7 +17,7 @@ class ImplicitCastSpacingSniffTest extends TestCase */ public function testImplicitCastSpacingSniffer(): void { - $this->assertSnifferFindsFixableErrors(new ImplicitCastSpacingSniff(), 4, 4); + $this->assertSnifferFindsFixableErrors(new ImplicitCastSpacingSniff(), 8, 8); } /** @@ -25,6 +25,6 @@ public function testImplicitCastSpacingSniffer(): void */ public function testImplicitCastSpacingFixer(): void { - $this->assertSnifferCanFixErrors(new ImplicitCastSpacingSniff(), 4); + $this->assertSnifferCanFixErrors(new ImplicitCastSpacingSniff(), 8); } } diff --git a/tests/_data/ImplicitCastSpacing/after.php b/tests/_data/ImplicitCastSpacing/after.php index f8432c9..55a07ff 100644 --- a/tests/_data/ImplicitCastSpacing/after.php +++ b/tests/_data/ImplicitCastSpacing/after.php @@ -6,7 +6,7 @@ class ImplicitCastSpacingExample { - public function run(bool $ready, int $count, int $mask, callable $callable): int + public function run(bool $ready, int $count, int $mask, callable $callable, array $items): int { $not = !$ready; $silenced = @$callable(); @@ -23,9 +23,40 @@ public function run(bool $ready, int $count, int $mask, callable $callable): int $fqcnMinus = \PHP_INT_MAX - 1; $doubleNegated = - -$count; $arrow = fn (): int => -$count; + $reference = &$items; + $validReference = &$items; + $validBitwiseAnd = $count & $mask; + + foreach ($items as &$item) { + $item = (int)$item; + } + unset($item); + + $this->byRef($items); return (int)$not + (int)$silenced + $negated + $flipped + (int)$validNot + (int)$validSilenced + $validNegated + $validSubtraction - + $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow(); + + $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow() + + count($reference) + count($validReference) + $validBitwiseAnd; + } + + /** + * @param array $items + * + * @return void + */ + protected function byRef(array &$items): void + { + $items[] = 1; + } + + /** + * @param int ...$args + * + * @return void + */ + protected function byRefVariadic(&...$args): void + { + $args[] = 1; } } diff --git a/tests/_data/ImplicitCastSpacing/before.php b/tests/_data/ImplicitCastSpacing/before.php index 89be656..d9ffb05 100644 --- a/tests/_data/ImplicitCastSpacing/before.php +++ b/tests/_data/ImplicitCastSpacing/before.php @@ -6,7 +6,7 @@ class ImplicitCastSpacingExample { - public function run(bool $ready, int $count, int $mask, callable $callable): int + public function run(bool $ready, int $count, int $mask, callable $callable, array $items): int { $not = ! $ready; $silenced = @ $callable(); @@ -23,9 +23,40 @@ public function run(bool $ready, int $count, int $mask, callable $callable): int $fqcnMinus = \PHP_INT_MAX - 1; $doubleNegated = - -$count; $arrow = fn (): int => - $count; + $reference = & $items; + $validReference = &$items; + $validBitwiseAnd = $count & $mask; + + foreach ($items as & $item) { + $item = (int)$item; + } + unset($item); + + $this->byRef($items); return (int)$not + (int)$silenced + $negated + $flipped + (int)$validNot + (int)$validSilenced + $validNegated + $validSubtraction - + $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow(); + + $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow() + + count($reference) + count($validReference) + $validBitwiseAnd; + } + + /** + * @param array $items + * + * @return void + */ + protected function byRef(array & $items): void + { + $items[] = 1; + } + + /** + * @param int ...$args + * + * @return void + */ + protected function byRefVariadic(& ...$args): void + { + $args[] = 1; } }