Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions PhpCollective/Sniffs/PHP/VoidCastSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

/**
* MIT License
* For full license information, please view the LICENSE file that was distributed with this source code.
*/

namespace PhpCollective\Sniffs\PHP;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
* Validates (void) cast usage - a PHP 8.5 feature.
*
* Ensures proper spacing and formatting around void casts.
*/
class VoidCastSniff implements Sniff
{
/**
* @inheritDoc
*/
public function register(): array
{
return [T_OPEN_PARENTHESIS];
}

/**
* @inheritDoc
*/
public function process(File $phpcsFile, $stackPtr): void
{
$tokens = $phpcsFile->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, ' ');
}
}
}
}
}
}
30 changes: 30 additions & 0 deletions tests/PhpCollective/Sniffs/PHP/VoidCastSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/**
* MIT License
* For full license information, please view the LICENSE file that was distributed with this source code.
*/

namespace PhpCollective\Test\PhpCollective\Sniffs\PHP;

use PhpCollective\Sniffs\PHP\VoidCastSniff;
use PhpCollective\Test\TestCase;

class VoidCastSniffTest extends TestCase
{
/**
* @return void
*/
public function testVoidCastSniffer(): void
{
$this->assertSnifferFindsFixableErrors(new VoidCastSniff(), 7, 7);
}

/**
* @return void
*/
public function testVoidCastFixer(): void
{
$this->assertSnifferCanFixErrors(new VoidCastSniff(), 7);
}
}
51 changes: 51 additions & 0 deletions tests/_data/VoidCast/after.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace PhpCollective;

class VoidCastExample
{
public function testVoidCast(): void
{
// Correct usage
(void) $this->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;
}
}
51 changes: 51 additions & 0 deletions tests/_data/VoidCast/before.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace PhpCollective;

class VoidCastExample
{
public function testVoidCast(): void
{
// Correct usage
(void) $this->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;
}
}