skip to Main Content

I am attempting to write a little simple phpcs (phpcbf) sniff to remove whitespace within empty curly braces. I’ve written quite a few small fixers, and this seemed like it should be very easy.

Basically with the move to promoted parameters, we’re ending up with various whitespace setups inside the empty curly braces following the construct like

public function __construct( protected string $cdnPath ) {
}
public function __construct( protected string $cdnPath ) { }
public function __construct( protected string $cdnPath ) {}

I just want to unify these on {}

The sniff as I have it so far

<?php

use PHP_CodeSnifferFilesFile;
use PHP_CodeSnifferSniffsSniff;

class CurlyBraceInnerWhiteSpaceSniff implements Sniff {

    public function register() : array {
        return [
            T_OPEN_CURLY_BRACKET,
        ];
    }

    /**
     * @param int $stackPtr
     */
    public function process( File $phpcsFile, $stackPtr ) : void {
        $tokens = $phpcsFile->getTokens();
        if( $tokens[$stackPtr + 1]['code'] !== T_WHITESPACE ) {
            return;
        }

        $closePtr = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true);
        if( $tokens[$closePtr]['code'] !== T_CLOSE_CURLY_BRACKET ) {
            return;
        }

        $fix = $phpcsFile->addFixableError('There must be no whitespace inside empty curly braces', $stackPtr + 1, 'CurlyBraceInnerWhiteSpace');
        if( $fix ) {
            $phpcsFile->fixer->beginChangeset();
            for( $i = $stackPtr + 1; $i < $closePtr; $i++ ) {
                $phpcsFile->fixer->replaceToken($i, '');
            }
            $phpcsFile->fixer->endChangeset();
        }
    }
}

It takes a VERY long time before giving up

running phpcbf with the -v reveals many many entries like this with [made 50 passes]... ERROR

Processing TransformCommand.php [PHP => 636 tokens in 89 lines]... DONE in 11ms (1 fixable violations)
        => Fixing file: 1/1 violations remaining [made 50 passes]... ERROR in 585ms

I found in the documentation somewhere that adding return $phpcsFile->numTokens; to process could help, by preventing the sniff from running against the same file twice, but it has not seemed to help at all.

3

Answers


  1. Chosen as BEST ANSWER

    The problem was that I had another sniff from another collection fighting my sniff.

    It wants to replace {} with { }. Having the two sniffs fighting caused the back and forth which is why it would try 50 times before giving up.


  2. The SpacesInsideParenthesesFixer could be converted into a SpacesInsideCurlyBracketsFixer.
    Not exactly sure how it would behave with new-line, but there’s also example patterns for that.

    Login or Signup to reply.
  3. Your implementation doesn’t catch empty lines or Allman-style bracing. I’d start by using the Generic.Functions.OpeningFunctionBraceKernighanRitchie sniff to make sure your open brace is on the same line. Then, note that the tokens array contains the scope level and the position of the matching closing brace:

    Array
    (
        [type] => T_OPEN_CURLY_BRACKET
        [code] => PHPCS_T_OPEN_CURLY_BRACKET
        [content] => {
        [line] => 3
        [column] => 1
        [length] => 1
        [bracket_opener] => 5
        [bracket_closer] => 20
        [scope_condition] => 1
        [scope_opener] => 5
        [scope_closer] => 20
        [level] => 0
        [conditions] => Array
            (
            )
    )
    

    So your sniff would basically become, "if this is a class method (i.e., level 1) and the only thing between the braces is whitespace, raise an error."

    public function process(File $phpcsFile, $stackPtr): void
    {
        $tokens = $phpcsFile->getTokens();
        // If the close immediately follows the open, we're good.
        $opener = $tokens[$stackPtr]['bracket_opener'];
        $closer = $tokens[$stackPtr]['bracket_closer'];
        if ($closer - $opener === 1) {
            return;
        }
        // Scan all the tokens between the open and close.
        for ($i = $opener + 1; $i < $closer; $i++) {
            // As soon as we find anything that's not whitespace, we're ok.
            if ($tokens[$i]['code'] !== T_WHITESPACE) {
                return;
            }
        }
        // Otherwise, all we had between open and close was whitespace.
        $fix = $phpcsFile->addFixableError(...);
        if ($fix) {
            $phpcsFile->fixer->beginChangeset();
            for ($i = $opener + 1; $i < $closer; $i++) {
                $phpcsFile->fixer->replaceToken($i, '');
            }
            $phpcsFile->fixer->endChangeset();
        }
    }
    

    You might also want to apply this only to class methods named __construct. You could find the start of the function declaration with $tokens[$stackPtr]['scope_condition'] and then work forward to find the name.

    Before:

    <?php
    class Foo
    {
        public function __construct() {
        
        
        }
    }
    

    Command:

    % vendor/bin/phpcbf --standard=custom Foo.php
    PHPCBF RESULT SUMMARY
    ---------------------------------------------------
    FILE                               FIXED  REMAINING
    ---------------------------------------------------
    /home/alex/temp/Foo.php            1      0
    ---------------------------------------------------
    A TOTAL OF 1 ERROR WERE FIXED IN 1 FILE
    ---------------------------------------------------
    
    Time: 38ms; Memory: 6MB
    

    After:

    <?php
    class Foo
    {
        public function __construct() {}
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search