Skip to content

Commit a6244e4

Browse files
authored
Cover the reference operator in ImplicitCastSpacing (#82)
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.
1 parent e1c734c commit a6244e4

4 files changed

Lines changed: 117 additions & 7 deletions

File tree

‎PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniff.php‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ImplicitCastSpacingSniff implements Sniff
2222
*/
2323
public function register(): array
2424
{
25-
return [T_BOOLEAN_NOT, T_NONE, T_ASPERAND, T_INC, T_DEC, T_MINUS];
25+
return [T_BOOLEAN_NOT, T_NONE, T_ASPERAND, T_INC, T_DEC, T_MINUS, T_BITWISE_AND];
2626
}
2727

2828
/**
@@ -38,6 +38,11 @@ public function process(File $phpcsFile, $stackPtr): void
3838
return;
3939
}
4040

41+
// `&` marks a reference here; as bitwise and it wants its spaces.
42+
if ($tokens[$stackPtr]['code'] === T_BITWISE_AND && !$this->isReferenceOperator($phpcsFile, $stackPtr)) {
43+
return;
44+
}
45+
4146
// A minus is only an implicit cast when it negates; as a subtraction it wants its spaces.
4247
if ($tokens[$stackPtr]['code'] === T_MINUS) {
4348
if (!$this->isUnaryOperator($phpcsFile, $stackPtr)) {
@@ -65,6 +70,48 @@ public function process(File $phpcsFile, $stackPtr): void
6570
}
6671
}
6772

73+
/**
74+
* `&` binds a reference rather than a bitwise and when it sits in front of a variable - or the
75+
* ellipsis of a by-reference variadic - and
76+
* either follows something that cannot end a value, or stands in a parameter list.
77+
*
78+
* The parameter list case needs its own test because a type hint precedes the marker in
79+
* `function f(array & $items)`, and a type hint reads exactly like the left operand of a
80+
* bitwise and. Requiring a variable on the right keeps a default such as
81+
* `function f(int $x = A & B)` out of it.
82+
*
83+
* @param \PHP_CodeSniffer\Files\File $phpcsFile
84+
* @param int $stackPtr
85+
*
86+
* @return bool
87+
*/
88+
protected function isReferenceOperator(File $phpcsFile, int $stackPtr): bool
89+
{
90+
$tokens = $phpcsFile->getTokens();
91+
92+
$nextIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
93+
if ($nextIndex === false || !in_array($tokens[$nextIndex]['code'], [T_VARIABLE, T_ELLIPSIS], true)) {
94+
return false;
95+
}
96+
97+
if ($this->isUnaryOperator($phpcsFile, $stackPtr)) {
98+
return true;
99+
}
100+
101+
foreach ($tokens[$stackPtr]['nested_parenthesis'] ?? [] as $opener => $closer) {
102+
if (!isset($tokens[$opener]['parenthesis_owner'])) {
103+
continue;
104+
}
105+
106+
$owner = $tokens[$opener]['parenthesis_owner'];
107+
if (in_array($tokens[$owner]['code'], [T_FUNCTION, T_CLOSURE, T_FN], true)) {
108+
return true;
109+
}
110+
}
111+
112+
return false;
113+
}
114+
68115
/**
69116
* A minus is unary only when what precedes it cannot end a value - an operator, an opening
70117
* bracket, a comma, `return` and so on.
@@ -111,6 +158,7 @@ protected function isUnaryOperator(File $phpcsFile, int $stackPtr): bool
111158
T_ECHO => T_ECHO,
112159
T_PRINT => T_PRINT,
113160
T_CASE => T_CASE,
161+
T_AS => T_AS,
114162
T_BOOLEAN_NOT => T_BOOLEAN_NOT,
115163
T_YIELD => T_YIELD,
116164
T_YIELD_FROM => T_YIELD_FROM,

‎tests/PhpCollective/Sniffs/WhiteSpace/ImplicitCastSpacingSniffTest.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ class ImplicitCastSpacingSniffTest extends TestCase
1717
*/
1818
public function testImplicitCastSpacingSniffer(): void
1919
{
20-
$this->assertSnifferFindsFixableErrors(new ImplicitCastSpacingSniff(), 4, 4);
20+
$this->assertSnifferFindsFixableErrors(new ImplicitCastSpacingSniff(), 8, 8);
2121
}
2222

2323
/**
2424
* @return void
2525
*/
2626
public function testImplicitCastSpacingFixer(): void
2727
{
28-
$this->assertSnifferCanFixErrors(new ImplicitCastSpacingSniff(), 4);
28+
$this->assertSnifferCanFixErrors(new ImplicitCastSpacingSniff(), 8);
2929
}
3030
}

