ProjectCodeTest.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  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 provideSrcConcreteClassCases
  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 provideSrcClassesNotAbuseInterfacesCases
  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::match('/@covers (\S*)/', $doc, $matches);
  299. self::assertNotFalse($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. self::assertTrue(
  306. $match === $class || $parentClassName === $match,
  307. sprintf('Unexpected @covers "%s" for "%s".', $match, $testClassName)
  308. );
  309. }
  310. }
  311. /**
  312. * @dataProvider provideClassesWherePregFunctionsAreForbiddenCases
  313. */
  314. public function testThereIsNoPregFunctionUsedDirectly(string $className): void
  315. {
  316. $rc = new \ReflectionClass($className);
  317. $tokens = Tokens::fromCode(file_get_contents($rc->getFileName()));
  318. $stringTokens = array_filter(
  319. $tokens->toArray(),
  320. static function (Token $token): bool {
  321. return $token->isGivenKind(T_STRING);
  322. }
  323. );
  324. $strings = array_map(
  325. static function (Token $token): string {
  326. return $token->getContent();
  327. },
  328. $stringTokens
  329. );
  330. $strings = array_unique($strings);
  331. $message = sprintf('Class %s must not use preg_*, it shall use Preg::* instead.', $className);
  332. self::assertNotContains('preg_filter', $strings, $message);
  333. self::assertNotContains('preg_grep', $strings, $message);
  334. self::assertNotContains('preg_match', $strings, $message);
  335. self::assertNotContains('preg_match_all', $strings, $message);
  336. self::assertNotContains('preg_replace', $strings, $message);
  337. self::assertNotContains('preg_replace_callback', $strings, $message);
  338. self::assertNotContains('preg_split', $strings, $message);
  339. }
  340. /**
  341. * @dataProvider provideTestClassCases
  342. */
  343. public function testExpectedInputOrder(string $testClassName): void
  344. {
  345. $reflectionClass = new \ReflectionClass($testClassName);
  346. $publicMethods = array_filter(
  347. $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
  348. static function (\ReflectionMethod $reflectionMethod) use ($reflectionClass): bool {
  349. return $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName();
  350. }
  351. );
  352. if ([] === $publicMethods) {
  353. $this->expectNotToPerformAssertions(); // no methods to test, all good!
  354. return;
  355. }
  356. /** @var \ReflectionMethod $method */
  357. foreach ($publicMethods as $method) {
  358. $parameters = $method->getParameters();
  359. if (\count($parameters) < 2) {
  360. $this->addToAssertionCount(1); // not enough parameters to test, all good!
  361. continue;
  362. }
  363. $expected = [
  364. 'expected' => false,
  365. 'input' => false,
  366. ];
  367. for ($i = \count($parameters) - 1; $i >= 0; --$i) {
  368. $name = $parameters[$i]->getName();
  369. if (isset($expected[$name])) {
  370. $expected[$name] = $i;
  371. }
  372. }
  373. $expected = array_filter($expected, static fn ($item): bool => false !== $item);
  374. if (\count($expected) < 2) {
  375. $this->addToAssertionCount(1); // not enough parameters to test, all good!
  376. continue;
  377. }
  378. self::assertLessThan(
  379. $expected['input'],
  380. $expected['expected'],
  381. sprintf('Public method "%s::%s" has parameter \'input\' before \'expected\'.', $reflectionClass->getName(), $method->getName())
  382. );
  383. }
  384. }
  385. /**
  386. * @dataProvider provideSrcClassCases
  387. * @dataProvider provideTestClassCases
  388. */
  389. public function testAllCodeContainSingleClassy(string $className): void
  390. {
  391. $headerTypes = [
  392. T_ABSTRACT,
  393. T_AS,
  394. T_COMMENT,
  395. T_DECLARE,
  396. T_DOC_COMMENT,
  397. T_FINAL,
  398. T_LNUMBER,
  399. T_NAMESPACE,
  400. T_NS_SEPARATOR,
  401. T_OPEN_TAG,
  402. T_STRING,
  403. T_USE,
  404. T_WHITESPACE,
  405. ];
  406. $rc = new \ReflectionClass($className);
  407. $file = $rc->getFileName();
  408. $tokens = Tokens::fromCode(file_get_contents($file));
  409. $classyIndex = null;
  410. self::assertTrue($tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds()), sprintf('File "%s" should contains a classy.', $file));
  411. $count = \count($tokens);
  412. for ($index = 1; $index < $count; ++$index) {
  413. if ($tokens[$index]->isClassy()) {
  414. $classyIndex = $index;
  415. break;
  416. }
  417. if (\defined('T_ATTRIBUTE') && $tokens[$index]->isGivenKind(T_ATTRIBUTE)) {
  418. $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
  419. continue;
  420. }
  421. if (!$tokens[$index]->isGivenKind($headerTypes) && !$tokens[$index]->equalsAny([';', '=', '(', ')'])) {
  422. self::fail(sprintf('File "%s" should only contains single classy, found "%s" @ %d.', $file, $tokens[$index]->toJson(), $index));
  423. }
  424. }
  425. self::assertNotNull($classyIndex, sprintf('File "%s" does not contain a classy.', $file));
  426. $nextTokenOfKind = $tokens->getNextTokenOfKind($classyIndex, ['{']);
  427. if (!\is_int($nextTokenOfKind)) {
  428. throw new \UnexpectedValueException('Classy without {} - braces.');
  429. }
  430. $classyEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nextTokenOfKind);
  431. self::assertNull($tokens->getNextNonWhitespace($classyEndIndex), sprintf('File "%s" should only contains a single classy.', $file));
  432. }
  433. /**
  434. * @dataProvider provideSrcClassCases
  435. */
  436. public function testInheritdocIsNotAbused(string $className): void
  437. {
  438. $rc = new \ReflectionClass($className);
  439. $allowedMethods = array_map(
  440. function (\ReflectionClass $interface): array {
  441. return $this->getPublicMethodNames($interface);
  442. },
  443. $rc->getInterfaces()
  444. );
  445. if (\count($allowedMethods) > 0) {
  446. $allowedMethods = array_merge(...array_values($allowedMethods));
  447. }
  448. $parentClass = $rc;
  449. while (false !== $parentClass = $parentClass->getParentClass()) {
  450. foreach ($parentClass->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
  451. $allowedMethods[] = $method->getName();
  452. }
  453. }
  454. $allowedMethods = array_unique($allowedMethods);
  455. $methodsWithInheritdoc = array_filter(
  456. $rc->getMethods(),
  457. static function (\ReflectionMethod $rm): bool {
  458. return false !== $rm->getDocComment() && stripos($rm->getDocComment(), '@inheritdoc');
  459. }
  460. );
  461. $methodsWithInheritdoc = array_map(
  462. static function (\ReflectionMethod $rm): string {
  463. return $rm->getName();
  464. },
  465. $methodsWithInheritdoc
  466. );
  467. $extraMethods = array_diff($methodsWithInheritdoc, $allowedMethods);
  468. self::assertEmpty(
  469. $extraMethods,
  470. sprintf(
  471. "Class '%s' should not have methods with '@inheritdoc' in PHPDoc that are not inheriting PHPDoc.\nViolations:\n%s",
  472. $className,
  473. implode("\n", array_map(static function ($item): string {
  474. return " * {$item}";
  475. }, $extraMethods))
  476. )
  477. );
  478. }
  479. public static function provideSrcClassCases(): array
  480. {
  481. return array_map(
  482. static function (string $item): array {
  483. return [$item];
  484. },
  485. self::getSrcClasses()
  486. );
  487. }
  488. public static function provideSrcClassesNotAbuseInterfacesCases(): array
  489. {
  490. return array_map(
  491. static function (string $item): array {
  492. return [$item];
  493. },
  494. array_filter(self::getSrcClasses(), static function (string $className): bool {
  495. $rc = new \ReflectionClass($className);
  496. $doc = false !== $rc->getDocComment()
  497. ? new DocBlock($rc->getDocComment())
  498. : null;
  499. if (
  500. $rc->isInterface()
  501. || (null !== $doc && \count($doc->getAnnotationsOfType('internal')) > 0)
  502. || \in_array($className, [
  503. \PhpCsFixer\Finder::class,
  504. AbstractFixerTestCase::class,
  505. AbstractIntegrationTestCase::class,
  506. Tokens::class,
  507. ], true)
  508. ) {
  509. return false;
  510. }
  511. $interfaces = $rc->getInterfaces();
  512. $interfacesCount = \count($interfaces);
  513. if (0 === $interfacesCount) {
  514. return false;
  515. }
  516. if (1 === $interfacesCount) {
  517. $interface = reset($interfaces);
  518. if ('Stringable' === $interface->getName()) {
  519. return false;
  520. }
  521. }
  522. return true;
  523. })
  524. );
  525. }
  526. public static function provideSrcConcreteClassCases(): array
  527. {
  528. return array_map(
  529. static fn (string $item): array => [$item],
  530. array_filter(
  531. self::getSrcClasses(),
  532. static function (string $className): bool {
  533. $rc = new \ReflectionClass($className);
  534. return !$rc->isTrait() && !$rc->isAbstract() && !$rc->isInterface();
  535. }
  536. )
  537. );
  538. }
  539. /**
  540. * @return iterable<array{class-string<TestCase>}>
  541. */
  542. public static function provideTestClassCases(): iterable
  543. {
  544. if (null === self::$testClassCases) {
  545. self::$testClassCases = array_map(
  546. static fn (string $item): array => [$item],
  547. self::getTestClasses(),
  548. );
  549. }
  550. yield from self::$testClassCases;
  551. }
  552. public static function provideClassesWherePregFunctionsAreForbiddenCases(): array
  553. {
  554. return array_map(
  555. static fn (string $item): array => [$item],
  556. array_filter(
  557. self::getSrcClasses(),
  558. static fn (string $className): bool => Preg::class !== $className,
  559. ),
  560. );
  561. }
  562. /**
  563. * @dataProvider providePhpUnitFixerExtendsAbstractPhpUnitFixerCases
  564. */
  565. public function testPhpUnitFixerExtendsAbstractPhpUnitFixer(string $className): void
  566. {
  567. $reflection = new \ReflectionClass($className);
  568. self::assertTrue($reflection->isSubclassOf(AbstractPhpUnitFixer::class));
  569. }
  570. public static function providePhpUnitFixerExtendsAbstractPhpUnitFixerCases(): iterable
  571. {
  572. $factory = new FixerFactory();
  573. $factory->registerBuiltInFixers();
  574. foreach ($factory->getFixers() as $fixer) {
  575. if (!str_starts_with($fixer->getName(), 'php_unit_')) {
  576. continue;
  577. }
  578. // this one fixes usage of PHPUnit classes
  579. if ($fixer instanceof PhpUnitNamespacedFixer) {
  580. continue;
  581. }
  582. if ($fixer instanceof AbstractProxyFixer) {
  583. continue;
  584. }
  585. yield [\get_class($fixer)];
  586. }
  587. }
  588. /**
  589. * @dataProvider provideSrcClassCases
  590. * @dataProvider provideTestClassCases
  591. */
  592. public function testConstantsAreInUpperCase(string $className): void
  593. {
  594. $rc = new \ReflectionClass($className);
  595. $reflectionClassConstants = $rc->getReflectionConstants();
  596. if (\count($reflectionClassConstants) < 1) {
  597. $this->expectNotToPerformAssertions();
  598. return;
  599. }
  600. foreach ($reflectionClassConstants as $constant) {
  601. $constantName = $constant->getName();
  602. self::assertSame(strtoupper($constantName), $constantName, $className);
  603. }
  604. }
  605. /**
  606. * @return iterable<string, string>
  607. */
  608. private function getUsedDataProviderMethodNames(string $testClassName): iterable
  609. {
  610. foreach ($this->getAnnotationsOfTestClass($testClassName, 'dataProvider') as $methodName => $dataProviderAnnotation) {
  611. if (1 === preg_match('/@dataProvider\s+(?P<methodName>\w+)/', $dataProviderAnnotation->getContent(), $matches)) {
  612. yield $methodName => $matches['methodName'];
  613. }
  614. }
  615. }
  616. /**
  617. * @return iterable<string, Annotation>
  618. */
  619. private function getAnnotationsOfTestClass(string $testClassName, string $annotation): iterable
  620. {
  621. $tokens = Tokens::fromCode(file_get_contents(
  622. str_replace('\\', \DIRECTORY_SEPARATOR, preg_replace('#^PhpCsFixer\\\Tests#', 'tests', $testClassName)).'.php'
  623. ));
  624. foreach ($tokens as $index => $token) {
  625. if (!$token->isGivenKind(T_DOC_COMMENT)) {
  626. continue;
  627. }
  628. $methodName = $tokens[$tokens->getNextTokenOfKind($index, [[T_STRING]])]->getContent();
  629. $docBlock = new DocBlock($token->getContent());
  630. $dataProviderAnnotations = $docBlock->getAnnotationsOfType($annotation);
  631. foreach ($dataProviderAnnotations as $dataProviderAnnotation) {
  632. yield $methodName => $dataProviderAnnotation;
  633. }
  634. }
  635. }
  636. /**
  637. * @return list<class-string>
  638. */
  639. private static function getSrcClasses(): array
  640. {
  641. static $classes;
  642. if (null !== $classes) {
  643. return $classes;
  644. }
  645. $finder = Finder::create()
  646. ->files()
  647. ->name('*.php')
  648. ->in(__DIR__.'/../../src')
  649. ->exclude([
  650. 'Resources',
  651. ])
  652. ;
  653. $classes = array_map(
  654. static function (SplFileInfo $file): string {
  655. return sprintf(
  656. '%s\\%s%s%s',
  657. 'PhpCsFixer',
  658. strtr($file->getRelativePath(), \DIRECTORY_SEPARATOR, '\\'),
  659. $file->getRelativePath() ? '\\' : '',
  660. $file->getBasename('.'.$file->getExtension())
  661. );
  662. },
  663. iterator_to_array($finder, false)
  664. );
  665. sort($classes);
  666. return $classes;
  667. }
  668. /**
  669. * @return list<class-string<TestCase>>
  670. */
  671. private static function getTestClasses(): array
  672. {
  673. static $classes;
  674. if (null !== $classes) {
  675. return $classes;
  676. }
  677. $finder = Finder::create()
  678. ->files()
  679. ->name('*.php')
  680. ->in(__DIR__.'/..')
  681. ->exclude([
  682. 'Fixtures',
  683. ])
  684. ;
  685. $classes = array_map(
  686. static function (SplFileInfo $file): string {
  687. return sprintf(
  688. 'PhpCsFixer\\Tests\\%s%s%s',
  689. strtr($file->getRelativePath(), \DIRECTORY_SEPARATOR, '\\'),
  690. $file->getRelativePath() ? '\\' : '',
  691. $file->getBasename('.'.$file->getExtension())
  692. );
  693. },
  694. iterator_to_array($finder, false)
  695. );
  696. $classes = array_filter($classes, static function (string $class): bool {
  697. return is_subclass_of($class, TestCase::class);
  698. });
  699. sort($classes);
  700. return $classes;
  701. }
  702. /**
  703. * @param \ReflectionClass<object> $rc
  704. *
  705. * @return string[]
  706. */
  707. private function getPublicMethodNames(\ReflectionClass $rc): array
  708. {
  709. return array_map(
  710. static function (\ReflectionMethod $rm): string {
  711. return $rm->getName();
  712. },
  713. $rc->getMethods(\ReflectionMethod::IS_PUBLIC)
  714. );
  715. }
  716. }