ProjectCodeTest.php 30 KB

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