Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions PhpCollective/Sniffs/Operators/OperatorSpacingSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

/**
* MIT License
* For full license information, please view the LICENSE file that was distributed with this source code.
*/

namespace PhpCollective\Sniffs\Operators;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Standards\PSR12\Sniffs\Operators\OperatorSpacingSniff as Psr12OperatorSpacingSniff;

/**
* Operator spacing, with the `|` in a `catch` type list treated as a type separator.
*
* PHP tokenizes a catch type list (`catch (ValueError|TypeError $e)`) with plain `T_BITWISE_OR`
* tokens, so the inherited sniff reads it as a bitwise operator and demands a space on both
* sides. That is the opposite of the type separator rule enforced by
* `Universal.Operators.TypeSeparatorSpacing`, which only listens for `T_TYPE_UNION` and never
* sees a catch list. Downstream projects such as cakephp/cakephp had to silence
* `PSR12.Operators.OperatorSpacing.NoSpaceBefore` and `PSR12.Operators.OperatorSpacing.NoSpaceAfter`
* in their `phpcs.xml` to allow the no-space form, and that also silenced the real bitwise
* operators. This sniff keeps every inherited check and treats the catch-list `|` like a union
* type separator: no space, fixable.
*/
class OperatorSpacingSniff extends Psr12OperatorSpacingSniff
{
/**
* @inheritDoc
*/
public function process(File $phpcsFile, int $stackPtr)
{
$tokens = $phpcsFile->getTokens();

if ($tokens[$stackPtr]['code'] === T_BITWISE_OR && $this->isCatchTypeSeparator($stackPtr, $tokens) === true) {
$this->checkTypeSeparator($phpcsFile, $stackPtr, $tokens, true);
$this->checkTypeSeparator($phpcsFile, $stackPtr, $tokens, false);

return;
}

return parent::process($phpcsFile, $stackPtr);
}

/**
* Whether the `|` at `$stackPtr` is a separator in a `catch` type list.
*
* The tokenizer does not mark these as `T_TYPE_UNION`; they are plain `T_BITWISE_OR` inside
* the parenthesis that the `catch` keyword owns. The innermost enclosing parenthesis is the
* last entry of `nested_parenthesis` (keys are open-token positions, in source order). A
* real bitwise `|` in the catch body has no parenthesis owned by `catch`, so it falls
* through to the inherited bitwise-operator check.
*
* @param int $stackPtr Position of the `|`.
* @param array<int, array<string, mixed>> $tokens The token stack.
*
* @return bool
*/
protected function isCatchTypeSeparator(int $stackPtr, array $tokens): bool
{
$nested = $tokens[$stackPtr]['nested_parenthesis'] ?? [];
$open = array_key_last($nested);
if ($open === null) {
return false;
}

$owner = $tokens[$open]['parenthesis_owner'] ?? null;

return $owner !== null && $tokens[$owner]['code'] === T_CATCH;
}

/**
* Reports and fixes the spacing on one side of the type separator. A union type separator
* carries no space, so any whitespace here is the violation.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr Position of the `|`.
* @param array<int, array<string, mixed>> $tokens The token stack.
* @param bool $before Check the side before the operator.
*
* @return void
*/
protected function checkTypeSeparator(File $phpcsFile, int $stackPtr, array $tokens, bool $before): void
{
$index = $before ? $stackPtr - 1 : $stackPtr + 1;
if (!isset($tokens[$index]) || $tokens[$index]['code'] !== T_WHITESPACE) {
return;
}

$side = $before ? 'before' : 'after';
$message = 'No space %s the `|` in a catch type list (it is a type separator); found %d';
$code = $before ? 'SpaceBefore' : 'SpaceAfter';
$fix = $phpcsFile->addFixableError($message, $stackPtr, $code, [$side, strlen($tokens[$index]['content'])]);

if ($fix === true) {
$phpcsFile->fixer->replaceToken($index, '');
}
}
}
5 changes: 5 additions & 0 deletions PhpCollective/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,13 @@
<exclude name="PSR12.Files.OpenTag"/>
<!-- Long casts are covered by SlevomatCodingStandard.PHP.TypeCast, which catches more. -->
<exclude name="PSR12.Keywords.ShortFormTypeKeywords"/>
<!-- Replaced by PhpCollective.Operators.OperatorSpacing, which additionally treats the
`|` in a catch type list as a type separator (no space) instead of a bitwise operator. -->
<exclude name="PSR12.Operators.OperatorSpacing"/>
</rule>

<rule ref="PhpCollective.Operators.OperatorSpacing"/>

<!--
Three rules used to report every long cast. SlevomatCodingStandard.PHP.TypeCast above is
the only one that covers all of them - it also rewrites `(double)` to `(float)` and copes
Expand Down
34 changes: 34 additions & 0 deletions tests/PhpCollective/Sniffs/Operators/OperatorSpacingSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* MIT License
* For full license information, please view the LICENSE file that was distributed with this source code.
*/

namespace PhpCollective\Test\PhpCollective\Sniffs\Operators;

use PhpCollective\Sniffs\Operators\OperatorSpacingSniff;
use PhpCollective\Test\TestCase;

class OperatorSpacingSniffTest extends TestCase
{
/**
* @return void
*/
public function testOperatorSpacingSniff(): void
{
// The `|` in a catch type list is a type separator: no space allowed, so the spaced
// separator is reported on both sides (2 fixable errors). The real bitwise operator in
// the catch body keeps the inherited spacing rule: the missing space after it is the
// third fixable error, proving the inherited check still runs for genuine operators.
$this->assertSnifferFindsFixableErrors(new OperatorSpacingSniff(), 3, 3);
}

/**
* @return void
*/
public function testOperatorSpacingFixer(): void
{
$this->assertSnifferCanFixErrors(new OperatorSpacingSniff(), 3);
}
}
7 changes: 7 additions & 0 deletions tests/_data/OperatorSpacing/after.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

try {
doThing();
} catch (ValueError|TypeError $e) {
$x = $a | $b;
}
7 changes: 7 additions & 0 deletions tests/_data/OperatorSpacing/before.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

try {
doThing();
} catch (ValueError | TypeError $e) {
$x = $a |$b;
}