diff --git a/PhpCollective/Sniffs/WhiteSpace/PipeOperatorSpacingSniff.php b/PhpCollective/Sniffs/WhiteSpace/PipeOperatorSpacingSniff.php new file mode 100644 index 0000000..fff8fa4 --- /dev/null +++ b/PhpCollective/Sniffs/WhiteSpace/PipeOperatorSpacingSniff.php @@ -0,0 +1,145 @@ +) - a PHP 8.5 feature. + * + * The pipe operator allows for more readable function chaining: + * $result = $input |> trim(...) |> strtolower(...); + */ +class PipeOperatorSpacingSniff implements Sniff +{ + /** + * @inheritDoc + */ + public function register(): array + { + return [T_BITWISE_OR]; + } + + /** + * @inheritDoc + */ + public function process(File $phpcsFile, $stackPtr): void + { + $tokens = $phpcsFile->getTokens(); + + // Check if this is part of a pipe operator (|>) + $nextNonWhitespace = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if (!$nextNonWhitespace || $tokens[$nextNonWhitespace]['code'] !== T_GREATER_THAN) { + return; + } + + // Check if there's whitespace between | and > + if ($nextNonWhitespace !== $stackPtr + 1) { + $fix = $phpcsFile->addFixableError( + 'Expected at least 1 space before ">"; 0 found', + $stackPtr, + 'SpaceBetweenPipe', + ); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $stackPtr + 1; $i < $nextNonWhitespace; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + } + + // Now check spacing around the pipe operator + $this->checkSpacingBefore($phpcsFile, $stackPtr); + $this->checkSpacingAfter($phpcsFile, $nextNonWhitespace); + } + + /** + * Check that there's exactly one space before the pipe operator + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $stackPtr + * + * @return void + */ + protected function checkSpacingBefore(File $phpcsFile, int $stackPtr): void + { + $tokens = $phpcsFile->getTokens(); + + $prevIndex = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); + if (!$prevIndex) { + return; + } + + // Check if we're at the start of a line + if ($tokens[$prevIndex]['line'] !== $tokens[$stackPtr]['line']) { + return; + } + + if ($tokens[$stackPtr - 1]['code'] !== T_WHITESPACE) { + $message = 'Expected at least 1 space before "|"; 0 found'; + $fix = $phpcsFile->addFixableError($message, $stackPtr, 'MissingBefore'); + if ($fix) { + $phpcsFile->fixer->addContentBefore($stackPtr, ' '); + } + } else { + $content = $tokens[$stackPtr - 1]['content']; + if ($content !== ' ' && $tokens[$prevIndex]['line'] === $tokens[$stackPtr]['line']) { + $message = 'Expected 1 space before "|", but %d found'; + $data = [strlen($content)]; + $fix = $phpcsFile->addFixableError($message, $stackPtr, 'TooManyBefore', $data); + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr - 1, ' '); + } + } + } + } + + /** + * Check that there's exactly one space after the pipe operator + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $greaterThanPtr Pointer to the > token + * + * @return void + */ + protected function checkSpacingAfter(File $phpcsFile, int $greaterThanPtr): void + { + $tokens = $phpcsFile->getTokens(); + + $nextIndex = $phpcsFile->findNext(T_WHITESPACE, $greaterThanPtr + 1, null, true); + if (!$nextIndex) { + return; + } + + // Check if next token is on a different line + if ($tokens[$nextIndex]['line'] !== $tokens[$greaterThanPtr]['line']) { + return; + } + + if ($tokens[$greaterThanPtr + 1]['code'] !== T_WHITESPACE) { + $message = 'Expected at least 1 space after ">"; 0 found'; + $fix = $phpcsFile->addFixableError($message, $greaterThanPtr, 'MissingAfter'); + if ($fix) { + $phpcsFile->fixer->addContent($greaterThanPtr, ' '); + } + } else { + $content = $tokens[$greaterThanPtr + 1]['content']; + if ($content !== ' ' && $tokens[$nextIndex]['line'] === $tokens[$greaterThanPtr]['line']) { + $message = 'Expected 1 space after ">", but %d found'; + $data = [strlen($content)]; + $fix = $phpcsFile->addFixableError($message, $greaterThanPtr, 'TooManyAfter', $data); + if ($fix) { + $phpcsFile->fixer->replaceToken($greaterThanPtr + 1, ' '); + } + } + } + } +} diff --git a/tests/PhpCollective/Sniffs/WhiteSpace/PipeOperatorSpacingSniffTest.php b/tests/PhpCollective/Sniffs/WhiteSpace/PipeOperatorSpacingSniffTest.php new file mode 100644 index 0000000..c0f3047 --- /dev/null +++ b/tests/PhpCollective/Sniffs/WhiteSpace/PipeOperatorSpacingSniffTest.php @@ -0,0 +1,30 @@ +assertSnifferFindsFixableErrors(new PipeOperatorSpacingSniff(), 10, 10); + } + + /** + * @return void + */ + public function testPipeOperatorSpacingFixer(): void + { + $this->assertSnifferCanFixErrors(new PipeOperatorSpacingSniff(), 10); + } +} diff --git a/tests/_data/PipeOperatorSpacing/after.php b/tests/_data/PipeOperatorSpacing/after.php new file mode 100644 index 0000000..d1fc770 --- /dev/null +++ b/tests/_data/PipeOperatorSpacing/after.php @@ -0,0 +1,32 @@ + trim(...) + |> strtolower(...); + + // Missing spaces + $bad1 = $input |> trim(...) |> strtolower(...); + + // Extra spaces before | + $bad2 = $input |> trim(...); + + // Extra spaces after > + $bad3 = $input |> trim(...); + + // Combination of issues + $bad4 = $input |> trim(...) |> strtolower(...); + + return $output; + } +} diff --git a/tests/_data/PipeOperatorSpacing/before.php b/tests/_data/PipeOperatorSpacing/before.php new file mode 100644 index 0000000..a9822c7 --- /dev/null +++ b/tests/_data/PipeOperatorSpacing/before.php @@ -0,0 +1,32 @@ + trim(...) + |> strtolower(...); + + // Missing spaces + $bad1 = $input|>trim(...)|>strtolower(...); + + // Extra spaces before | + $bad2 = $input |> trim(...); + + // Extra spaces after > + $bad3 = $input |> trim(...); + + // Combination of issues + $bad4 = $input|>trim(...) |> strtolower(...); + + return $output; + } +}