* Dariusz Rumiński * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Tests\Test; use GeckoPackages\PHPUnit\Constraints\SameStringsConstraint; use PhpCsFixer\Fixer\ConfigurableFixerInterface; use PhpCsFixer\Fixer\FixerInterface; use PhpCsFixer\FixerFactory; use PhpCsFixer\Linter\Linter; use PhpCsFixer\Linter\LinterInterface; use PhpCsFixer\RuleSet; use PhpCsFixer\Tests\Test\Assert\AssertTokensTrait; use PhpCsFixer\Tests\TestCase; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Utils; use Prophecy\Argument; /** * @author Dariusz Rumiński * * @internal */ abstract class AbstractFixerTestCase extends TestCase { use AssertTokensTrait; /** * @var LinterInterface */ protected $linter; /** * @var null|ConfigurableFixerInterface|FixerInterface */ protected $fixer; /** * @var null|string */ private $fixerClassName; protected function setUp() { parent::setUp(); $this->linter = $this->getLinter(); $this->fixer = $this->createFixer(); // @todo remove at 3.0 together with env var itself if (getenv('PHP_CS_FIXER_TEST_USE_LEGACY_TOKENIZER')) { Tokens::setLegacyMode(true); } } protected function tearDown() { parent::tearDown(); // @todo remove at 3.0 Tokens::setLegacyMode(false); } /** * @return FixerInterface */ protected function createFixer() { $fixerClassName = $this->getFixerClassName(); return new $fixerClassName(); } /** * Create fixer factory with all needed fixers registered. * * @return FixerFactory */ protected function createFixerFactory() { return FixerFactory::create()->registerBuiltInFixers(); } /** * @return string */ protected function getFixerName() { $reflection = new \ReflectionClass($this); $name = preg_replace('/FixerTest$/', '', $reflection->getShortName()); return Utils::camelCaseToUnderscore($name); } /** * @param string $filename * * @return \SplFileInfo */ protected function getTestFile($filename = __FILE__) { static $files = []; if (!isset($files[$filename])) { $files[$filename] = new \SplFileInfo($filename); } return $files[$filename]; } /** * Tests if a fixer fixes a given string to match the expected result. * * It is used both if you want to test if something is fixed or if it is not touched by the fixer. * It also makes sure that the expected output does not change when run through the fixer. That means that you * do not need two test cases like [$expected] and [$expected, $input] (where $expected is the same in both cases) * as the latter covers both of them. * This method throws an exception if $expected and $input are equal to prevent test cases that accidentally do * not test anything. * * @param string $expected The expected fixer output * @param null|string $input The fixer input, or null if it should intentionally be equal to the output * @param null|\SplFileInfo $file The file to fix, or null if unneeded */ protected function doTest($expected, $input = null, \SplFileInfo $file = null) { if ($expected === $input) { throw new \InvalidArgumentException('Input parameter must not be equal to expected parameter.'); } $file = $file ?: $this->getTestFile(); $fileIsSupported = $this->fixer->supports($file); if (null !== $input) { $this->assertNull($this->lintSource($input)); Tokens::clearCache(); $tokens = Tokens::fromCode($input); if ($fileIsSupported) { $this->assertTrue($this->fixer->isCandidate($tokens), 'Fixer must be a candidate for input code.'); $this->assertFalse($tokens->isChanged(), 'Fixer must not touch Tokens on candidate check.'); $fixResult = $this->fixer->fix($file, $tokens); $this->assertNull($fixResult, '->fix method must return null.'); } $this->assertThat( $tokens->generateCode(), new SameStringsConstraint($expected), 'Code build on input code must match expected code.' ); $this->assertTrue($tokens->isChanged(), 'Tokens collection built on input code must be marked as changed after fixing.'); $tokens->clearEmptyTokens(); $this->assertSame( count($tokens), count(array_unique(array_map(static function (Token $token) { return spl_object_hash($token); }, $tokens->toArray()))), 'Token items inside Tokens collection must be unique.' ); Tokens::clearCache(); $expectedTokens = Tokens::fromCode($expected); $this->assertTokens($expectedTokens, $tokens); } $this->assertNull($this->lintSource($expected)); Tokens::clearCache(); $tokens = Tokens::fromCode($expected); if ($fileIsSupported) { $fixResult = $this->fixer->fix($file, $tokens); $this->assertNull($fixResult, '->fix method must return null.'); } $this->assertThat( $tokens->generateCode(), new SameStringsConstraint($expected), 'Code build on expected code must not change.' ); $this->assertFalse($tokens->isChanged(), 'Tokens collection built on expected code must not be marked as changed after fixing.'); } /** * @param string $source * * @return null|string */ protected function lintSource($source) { try { $this->linter->lintSource($source)->check(); } catch (\Exception $e) { return $e->getMessage()."\n\nSource:\n${source}"; } } private function assertTokens(Tokens $expectedTokens, Tokens $inputTokens) { foreach ($expectedTokens as $index => $expectedToken) { $option = ['JSON_PRETTY_PRINT']; $inputToken = $inputTokens[$index]; $this->assertTrue( $expectedToken->equals($inputToken), sprintf("The token at index %d must be:\n%s,\ngot:\n%s.", $index, $expectedToken->toJson($option), $inputToken->toJson($option)) ); $expectedTokenKind = $expectedToken->isArray() ? $expectedToken->getId() : $expectedToken->getContent(); $this->assertTrue( $inputTokens->isTokenKindFound($expectedTokenKind), sprintf('The token kind %s must be found in fixed tokens collection.', $expectedTokenKind) ); } $this->assertSame($expectedTokens->count(), $inputTokens->count(), 'Both collections must have the same length.'); } /** * @return LinterInterface */ private function getLinter() { static $linter = null; if (null === $linter) { if (getenv('SKIP_LINT_TEST_CASES')) { $linterProphecy = $this->prophesize(\PhpCsFixer\Linter\LinterInterface::class); $linterProphecy ->lintSource(Argument::type('string')) ->willReturn($this->prophesize(\PhpCsFixer\Linter\LintingResultInterface::class)->reveal()); $linter = $linterProphecy->reveal(); } else { $linter = new Linter(); } } return $linter; } /** * @return string */ private function getFixerClassName() { if (null !== $this->fixerClassName) { return $this->fixerClassName; } try { $fixers = $this->createFixerFactory() ->useRuleSet(new RuleSet([$this->getFixerName() => true])) ->getFixers() ; } catch (\UnexpectedValueException $e) { throw new \UnexpectedValueException('Cannot determine fixer class, perhaps you forget to override `getFixerName` or `createFixerFactory` method?', 0, $e); } if (1 !== count($fixers)) { throw new \UnexpectedValueException(sprintf('Determine fixer class should result in one fixer, got "%d". Perhaps you configured the fixer to "false" ?', count($fixers))); } $this->fixerClassName = get_class($fixers[0]); return $this->fixerClassName; } }