‎tests/_data/ImplicitCastSpacing/after.php‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class ImplicitCastSpacingExample
88
{
9-
public function run(bool $ready, int $count, int $mask, callable $callable): int
9+
public function run(bool $ready, int $count, int $mask, callable $callable, array $items): int
1010
{
1111
$not = !$ready;
1212
$silenced = @$callable();
@@ -23,9 +23,40 @@ public function run(bool $ready, int $count, int $mask, callable $callable): int
2323
$fqcnMinus = \PHP_INT_MAX - 1;
2424
$doubleNegated = - -$count;
2525
$arrow = fn (): int => -$count;
26+
$reference = &$items;
27+
$validReference = &$items;
28+
$validBitwiseAnd = $count & $mask;
29+
30+
foreach ($items as &$item) {
31+
$item = (int)$item;
32+
}
33+
unset($item);
34+
35+
$this->byRef($items);
2636

2737
return (int)$not + (int)$silenced + $negated + $flipped + (int)$validNot
2838
+ (int)$validSilenced + $validNegated + $validSubtraction
29-
+ $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow();
39+
+ $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow()
40+
+ count($reference) + count($validReference) + $validBitwiseAnd;
41+
}
42+
43+
/**
44+
* @param array<int> $items
45+
*
46+
* @return void
47+
*/
48+
protected function byRef(array &$items): void
49+
{
50+
$items[] = 1;
51+
}
52+
53+
/**
54+
* @param int ...$args
55+
*
56+
* @return void
57+
*/
58+
protected function byRefVariadic(&...$args): void
59+
{
60+
$args[] = 1;
3061
}
3162
}

‎tests/_data/ImplicitCastSpacing/before.php‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class ImplicitCastSpacingExample
88
{
9-
public function run(bool $ready, int $count, int $mask, callable $callable): int
9+
public function run(bool $ready, int $count, int $mask, callable $callable, array $items): int
1010
{
1111
$not = ! $ready;
1212
$silenced = @ $callable();
@@ -23,9 +23,40 @@ public function run(bool $ready, int $count, int $mask, callable $callable): int
2323
$fqcnMinus = \PHP_INT_MAX - 1;
2424
$doubleNegated = - -$count;
2525
$arrow = fn (): int => - $count;
26+
$reference = & $items;
27+
$validReference = &$items;
28+
$validBitwiseAnd = $count & $mask;
29+
30+
foreach ($items as & $item) {
31+
$item = (int)$item;
32+
}
33+
unset($item);
34+
35+
$this->byRef($items);
2636

2737
return (int)$not + (int)$silenced + $negated + $flipped + (int)$validNot
2838
+ (int)$validSilenced + $validNegated + $validSubtraction
29-
+ $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow();
39+
+ $constantMinus + $boolMinus + $nullMinus + $fqcnMinus + $doubleNegated + $arrow()
40+
+ count($reference) + count($validReference) + $validBitwiseAnd;
41+
}
42+
43+
/**
44+
* @param array<int> $items
45+
*
46+
* @return void
47+
*/
48+
protected function byRef(array & $items): void
49+
{
50+
$items[] = 1;
51+
}
52+
53+
/**
54+
* @param int ...$args
55+
*
56+
* @return void
57+
*/
58+
protected function byRefVariadic(& ...$args): void
59+
{
60+
$args[] = 1;
3061
}
3162
}

0 commit comments

Comments
 (0)