From 0760defcab8b5d65cffb1fbed10ded81f0c3776f Mon Sep 17 00:00:00 2001 From: XRafik Date: Sun, 25 Oct 2026 15:41:31 +0000 Subject: [PATCH] Treat the | in a catch type list as a union type separator PHP tokenizes a catch type list (catch (ValueError|TypeError $e)) with plain T_BITWISE_OR tokens, so PSR12.Operators.OperatorSpacing read it as a bitwise operator and demanded a space on both sides - the opposite of the type separator rule from 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 both NoSpaceBefore and NoSpaceAfter in their phpcs.xml, which also silenced the real bitwise operators. The new PhpCollective.Operators.OperatorSpacing extends the PSR12 sniff, keeps every inherited check, and re-rules the catch-list | like a union type separator: no space, fixable. The ruleset swaps in the subclass and excludes the vendor sniff. Closes #88. --- .../Sniffs/Operators/OperatorSpacingSniff.php | 99 +++++++++++++++++++ PhpCollective/ruleset.xml | 5 + .../Operators/OperatorSpacingSniffTest.php | 34 +++++++ tests/_data/OperatorSpacing/after.php | 7 ++ tests/_data/OperatorSpacing/before.php | 7 ++ 5 files changed, 152 insertions(+) create mode 100644 PhpCollective/Sniffs/Operators/OperatorSpacingSniff.php create mode 100644 tests/PhpCollective/Sniffs/Operators/OperatorSpacingSniffTest.php create mode 100644 tests/_data/OperatorSpacing/after.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..ffc338a --- /dev/null +++ b/PhpCollective/Sniffs/Operators/OperatorSpacingSniff.php @@ -0,0 +1,99 @@ +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> $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> $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, ''); + } + } +} diff --git a/PhpCollective/ruleset.xml b/PhpCollective/ruleset.xml index e5a84ee..bf57c76 100644 --- a/PhpCollective/ruleset.xml +++ b/PhpCollective/ruleset.xml @@ -237,8 +237,13 @@ + + + +