From 220ef55488a2157f386d4fa869dd078bba480028 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 25 Oct 2026 14:55:32 +0100 Subject: [PATCH] Write multi-catch type separators without spaces Union and intersection types are enforced without spaces around `|` and `&`, but the `|` in a multi-catch is tokenized as a bitwise OR, so PSR12.Operators.OperatorSpacing required `catch (A | B $e)`, the opposite style. New PhpCollective.WhiteSpace.CatchTypeSeparatorSpacing reports spaces around a catch type separator and removes them; a separator next to a newline is reported but not fixed. PSR12.Operators.OperatorSpacing is replaced by PhpCollective.Operators.OperatorSpacing, a subclass that skips catch separators and otherwise behaves the same, including the skip over declare() statements. --- .../Sniffs/Operators/OperatorSpacingSniff.php | 34 +++++++ .../CatchTypeSeparatorSpacingSniff.php | 90 +++++++++++++++++++ PhpCollective/ruleset.xml | 1 + docs/sniffs.md | 9 +- .../Operators/OperatorSpacingSniffTest.php | 26 ++++++ .../CatchTypeSeparatorSpacingSniffTest.php | 24 +++++ .../_data/CatchTypeSeparatorSpacing/after.php | 17 ++++ .../CatchTypeSeparatorSpacing/before.php | 17 ++++ tests/_data/OperatorSpacing/before.php | 10 +++ 9 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 PhpCollective/Sniffs/Operators/OperatorSpacingSniff.php create mode 100644 PhpCollective/Sniffs/WhiteSpace/CatchTypeSeparatorSpacingSniff.php create mode 100644 tests/PhpCollective/Sniffs/Operators/OperatorSpacingSniffTest.php create mode 100644 tests/PhpCollective/Sniffs/WhiteSpace/CatchTypeSeparatorSpacingSniffTest.php create mode 100644 tests/_data/CatchTypeSeparatorSpacing/after.php create mode 100644 tests/_data/CatchTypeSeparatorSpacing/before.php create mode 100644 tests/_data/OperatorSpacing/before.php diff --git a/PhpCollective/Sniffs/Operators/OperatorSpacingSniff.php b/PhpCollective/Sniffs/Operators/OperatorSpacingSniff.php new file mode 100644 index 0000000..071ff70 --- /dev/null +++ b/PhpCollective/Sniffs/Operators/OperatorSpacingSniff.php @@ -0,0 +1,34 @@ +getTokens(); + $before = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + $after = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + + if ($before === false || $after === false) { + return; + } + + $newline = $tokens[$before]['line'] !== $tokens[$stackPtr]['line'] + || $tokens[$after]['line'] !== $tokens[$stackPtr]['line']; + $spacing = false; + for ($i = $before + 1; $i < $after; $i++) { + if ($tokens[$i]['code'] === T_WHITESPACE) { + $spacing = true; + + break; + } + } + if (!$newline && !$spacing) { + return; + } + + $message = 'Expected no whitespace around the catch type separator'; + if ($newline) { + $phpcsFile->addError($message, $stackPtr, 'SpacesAround'); + + return; + } + + $fix = $phpcsFile->addFixableError($message, $stackPtr, 'SpacesAround'); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $before + 1; $i < $after; $i++) { + if ($i !== $stackPtr && $tokens[$i]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + $phpcsFile->fixer->endChangeset(); + } + } + + public static function isCatchTypeSeparator(File $phpcsFile, int $stackPtr): bool + { + $tokens = $phpcsFile->getTokens(); + if ($tokens[$stackPtr]['code'] !== T_BITWISE_OR || empty($tokens[$stackPtr]['nested_parenthesis'])) { + return false; + } + + $openers = array_keys($tokens[$stackPtr]['nested_parenthesis']); + $opener = end($openers); + + return isset($tokens[$opener]['parenthesis_owner']) + && $tokens[$tokens[$opener]['parenthesis_owner']]['code'] === T_CATCH; + } +} diff --git a/PhpCollective/ruleset.xml b/PhpCollective/ruleset.xml index e5a84ee..fa271fd 100644 --- a/PhpCollective/ruleset.xml +++ b/PhpCollective/ruleset.xml @@ -237,6 +237,7 @@ +