PageRenderTime 26ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/fabpot/php-cs-fixer/COOKBOOK-FIXERS.md

https://gitlab.com/yousafsyed/easternglamor
Markdown | 466 lines | 375 code | 91 blank | 0 comment | 0 complexity | d659e77a6fb5ce5117024c42083eff20 MD5 | raw file
  1. Cookbook - Making a new Fixer for PHP CS Fixer
  2. ==============================================
  3. You want to make a new fixer to PHP CS Fixer and do not know how to
  4. start. Follow this document and you will be able to do it.
  5. ## Background
  6. In order to be able to create a new fixer, you need some background.
  7. PHP CS Fixer is a transcompiler which takes valid PHP code and pretty
  8. print valid PHP code. It does all transformations in multiple passes,
  9. a.k.a., multi-pass compiler.
  10. Therefore, a new fixer is meant to be ideally
  11. [idempotent](http://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning),
  12. or at least atomic in its actions. More on this later.
  13. All contributions go through a code review process. Do not feel
  14. discouraged - it is meant only to give more people more chance to
  15. contribute, and to detect bugs ([Linus'
  16. Law](http://en.wikipedia.org/wiki/Linus%27s_Law)).
  17. If possible, try to get acquainted with the public interface for the
  18. [Symfony/CS/Tokenizer/Tokens.php](Symfony/CS/Tokenizer/Tokens.php)
  19. and [Symfony/CS/Tokenizer/Token.php](Symfony/CS/Tokenizer/Token.php)
  20. classes.
  21. ## Assumptions
  22. * You are familiar with Test Driven Development.
  23. * Forked FriendsOfPHP/PHP-CS-Fixer into your own Github Account.
  24. * Cloned your forked repository locally.
  25. * Downloaded PHP CS Fixer and have executed `php composer.phar
  26. install`.
  27. * You have read [`CONTRIBUTING.md`](CONTRIBUTING.md)
  28. ## Step by step
  29. For this step-by-step, we are going to create a simple fixer that
  30. removes all comments of the code that are preceded by ';' (semicolon).
  31. We are calling it `remove_comments` (code name), or,
  32. `RemoveCommentsFixer` (class name).
  33. ### Step 1 - Creating files
  34. Create a new file in
  35. `PHP-CS-Fixer/Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`.
  36. Put this content inside:
  37. ```php
  38. <?php
  39. /*
  40. * This file is part of the PHP CS utility.
  41. *
  42. * (c) Fabien Potencier <fabien@symfony.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 Symfony\CS\Fixer\Contrib;
  48. use Symfony\CS\AbstractFixer;
  49. use Symfony\CS\Tokenizer\Tokens;
  50. /**
  51. * @author Your name <your@email.com>
  52. */
  53. final class RemoveCommentsFixer extends AbstractFixer
  54. {
  55. }
  56. ```
  57. Note how the class and file name match. Also keep in mind that all
  58. fixers must implement `FixerInterface`. In this case, the fixer is
  59. inheriting from `AbstractFixer`, which fulfills the interface with some
  60. default behavior.
  61. Now let us create the test file at
  62. `Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php` . Put this
  63. content inside:
  64. ```php
  65. <?php
  66. /*
  67. * This file is part of the PHP CS utility.
  68. *
  69. * (c) Fabien Potencier <fabien@symfony.com>
  70. *
  71. * This source file is subject to the MIT license that is bundled
  72. * with this source code in the file LICENSE.
  73. */
  74. namespace Symfony\CS\Tests\Fixer\Contrib;
  75. use Symfony\CS\Tests\Fixer\AbstractFixerTestBase;
  76. /**
  77. * @author Your name <your@email.com>
  78. *
  79. * @internal
  80. */
  81. final class RemoveCommentsFixerTest extends AbstractFixerTestBase
  82. {
  83. /**
  84. * @dataProvider provideFixCases
  85. */
  86. public function testFix($expected, $input = null)
  87. {
  88. $this->makeTest($expected, $input);
  89. }
  90. public function provideFixCases()
  91. {
  92. return array();
  93. }
  94. }
  95. ```
  96. The files are created, one thing is still missing though: we need to
  97. update the README.md. Fortunately, PHP CS Fixer can help you here.
  98. Execute the following command in your command shell:
  99. `# php php-cs-fixer readme > README.rst`
  100. ### Step 2 - Using tests to define fixers behavior
  101. Now that the files are created, you can start writing test to define the
  102. behavior of the fixer. You have to do it in two ways: first, ensuring
  103. the fixer changes what it should be changing; second, ensuring that
  104. fixer does not change what is not supposed to change. Thus:
  105. #### Keeping things as they are:
  106. `Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php`@provideFixCases:
  107. ```php
  108. ...
  109. public function provideFixCases()
  110. {
  111. return array(
  112. array('<?php echo "This should not be changed";') // Each sub-array is a test
  113. );
  114. }
  115. ...
  116. ```
  117. #### Ensuring things change:
  118. `Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php`@provideFixCases:
  119. ```php
  120. ...
  121. public function provideFixCases()
  122. {
  123. return array(
  124. array(
  125. '<?php echo "This should be changed"; ', // This is expected output
  126. '<?php echo "This should be changed"; /* Comment */', // This is input
  127. )
  128. );
  129. }
  130. ...
  131. ```
  132. Note that expected outputs are **always** tested alone to ensure your fixer will not change it.
  133. We want to have a failing test to start with, so the test file now looks
  134. like:
  135. `Symfony/CS/Tests/Fixer/Contrib/RemoveCommentsFixerTest.php`
  136. ```php
  137. <?php
  138. /*
  139. * This file is part of the PHP CS utility.
  140. *
  141. * (c) Fabien Potencier <fabien@symfony.com>
  142. *
  143. * This source file is subject to the MIT license that is bundled
  144. * with this source code in the file LICENSE.
  145. */
  146. namespace Symfony\CS\Tests\Fixer\Contrib;
  147. use Symfony\CS\Tests\Fixer\AbstractFixerTestBase;
  148. /**
  149. * @author Your name <your@email.com>
  150. *
  151. * @internal
  152. */
  153. final class RemoveCommentsFixerTest extends AbstractFixerTestBase
  154. {
  155. /**
  156. * @dataProvider provideFixCases
  157. */
  158. public function testFix($expected, $input = null)
  159. {
  160. $this->makeTest($expected, $input);
  161. }
  162. public function provideFixCases()
  163. {
  164. return array(
  165. array(
  166. '<?php echo "This should be changed"; ', // This is expected output
  167. '<?php echo "This should be changed"; /* Comment */', // This is input
  168. )
  169. );
  170. }
  171. }
  172. ```
  173. ### Step 3 - Implement your solution
  174. You have defined the behavior of your fixer in tests. Now it is time to
  175. implement it.
  176. We need first to create one method to describe what this fixer does:
  177. `Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`:
  178. ```php
  179. final class RemoveCommentsFixer extends AbstractFixer
  180. {
  181. ...
  182. /**
  183. * {@inheritdoc}
  184. */
  185. public function getDescription()
  186. {
  187. return 'Removes all comments of the code that are preceded by ";" (semicolon).'; // Trailing dot is important. We thrive to use English grammar properly.
  188. }
  189. }
  190. ```
  191. For now, let us just make a fixer that applies no modification:
  192. `Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`:
  193. ```php
  194. final class RemoveCommentsFixer extends AbstractFixer
  195. {
  196. /**
  197. * {@inheritdoc}
  198. */
  199. public function fix(\SplFileInfo $file, $content)
  200. {
  201. $tokens = Tokens::fromCode($content);
  202. return $tokens->generateCode();
  203. }
  204. }
  205. ```
  206. Run `vendor/bin/phpunit`. You are going to see that the tests fails.
  207. ### Break
  208. Now we have pretty much a cradle to work with. A file with a failing
  209. test, and the fixer, that for now does not do anything.
  210. How do fixers work? In the PHP CS Fixer, they work by iterating through
  211. pieces of codes (each being a Token), and inspecting what exists before
  212. and after that bit and making a decision, usually:
  213. * Adding code.
  214. * Modifying code.
  215. * Deleting code.
  216. * Ignoring code.
  217. In our case, we want to find all comments, and foreach (pun intended)
  218. one of them check if they are preceded by a semicolon symbol.
  219. Now you need to do some reading, because all these symbols obey a list
  220. defined by the PHP compiler. It is the ["List of Parser
  221. Tokens"](http://php.net/manual/en/tokens.php).
  222. Internally, PHP CS Fixer transforms some of PHP native tokens into custom
  223. tokens through the use of [Transfomers](Symfony/CS/Tokenizer/Transformer),
  224. they aim to help you reason about the changes you may want to do in the
  225. fixers.
  226. So we can get to move forward, humor me in believing that comments have
  227. one symbol name: `T_COMMENT`.
  228. ### Step 3 - Implement your solution - continuation.
  229. We do not want all symbols to be analysed. Only `T_COMMENT`. So let us
  230. iterate the token(s) we are interested in.
  231. `Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`:
  232. ```php
  233. final class RemoveCommentsFixer extends AbstractFixer
  234. {
  235. /**
  236. * {@inheritdoc}
  237. */
  238. public function fix(\SplFileInfo $file, $content)
  239. {
  240. $tokens = Tokens::fromCode($content);
  241. $foundComments = $tokens->findGivenKind(T_COMMENT);
  242. foreach($foundComments as $index => $token){
  243. }
  244. return $tokens->generateCode();
  245. }
  246. }
  247. ```
  248. OK, now for each `T_COMMENT`, all we need to do is check if the previous
  249. token is a semicolon.
  250. `Symfony/CS/Fixer/Contrib/RemoveCommentsFixer.php`:
  251. ```php
  252. final class RemoveCommentsFixer extends AbstractFixer
  253. {
  254. /**
  255. * {@inheritdoc}
  256. */
  257. public function fix(\SplFileInfo $file, $content)
  258. {
  259. $tokens = Tokens::fromCode($content);
  260. $foundComments = $tokens->findGivenKind(T_COMMENT);
  261. foreach($foundComments as $index => $token){
  262. $prevTokenIndex = $tokens->getPrevMeaningfulToken($index);
  263. $prevToken = $tokens[$prevTokenIndex];
  264. if($prevToken->equals(';')){
  265. $token->clear();
  266. }
  267. }
  268. return $tokens->generateCode();
  269. }
  270. }
  271. ```
  272. So the fixer in the end looks like this:
  273. ```php
  274. <?php
  275. /*
  276. * This file is part of the PHP CS utility.
  277. *
  278. * (c) Fabien Potencier <fabien@symfony.com>
  279. *
  280. * This source file is subject to the MIT license that is bundled
  281. * with this source code in the file LICENSE.
  282. *
  283. */
  284. namespace Symfony\CS\Fixer\Contrib;
  285. use Symfony\CS\AbstractFixer;
  286. use Symfony\CS\Tokenizer\Tokens;
  287. /**
  288. * @author Your name <your@email.com>
  289. */
  290. final class RemoveCommentsFixer extends AbstractFixer {
  291. /**
  292. * {@inheritdoc}
  293. */
  294. public function fix(\SplFileInfo $file, $content) {
  295. $tokens = Tokens::fromCode($content);
  296. $foundComments = $tokens->findGivenKind(T_COMMENT);
  297. foreach ($foundComments as $index => $token) {
  298. $prevTokenIndex = $tokens->getPrevMeaningfulToken($index);
  299. $prevToken = $tokens[$prevTokenIndex];
  300. if ($prevToken->equals(';')) {
  301. $token->clear();
  302. }
  303. }
  304. return $tokens->generateCode();
  305. }
  306. /**
  307. * {@inheritdoc}
  308. */
  309. public function getDescription() {
  310. return 'Removes all comments of the code that are preceded by ";" (semicolon).';// Trailing dot is important. We thrive to use English grammar properly.
  311. }
  312. }
  313. ```
  314. ### Step 4 - Format, Commit, PR.
  315. Note that so far, we have not coded adhering to PSR-1/2. This is done on
  316. purpose. For every commit you make, you must use PHP CS Fixer to fix
  317. itself. Thus, on the command line call:
  318. `$ php php-cs-fixer fix`
  319. This will fix all the coding style mistakes.
  320. After the final CS fix, you are ready to commit. Do it.
  321. Now, go to Github and open a Pull Request.
  322. ### Step 5 - Peer review: it is all about code and community building.
  323. Congratulations, you have made your first fixer. Be proud. Your work
  324. will be reviewed carefully by PHP CS Fixer community.
  325. The review usually flows like this:
  326. 1. People will check your code for common mistakes and logical
  327. caveats. Usually, the person building a fixer is blind about some
  328. behavior mistakes of fixers. Expect to write few more tests to cater for
  329. the reviews.
  330. 2. People will discuss the relevance of your fixer. If it is
  331. something that goes along with Symfony style standards, or PSR-1/PSR-2
  332. standards, they will ask you to move from Symfony/CS/Fixers/Contrib to
  333. Symfony/CS/Fixers/{Symfony, PSR2, etc}.
  334. 3. People will also discuss whether your fixer is idempotent or not.
  335. If they understand that your fixer must always run before or after a
  336. certain fixer, they will ask you to override a method named
  337. `getPriority()`. Do not be afraid of asking the reviewer for help on how
  338. to do it.
  339. 4. People may ask you to rebase your code to unify commits or to get
  340. rid of merge commits.
  341. 5. Go to 1 until no actions are needed anymore.
  342. Your fixer will be incorporated in the next release.
  343. # Congratulations! You have done it.
  344. ## Q&A
  345. #### Why is not my PR merged yet?
  346. PHP CS Fixer is used by many people, that expect it to be stable. So
  347. sometimes, few PR are delayed a bit so to avoid cluttering at @dev
  348. channel on composer.
  349. Other possibility is that reviewers are giving time to other members of
  350. PHP CS Fixer community to partake on the review debates of your fixer.
  351. In any case, we care a lot about what you do and we want to see it being
  352. part of the application as soon as possible.
  353. #### May I use short arrays (`$a = []`)?
  354. No. Short arrays were introduced in PHP 5.4 and PHP CS Fixer still
  355. supports PHP 5.3.6.
  356. #### Why are you steering me to create my fixer at CONTRIB_LEVEL ?
  357. CONTRIB_LEVEL is the most lax level - and it is far more likely to have
  358. your fixer accepted at CONTRIB_LEVEL and later changed to SYMFOMY_LEVEL
  359. or PSR2_LEVEL; than the other way around.
  360. If you make your contribution directly at PSR2_LEVEL, eventually the
  361. relevance debate will take place and your fixer might be pushed to
  362. CONTRIB_LEVEL.
  363. #### Why am I asked to use `getPrevMeaningfulToken()` instead of `getPrevNonWhitespace()`?
  364. The main difference is that `getPrevNonWhitespace()` ignores only
  365. whitespaces (`T_WHITESPACE`), while `getPrevMeaningfulToken()` ignores
  366. whitespaces and comments. And usually that is what you want. For
  367. example:
  368. ```php
  369. $a->/*comment*/func();
  370. ```
  371. If you are inspecting `func()`, and you want to check whether this is
  372. part of an object, if you use `getPrevNonWhitespace()` you are going to
  373. get `/*comment*/`, which might belie your test. On the other hand, if
  374. you use `getPrevMeaningfulToken()`, no matter if you have got a comment
  375. or a whitespace, the returned token will always be `->`.