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 @@ + + + +