ProjectCodeTest.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Tests\AutoReview;
  13. use PhpCsFixer\AbstractProxyFixer;
  14. use PhpCsFixer\DocBlock\Annotation;
  15. use PhpCsFixer\DocBlock\DocBlock;
  16. use PhpCsFixer\Fixer\AbstractPhpUnitFixer;
  17. use PhpCsFixer\Fixer\PhpUnit\PhpUnitNamespacedFixer;
  18. use PhpCsFixer\FixerFactory;
  19. use PhpCsFixer\Preg;
  20. use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
  21. use PhpCsFixer\Tests\Test\AbstractIntegrationTestCase;
  22. use PhpCsFixer\Tests\TestCase;
  23. use PhpCsFixer\Tokenizer\Token;
  24. use PhpCsFixer\Tokenizer\Tokens;
  25. use Symfony\Component\Finder\Finder;
  26. use Symfony\Component\Finder\SplFileInfo;
  27. /**
  28. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  29. *
  30. * @internal
  31. *
  32. * @coversNothing
  33. *
  34. * @group auto-review
  35. * @group covers-nothing
  36. */
  37. final class ProjectCodeTest extends TestCase
  38. {
  39. /**
  40. * @var null|list<array{class-string<TestCase>}>
  41. */
  42. private static ?array $testClassCases = null;
  43. /**
  44. * This structure contains older classes that are not yet covered by tests.
  45. *
  46. * It may only shrink, never add anything to it.
  47. *
  48. * @var string[]
  49. */
  50. private static $classesWithoutTests = [
  51. \PhpCsFixer\Console\Command\DocumentationCommand::class,
  52. \PhpCsFixer\Console\SelfUpdate\GithubClient::class,
  53. \PhpCsFixer\Documentation\DocumentationLocator::class,
  54. \PhpCsFixer\Documentation\FixerDocumentGenerator::class,
  55. \PhpCsFixer\Documentation\ListDocumentGenerator::class,
  56. \PhpCsFixer\Documentation\RstUtils::class,
  57. \PhpCsFixer\Documentation\RuleSetDocumentationGenerator::class,
  58. \PhpCsFixer\Runner\FileCachingLintingIterator::class,
  59. ];
  60. public static function tearDownAfterClass(): void
  61. {
  62. self::$testClassCases = null;
  63. }
  64. public function testThatClassesWithoutTestsVarIsProper(): void
  65. {
  66. $unknownClasses = array_filter(
  67. self::$classesWithoutTests,
  68. static fn (string $class): bool => !class_exists($class) && !trait_exists($class),
  69. );
  70. self::assertSame([], $unknownClasses);
  71. }
  72. /**
  73. * @dataProvider provideThatSrcClassHaveTestClassCases
  74. */
  75. public function testThatSrcClassHaveTestClass(string $className): void
  76. {
  77. $testClassName = 'PhpCsFixer\\Tests'.substr($className, 10).'Test';
  78. if (\in_array($className, self::$classesWithoutTests, true)) {
  79. self::assertFalse(class_exists($testClassName), sprintf('Class "%s" already has tests, so it should be removed from "%s::$classesWithoutTests".', $className, __CLASS__));
  80. self::markTestIncomplete(sprintf('Class "%s" has no tests yet, please help and add it.', $className));
  81. }
  82. self::assertTrue(class_exists($testClassName), sprintf('Expected test class "%s" for "%s" not found.', $testClassName, $className));
  83. self::assertTrue(is_subclass_of($testClassName, TestCase::class), sprintf('Expected test class "%s" to be a subclass of "\PhpCsFixer\Tests\TestCase".', $testClassName));
  84. }
  85. /**
  86. * @dataProvider provideThatSrcClassesNotAbuseInterfacesCases
  87. */
  88. public function testThatSrcClassesNotAbuseInterfaces(string $className): void
  89. {
  90. $rc = new \ReflectionClass($className);
  91. $allowedMethods = array_map(
  92. function (\ReflectionClass $interface): array {
  93. return $this->getPublicMethodNames($interface);
  94. },
  95. $rc->getInterfaces()
  96. );
  97. if (\count($allowedMethods) > 0) {
  98. $allowedMethods = array_unique(array_merge(...array_values($allowedMethods)));
  99. }
  100. $allowedMethods[] = '__construct';
  101. $allowedMethods[] = '__destruct';
  102. $allowedMethods[] = '__wakeup';
  103. $exceptionMethods = [
  104. 'configure', // due to AbstractFixer::configure
  105. 'getConfigurationDefinition', // due to AbstractFixer::getConfigurationDefinition
  106. 'getDefaultConfiguration', // due to AbstractFixer::getDefaultConfiguration
  107. 'setWhitespacesConfig', // due to AbstractFixer::setWhitespacesConfig
  108. ];
  109. $definedMethods = $this->getPublicMethodNames($rc);
  110. $extraMethods = array_diff(
  111. $definedMethods,
  112. $allowedMethods,
  113. $exceptionMethods
  114. );
  115. sort($extraMethods);
  116. self::assertEmpty(
  117. $extraMethods,
  118. sprintf(
  119. "Class '%s' should not have public methods that are not part of implemented interfaces.\nViolations:\n%s",
  120. $className,
  121. implode("\n", array_map(static function (string $item): string {
  122. return " * {$item}";
  123. }, $extraMethods))
  124. )
  125. );
  126. }
  127. /**
  128. * @dataProvider provideSrcClassCases
  129. */
  130. public function testThatSrcClassesNotExposeProperties(string $className): void
  131. {
  132. $rc = new \ReflectionClass($className);
  133. self::assertEmpty(
  134. $rc->getProperties(\ReflectionProperty::IS_PUBLIC),
  135. sprintf('Class \'%s\' should not have public properties.', $className)
  136. );
  137. if ($rc->isFinal()) {
  138. return;
  139. }
  140. $allowedProps = [];
  141. $definedProps = $rc->getProperties(\ReflectionProperty::IS_PROTECTED);
  142. if (false !== $rc->getParentClass()) {
  143. $allowedProps = $rc->getParentClass()->getProperties(\ReflectionProperty::IS_PROTECTED);
  144. }
  145. $allowedProps = array_map(static function (\ReflectionProperty $item): string {
  146. return $item->getName();
  147. }, $allowedProps);
  148. $definedProps = array_map(static function (\ReflectionProperty $item): string {
  149. return $item->getName();
  150. }, $definedProps);
  151. $exceptionPropsPerClass = [
  152. \PhpCsFixer\AbstractPhpdocTypesFixer::class => ['tags'],
  153. \PhpCsFixer\AbstractFixer::class => ['configuration', 'configurationDefinition', 'whitespacesConfig'],
  154. AbstractProxyFixer::class => ['proxyFixers'],
  155. ];
  156. $extraProps = array_diff(
  157. $definedProps,
  158. $allowedProps,
  159. $exceptionPropsPerClass[$className] ?? []
  160. );
  161. sort($extraProps);
  162. self::assertEmpty(
  163. $extraProps,
  164. sprintf(
  165. "Class '%s' should not have protected properties.\nViolations:\n%s",
  166. $className,
  167. implode("\n", array_map(static function (string $item): string {
  168. return " * {$item}";
  169. }, $extraProps))
  170. )
  171. );
  172. }
  173. /**
  174. * @dataProvider provideTestClassCases
  175. */
  176. public function testThatTestClassesAreTraitOrAbstractOrFinal(string $testClassName): void
  177. {
  178. $rc = new \ReflectionClass($testClassName);
  179. self::assertTrue(
  180. $rc->isTrait() || $rc->isAbstract() || $rc->isFinal(),
  181. sprintf('Test class %s should be trait, abstract or final.', $testClassName)
  182. );
  183. }
  184. /**
  185. * @dataProvider provideTestClassCases
  186. */
  187. public function testThatTestClassesAreInternal(string $testClassName): void
  188. {
  189. $rc = new \ReflectionClass($testClassName);
  190. $doc = new DocBlock($rc->getDocComment());
  191. self::assertNotEmpty(
  192. $doc->getAnnotationsOfType('internal'),
  193. sprintf('Test class %s should have internal annotation.', $testClassName)
  194. );
  195. }
  196. /**
  197. * @dataProvider provideTestClassCases
  198. */
  199. public function testThatTestClassesPublicMethodsAreCorrectlyNamed(string $testClassName): void
  200. {
  201. $reflectionClass = new \ReflectionClass($testClassName);
  202. $publicMethods = array_filter(
  203. $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
  204. static function (\ReflectionMethod $reflectionMethod) use ($reflectionClass): bool {
  205. return $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName();
  206. }
  207. );
  208. if ([] === $publicMethods) {
  209. $this->expectNotToPerformAssertions(); // no methods to test, all good!
  210. return;
  211. }
  212. foreach ($publicMethods as $method) {
  213. self::assertMatchesRegularExpression(
  214. '/^(test|expect|provide|setUpBeforeClass$|tearDownAfterClass$)/',
  215. $method->getName(),
  216. sprintf('Public method "%s::%s" is not properly named.', $reflectionClass->getName(), $method->getName())
  217. );
  218. }
  219. }
  220. /**
  221. * @dataProvider provideDataProviderMethodCases
  222. */
  223. public function testThatTestDataProvidersAreUsed(string $testClassName, \ReflectionMethod $dataProvider): void
  224. {
  225. $usedDataProviderMethodNames = [];
  226. foreach ($this->getUsedDataProviderMethodNames($testClassName) as $providerName) {
  227. $usedDataProviderMethodNames[] = $providerName;
  228. }
  229. $dataProviderName = $dataProvider->getName();
  230. self::assertContains(
  231. $dataProviderName,
  232. $usedDataProviderMethodNames,
  233. sprintf('Data provider in "%s" with name "%s" is not used.', $dataProvider->getDeclaringClass()->getName(), $dataProviderName)
  234. );
  235. }
  236. /**
  237. * @dataProvider provideDataProviderMethodCases
  238. */
  239. public function testThatTestDataProvidersReturnIterableOrArray(string $testClassName, \ReflectionMethod $dataProvider): void
  240. {
  241. $dataProviderName = $dataProvider->getName();
  242. $returnType = $dataProvider->getReturnType();
  243. self::assertInstanceOf(
  244. \ReflectionNamedType::class,
  245. $returnType,
  246. sprintf('Data provider in "%s" with name "%s" has no return type.', $dataProvider->getDeclaringClass()->getName(), $dataProviderName)
  247. );
  248. $returnTypeName = $returnType->getName();
  249. self::assertTrue(
  250. 'array' === $returnTypeName || 'iterable' === $returnTypeName,
  251. sprintf('Data provider in "%s" with name "%s" has return type "%s", expected "array" or "iterable".', $dataProvider->getDeclaringClass()->getName(), $dataProviderName, $returnTypeName)
  252. );
  253. }
  254. /**
  255. * @dataProvider provideDataProviderMethodCases
  256. */
  257. public function testThatTestDataProvidersAreCorrectlyNamed(string $testClassName, \ReflectionMethod $dataProvider): void
  258. {
  259. $dataProviderName = $dataProvider->getShortName();
  260. self::assertMatchesRegularExpression('/^provide[A-Z]\S+Cases$/', $dataProviderName, sprintf(
  261. 'Data provider in "%s" with name "%s" is not correctly named.',
  262. $testClassName,
  263. $dataProviderName
  264. ));
  265. }
  266. public static function provideDataProviderMethodCases(): iterable
  267. {
  268. foreach (self::provideTestClassCases() as $testClassName) {
  269. $testClassName = reset($testClassName);
  270. $reflectionClass = new \ReflectionClass($testClassName);
  271. $methods = array_filter(
  272. $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
  273. static function (\ReflectionMethod $reflectionMethod) use ($reflectionClass): bool {
  274. return $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName()
  275. && str_starts_with($reflectionMethod->getName(), 'provide');
  276. }
  277. );
  278. foreach ($methods as $method) {
  279. yield [$testClassName, $method];
  280. }
  281. }
  282. }
  283. /**
  284. * @dataProvider provideTestClassCases
  285. */
  286. public function testThatTestClassCoversAreCorrect(string $testClassName): void
  287. {
  288. $reflectionClass = new \ReflectionClass($testClassName);
  289. if ($reflectionClass->isAbstract() || $reflectionClass->isInterface()) {
  290. $this->expectNotToPerformAssertions();
  291. return;
  292. }
  293. $doc = $reflectionClass->getDocComment();
  294. self::assertNotFalse($doc);
  295. if (1 === Preg::match('/@coversNothing/', $doc, $matches)) {
  296. return;
  297. }
  298. $covers = Preg::matchAll('/@covers (\S*)/', $doc, $matches);
  299. self::assertGreaterThanOrEqual(1, $covers, sprintf('Missing @covers in PHPDoc of test class "%s".', $testClassName));
  300. array_shift($matches);
  301. $class = '\\'.str_replace('PhpCsFixer\Tests\\', 'PhpCsFixer\\', substr($testClassName, 0, -4));
  302. $parentClass = (new \ReflectionClass($class))->getParentClass();
  303. $parentClassName = false === $parentClass ? null : '\\'.$parentClass->getName();
  304. foreach ($matches as $match) {
  305. $classMatch = array_shift($match);
  306. self::assertTrue(
  307. $classMatch === $class || $parentClassName === $classMatch,
  308. sprintf('Unexpected @covers "%s" for "%s".', $classMatch, $testClassName)
  309. );
  310. }
  311. }
  312. /**
  313. * @dataProvider provideThereIsNoPregFunctionUsedDirectlyCases
  314. */
  315. public function testThereIsNoPregFunctionUsedDirectly(string $className): void
  316. {
  317. $rc = new \ReflectionClass($className);
  318. $tokens = Tokens::fromCode(file_get_contents($rc->getFileName()));
  319. $stringTokens = array_filter(
  320. $tokens->toArray(),
  321. static function (Token $token): bool {
  322. return $token->isGivenKind(T_STRING);
  323. }
  324. );
  325. $strings = array_map(
  326. static function (Token $token): string {
  327. return $token->getContent();
  328. },
  329. $stringTokens
  330. );
  331. $strings = array_unique($strings);
  332. $message = sprintf('Class %s must not use preg_*, it shall use Preg::* instead.', $className);
  333. self::assertNotContains('preg_filter', $strings, $message);
  334. self::assertNotContains('preg_grep', $strings, $message);
  335. self::assertNotContains('preg_match', $strings, $message);
  336. self::assertNotContains('preg_match_all', $strings, $message);
  337. self::assertNotContains('preg_replace', $strings, $message);
  338. self::assertNotContains('preg_replace_callback', $strings, $message);
  339. self::assertNotContains('preg_split', $strings, $message);
  340. }
  341. /**
  342. * @dataProvider provideTestClassCases
  343. */
  344. public function testExpectedInputOrder(string $testClassName): void
  345. {
  346. $reflectionClass = new \ReflectionClass($testClassName);
  347. $publicMethods = array_filter(
  348. $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
  349. static function (\ReflectionMethod $reflectionMethod) use ($reflectionClass): bool {
  350. return $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName();
  351. }
  352. );
  353. if ([] === $publicMethods) {
  354. $this->expectNotToPerformAssertions(); // no methods to test, all good!
  355. return;
  356. }
  357. /** @var \ReflectionMethod $method */
  358. foreach ($publicMethods as $method) {
  359. $parameters = $method->getParameters();
  360. if (\count($parameters) < 2) {
  361. $this->addToAssertionCount(1); // not enough parameters to test, all good!
  362. continue;
  363. }
  364. $expected = [
  365. 'expected' => false,
  366. 'input' => false,
  367. ];
  368. for ($i = \count($parameters) - 1; $i >= 0; --$i) {
  369. $name = $parameters[$i]->getName();
  370. if (isset($expected[$name])) {
  371. $expected[$name] = $i;
  372. }
  373. }
  374. $expected = array_filter($expected, static fn ($item): bool => false !== $item);
  375. if (\count($expected) < 2) {
  376. $this->addToAssertionCount(1); // not enough parameters to test, all good!
  377. continue;
  378. }
  379. self::assertLessThan(
  380. $expected['input'],
  381. $expected['expected'],
  382. sprintf('Public method "%s::%s" has parameter \'input\' before \'expected\'.', $reflectionClass->getName(), $method->getName())
  383. );
  384. }
  385. }
  386. /**
  387. * @dataProvider provideSrcClassCases
  388. * @dataProvider provideTestClassCases
  389. */
  390. public function testAllCodeContainSingleClassy(string $className): void
  391. {
  392. $headerTypes = [
  393. T_ABSTRACT,
  394. T_AS,
  395. T_COMMENT,
  396. T_DECLARE,
  397. T_DOC_COMMENT,
  398. T_FINAL,
  399. T_LNUMBER,
  400. T_NAMESPACE,
  401. T_NS_SEPARATOR,
  402. T_OPEN_TAG,
  403. T_STRING,
  404. T_USE,
  405. T_WHITESPACE,
  406. ];
  407. $rc = new \ReflectionClass($className);
  408. $file = $rc->getFileName();
  409. $tokens = Tokens::fromCode(file_get_contents($file));
  410. $classyIndex = null;
  411. self::assertTrue($tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds()), sprintf('File "%s" should contains a classy.', $file));
  412. $count = \count($tokens);
  413. for ($index = 1; $index < $count; ++$index) {
  414. if ($tokens[$index]->isClassy()) {
  415. $classyIndex = $index;
  416. break;
  417. }
  418. if (\defined('T_ATTRIBUTE') && $tokens[$index]->isGivenKind(T_ATTRIBUTE)) {
  419. $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
  420. continue;
  421. }
  422. if (!$tokens[$index]->isGivenKind($headerTypes) && !$tokens[$index]->equalsAny([';', '=', '(', ')'])) {
  423. self::fail(sprintf('File "%s" should only contains single classy, found "%s" @ %d.', $file, $tokens[$index]->toJson(), $index));
  424. }
  425. }
  426. self::assertNotNull($classyIndex, sprintf('File "%s" does not contain a classy.', $file));
  427. $nextTokenOfKind = $tokens->getNextTokenOfKind($classyIndex, ['{']);
  428. if (!\is_int($nextTokenOfKind)) {
  429. throw new \UnexpectedValueException('Classy without {} - braces.');
  430. }
  431. $classyEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nextTokenOfKind);
  432. self::assertNull($tokens->getNextNonWhitespace($classyEndIndex), sprintf('File "%s" should only contains a single classy.', $file));
  433. }
  434. /**
  435. * @dataProvider provideSrcClassCases
  436. */
  437. public function testInheritdocIsNotAbused(string $className): void
  438. {
  439. $rc = new \ReflectionClass($className);
  440. $allowedMethods = array_map(
  441. function (\ReflectionClass $interface): array {
  442. return $this->getPublicMethodNames($interface);
  443. },
  444. $rc->getInterfaces()
  445. );
  446. if (\count($allowedMethods) > 0) {
  447. $allowedMethods = array_merge(...array_values($allowedMethods));
  448. }
  449. $parentClass = $rc;
  450. while (false !== $parentClass = $parentClass->getParentClass()) {
  451. foreach ($parentClass->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
  452. $allowedMethods[] = $method->getName();
  453. }
  454. }
  455. $allowedMethods = array_unique($allowedMethods);
  456. $methodsWithInheritdoc = array_filter(
  457. $rc->getMethods(),
  458. static function (\ReflectionMethod $rm): bool {
  459. return false !== $rm->getDocComment() && stripos($rm->getDocComment(), '@inheritdoc');
  460. }
  461. );
  462. $methodsWithInheritdoc = array_map(
  463. static function (\ReflectionMethod $rm): string {
  464. return $rm->getName();
  465. },
  466. $methodsWithInheritdoc
  467. );
  468. $extraMethods = array_diff($methodsWithInheritdoc, $allowedMethods);
  469. self::assertEmpty(
  470. $extraMethods,
  471. sprintf(
  472. "Class '%s' should not have methods with '@inheritdoc' in PHPDoc that are not inheriting PHPDoc.\nViolations:\n%s",
  473. $className,
  474. implode("\n", array_map(static function ($item): string {
  475. return " * {$item}";
  476. }, $extraMethods))
  477. )
  478. );
  479. }
  480. public static function provideSrcClassCases(): array
  481. {
  482. return array_map(
  483. static function (string $item): array {
  484. return [$item];
  485. },
  486. self::getSrcClasses()
  487. );
  488. }
  489. public static function provideThatSrcClassesNotAbuseInterfacesCases(): array
  490. {
  491. return array_map(
  492. static function (string $item): array {
  493. return [$item];
  494. },
  495. array_filter(self::getSrcClasses(), static function (string $className): bool {
  496. $rc = new \ReflectionClass($className);
  497. $doc = false !== $rc->getDocComment()
  498. ? new DocBlock($rc->getDocComment())
  499. : null;
  500. if (
  501. $rc->isInterface()
  502. || (null !== $doc && \count($doc->getAnnotationsOfType('internal')) > 0)
  503. || \in_array($className, [
  504. \PhpCsFixer\Finder::class,
  505. AbstractFixerTestCase::class,
  506. AbstractIntegrationTestCase::class,
  507. Tokens::class,
  508. ], true)
  509. ) {
  510. return false;
  511. }
  512. $interfaces = $rc->getInterfaces();
  513. $interfacesCount = \count($interfaces);
  514. if (0 === $interfacesCount) {
  515. return false;
  516. }
  517. if (1 === $interfacesCount) {
  518. $interface = reset($interfaces);
  519. if ('Stringable' === $interface->getName()) {
  520. return false;
  521. }
  522. }
  523. return true;
  524. })
  525. );
  526. }
  527. public static function provideThatSrcClassHaveTestClassCases(): array
  528. {
  529. return array_map(
  530. static fn (string $item): array => [$item],
  531. array_filter(
  532. self::getSrcClasses(),
  533. static function (string $className): bool {
  534. $rc = new \ReflectionClass($className);
  535. return !$rc->isTrait() && !$rc->isAbstract() && !$rc->isInterface();
  536. }
  537. )
  538. );
  539. }
  540. /**
  541. * @return iterable<array{class-string<TestCase>}>
  542. */
  543. public static function provideTestClassCases(): iterable
  544. {
  545. if (null === self::$testClassCases) {
  546. self::$testClassCases = array_map(
  547. static fn (string $item): array => [$item],
  548. self::getTestClasses(),
  549. );
  550. }
  551. yield from self::$testClassCases;
  552. }
  553. public static function provideThereIsNoPregFunctionUsedDirectlyCases(): array
  554. {
  555. return array_map(
  556. static fn (string $item): array => [$item],
  557. array_filter(
  558. self::getSrcClasses(),
  559. static fn (string $className): bool => Preg::class !== $className,
  560. ),
  561. );
  562. }
  563. /**
  564. * @dataProvider providePhpUnitFixerExtendsAbstractPhpUnitFixerCases
  565. */
  566. public function testPhpUnitFixerExtendsAbstractPhpUnitFixer(string $className): void
  567. {
  568. $reflection = new \ReflectionClass($className);
  569. self::assertTrue($reflection->isSubclassOf(AbstractPhpUnitFixer::class));
  570. }
  571. public static function providePhpUnitFixerExtendsAbstractPhpUnitFixerCases(): iterable
  572. {
  573. $factory = new FixerFactory();
  574. $factory->registerBuiltInFixers();
  575. foreach ($factory->getFixers() as $fixer) {
  576. if (!str_starts_with($fixer->getName(), 'php_unit_')) {
  577. continue;
  578. }
  579. // this one fixes usage of PHPUnit classes
  580. if ($fixer instanceof PhpUnitNamespacedFixer) {
  581. continue;
  582. }
  583. if ($fixer instanceof AbstractProxyFixer) {
  584. continue;
  585. }
  586. yield [\get_class($fixer)];
  587. }
  588. }
  589. /**
  590. * @dataProvider provideSrcClassCases
  591. * @dataProvider provideTestClassCases
  592. */
  593. public function testConstantsAreInUpperCase(string $className): void
  594. {
  595. $rc = new \ReflectionClass($className);
  596. $reflectionClassConstants = $rc->getReflectionConstants();
  597. if (\count($reflectionClassConstants) < 1) {
  598. $this->expectNotToPerformAssertions();
  599. return;
  600. }
  601. foreach ($reflectionClassConstants as $constant) {
  602. $constantName = $constant->getName();
  603. self::assertSame(strtoupper($constantName), $constantName, $className);
  604. }
  605. }
  606. /**
  607. * @return iterable<string, string>
  608. */
  609. private function getUsedDataProviderMethodNames(string $testClassName): iterable
  610. {
  611. foreach ($this->getAnnotationsOfTestClass($testClassName, 'dataProvider') as $methodName => $dataProviderAnnotation) {
  612. if (1 === preg_match('/@dataProvider\s+(?P<methodName>\w+)/', $dataProviderAnnotation->getContent(), $matches)) {
  613. yield $methodName => $matches['methodName'];
  614. }
  615. }
  616. }
  617. /**
  618. * @return iterable<string, Annotation>
  619. */
  620. private function getAnnotationsOfTestClass(string $testClassName, string $annotation): iterable
  621. {
  622. $tokens = Tokens::fromCode(file_get_contents(
  623. str_replace('\\', \DIRECTORY_SEPARATOR, preg_replace('#^PhpCsFixer\\\Tests#', 'tests', $testClassName)).'.php'
  624. ));
  625. foreach ($tokens as $index => $token) {
  626. if (!$token->isGivenKind(T_DOC_COMMENT)) {
  627. continue;
  628. }
  629. $methodName = $tokens[$tokens->getNextTokenOfKind($index, [[T_STRING]])]->getContent();
  630. $docBlock = new DocBlock($token->getContent());
  631. $dataProviderAnnotations = $docBlock->getAnnotationsOfType($annotation);
  632. foreach ($dataProviderAnnotations as $dataProviderAnnotation) {
  633. yield $methodName => $dataProviderAnnotation;
  634. }
  635. }
  636. }
  637. /**
  638. * @return list<class-string>
  639. */
  640. private static function getSrcClasses(): array
  641. {
  642. static $classes;
  643. if (null !== $classes) {
  644. return $classes;
  645. }
  646. $finder = Finder::create()
  647. ->files()
  648. ->name('*.php')
  649. ->in(__DIR__.'/../../src')
  650. ->exclude([
  651. 'Resources',
  652. ])
  653. ;
  654. $classes = array_map(
  655. static function (SplFileInfo $file): string {
  656. return sprintf(
  657. '%s\\%s%s%s',
  658. 'PhpCsFixer',
  659. strtr($file->getRelativePath(), \DIRECTORY_SEPARATOR, '\\'),
  660. $file->getRelativePath() ? '\\' : '',
  661. $file->getBasename('.'.$file->getExtension())
  662. );
  663. },
  664. iterator_to_array($finder, false)
  665. );
  666. sort($classes);
  667. return $classes;
  668. }
  669. /**
  670. * @return list<class-string<TestCase>>
  671. */
  672. private static function getTestClasses(): array
  673. {
  674. static $classes;
  675. if (null !== $classes) {
  676. return $classes;
  677. }
  678. $finder = Finder::create()
  679. ->files()
  680. ->name('*.php')
  681. ->in(__DIR__.'/..')
  682. ->exclude([
  683. 'Fixtures',
  684. ])
  685. ;
  686. $classes = array_map(
  687. static function (SplFileInfo $file): string {
  688. return sprintf(
  689. 'PhpCsFixer\\Tests\\%s%s%s',
  690. strtr($file->getRelativePath(), \DIRECTORY_SEPARATOR, '\\'),
  691. $file->getRelativePath() ? '\\' : '',
  692. $file->getBasename('.'.$file->getExtension())
  693. );
  694. },
  695. iterator_to_array($finder, false)
  696. );
  697. $classes = array_filter($classes, static function (string $class): bool {
  698. return is_subclass_of($class, TestCase::class);
  699. });
  700. sort($classes);
  701. return $classes;
  702. }
  703. /**
  704. * @param \ReflectionClass<object> $rc
  705. *
  706. * @return string[]
  707. */
  708. private function getPublicMethodNames(\ReflectionClass $rc): array
  709. {
  710. return array_map(
  711. static function (\ReflectionMethod $rm): string {
  712. return $rm->getName();
  713. },
  714. $rc->getMethods(\ReflectionMethod::IS_PUBLIC)
  715. );
  716. }
  717. }