Symfony CS Custom Fixer + Tests: Do focus on your goal!

Once again I received a closing ticket here https://github.com/fabpot/PHP-CS-Fixer/issues/381 in response to a feature request. There was no explanation why it was closed. However @stof kindly provided some reasoning. However what surprised me is what happened afterwards!

But it was great that @sstok picked it up and left a gist. This is amazing how the community can be so encouraging! Really thanks!

So I went and plugged this custom short syntax converter and here is the recipe!

<?php
 
namespace Decoupling\Fixer;
 
use Symfony\CS\FixerInterface;
use Symfony\CS\Tokens;
 
class ShortArraySyntaxFixer implements FixerInterface
{
    public function fix(\SplFileInfo $file, $content)
    {
        $tokens = Tokens::fromCode($content);
 
        for ($index = 0, $c = $tokens->count(); $index < $c; $index++) {
            $token = $tokens[$index];
 
            if (Tokens::isKeyword($token) && T_ARRAY === $token[0] && '(' === $tokens->getNextNonWhitespace($index)) {
                $this->fixArray($tokens, $index);
                continue;
            }
        }
 
        return $tokens->generateCode();
    }
 
    private function fixArray(Tokens $tokens, &$index)
    {
        $bracesLevel = 0;
 
        unset($tokens[$index]);
        $index++;
 
        for ($c = $tokens->count(); $index < $c; $index++) {
            $token = $tokens[$index];
 
            if ('(' === $token) {
                if (0 === $bracesLevel) {
                    $tokens[$index] = '[';
                }
 
                ++$bracesLevel;
                continue;
            }
 
            if (Tokens::isKeyword($token) && T_ARRAY === $token[0] && '(' === $tokens->getNextNonWhitespace($index)) {
                $this->fixArray($tokens, $index);
                continue;
            }
 
            if (')' === $token) {
                --$bracesLevel;
 
                if (0 === $bracesLevel) {
                    $tokens[$index] = ']';
                    break;
                }
            }
        }
    }
 
    public function getLevel()
    {
        return FixerInterface::ALL_LEVEL;
    }
 
    public function getPriority()
    {
        return 0;
    }
 
    public function supports(\SplFileInfo $file)
    {
        return 'php' === pathinfo($file->getFilename(), PATHINFO_EXTENSION);
    }
 
    public function getName()
    {
        return 'short_array_syntax';
    }
 
    public function getDescription()
    {
        return 'PHP array\'s should use the PHP 5.4 short-syntax';
    }
}

And its corresponding test:

<?php
 
namespace Decoupling\Tests\Fixer;
 
use Decoupling\Fixer\ShortArraySyntaxFixer;
 
class ShortArraySyntaxFixerTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider provideExamples
     */
    public function testFix($expected, $input)
    {
        $fixer = new ShortArraySyntaxFixer();
        $file = $this->getTestFile();
 
        $this->assertEquals($expected, $fixer->fix($file, $input));
    }
 
    public function provideExamples()
    {
        return [
            ['<?php $x = [];', '<?php $x = array();'],
            ['<?php $x = []; $y = [];', '<?php $x = array(); $y = array();'],
            ['<?php $x = [ ];', '<?php $x = array( );'],
            ['<?php $x = [\'foo\'];', '<?php $x = array(\'foo\');'],
            ['<?php $x = [ \'foo\' ];', '<?php $x = array( \'foo\' );'],
            ['<?php $x = [($y ? true : false)];', '<?php $x = array(($y ? true : false));'],
            ['<?php $x = [($y ? [true] : [false])];', '<?php $x = array(($y ? array(true) : array(false)));'],//
            ['<?php $x = [($y ? [true] : [ false ])];', '<?php $x = array(($y ? array(true) : array( false )));'],
            ['<?php $x = [($y ? ["t" => true] : ["f" => false])];', '<?php $x = array(($y ? array("t" => true) : array("f" => false)));'],
            ['<?php print_r([($y ? true : false)]);', '<?php print_r(array(($y ? true : false)));'],
            ['<?php $x = [[[]]];', '<?php $x = array(array(array()));'],
            ['<?php $x = [[[]]]; $y = [[[]]];', '<?php $x = array(array(array())); $y = array(array(array()));'],
        ];
    }
 
    private function getTestFile($filename = __FILE__)
    {
        static $files = [];
 
        if (!isset($files[$filename])) {
            $files[$filename] = new \SplFileInfo($filename);
        }
 
        return $files[$filename];
    }
}

This is also in a gist @sstok posted, however i have applied the fixer on the code itself 🙂 and also fixed missing namespaces and opening php tags.

Your typical .php_cs then becomes:

<?php
 
require_once __DIR__.'/src/Decoupling/Fixer/ShortArraySyntaxFixer.php';
 
$finder = Symfony\CS\Finder\DefaultFinder::create()
    ->notName('README.md')
    ->notName('.php_cs')
    ->notName('composer.*')
    ->notName('phpunit.xml*')
    ->notName('*.xml')
    ->exclude('app')
    ->exclude('bin')
    ->exclude('migrations')
    ->exclude('vendor')
    ->exclude('web/bundles')
    ->in(__DIR__)
;
 
return Symfony\CS\Config\Config::create()
    ->addCustomFixer(new Decoupling\Fixer\ShortArraySyntaxFixer)
    ->fixers(
        [
            'encoding',
            'linefeed',
            'indentation',
            'trailing_spaces',
            'object_operator',
            'phpdoc_params',
            'visibility',
            'short_tag',
            'php_closing_tag',
            'return',
            'extra_empty_lines',
            'braces',
            'lowercase_constants',
            'lowercase_keywords',
            'include',
            'function_declaration',
            'controls_spaces',
            'spaces_cast',
            'psr0',
            'elseif',
            'eof_ending',
            'one_class_per_file',
            'unused_use',
            'short_array_syntax',
        ]
    )
    ->finder($finder)
;

Now go and run it:

~ php-cs-fixer fix .

It worked like a charm in my project and I am happy to use it even in gush now, PR coming!

joyfully undeservedly ending this post!

your friend @cordoval

4 thoughts on “Symfony CS Custom Fixer + Tests: Do focus on your goal!

  1. Don’t forget the link to the Gist 😉
    https://gist.github.com/sstok/6fb2d2fa69615c3db3a0

    Btw. You don’t need the fixers array because any custom Fixer is automatically enabled. Only when you want to define the fixers explicitly, this needs to be done.

    And dot-files like .php_cs are automatically ignored.
    Same goes for vendor, unless you have a really custom Finder 🙂

    Default rules can be found here.
    https://github.com/fabpot/PHP-CS-Fixer/blob/master/Symfony/CS/Finder/DefaultFinder.php

  2. The project I’m working on uses tabs, not spaces so I created a new fixer to take care of cleaning up any lingering spaces. I tried following your instructions to create a custom fixer and am running into to trouble trying to run it:

    $ php-cs-fixer fix ./app/Console/Command/InstacartShell.php –verbose –diff –dry-run –config-file=’.php_cs’

    PHP Fatal error: Class ‘Symfony\CS\AbstractFixer’ not found in /Users/justin.tilson/Sites/salt/lib/PhpCsFixer/Fixer/SpaceToTabFixer.php on line 9

    I can send over my SpaceToTabFixer.php & .php_cs file if that helps.

    thx.

Leave a Reply to Luis Cordova Cancel reply

Your email address will not be published. Required fields are marked *