From 5520fe18410e7ba09a32c075628f976303bada8e Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 17 Nov 2025 21:12:57 +0100 Subject: [PATCH 1/2] Add VoidCastSniff for PHP 8.5 void cast support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds support for the PHP 8.5 `(void)` cast feature by implementing a sniff that enforces consistent spacing and formatting. The sniff ensures: - No space before the cast - No space within the cast parentheses - Exactly one space after the cast - Proper handling of edge cases All violations are auto-fixable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- PhpCollective/Sniffs/PHP/VoidCastSniff.php | 183 ++++++++++++++++++ .../Sniffs/PHP/VoidCastSniffTest.php | 30 +++ tests/_data/VoidCast/after.php | 51 +++++ tests/_data/VoidCast/before.php | 51 +++++ tests/_data/VoidCast/before.tokens.php | 102 ++++++++++ 5 files changed, 417 insertions(+) create mode 100644 PhpCollective/Sniffs/PHP/VoidCastSniff.php create mode 100644 tests/PhpCollective/Sniffs/PHP/VoidCastSniffTest.php create mode 100644 tests/_data/VoidCast/after.php create mode 100644 tests/_data/VoidCast/before.php create mode 100644 tests/_data/VoidCast/before.tokens.php diff --git a/PhpCollective/Sniffs/PHP/VoidCastSniff.php b/PhpCollective/Sniffs/PHP/VoidCastSniff.php new file mode 100644 index 0000000..0e25f68 --- /dev/null +++ b/PhpCollective/Sniffs/PHP/VoidCastSniff.php @@ -0,0 +1,183 @@ +getTokens(); + + // Check if this is a (void) cast pattern + $nextNonWhitespace = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if (!$nextNonWhitespace || $tokens[$nextNonWhitespace]['code'] !== T_STRING) { + return; + } + + if (strtolower($tokens[$nextNonWhitespace]['content']) !== 'void') { + return; + } + + $closeParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, $nextNonWhitespace + 1, null, true); + if (!$closeParenthesis || $tokens[$closeParenthesis]['code'] !== T_CLOSE_PARENTHESIS) { + return; + } + + // We have a (void) cast - check spacing + $this->checkSpacingBeforeCast($phpcsFile, $stackPtr); + $this->checkSpacingWithinCast($phpcsFile, $stackPtr, $nextNonWhitespace, $closeParenthesis); + $this->checkSpacingAfterCast($phpcsFile, $closeParenthesis); + } + + /** + * Check that there's no space before the opening parenthesis of the cast + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $stackPtr + * + * @return void + */ + protected function checkSpacingBeforeCast(File $phpcsFile, int $stackPtr): void + { + $tokens = $phpcsFile->getTokens(); + + // Check if previous token is whitespace at statement start, which is OK + $prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + if (!$prevIndex) { + return; + } + + // If there's whitespace before the cast and we're not at statement start, it might be intentional + // We mainly want to avoid cases like `foo (void)bar()` + if ($tokens[$stackPtr - 1]['code'] === T_WHITESPACE) { + $prevToken = $tokens[$prevIndex]; + // Only warn if the previous non-whitespace token suggests this is mid-expression + if ( + in_array($prevToken['code'], [T_STRING, T_VARIABLE, T_CLOSE_PARENTHESIS, T_CLOSE_SQUARE_BRACKET], true) + && $prevToken['line'] === $tokens[$stackPtr]['line'] + ) { + $fix = $phpcsFile->addFixableError( + 'No space expected before void cast', + $stackPtr - 1, + 'SpaceBeforeCast', + ); + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr - 1, ''); + } + } + } + } + + /** + * Check that there's no space within the cast (void) not ( void ) + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $openParen + * @param int $voidToken + * @param int $closeParen + * + * @return void + */ + protected function checkSpacingWithinCast(File $phpcsFile, int $openParen, int $voidToken, int $closeParen): void + { + $tokens = $phpcsFile->getTokens(); + + // Check space after opening parenthesis + if ($voidToken !== $openParen + 1) { + $fix = $phpcsFile->addFixableError( + 'No space expected after opening parenthesis in void cast', + $openParen + 1, + 'SpaceAfterOpenParen', + ); + if ($fix) { + for ($i = $openParen + 1; $i < $voidToken; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + } + + // Check space before closing parenthesis + if ($closeParen !== $voidToken + 1) { + $fix = $phpcsFile->addFixableError( + 'No space expected before closing parenthesis in void cast', + $voidToken + 1, + 'SpaceBeforeCloseParen', + ); + if ($fix) { + for ($i = $voidToken + 1; $i < $closeParen; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + } + } + + /** + * Check that there's exactly one space after the cast + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $closeParen + * + * @return void + */ + protected function checkSpacingAfterCast(File $phpcsFile, int $closeParen): void + { + $tokens = $phpcsFile->getTokens(); + + $nextToken = $closeParen + 1; + if (!isset($tokens[$nextToken])) { + return; + } + + if ($tokens[$nextToken]['code'] !== T_WHITESPACE) { + $fix = $phpcsFile->addFixableError( + 'Expected 1 space after void cast, but 0 found', + $closeParen, + 'MissingSpaceAfter', + ); + if ($fix) { + $phpcsFile->fixer->addContent($closeParen, ' '); + } + } else { + $nextNonWhitespace = $phpcsFile->findNext(Tokens::$emptyTokens, $nextToken, null, true); + if ($nextNonWhitespace && $tokens[$nextNonWhitespace]['line'] === $tokens[$closeParen]['line']) { + // Same line - should be exactly one space + if ($tokens[$nextToken]['content'] !== ' ') { + $fix = $phpcsFile->addFixableError( + 'Expected 1 space after void cast, but %d found', + $nextToken, + 'TooManySpacesAfter', + [strlen($tokens[$nextToken]['content'])], + ); + if ($fix) { + $phpcsFile->fixer->replaceToken($nextToken, ' '); + } + } + } + } + } +} diff --git a/tests/PhpCollective/Sniffs/PHP/VoidCastSniffTest.php b/tests/PhpCollective/Sniffs/PHP/VoidCastSniffTest.php new file mode 100644 index 0000000..1c68c75 --- /dev/null +++ b/tests/PhpCollective/Sniffs/PHP/VoidCastSniffTest.php @@ -0,0 +1,30 @@ +assertSnifferFindsFixableErrors(new VoidCastSniff(), 7, 7); + } + + /** + * @return void + */ + public function testVoidCastFixer(): void + { + $this->assertSnifferCanFixErrors(new VoidCastSniff(), 7); + } +} diff --git a/tests/_data/VoidCast/after.php b/tests/_data/VoidCast/after.php new file mode 100644 index 0000000..c6e1965 --- /dev/null +++ b/tests/_data/VoidCast/after.php @@ -0,0 +1,51 @@ +methodWithReturn(); + + // Missing space after cast + (void) $this->anotherMethod(); + + // Space inside cast + (void) $this->yetAnotherMethod(); + + // Extra spaces after cast + (void) $this->oneMoreMethod(); + + // Combination of issues + (void) $this->finalMethod(); + } + + private function methodWithReturn(): string + { + return 'result'; + } + + private function anotherMethod(): int + { + return 42; + } + + private function yetAnotherMethod(): bool + { + return true; + } + + private function oneMoreMethod(): array + { + return []; + } + + private function finalMethod(): mixed + { + return null; + } +} diff --git a/tests/_data/VoidCast/before.php b/tests/_data/VoidCast/before.php new file mode 100644 index 0000000..bba668d --- /dev/null +++ b/tests/_data/VoidCast/before.php @@ -0,0 +1,51 @@ +methodWithReturn(); + + // Missing space after cast + (void)$this->anotherMethod(); + + // Space inside cast + ( void ) $this->yetAnotherMethod(); + + // Extra spaces after cast + (void) $this->oneMoreMethod(); + + // Combination of issues + ( void )$this->finalMethod(); + } + + private function methodWithReturn(): string + { + return 'result'; + } + + private function anotherMethod(): int + { + return 42; + } + + private function yetAnotherMethod(): bool + { + return true; + } + + private function oneMoreMethod(): array + { + return []; + } + + private function finalMethod(): mixed + { + return null; + } +} diff --git a/tests/_data/VoidCast/before.tokens.php b/tests/_data/VoidCast/before.tokens.php new file mode 100644 index 0000000..82b8b5c --- /dev/null +++ b/tests/_data/VoidCast/before.tokens.php @@ -0,0 +1,102 @@ +methodWithReturn(); +// T_WHITESPACE T_OPEN_PARENTHESIS T_STRING T_CLOSE_PARENTHESIS T_WHITESPACE T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE + +// T_WHITESPACE + // Missing space after cast +// T_WHITESPACE T_COMMENT + (void)$this->anotherMethod(); +// T_WHITESPACE T_OPEN_PARENTHESIS T_STRING T_CLOSE_PARENTHESIS T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE + +// T_WHITESPACE + // Space inside cast +// T_WHITESPACE T_COMMENT + ( void ) $this->yetAnotherMethod(); +// T_WHITESPACE T_OPEN_PARENTHESIS T_WHITESPACE T_STRING T_WHITESPACE T_CLOSE_PARENTHESIS T_WHITESPACE T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE + +// T_WHITESPACE + // Extra spaces after cast +// T_WHITESPACE T_COMMENT + (void) $this->oneMoreMethod(); +// T_WHITESPACE T_OPEN_PARENTHESIS T_STRING T_CLOSE_PARENTHESIS T_WHITESPACE T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE + +// T_WHITESPACE + // Combination of issues +// T_WHITESPACE T_COMMENT + ( void )$this->finalMethod(); +// T_WHITESPACE T_OPEN_PARENTHESIS T_WHITESPACE T_STRING T_WHITESPACE T_CLOSE_PARENTHESIS T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE + } +// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE + +// T_WHITESPACE + private function methodWithReturn(): string +// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE + { +// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE + return 'result'; +// T_WHITESPACE T_RETURN T_WHITESPACE T_CONSTANT_ENCAPSED_STRING T_SEMICOLON T_WHITESPACE + } +// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE + +// T_WHITESPACE + private function anotherMethod(): int +// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE + { +// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE + return 42; +// T_WHITESPACE T_RETURN T_WHITESPACE T_LNUMBER T_SEMICOLON T_WHITESPACE + } +// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE + +// T_WHITESPACE + private function yetAnotherMethod(): bool +// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE + { +// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE + return true; +// T_WHITESPACE T_RETURN T_WHITESPACE T_TRUE T_SEMICOLON T_WHITESPACE + } +// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE + +// T_WHITESPACE + private function oneMoreMethod(): array +// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE + { +// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE + return []; +// T_WHITESPACE T_RETURN T_WHITESPACE T_OPEN_SHORT_ARRAY T_CLOSE_SHORT_ARRAY T_SEMICOLON T_WHITESPACE + } +// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE + +// T_WHITESPACE + private function finalMethod(): mixed +// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE + { +// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE + return null; +// T_WHITESPACE T_RETURN T_WHITESPACE T_NULL T_SEMICOLON T_WHITESPACE + } +// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE +} +// T_CLOSE_CURLY_BRACKET T_WHITESPACE \ No newline at end of file From 88f41102ccf4a52053fc479dbba409c8e48ad5d4 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Fri, 21 Nov 2025 21:57:27 +0100 Subject: [PATCH 2/2] Delete tests/_data/VoidCast/before.tokens.php --- tests/_data/VoidCast/before.tokens.php | 102 ------------------------- 1 file changed, 102 deletions(-) delete mode 100644 tests/_data/VoidCast/before.tokens.php diff --git a/tests/_data/VoidCast/before.tokens.php b/tests/_data/VoidCast/before.tokens.php deleted file mode 100644 index 82b8b5c..0000000 --- a/tests/_data/VoidCast/before.tokens.php +++ /dev/null @@ -1,102 +0,0 @@ -methodWithReturn(); -// T_WHITESPACE T_OPEN_PARENTHESIS T_STRING T_CLOSE_PARENTHESIS T_WHITESPACE T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE - -// T_WHITESPACE - // Missing space after cast -// T_WHITESPACE T_COMMENT - (void)$this->anotherMethod(); -// T_WHITESPACE T_OPEN_PARENTHESIS T_STRING T_CLOSE_PARENTHESIS T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE - -// T_WHITESPACE - // Space inside cast -// T_WHITESPACE T_COMMENT - ( void ) $this->yetAnotherMethod(); -// T_WHITESPACE T_OPEN_PARENTHESIS T_WHITESPACE T_STRING T_WHITESPACE T_CLOSE_PARENTHESIS T_WHITESPACE T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE - -// T_WHITESPACE - // Extra spaces after cast -// T_WHITESPACE T_COMMENT - (void) $this->oneMoreMethod(); -// T_WHITESPACE T_OPEN_PARENTHESIS T_STRING T_CLOSE_PARENTHESIS T_WHITESPACE T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE - -// T_WHITESPACE - // Combination of issues -// T_WHITESPACE T_COMMENT - ( void )$this->finalMethod(); -// T_WHITESPACE T_OPEN_PARENTHESIS T_WHITESPACE T_STRING T_WHITESPACE T_CLOSE_PARENTHESIS T_VARIABLE T_OBJECT_OPERATOR T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_SEMICOLON T_WHITESPACE - } -// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE - -// T_WHITESPACE - private function methodWithReturn(): string -// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE - { -// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE - return 'result'; -// T_WHITESPACE T_RETURN T_WHITESPACE T_CONSTANT_ENCAPSED_STRING T_SEMICOLON T_WHITESPACE - } -// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE - -// T_WHITESPACE - private function anotherMethod(): int -// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE - { -// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE - return 42; -// T_WHITESPACE T_RETURN T_WHITESPACE T_LNUMBER T_SEMICOLON T_WHITESPACE - } -// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE - -// T_WHITESPACE - private function yetAnotherMethod(): bool -// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE - { -// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE - return true; -// T_WHITESPACE T_RETURN T_WHITESPACE T_TRUE T_SEMICOLON T_WHITESPACE - } -// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE - -// T_WHITESPACE - private function oneMoreMethod(): array -// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE - { -// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE - return []; -// T_WHITESPACE T_RETURN T_WHITESPACE T_OPEN_SHORT_ARRAY T_CLOSE_SHORT_ARRAY T_SEMICOLON T_WHITESPACE - } -// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE - -// T_WHITESPACE - private function finalMethod(): mixed -// T_WHITESPACE T_PRIVATE T_WHITESPACE T_FUNCTION T_WHITESPACE T_STRING T_OPEN_PARENTHESIS T_CLOSE_PARENTHESIS T_COLON T_WHITESPACE T_STRING T_WHITESPACE - { -// T_WHITESPACE T_OPEN_CURLY_BRACKET T_WHITESPACE - return null; -// T_WHITESPACE T_RETURN T_WHITESPACE T_NULL T_SEMICOLON T_WHITESPACE - } -// T_WHITESPACE T_CLOSE_CURLY_BRACKET T_WHITESPACE -} -// T_CLOSE_CURLY_BRACKET T_WHITESPACE \ No newline at end of file