cookbook_fixers.rst 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. ==============================================
  2. Cookbook - Making a new Fixer for PHP CS Fixer
  3. ==============================================
  4. You want to make a new fixer to PHP CS Fixer and do not know how to
  5. start. Follow this document and you will be able to do it.
  6. Background
  7. ----------
  8. In order to be able to create a new fixer, you need some background.
  9. PHP CS Fixer is a transcompiler which takes valid PHP code and pretty
  10. print valid PHP code. It does all transformations in multiple passes,
  11. a.k.a., multi-pass compiler.
  12. Therefore, a new fixer is meant to be ideally idempotent_, or at least atomic
  13. in its actions. More on this later.
  14. All contributions go through a code review process. Do not feel
  15. discouraged - it is meant only to give more people more chance to
  16. contribute, and to detect bugs (`Linus's Law`_).
  17. If possible, try to get acquainted with the public interface for the
  18. `Tokens class`_ and `Token class`_ classes.
  19. Assumptions
  20. -----------
  21. * You are familiar with Test Driven Development.
  22. * Forked FriendsOfPHP/PHP-CS-Fixer into your own GitHub Account.
  23. * Cloned your forked repository locally.
  24. * Installed the dependencies of PHP CS Fixer using Composer_.
  25. * You have read `CONTRIBUTING.md`_.
  26. Step by step
  27. ------------
  28. For this step-by-step, we are going to create a simple fixer that
  29. removes all comments from the code that are preceded by ';' (semicolon).
  30. We are calling it ``remove_comments`` (code name), or,
  31. ``RemoveCommentsFixer`` (class name).
  32. Step 1 - Creating files
  33. _______________________
  34. Create a new file in ``src/Fixer/Comment/RemoveCommentsFixer.php``.
  35. Put this content inside:
  36. .. code-block:: php
  37. <?php
  38. /*
  39. * This file is part of PHP CS Fixer.
  40. *
  41. * (c) Fabien Potencier <fabien@symfony.com>
  42. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  43. *
  44. * This source file is subject to the MIT license that is bundled
  45. * with this source code in the file LICENSE.
  46. */
  47. namespace PhpCsFixer\Fixer\Comment;
  48. use PhpCsFixer\AbstractFixer;
  49. use PhpCsFixer\Tokenizer\Tokens;
  50. /**
  51. * @author Your name <your@email.com>
  52. */
  53. final class RemoveCommentsFixer extends AbstractFixer
  54. {
  55. /**
  56. * {@inheritdoc}
  57. */
  58. public function getDefinition()
  59. {
  60. // Return a definition of the fixer, it will be used in the documentation.
  61. }
  62. /**
  63. * {@inheritdoc}
  64. */
  65. public function isCandidate(Tokens $tokens)
  66. {
  67. // Check whether the collection is a candidate for fixing.
  68. // Has to be ultra cheap to execute.
  69. }
  70. /**
  71. * {@inheritdoc}
  72. */
  73. protected function applyFix(\SplFileInfo $file, Tokens $tokens)
  74. {
  75. // Add the fixing logic of the fixer here.
  76. }
  77. }
  78. Note how the class and file name match. Also keep in mind that all
  79. fixers must implement ``Fixer\FixerInterface``. In this case, the fixer is
  80. inheriting from ``AbstractFixer``, which fulfills the interface with some
  81. default behavior.
  82. Now let us create the test file at
  83. ``tests/Fixer/Comment/RemoveCommentsFixerTest.php``. Put this content inside:
  84. .. code-block:: php
  85. <?php
  86. /*
  87. * This file is part of PHP CS Fixer.
  88. *
  89. * (c) Fabien Potencier <fabien@symfony.com>
  90. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  91. *
  92. * This source file is subject to the MIT license that is bundled
  93. * with this source code in the file LICENSE.
  94. */
  95. namespace PhpCsFixer\Tests\Fixer\Comment;
  96. use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
  97. /**
  98. * @author Your name <your@email.com>
  99. *
  100. * @internal
  101. *
  102. * @covers \PhpCsFixer\Fixer\Comment\RemoveCommentsFixer
  103. */
  104. final class RemoveCommentsFixerTest extends AbstractFixerTestCase
  105. {
  106. /**
  107. * @param string $expected
  108. * @param null|string $input
  109. *
  110. * @dataProvider provideFixCases
  111. */
  112. public function testFix($expected, $input = null)
  113. {
  114. $this->doTest($expected, $input);
  115. }
  116. public function provideFixCases()
  117. {
  118. return [];
  119. }
  120. }
  121. Step 2 - Using tests to define fixers behavior
  122. ______________________________________________
  123. Now that the files are created, you can start writing tests to define the
  124. behavior of the fixer. You have to do it in two ways: first, ensuring
  125. the fixer changes what it should be changing; second, ensuring that
  126. fixer does not change what is not supposed to change. Thus:
  127. Keeping things as they are:
  128. .. code-block:: php
  129. // tests/Fixer/Comment/RemoveCommentsFixerTest.php
  130. // ...
  131. public function provideFixCases()
  132. {
  133. return [
  134. ['<?php echo "This should not be changed";'], // Each sub-array is a test
  135. ];
  136. }
  137. // ...
  138. Ensuring things change:
  139. .. code-block:: php
  140. // tests/Fixer/Comment/RemoveCommentsFixerTest.php
  141. // ...
  142. public function provideFixCases()
  143. {
  144. return [
  145. [
  146. '<?php echo "This should be changed"; ', // This is expected output
  147. '<?php echo "This should be changed"; /* Comment */', // This is input
  148. ],
  149. ];
  150. }
  151. // ...
  152. Note that expected outputs are **always** tested alone to ensure your fixer will not change it.
  153. We want to have a failing test to start with, so the test file now looks
  154. like:
  155. .. code-block:: php
  156. <?php
  157. // tests/Fixer/Comment/RemoveCommentsFixerTest.php
  158. /*
  159. * This file is part of PHP CS Fixer.
  160. *
  161. * (c) Fabien Potencier <fabien@symfony.com>
  162. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  163. *
  164. * This source file is subject to the MIT license that is bundled
  165. * with this source code in the file LICENSE.
  166. */
  167. namespace PhpCsFixer\Tests\Fixer\Comment;
  168. use PhpCsFixer\Tests\Fixer\AbstractFixerTestBase;
  169. /**
  170. * @author Your name <your@email.com>
  171. *
  172. * @internal
  173. */
  174. final class RemoveCommentsFixerTest extends AbstractFixerTestBase
  175. {
  176. /**
  177. * @param string $expected
  178. * @param null|string $input
  179. *
  180. * @dataProvider provideFixCases
  181. */
  182. public function testFix($expected, $input = null)
  183. {
  184. $this->doTest($expected, $input);
  185. }
  186. public function provideFixCases()
  187. {
  188. return [
  189. [
  190. '<?php echo "This should be changed"; ', // This is expected output
  191. '<?php echo "This should be changed"; /* Comment */', // This is input
  192. ],
  193. ];
  194. }
  195. }
  196. Step 3 - Implement your solution
  197. ________________________________
  198. You have defined the behavior of your fixer in tests. Now it is time to
  199. implement it.
  200. First, we need to create one method to describe what this fixer does:
  201. .. code-block:: php
  202. // src/Fixer/Comment/RemoveCommentsFixer.php
  203. final class RemoveCommentsFixer extends AbstractFixer
  204. {
  205. /**
  206. * {@inheritdoc}
  207. */
  208. public function getDefinition()
  209. {
  210. return new FixerDefinition(
  211. 'Removes all comments of the code that are preceded by ";" (semicolon).', // Trailing dot is important. We thrive to use English grammar properly.
  212. [
  213. new CodeSample(
  214. '<?php echo 123; /* Comment */'
  215. ),
  216. ]
  217. );
  218. }
  219. }
  220. Next, we need to update the documentation.
  221. Fortunately, PHP CS Fixer can help you here.
  222. Execute the following command in your command shell:
  223. .. code-block:: console
  224. $ php dev-tools/doc.php
  225. Next, we must filter what type of tokens we want to fix. Here, we are interested in code that contains ``T_COMMENT`` tokens:
  226. .. code-block:: php
  227. // src/Fixer/Comment/RemoveCommentsFixer.php
  228. final class RemoveCommentsFixer extends AbstractFixer
  229. {
  230. // ...
  231. /**
  232. * {@inheritdoc}
  233. */
  234. public function isCandidate(Tokens $tokens)
  235. {
  236. return $tokens->isTokenKindFound(T_COMMENT);
  237. }
  238. }
  239. For now, let us just make a fixer that applies no modification:
  240. .. code-block:: php
  241. // src/Fixer/Comment/RemoveCommentsFixer.php
  242. class RemoveCommentsFixer extends AbstractFixer
  243. {
  244. // ...
  245. /**
  246. * {@inheritdoc}
  247. */
  248. protected function applyFix(\SplFileInfo $file, Tokens $tokens)
  249. {
  250. // no action
  251. }
  252. }
  253. Run ``$ phpunit tests/Fixer/Comment/RemoveCommentsFixerTest.php``.
  254. You are going to see that the tests fail.
  255. Break
  256. _____
  257. Now we have pretty much a cradle to work with. A file with a failing
  258. test, and the fixer, that for now does not do anything.
  259. How do fixers work? In the PHP CS Fixer, they work by iterating through
  260. pieces of codes (each being a Token), and inspecting what exists before
  261. and after that bit and making a decision, usually:
  262. * Adding code.
  263. * Modifying code.
  264. * Deleting code.
  265. * Ignoring code.
  266. In our case, we want to find all comments, and foreach (pun intended)
  267. one of them check if they are preceded by a semicolon symbol.
  268. Now you need to do some reading, because all these symbols obey a list
  269. defined by the PHP compiler. It is the `List of Parser Tokens`_.
  270. Internally, PHP CS Fixer transforms some of PHP native tokens into custom
  271. tokens through the use of Transformers_, they aim to help you reason about the
  272. changes you may want to do in the fixers.
  273. So we can get to move forward, humor me in believing that comments have
  274. one symbol name: ``T_COMMENT``.
  275. Step 3 - Implement your solution - continuation.
  276. ________________________________________________
  277. We do not want all symbols to be analysed. Only ``T_COMMENT``. So let us
  278. iterate the token(s) we are interested in.
  279. .. code-block:: php
  280. // src/Fixer/Comment/RemoveCommentsFixer.php
  281. final class RemoveCommentsFixer extends AbstractFixer
  282. {
  283. // ...
  284. /**
  285. * {@inheritdoc}
  286. */
  287. protected function applyFix(\SplFileInfo $file, Tokens $tokens)
  288. {
  289. foreach ($tokens as $index => $token) {
  290. if (!$token->isGivenKind(T_COMMENT)) {
  291. continue;
  292. }
  293. // need to figure out what to do here!
  294. }
  295. }
  296. }
  297. OK, now for each ``T_COMMENT``, all we need to do is check if the previous
  298. token is a semicolon.
  299. .. code-block:: php
  300. // src/Fixer/Comment/RemoveCommentsFixer.php
  301. final class RemoveCommentsFixer extends AbstractFixer
  302. {
  303. // ...
  304. /**
  305. * {@inheritdoc}
  306. */
  307. protected function applyFix(\SplFileInfo $file, Tokens $tokens)
  308. {
  309. foreach ($tokens as $index => $token) {
  310. if (!$token->isGivenKind(T_COMMENT)) {
  311. continue;
  312. }
  313. $prevTokenIndex = $tokens->getPrevMeaningfulToken($index);
  314. $prevToken = $tokens[$prevTokenIndex];
  315. if ($prevToken->equals(';')) {
  316. $tokens->clearAt($index);
  317. }
  318. }
  319. }
  320. }
  321. So the fixer in the end looks like this:
  322. .. code-block:: php
  323. <?php
  324. /*
  325. * This file is part of PHP CS Fixer.
  326. *
  327. * (c) Fabien Potencier <fabien@symfony.com>
  328. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  329. *
  330. * This source file is subject to the MIT license that is bundled
  331. * with this source code in the file LICENSE.
  332. */
  333. namespace PhpCsFixer\Fixer\Comment;
  334. use PhpCsFixer\AbstractFixer;
  335. use PhpCsFixer\Tokenizer\Tokens;
  336. /**
  337. * @author Your name <your@email.com>
  338. */
  339. final class RemoveCommentsFixer extends AbstractFixer
  340. {
  341. /**
  342. * {@inheritdoc}
  343. */
  344. public function getDefinition()
  345. {
  346. return new FixerDefinition(
  347. 'Removes all comments of the code that are preceded by ";" (semicolon).', // Trailing dot is important. We thrive to use English grammar properly.
  348. [
  349. new CodeSample(
  350. '<?php echo 123; /* Comment */'
  351. ),
  352. ]
  353. );
  354. }
  355. /**
  356. * {@inheritdoc}
  357. */
  358. public function isCandidate(Tokens $tokens)
  359. {
  360. return $tokens->isTokenKindFound(T_COMMENT);
  361. }
  362. /**
  363. * {@inheritdoc}
  364. */
  365. protected function applyFix(\SplFileInfo $file, Tokens $tokens) {
  366. foreach($tokens as $index => $token){
  367. if (!$token->isGivenKind(T_COMMENT)) {
  368. continue;
  369. }
  370. $prevTokenIndex = $tokens->getPrevMeaningfulToken($index);
  371. $prevToken = $tokens[$prevTokenIndex];
  372. if ($prevToken->equals(';')) {
  373. $tokens->clearAt($index);
  374. }
  375. }
  376. }
  377. }
  378. Step 4 - Format, Commit, PR.
  379. ____________________________
  380. Note that so far, we have not coded adhering to PSR-1/2. This is done on
  381. purpose. For every commit you make, you must use PHP CS Fixer to fix
  382. itself. Thus, on the command line call:
  383. .. code-block:: console
  384. $ php php-cs-fixer fix
  385. This will fix all the coding style mistakes.
  386. After the final CS fix, you are ready to commit. Do it.
  387. Now, go to GitHub and open a Pull Request.
  388. Step 5 - Peer review: it is all about code and community building.
  389. __________________________________________________________________
  390. Congratulations, you have made your first fixer. Be proud. Your work
  391. will be reviewed carefully by PHP CS Fixer community.
  392. The review usually flows like this:
  393. 1. People will check your code for common mistakes and logical
  394. caveats. Usually, the person building a fixer is blind about some
  395. behavior mistakes of fixers. Expect to write few more tests to cater for
  396. the reviews.
  397. 2. People will discuss the relevance of your fixer. If it is
  398. something that goes along with Symfony style standards, or PSR-1/PSR-2
  399. standards, they will ask you to add it to existing ruleset.
  400. 3. People will also discuss whether your fixer is idempotent or not.
  401. If they understand that your fixer must always run before or after a
  402. certain fixer, they will ask you to override a method named
  403. ``getPriority()``. Do not be afraid of asking the reviewer for help on how
  404. to do it.
  405. 4. People may ask you to rebase your code to unify commits or to get
  406. rid of merge commits.
  407. 5. Go to 1 until no actions are needed anymore.
  408. Your fixer will be incorporated in the next release.
  409. Congratulations! You have done it.
  410. Q&A
  411. ---
  412. Why is not my PR merged yet?
  413. PHP CS Fixer is used by many people, that expect it to be stable. So
  414. sometimes, few PR are delayed a bit so to avoid cluttering at @dev
  415. channel on composer.
  416. Other possibility is that reviewers are giving time to other members of
  417. PHP CS Fixer community to partake on the review debates of your fixer.
  418. In any case, we care a lot about what you do and we want to see it being
  419. part of the application as soon as possible.
  420. Why am I asked to use ``getPrevMeaningfulToken()`` instead of ``getPrevNonWhitespace()``?
  421. The main difference is that ``getPrevNonWhitespace()`` ignores only
  422. whitespaces (``T_WHITESPACE``), while ``getPrevMeaningfulToken()`` ignores
  423. whitespaces and comments. And usually that is what you want. For
  424. example:
  425. .. code-block:: php
  426. $a->/*comment*/func();
  427. If you are inspecting ``func()``, and you want to check whether this is
  428. part of an object, if you use ``getPrevNonWhitespace()`` you are going to
  429. get ``/*comment*/``, which might belie your test. On the other hand, if
  430. you use ``getPrevMeaningfulToken()``, no matter if you have got a comment
  431. or a whitespace, the returned token will always be ``->``.
  432. .. _Composer: https://getcomposer.org
  433. .. _CONTRIBUTING.md: ../CONTRIBUTING.md
  434. .. _idempotent: https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning
  435. .. _Linus's Law: https://en.wikipedia.org/wiki/Linus%27s_Law
  436. .. _List of Parser Tokens: https://php.net/manual/en/tokens.php
  437. .. _Token class: ../src/Tokenizer/Token.php
  438. .. _Tokens class: ../src/Tokenizer/Tokens.php
  439. .. _Transformers: ../src/Tokenizer/Transformer