ProjectCodeTest.php 33 KB

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