COOKBOOK-FIXERS.md 13.6 KB
Newer Older
Ketan's avatar
Ketan committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489
Cookbook - Making a new Fixer for PHP CS Fixer
==============================================

You want to make a new fixer to PHP CS Fixer and do not know how to
start. Follow this document and you will be able to do it.

## Background

In order to be able to create a new fixer, you need some background.
PHP CS Fixer is a transcompiler which takes valid PHP code and pretty
print valid PHP code. It does all transformations in multiple passes,
a.k.a., multi-pass compiler.

Therefore, a new fixer is meant to be ideally
[idempotent](http://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning),
or at least atomic in its actions. More on this later.

All contributions go through a code review process. Do not feel
discouraged - it is meant only to give more people more chance to
contribute, and to detect bugs ([Linus'
Law](http://en.wikipedia.org/wiki/Linus%27s_Law)).

If possible, try to get acquainted with the public interface for the
[Tokens class](Symfony/CS/Tokenizer/Tokens.php)
and [Token class](Symfony/CS/Tokenizer/Token.php)
classes.

## Assumptions

* You are familiar with Test Driven Development.
* Forked FriendsOfPHP/PHP-CS-Fixer into your own Github Account.
* Cloned your forked repository locally.
* Installed the dependencies of PHP CS Fixer using [Composer](https://getcomposer.org/).
* You have read [`CONTRIBUTING.md`](CONTRIBUTING.md).

## Step by step

For this step-by-step, we are going to create a simple fixer that
removes all comments of the code that are preceded by ';' (semicolon).

We are calling it `remove_comments` (code name), or,
`RemoveCommentsFixer` (class name).

### Step 1 - Creating files

Create a new file in
`PHP-CS-Fixer/Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`.
Put this content inside:
```php
<?php

/*
 * This file is part of the PHP CS utility.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace Symfony\CS\Fixer\Contrib;

use Symfony\CS\AbstractFixer;
use Symfony\CS\Tokenizer\Tokens;

/**
 * @author Your name <your@email.com>
 */
final class RemoveCommentsFixer extends AbstractFixer
{
    /**
     * {@inheritdoc}
     */
    public function fix(\SplFileInfo $file, $content)
    {
        // Add the fixing logic of the fixer here.
    }

    /**
     * {@inheritdoc}
     */
    public function getDescription()
    {
        // Return a short description of the Fixer, it will be used in the README.rst.
    }
}
```

Note how the class and file name match. Also keep in mind that all
fixers must implement `FixerInterface`. In this case, the fixer is
inheriting from `AbstractFixer`, which fulfills the interface with some
default behavior.

Now let us create the test file at
`Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php` . Put this
content inside:

```php
<?php

/*
 * This file is part of the PHP CS utility.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace Symfony\CS\Tests\Fixer\Contrib;

use Symfony\CS\Tests\Fixer\AbstractFixerTestBase;

/**
 * @author Your name <your@email.com>
 *
 * @internal
 */
final class RemoveCommentsFixerTest extends AbstractFixerTestBase
{
    /**
     * @dataProvider provideFixCases
     */
    public function testFix($expected, $input = null)
    {
        $this->makeTest($expected, $input);
    }

    public function provideFixCases()
    {
        return array();
    }
}
```

The files are created, one thing is still missing though: we need to
update the README.md. Fortunately, PHP CS Fixer can help you here.
Execute the following command in your command shell:

`$ php php-cs-fixer readme > README.rst`

### Step 2 - Using tests to define fixers behavior

Now that the files are created, you can start writing test to define the
behavior of the fixer. You have to do it in two ways: first, ensuring
the fixer changes what it should be changing; second, ensuring that
fixer does not change what is not supposed to change. Thus:

#### Keeping things as they are:
`Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php`@provideFixCases:
```php
    ...
    public function provideFixCases()
    {
    	return array(
    		array('<?php echo "This should not be changed";') // Each sub-array is a test
    	);
    }
    ...
```

#### Ensuring things change:
`Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php`@provideFixCases:
```php
    ...
    public function provideFixCases()
    {
    	return array(
    		array(
    			'<?php echo "This should be changed"; ', // This is expected output
    			'<?php echo "This should be changed"; /* Comment */', // This is input
    		)
    	);
    }
    ...
```

Note that expected outputs are **always** tested alone to ensure your fixer will not change it.

We want to have a failing test to start with, so the test file now looks
like:
`Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php`
```php
<?php

/*
 * This file is part of the PHP CS utility.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace Symfony\CS\Tests\Fixer\Contrib;

use Symfony\CS\Tests\Fixer\AbstractFixerTestBase;

/**
 * @author Your name <your@email.com>
 *
 * @internal
 */
final class RemoveCommentsFixerTest extends AbstractFixerTestBase
{
    /**
     * @dataProvider provideFixCases
     */
    public function testFix($expected, $input = null)
    {
        $this->makeTest($expected, $input);
    }

    public function provideFixCases()
    {
        return array(
            array(
               '<?php echo "This should be changed"; ', // This is expected output
               '<?php echo "This should be changed"; /* Comment */', // This is input
            )
        );
    }
}
```


### Step 3 - Implement your solution

You have defined the behavior of your fixer in tests. Now it is time to
implement it.

We need first to create one method to describe what this fixer does:
`Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`:
```php
final class RemoveCommentsFixer extends AbstractFixer
{
    ...
    /**
     * {@inheritdoc}
     */
    public function getDescription()
    {
        return 'Removes all comments of the code that are preceded by ";" (semicolon).'; // Trailing dot is important. We thrive to use English grammar properly.
    }
}
```

For now, let us just make a fixer that applies no modification:
`Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`:
```php
final class RemoveCommentsFixer extends AbstractFixer
{
    /**
     * {@inheritdoc}
     */
    public function fix(\SplFileInfo $file, $content)
    {
        $tokens = Tokens::fromCode($content);

        return $tokens->generateCode();
    }
}
```

Run `$ phpunit Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php`.
You are going to see that the tests fails.

### Break
Now we have pretty much a cradle to work with. A file with a failing
test, and the fixer, that for now does not do anything.

How do fixers work? In the PHP CS Fixer, they work by iterating through
pieces of codes (each being a Token), and inspecting what exists before
and after that bit and making a decision, usually:

 * Adding code.
 * Modifying code.
 * Deleting code.
 * Ignoring code.

In our case, we want to find all comments, and foreach (pun intended)
one of them check if they are preceded by a semicolon symbol.

Now you need to do some reading, because all these symbols obey a list
defined by the PHP compiler. It is the ["List of Parser
Tokens"](http://php.net/manual/en/tokens.php).

Internally, PHP CS Fixer transforms some of PHP native tokens into custom
tokens through the use of [Transfomers](Symfony/CS/Tokenizer/Transformer),
they aim to help you reason about the changes you may want to do in the
fixers.

So we can get to move forward, humor me in believing that comments have
one symbol name: `T_COMMENT`.

### Step 3 - Implement your solution - continuation.

We do not want all symbols to be analysed. Only `T_COMMENT`. So let us
iterate the token(s) we are interested in.
`Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`:
```php
final class RemoveCommentsFixer extends AbstractFixer
{
    /**
     * {@inheritdoc}
     */
    public function fix(\SplFileInfo $file, $content)
    {
        $tokens = Tokens::fromCode($content);

        $foundComments = $tokens->findGivenKind(T_COMMENT);
        foreach($foundComments as $index => $token){

        }

        return $tokens->generateCode();
    }
}
```

OK, now for each `T_COMMENT`, all we need to do is check if the previous
token is a semicolon.
`Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`:
```php
final class RemoveCommentsFixer extends AbstractFixer
{
    /**
     * {@inheritdoc}
     */
    public function fix(\SplFileInfo $file, $content)
    {
        $tokens = Tokens::fromCode($content);

        $foundComments = $tokens->findGivenKind(T_COMMENT);
        foreach($foundComments as $index => $token){
            $prevTokenIndex = $tokens->getPrevMeaningfulToken($index);
            $prevToken = $tokens[$prevTokenIndex];

            if($prevToken->equals(';')){
                $token->clear();
            }
        }

        return $tokens->generateCode();
    }
}
```

So the fixer in the end looks like this:
```php
<?php

/*
 * This file is part of the PHP CS utility.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 *
 */

namespace Symfony\CS\Fixer\Contrib;

use Symfony\CS\AbstractFixer;
use Symfony\CS\Tokenizer\Tokens;

/**
 * @author Your name <your@email.com>
 */
final class RemoveCommentsFixer extends AbstractFixer {
    /**
     * {@inheritdoc}
     */
    public function fix(\SplFileInfo $file, $content) {
        $tokens = Tokens::fromCode($content);

        $foundComments = $tokens->findGivenKind(T_COMMENT);
        foreach ($foundComments as $index => $token) {
            $prevTokenIndex = $tokens->getPrevMeaningfulToken($index);
            $prevToken = $tokens[$prevTokenIndex];

            if ($prevToken->equals(';')) {
                $token->clear();
            }
        }

        return $tokens->generateCode();
    }

    /**
     * {@inheritdoc}
     */
    public function getDescription() {
        return 'Removes all comments of the code that are preceded by ";" (semicolon).';// Trailing dot is important. We thrive to use English grammar properly.
    }
}
```

### Step 4 - Format, Commit, PR.

Note that so far, we have not coded adhering to PSR-1/2. This is done on
purpose. For every commit you make, you must use PHP CS Fixer to fix
itself. Thus, on the command line call:

`$ php php-cs-fixer fix`

This will fix all the coding style mistakes.

After the final CS fix, you are ready to commit. Do it.

Now, go to Github and open a Pull Request.


### Step 5 - Peer review: it is all about code and community building.

Congratulations, you have made your first fixer. Be proud. Your work
will be reviewed carefully by PHP CS Fixer community.

The review usually flows like this:

1. People will check your code for common mistakes and logical
caveats. Usually, the person building a fixer is blind about some
behavior mistakes of fixers. Expect to write few more tests to cater for
the reviews.
2. People will discuss the relevance of your fixer. If it is
something that goes along with Symfony style standards, or PSR-1/PSR-2
standards, they will ask you to move from Symfony/CS/Fixers/Contrib to
Symfony/CS/Fixers/{Symfony, PSR2, etc}.
3. People will also discuss whether your fixer is idempotent or not.
If they understand that your fixer must always run before or after a
certain fixer, they will ask you to override a method named
`getPriority()`. Do not be afraid of asking the reviewer for help on how
to do it.
4. People may ask you to rebase your code to unify commits or to get
rid of merge commits.
5. Go to 1 until no actions are needed anymore.

Your fixer will be incorporated in the next release.

# Congratulations! You have done it.



## Q&A

#### Why is not my PR merged yet?

PHP CS Fixer is used by many people, that expect it to be stable. So
sometimes, few PR are delayed a bit so to avoid cluttering at @dev
channel on composer.

Other possibility is that reviewers are giving time to other members of
PHP CS Fixer community to partake on the review debates of your fixer.

In any case, we care a lot about what you do and we want to see it being
part of the application as soon as possible.

#### May I use short arrays (`$a = []`)?

No. Short arrays were introduced in PHP 5.4 and PHP CS Fixer still
supports PHP 5.3.6.

#### Why are you steering me to create my fixer at CONTRIB_LEVEL ?

CONTRIB_LEVEL is the most lax level - and it is far more likely to have
your fixer accepted at CONTRIB_LEVEL and later changed to SYMFOMY_LEVEL
or PSR2_LEVEL; than the other way around.

If you make your contribution directly at PSR2_LEVEL, eventually the
relevance debate will take place and your fixer might be pushed to
CONTRIB_LEVEL.

#### Why am I asked to use `getPrevMeaningfulToken()` instead of `getPrevNonWhitespace()`?

The main difference is that `getPrevNonWhitespace()` ignores only
whitespaces (`T_WHITESPACE`), while `getPrevMeaningfulToken()` ignores
whitespaces and comments. And usually that is what you want. For
example:

```php
$a->/*comment*/func();
```

If you are inspecting `func()`, and you want to check whether this is
part of an object, if you use `getPrevNonWhitespace()` you are going to
get `/*comment*/`, which might belie your test. On the other hand, if
you use `getPrevMeaningfulToken()`, no matter if you have got a comment
or a whitespace, the returned token will always be `->`.