AbstractFixerTestCase.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer\Tests\Test;
  12. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  13. use PhpCsFixer\Fixer\FixerInterface;
  14. use PhpCsFixer\FixerFactory;
  15. use PhpCsFixer\Linter\CachingLinter;
  16. use PhpCsFixer\Linter\Linter;
  17. use PhpCsFixer\Linter\LinterInterface;
  18. use PhpCsFixer\RuleSet;
  19. use PhpCsFixer\Tests\Test\Assert\AssertTokensTrait;
  20. use PhpCsFixer\Tests\TestCase;
  21. use PhpCsFixer\Tokenizer\Token;
  22. use PhpCsFixer\Tokenizer\Tokens;
  23. use PhpCsFixer\Utils;
  24. /**
  25. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  26. *
  27. * @internal
  28. */
  29. abstract class AbstractFixerTestCase extends TestCase
  30. {
  31. use AssertTokensTrait;
  32. /**
  33. * @var LinterInterface
  34. */
  35. protected $linter;
  36. /**
  37. * @var null|ConfigurableFixerInterface|FixerInterface
  38. */
  39. protected $fixer;
  40. /**
  41. * @var null|string
  42. */
  43. private $fixerClassName;
  44. protected function setUp()
  45. {
  46. parent::setUp();
  47. $this->linter = $this->getLinter();
  48. $this->fixer = $this->createFixer();
  49. }
  50. /**
  51. * @return FixerInterface
  52. */
  53. protected function createFixer()
  54. {
  55. $fixerClassName = $this->getFixerClassName();
  56. return new $fixerClassName();
  57. }
  58. /**
  59. * Create fixer factory with all needed fixers registered.
  60. *
  61. * @return FixerFactory
  62. */
  63. protected function createFixerFactory()
  64. {
  65. return FixerFactory::create()->registerBuiltInFixers();
  66. }
  67. /**
  68. * @return string
  69. */
  70. protected function getFixerName()
  71. {
  72. $reflection = new \ReflectionClass($this);
  73. $name = preg_replace('/FixerTest$/', '', $reflection->getShortName());
  74. return Utils::camelCaseToUnderscore($name);
  75. }
  76. /**
  77. * @param string $filename
  78. *
  79. * @return \SplFileInfo
  80. */
  81. protected function getTestFile($filename = __FILE__)
  82. {
  83. static $files = [];
  84. if (!isset($files[$filename])) {
  85. $files[$filename] = new \SplFileInfo($filename);
  86. }
  87. return $files[$filename];
  88. }
  89. /**
  90. * Tests if a fixer fixes a given string to match the expected result.
  91. *
  92. * It is used both if you want to test if something is fixed or if it is not touched by the fixer.
  93. * It also makes sure that the expected output does not change when run through the fixer. That means that you
  94. * do not need two test cases like [$expected] and [$expected, $input] (where $expected is the same in both cases)
  95. * as the latter covers both of them.
  96. * This method throws an exception if $expected and $input are equal to prevent test cases that accidentally do
  97. * not test anything.
  98. *
  99. * @param string $expected The expected fixer output
  100. * @param null|string $input The fixer input, or null if it should intentionally be equal to the output
  101. * @param null|\SplFileInfo $file The file to fix, or null if unneeded
  102. */
  103. protected function doTest($expected, $input = null, \SplFileInfo $file = null)
  104. {
  105. if ($expected === $input) {
  106. throw new \InvalidArgumentException('Input parameter must not be equal to expected parameter.');
  107. }
  108. $file = $file ?: $this->getTestFile();
  109. $fileIsSupported = $this->fixer->supports($file);
  110. if (null !== $input) {
  111. $this->assertNull($this->lintSource($input));
  112. Tokens::clearCache();
  113. $tokens = Tokens::fromCode($input);
  114. if ($fileIsSupported) {
  115. $this->assertTrue($this->fixer->isCandidate($tokens), 'Fixer must be a candidate for input code.');
  116. $this->assertFalse($tokens->isChanged(), 'Fixer must not touch Tokens on candidate check.');
  117. $fixResult = $this->fixer->fix($file, $tokens);
  118. $this->assertNull($fixResult, '->fix method must return null.');
  119. }
  120. $this->assertThat(
  121. $tokens->generateCode(),
  122. self::createIsIdenticalStringConstraint($expected),
  123. 'Code build on input code must match expected code.'
  124. );
  125. $this->assertTrue($tokens->isChanged(), 'Tokens collection built on input code must be marked as changed after fixing.');
  126. $tokens->clearEmptyTokens();
  127. $this->assertSame(
  128. count($tokens),
  129. count(array_unique(array_map(static function (Token $token) {
  130. return spl_object_hash($token);
  131. }, $tokens->toArray()))),
  132. 'Token items inside Tokens collection must be unique.'
  133. );
  134. Tokens::clearCache();
  135. $expectedTokens = Tokens::fromCode($expected);
  136. $this->assertTokens($expectedTokens, $tokens);
  137. }
  138. $this->assertNull($this->lintSource($expected));
  139. Tokens::clearCache();
  140. $tokens = Tokens::fromCode($expected);
  141. if ($fileIsSupported) {
  142. $fixResult = $this->fixer->fix($file, $tokens);
  143. $this->assertNull($fixResult, '->fix method must return null.');
  144. }
  145. $this->assertThat(
  146. $tokens->generateCode(),
  147. self::createIsIdenticalStringConstraint($expected),
  148. 'Code build on expected code must not change.'
  149. );
  150. $this->assertFalse($tokens->isChanged(), 'Tokens collection built on expected code must not be marked as changed after fixing.');
  151. }
  152. /**
  153. * @param string $source
  154. *
  155. * @return null|string
  156. */
  157. protected function lintSource($source)
  158. {
  159. try {
  160. $this->linter->lintSource($source)->check();
  161. } catch (\Exception $e) {
  162. return $e->getMessage()."\n\nSource:\n${source}";
  163. }
  164. }
  165. private function assertTokens(Tokens $expectedTokens, Tokens $inputTokens)
  166. {
  167. foreach ($expectedTokens as $index => $expectedToken) {
  168. $option = ['JSON_PRETTY_PRINT'];
  169. $inputToken = $inputTokens[$index];
  170. $this->assertTrue(
  171. $expectedToken->equals($inputToken),
  172. sprintf("The token at index %d must be:\n%s,\ngot:\n%s.", $index, $expectedToken->toJson($option), $inputToken->toJson($option))
  173. );
  174. $expectedTokenKind = $expectedToken->isArray() ? $expectedToken->getId() : $expectedToken->getContent();
  175. $this->assertTrue(
  176. $inputTokens->isTokenKindFound($expectedTokenKind),
  177. sprintf('The token kind %s must be found in fixed tokens collection.', $expectedTokenKind)
  178. );
  179. }
  180. $this->assertSame($expectedTokens->count(), $inputTokens->count(), 'Both collections must have the same length.');
  181. }
  182. /**
  183. * @return LinterInterface
  184. */
  185. private function getLinter()
  186. {
  187. static $linter = null;
  188. if (null === $linter) {
  189. $linter = new CachingLinter(new Linter());
  190. }
  191. return $linter;
  192. }
  193. /**
  194. * @return string
  195. */
  196. private function getFixerClassName()
  197. {
  198. if (null !== $this->fixerClassName) {
  199. return $this->fixerClassName;
  200. }
  201. try {
  202. $fixers = $this->createFixerFactory()
  203. ->useRuleSet(new RuleSet([$this->getFixerName() => true]))
  204. ->getFixers()
  205. ;
  206. } catch (\UnexpectedValueException $e) {
  207. throw new \UnexpectedValueException('Cannot determine fixer class, perhaps you forget to override `getFixerName` or `createFixerFactory` method?', 0, $e);
  208. }
  209. if (1 !== count($fixers)) {
  210. throw new \UnexpectedValueException(sprintf('Determine fixer class should result in one fixer, got "%d". Perhaps you configured the fixer to "false" ?', count($fixers)));
  211. }
  212. $this->fixerClassName = get_class($fixers[0]);
  213. return $this->fixerClassName;
  214. }
  215. /**
  216. * @todo Remove me when this class will end up in dedicated package.
  217. *
  218. * @param string $expected
  219. */
  220. private static function createIsIdenticalStringConstraint($expected)
  221. {
  222. $candidates = array_filter([
  223. 'PhpCsFixer\PhpunitConstraintIsIdenticalString\Constraint\IsIdenticalString',
  224. 'PHPUnit\Framework\Constraint\IsIdentical',
  225. 'PHPUnit_Framework_Constraint_IsIdentical',
  226. ], function ($className) { return class_exists($className); });
  227. if (empty($candidates)) {
  228. throw new \RuntimeException('PHPUnit not installed?!');
  229. }
  230. $candidate = array_shift($candidates);
  231. return new $candidate($expected);
  232. }
  233. }