ProjectCodeTest.php 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236
  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\AbstractPhpdocToTypeDeclarationFixer;
  15. use PhpCsFixer\AbstractPhpdocTypesFixer;
  16. use PhpCsFixer\AbstractProxyFixer;
  17. use PhpCsFixer\Console\Command\FixCommand;
  18. use PhpCsFixer\DocBlock\Annotation;
  19. use PhpCsFixer\DocBlock\DocBlock;
  20. use PhpCsFixer\Fixer\AbstractPhpUnitFixer;
  21. use PhpCsFixer\Fixer\ConfigurableFixerTrait;
  22. use PhpCsFixer\Fixer\PhpUnit\PhpUnitNamespacedFixer;
  23. use PhpCsFixer\FixerConfiguration\AliasedFixerOptionBuilder;
  24. use PhpCsFixer\FixerFactory;
  25. use PhpCsFixer\Preg;
  26. use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
  27. use PhpCsFixer\Tests\Test\AbstractIntegrationTestCase;
  28. use PhpCsFixer\Tests\TestCase;
  29. use PhpCsFixer\Tokenizer\Token;
  30. use PhpCsFixer\Tokenizer\Tokens;
  31. use PhpCsFixer\Tokenizer\TokensAnalyzer;
  32. use Symfony\Component\Finder\Finder;
  33. use Symfony\Component\Finder\SplFileInfo;
  34. /**
  35. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  36. *
  37. * @internal
  38. *
  39. * @coversNothing
  40. *
  41. * @group auto-review
  42. * @group covers-nothing
  43. */
  44. final class ProjectCodeTest extends TestCase
  45. {
  46. /**
  47. * @var null|array<string, array{class-string<TestCase>}>
  48. */
  49. private static ?array $testClassCases = null;
  50. /**
  51. * @var null|array<string, array{class-string}>
  52. */
  53. private static ?array $srcClassCases = null;
  54. /**
  55. * @var null|array<string, array{class-string, string}>
  56. */
  57. private static ?array $dataProviderMethodCases = null;
  58. /**
  59. * @var array<class-string, Tokens>
  60. */
  61. private static array $tokensCache = [];
  62. public static function tearDownAfterClass(): void
  63. {
  64. self::$srcClassCases = null;
  65. self::$testClassCases = null;
  66. self::$tokensCache = [];
  67. }
  68. /**
  69. * @dataProvider provideThatSrcClassHaveTestClassCases
  70. */
  71. public function testThatSrcClassHaveTestClass(string $className): void
  72. {
  73. $testClassName = 'PhpCsFixer\Tests'.substr($className, 10).'Test';
  74. self::assertTrue(class_exists($testClassName), \sprintf('Expected test class "%s" for "%s" not found.', $testClassName, $className));
  75. }
  76. /**
  77. * @return iterable<array{string}>
  78. */
  79. public static function provideThatSrcClassHaveTestClassCases(): iterable
  80. {
  81. return array_map(
  82. static fn (string $item): array => [$item],
  83. array_filter(
  84. self::getSrcClasses(),
  85. static function (string $className): bool {
  86. $rc = new \ReflectionClass($className);
  87. return !$rc->isTrait() && !$rc->isAbstract() && !$rc->isInterface() && \count($rc->getMethods(\ReflectionMethod::IS_PUBLIC)) > 0;
  88. }
  89. )
  90. );
  91. }
  92. /**
  93. * This test requires 8.2+, so it can properly detect readonly parent class.
  94. *
  95. * @dataProvider provideSrcClassCases
  96. *
  97. * @param class-string $className
  98. *
  99. * @requires PHP 8.2
  100. */
  101. public function testThatSrcClassesAreReadonlyWhenPossible(string $className): void
  102. {
  103. $rc = new \ReflectionClass($className);
  104. $rcProperties = $rc->getProperties();
  105. if (0 === \count($rcProperties)) {
  106. $this->addToAssertionCount(1);
  107. return; // public properties present, no need for class to be readonly
  108. }
  109. $parentClass = $rc->getParentClass();
  110. if (\PHP_VERSION_ID >= 8_02_00 && false !== $parentClass && !$parentClass->isReadOnly()) {
  111. $this->addToAssertionCount(1);
  112. return; // Parent class is _not_ readonly, child class cannot be readonly in such case
  113. }
  114. $rc = new \ReflectionClass($className);
  115. $docComment = $rc->getDocComment();
  116. $doc = new DocBlock(false !== $docComment ? $docComment : '/** */');
  117. $readonly = \count($doc->getAnnotationsOfType('readonly')) > 0;
  118. $exceptions = [
  119. AliasedFixerOptionBuilder::class,
  120. ];
  121. // we allow exceptions to _not_ follow the rule,
  122. // but when they are ready to start following it - we shall remove them from exceptions list
  123. if (\in_array($className, $exceptions, true)) {
  124. self::assertFalse($readonly);
  125. return;
  126. }
  127. if ($readonly) {
  128. $this->addToAssertionCount(1);
  129. return; // already readonly
  130. }
  131. $tokens = $this->createTokensForClass($className);
  132. $constructorSequence = $tokens->findSequence([
  133. [T_FUNCTION],
  134. [T_STRING, '__construct'],
  135. '(',
  136. ]);
  137. if (null !== $constructorSequence) {
  138. $tokens = clone $tokens;
  139. $openIndex = $tokens->getNextTokenOfKind(array_key_last($constructorSequence), ['{']);
  140. $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openIndex);
  141. $tokens->overrideRange($openIndex + 1, $closeIndex - 1, []);
  142. }
  143. $tokensContent = $tokens->generateCode();
  144. $propertyNames = array_map(static fn (\ReflectionProperty $item) => $item->getName(), $rcProperties);
  145. $overrideFound = Preg::match(
  146. '/(?:self::\$|static::\$|\$this->)(?:'.implode('|', $propertyNames).')(?:\[[^=]*\])?\s*(?:=|(?:\?\?=))/',
  147. $tokensContent
  148. );
  149. if ($overrideFound) {
  150. $this->addToAssertionCount(1);
  151. return; // properties are mutable during lifecycle of instance, class is not readonly
  152. }
  153. self::fail(
  154. \sprintf('The class "%s" should have readonly annotation.', $className)
  155. );
  156. }
  157. /**
  158. * @dataProvider provideThatSrcClassesNotAbuseInterfacesCases
  159. *
  160. * @param class-string $className
  161. */
  162. public function testThatSrcClassesNotAbuseInterfaces(string $className): void
  163. {
  164. $rc = new \ReflectionClass($className);
  165. $allowedMethods = array_map(
  166. fn (\ReflectionClass $interface): array => $this->getPublicMethodNames($interface),
  167. $rc->getInterfaces()
  168. );
  169. if (\count($allowedMethods) > 0) {
  170. $allowedMethods = array_unique(array_merge(...array_values($allowedMethods)));
  171. }
  172. $allowedMethods[] = '__construct';
  173. $allowedMethods[] = '__destruct';
  174. $allowedMethods[] = '__wakeup';
  175. $exceptionMethods = [
  176. 'configure', // due to AbstractFixer::configure
  177. 'getConfigurationDefinition', // due to AbstractFixer::getConfigurationDefinition
  178. 'getDefaultConfiguration', // due to AbstractFixer::getDefaultConfiguration
  179. 'setWhitespacesConfig', // due to AbstractFixer::setWhitespacesConfig
  180. 'createConfigurationDefinition', // due to AbstractProxyFixer calling `createConfigurationDefinition` of proxied rule
  181. ];
  182. $definedMethods = $this->getPublicMethodNames($rc);
  183. $extraMethods = array_diff(
  184. $definedMethods,
  185. $allowedMethods,
  186. $exceptionMethods
  187. );
  188. sort($extraMethods);
  189. self::assertEmpty(
  190. $extraMethods,
  191. \sprintf(
  192. "Class '%s' should not have public methods that are not part of implemented interfaces.\nViolations:\n%s",
  193. $className,
  194. implode("\n", array_map(static fn (string $item): string => " * {$item}", $extraMethods))
  195. )
  196. );
  197. }
  198. /**
  199. * @return iterable<array{string}>
  200. */
  201. public static function provideThatSrcClassesNotAbuseInterfacesCases(): iterable
  202. {
  203. return array_map(
  204. static fn (string $item): array => [$item],
  205. array_filter(self::getSrcClasses(), static function (string $className): bool {
  206. $rc = new \ReflectionClass($className);
  207. $doc = false !== $rc->getDocComment()
  208. ? new DocBlock($rc->getDocComment())
  209. : null;
  210. if (
  211. $rc->isInterface()
  212. || (null !== $doc && \count($doc->getAnnotationsOfType('internal')) > 0)
  213. || \in_array($className, [
  214. \PhpCsFixer\Finder::class,
  215. AbstractFixerTestCase::class,
  216. AbstractIntegrationTestCase::class,
  217. Tokens::class,
  218. ], true)
  219. ) {
  220. return false;
  221. }
  222. $interfaces = $rc->getInterfaces();
  223. $interfacesCount = \count($interfaces);
  224. if (0 === $interfacesCount) {
  225. return false;
  226. }
  227. if (1 === $interfacesCount) {
  228. $interface = reset($interfaces);
  229. if (\Stringable::class === $interface->getName()) {
  230. return false;
  231. }
  232. }
  233. return true;
  234. })
  235. );
  236. }
  237. /**
  238. * @dataProvider provideSrcClassCases
  239. *
  240. * @param class-string $className
  241. */
  242. public function testThatSrcClassesNotExposeProperties(string $className): void
  243. {
  244. $rc = new \ReflectionClass($className);
  245. self::assertEmpty(
  246. $rc->getProperties(\ReflectionProperty::IS_PUBLIC),
  247. \sprintf('Class \'%s\' should not have public properties.', $className)
  248. );
  249. if ($rc->isFinal()) {
  250. return;
  251. }
  252. $allowedProps = [];
  253. $definedProps = $rc->getProperties(\ReflectionProperty::IS_PROTECTED);
  254. if (false !== $rc->getParentClass()) {
  255. $allowedProps = $rc->getParentClass()->getProperties(\ReflectionProperty::IS_PROTECTED);
  256. }
  257. $allowedProps = array_map(static fn (\ReflectionProperty $item): string => $item->getName(), $allowedProps);
  258. $definedProps = array_map(static fn (\ReflectionProperty $item): string => $item->getName(), $definedProps);
  259. $exceptionPropsPerClass = [
  260. AbstractFixer::class => ['configuration', 'configurationDefinition', 'whitespacesConfig'],
  261. AbstractPhpdocToTypeDeclarationFixer::class => ['configuration'],
  262. AbstractPhpdocTypesFixer::class => ['tags'],
  263. AbstractProxyFixer::class => ['proxyFixers'],
  264. ConfigurableFixerTrait::class => ['configuration'],
  265. FixCommand::class => ['defaultDescription', 'defaultName'], // TODO: PHP 8.0+, remove properties and test when PHP 8+ is required
  266. ];
  267. $extraProps = array_diff(
  268. $definedProps,
  269. $allowedProps,
  270. $exceptionPropsPerClass[$className] ?? []
  271. );
  272. sort($extraProps);
  273. self::assertEmpty(
  274. $extraProps,
  275. \sprintf(
  276. "Class '%s' should not have protected properties.\nViolations:\n%s",
  277. $className,
  278. implode("\n", array_map(static fn (string $item): string => " * {$item}", $extraProps))
  279. )
  280. );
  281. }
  282. /**
  283. * @dataProvider provideTestClassCases
  284. */
  285. public function testThatTestClassExtendsPhpCsFixerTestCaseClass(string $className): void
  286. {
  287. self::assertTrue(is_subclass_of($className, TestCase::class), \sprintf('Expected test class "%s" to be a subclass of "%s".', $className, TestCase::class));
  288. }
  289. /**
  290. * @dataProvider provideTestClassCases
  291. *
  292. * @param class-string<TestCase> $testClassName
  293. */
  294. public function testThatTestClassesAreTraitOrAbstractOrFinal(string $testClassName): void
  295. {
  296. $rc = new \ReflectionClass($testClassName);
  297. self::assertTrue(
  298. $rc->isTrait() || $rc->isAbstract() || $rc->isFinal(),
  299. \sprintf('Test class %s should be trait, abstract or final.', $testClassName)
  300. );
  301. }
  302. /**
  303. * @dataProvider provideTestClassCases
  304. *
  305. * @param class-string<TestCase> $testClassName
  306. */
  307. public function testThatTestClassesAreInternal(string $testClassName): void
  308. {
  309. $rc = new \ReflectionClass($testClassName);
  310. $doc = new DocBlock($rc->getDocComment());
  311. self::assertNotEmpty(
  312. $doc->getAnnotationsOfType('internal'),
  313. \sprintf('Test class %s should have internal annotation.', $testClassName)
  314. );
  315. }
  316. /**
  317. * @dataProvider provideTestClassCases
  318. *
  319. * @param class-string<TestCase> $testClassName
  320. */
  321. public function testThatTestClassesPublicMethodsAreCorrectlyNamed(string $testClassName): void
  322. {
  323. $reflectionClass = new \ReflectionClass($testClassName);
  324. $publicMethods = array_filter(
  325. $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
  326. static fn (\ReflectionMethod $reflectionMethod): bool => $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName()
  327. );
  328. if ([] === $publicMethods) {
  329. $this->expectNotToPerformAssertions(); // no methods to test, all good!
  330. return;
  331. }
  332. foreach ($publicMethods as $method) {
  333. self::assertMatchesRegularExpression(
  334. '/^(test|expect|provide|setUpBeforeClass$|tearDownAfterClass$|__construct$)/',
  335. $method->getName(),
  336. \sprintf('Public method "%s::%s" is not properly named.', $reflectionClass->getName(), $method->getName())
  337. );
  338. }
  339. }
  340. /**
  341. * @dataProvider provideDataProviderMethodCases
  342. *
  343. * @param class-string<TestCase> $testClassName
  344. */
  345. public function testThatTestDataProvidersAreUsed(string $testClassName, string $dataProviderName): void
  346. {
  347. $usedDataProviderMethodNames = [];
  348. foreach ($this->getUsedDataProviderMethodNames($testClassName) as $providerName) {
  349. $usedDataProviderMethodNames[] = $providerName;
  350. }
  351. self::assertContains(
  352. $dataProviderName,
  353. $usedDataProviderMethodNames,
  354. \sprintf('Data provider "%s::%s" is not used.', $testClassName, $dataProviderName),
  355. );
  356. }
  357. /**
  358. * @dataProvider provideDataProviderMethodCases
  359. */
  360. public function testThatTestDataProvidersAreCorrectlyNamed(string $testClassName, string $dataProviderName): void
  361. {
  362. self::assertMatchesRegularExpression('/^provide[A-Z]\S+Cases$/', $dataProviderName, \sprintf(
  363. 'Data provider "%s::%s" is not correctly named.',
  364. $testClassName,
  365. $dataProviderName,
  366. ));
  367. }
  368. /**
  369. * @dataProvider provideTestClassCases
  370. *
  371. * @param class-string<TestCase> $testClassName
  372. */
  373. public function testThatTestClassCoversAreCorrect(string $testClassName): void
  374. {
  375. $reflectionClass = new \ReflectionClass($testClassName);
  376. if ($reflectionClass->isAbstract() || $reflectionClass->isInterface()) {
  377. $this->expectNotToPerformAssertions();
  378. return;
  379. }
  380. $doc = $reflectionClass->getDocComment();
  381. self::assertNotFalse($doc);
  382. if (Preg::match('/@coversNothing/', $doc)) {
  383. return;
  384. }
  385. $covers = Preg::matchAll('/@covers (\S*)/', $doc, $matches);
  386. self::assertGreaterThanOrEqual(1, $covers, \sprintf('Missing @covers in PHPDoc of test class "%s".', $testClassName));
  387. array_shift($matches);
  388. /** @var class-string $class */
  389. $class = '\\'.str_replace('PhpCsFixer\Tests\\', 'PhpCsFixer\\', substr($testClassName, 0, -4));
  390. $parentClass = (new \ReflectionClass($class))->getParentClass();
  391. $parentClassName = false === $parentClass ? null : '\\'.$parentClass->getName();
  392. foreach ($matches as $match) {
  393. $classMatch = array_shift($match);
  394. self::assertTrue(
  395. $classMatch === $class || $parentClassName === $classMatch,
  396. \sprintf('Unexpected @covers "%s" for "%s".', $classMatch, $testClassName)
  397. );
  398. }
  399. }
  400. /**
  401. * @dataProvider provideSrcClassCases
  402. * @dataProvider provideTestClassCases
  403. *
  404. * @param class-string $className
  405. */
  406. public function testThereIsNoUsageOfExtract(string $className): void
  407. {
  408. $calledFunctions = $this->extractFunctionNamesCalledInClass($className);
  409. $message = \sprintf('Class %s must not use "extract()", explicitly extract only the keys that are needed - you never know what\'s else inside.', $className);
  410. self::assertNotContains('extract', $calledFunctions, $message);
  411. }
  412. /**
  413. * @dataProvider provideThereIsNoPregFunctionUsedDirectlyCases
  414. *
  415. * @param class-string $className
  416. */
  417. public function testThereIsNoPregFunctionUsedDirectly(string $className): void
  418. {
  419. $calledFunctions = $this->extractFunctionNamesCalledInClass($className);
  420. $message = \sprintf('Class %s must not use preg_*, it shall use Preg::* instead.', $className);
  421. self::assertNotContains('preg_filter', $calledFunctions, $message);
  422. self::assertNotContains('preg_grep', $calledFunctions, $message);
  423. self::assertNotContains('preg_match', $calledFunctions, $message);
  424. self::assertNotContains('preg_match_all', $calledFunctions, $message);
  425. self::assertNotContains('preg_replace', $calledFunctions, $message);
  426. self::assertNotContains('preg_replace_callback', $calledFunctions, $message);
  427. self::assertNotContains('preg_split', $calledFunctions, $message);
  428. }
  429. /**
  430. * @return iterable<array{string}>
  431. */
  432. public static function provideThereIsNoPregFunctionUsedDirectlyCases(): iterable
  433. {
  434. return array_map(
  435. static fn (string $item): array => [$item],
  436. array_filter(
  437. self::getSrcClasses(),
  438. static fn (string $className): bool => Preg::class !== $className,
  439. ),
  440. );
  441. }
  442. /**
  443. * @dataProvider provideTestClassCases
  444. *
  445. * @param class-string $className
  446. */
  447. public function testThereIsNoUsageOfSetAccessible(string $className): void
  448. {
  449. $calledFunctions = $this->extractFunctionNamesCalledInClass($className);
  450. $message = \sprintf('Class %s must not use "setAccessible()", use "Closure::bind()" instead.', $className);
  451. self::assertNotContains('setAccessible', $calledFunctions, $message);
  452. }
  453. /**
  454. * @dataProvider provideTestClassCases
  455. *
  456. * @param class-string<TestCase> $className
  457. */
  458. public function testNoPHPUnitMockUsed(string $className): void
  459. {
  460. $calledFunctions = $this->extractFunctionNamesCalledInClass($className);
  461. $message = \sprintf('Class %s must not use PHPUnit\'s mock, it shall use anonymous class instead.', $className);
  462. self::assertNotContains('getMockBuilder', $calledFunctions, $message);
  463. self::assertNotContains('createMock', $calledFunctions, $message);
  464. self::assertNotContains('createMockForIntersectionOfInterfaces', $calledFunctions, $message);
  465. self::assertNotContains('createPartialMock', $calledFunctions, $message);
  466. self::assertNotContains('createTestProxy', $calledFunctions, $message);
  467. self::assertNotContains('getMockForAbstractClass', $calledFunctions, $message);
  468. self::assertNotContains('getMockFromWsdl', $calledFunctions, $message);
  469. self::assertNotContains('getMockForTrait', $calledFunctions, $message);
  470. self::assertNotContains('getMockClass', $calledFunctions, $message);
  471. self::assertNotContains('createConfiguredMock', $calledFunctions, $message);
  472. self::assertNotContains('getObjectForTrait', $calledFunctions, $message);
  473. }
  474. /**
  475. * @dataProvider provideTestClassCases
  476. *
  477. * @param class-string<TestCase> $testClassName
  478. */
  479. public function testExpectedInputOrder(string $testClassName): void
  480. {
  481. $reflectionClass = new \ReflectionClass($testClassName);
  482. $publicMethods = array_filter(
  483. $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
  484. static fn (\ReflectionMethod $reflectionMethod): bool => $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName()
  485. );
  486. if ([] === $publicMethods) {
  487. $this->expectNotToPerformAssertions(); // no methods to test, all good!
  488. return;
  489. }
  490. /** @var \ReflectionMethod $method */
  491. foreach ($publicMethods as $method) {
  492. $parameters = $method->getParameters();
  493. if (\count($parameters) < 2) {
  494. $this->addToAssertionCount(1); // not enough parameters to test, all good!
  495. continue;
  496. }
  497. $expected = [
  498. 'expected' => false,
  499. 'input' => false,
  500. ];
  501. for ($i = \count($parameters) - 1; $i >= 0; --$i) {
  502. $name = $parameters[$i]->getName();
  503. if (isset($expected[$name])) {
  504. $expected[$name] = $i;
  505. }
  506. }
  507. $expectedFound = array_filter($expected, static fn ($item): bool => false !== $item);
  508. if (\count($expectedFound) < 2) {
  509. $this->addToAssertionCount(1); // not enough parameters to test, all good!
  510. continue;
  511. }
  512. self::assertLessThan(
  513. $expected['input'],
  514. $expected['expected'],
  515. \sprintf('Public method "%s::%s" has parameter \'input\' before \'expected\'.', $reflectionClass->getName(), $method->getName())
  516. );
  517. }
  518. }
  519. /**
  520. * @dataProvider provideDataProviderMethodCases
  521. *
  522. * @param class-string<TestCase> $testClassName
  523. * @param non-empty-string $dataProviderName
  524. */
  525. public function testDataProvidersAreNonPhpVersionConditional(string $testClassName, string $dataProviderName): void
  526. {
  527. $tokens = $this->createTokensForClass($testClassName);
  528. $tokensAnalyzer = new TokensAnalyzer($tokens);
  529. $dataProviderElements = array_filter($tokensAnalyzer->getClassyElements(), static function (array $v, int $k) use ($tokens, $dataProviderName) {
  530. $nextToken = $tokens[$tokens->getNextMeaningfulToken($k)];
  531. // element is data provider method
  532. return 'method' === $v['type'] && $nextToken->equals([T_STRING, $dataProviderName]);
  533. }, ARRAY_FILTER_USE_BOTH);
  534. if (1 !== \count($dataProviderElements)) {
  535. throw new \UnexpectedValueException(\sprintf('Data provider "%s::%s" should be found exactly once, got %d times.', $testClassName, $dataProviderName, \count($dataProviderElements)));
  536. }
  537. $methodIndex = array_key_first($dataProviderElements);
  538. $startIndex = $tokens->getNextTokenOfKind($methodIndex, ['{']);
  539. $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startIndex);
  540. $versionTokens = array_filter($tokens->findGivenKind(T_STRING, $startIndex, $endIndex), static fn (Token $v): bool => $v->equalsAny([
  541. [T_STRING, 'PHP_VERSION_ID'],
  542. [T_STRING, 'PHP_MAJOR_VERSION'],
  543. [T_STRING, 'PHP_MINOR_VERSION'],
  544. [T_STRING, 'PHP_RELEASE_VERSION'],
  545. [T_STRING, 'phpversion'],
  546. ], false));
  547. self::assertCount(
  548. 0,
  549. $versionTokens,
  550. \sprintf(
  551. 'Data provider "%s::%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".',
  552. $testClassName,
  553. $dataProviderName,
  554. ),
  555. );
  556. }
  557. /**
  558. * @dataProvider provideDataProviderMethodCases
  559. */
  560. public function testDataProvidersDeclaredReturnType(string $testClassName, string $dataProviderName): void
  561. {
  562. $dataProvider = new \ReflectionMethod($testClassName, $dataProviderName);
  563. self::assertSame('iterable', $dataProvider->hasReturnType() && $dataProvider->getReturnType() instanceof \ReflectionNamedType ? $dataProvider->getReturnType()->getName() : null, \sprintf('Data provider "%s::%s" must provide `iterable` as return in method prototype.', $testClassName, $dataProviderName));
  564. $doc = new DocBlock(false !== $dataProvider->getDocComment() ? $dataProvider->getDocComment() : '/** */');
  565. $returnDocs = $doc->getAnnotationsOfType('return');
  566. if (\count($returnDocs) > 1) {
  567. throw new \UnexpectedValueException(\sprintf('Multiple "%s::%s@return" annotations.', $testClassName, $dataProviderName));
  568. }
  569. if (1 !== \count($returnDocs)) {
  570. $this->addToAssertionCount(1); // no @return annotation, all good!
  571. return;
  572. }
  573. $returnDoc = $returnDocs[0];
  574. $types = $returnDoc->getTypes();
  575. self::assertCount(1, $types, \sprintf('Data provider "%s::%s@return" must provide single type.', $testClassName, $dataProviderName));
  576. self::assertMatchesRegularExpression('/^iterable\</', $types[0], \sprintf('Data provider "%s::%s@return" must return iterable.', $testClassName, $dataProviderName));
  577. self::assertMatchesRegularExpression('/^iterable\<(?:(?:int\|)?string, )?array\{/', $types[0], \sprintf('Data provider "%s::%s@return" must return iterable of tuples (eg `iterable<string, array{string, string}>`).', $testClassName, $dataProviderName));
  578. }
  579. /**
  580. * @dataProvider provideSrcClassCases
  581. * @dataProvider provideTestClassCases
  582. *
  583. * @param class-string $className
  584. */
  585. public function testAllCodeContainSingleClassy(string $className): void
  586. {
  587. $headerTypes = [
  588. T_ABSTRACT,
  589. T_AS,
  590. T_COMMENT,
  591. T_DECLARE,
  592. T_DOC_COMMENT,
  593. T_FINAL,
  594. T_LNUMBER,
  595. T_NAMESPACE,
  596. T_NS_SEPARATOR,
  597. T_OPEN_TAG,
  598. T_STRING,
  599. T_USE,
  600. T_WHITESPACE,
  601. ];
  602. if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
  603. $headerTypes[] = T_READONLY;
  604. }
  605. $tokens = $this->createTokensForClass($className);
  606. $classyIndex = null;
  607. self::assertTrue($tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds()), \sprintf('File for "%s" should contains a classy.', $className));
  608. $count = \count($tokens);
  609. for ($index = 1; $index < $count; ++$index) {
  610. if ($tokens[$index]->isClassy()) {
  611. $classyIndex = $index;
  612. break;
  613. }
  614. if (\defined('T_ATTRIBUTE') && $tokens[$index]->isGivenKind(T_ATTRIBUTE)) {
  615. $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
  616. continue;
  617. }
  618. if (!$tokens[$index]->isGivenKind($headerTypes) && !$tokens[$index]->equalsAny([';', '=', '(', ')'])) {
  619. self::fail(\sprintf('File for "%s" should only contains single classy, found "%s" @ %d.', $className, $tokens[$index]->toJson(), $index));
  620. }
  621. }
  622. self::assertNotNull($classyIndex, \sprintf('File for "%s" does not contain a classy.', $className));
  623. $nextTokenOfKind = $tokens->getNextTokenOfKind($classyIndex, ['{']);
  624. if (!\is_int($nextTokenOfKind)) {
  625. throw new \UnexpectedValueException('Classy without {} - braces.');
  626. }
  627. $classyEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nextTokenOfKind);
  628. self::assertNull($tokens->getNextMeaningfulToken($classyEndIndex), \sprintf('File for "%s" should only contains a single classy.', $className));
  629. }
  630. /**
  631. * @dataProvider provideSrcClassCases
  632. *
  633. * @param class-string $className
  634. */
  635. public function testInheritdocIsNotAbused(string $className): void
  636. {
  637. $rc = new \ReflectionClass($className);
  638. $allowedMethods = array_map(
  639. fn (\ReflectionClass $interface): array => $this->getPublicMethodNames($interface),
  640. $rc->getInterfaces()
  641. );
  642. if (\count($allowedMethods) > 0) {
  643. $allowedMethods = array_merge(...array_values($allowedMethods));
  644. }
  645. $parentClass = $rc;
  646. while (false !== $parentClass = $parentClass->getParentClass()) {
  647. foreach ($parentClass->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
  648. $allowedMethods[] = $method->getName();
  649. }
  650. }
  651. $allowedMethods = array_unique($allowedMethods);
  652. $methodsWithInheritdoc = array_filter(
  653. $rc->getMethods(),
  654. static fn (\ReflectionMethod $rm): bool => false !== $rm->getDocComment() && stripos($rm->getDocComment(), '@inheritdoc')
  655. );
  656. $methodsWithInheritdoc = array_map(
  657. static fn (\ReflectionMethod $rm): string => $rm->getName(),
  658. $methodsWithInheritdoc
  659. );
  660. $extraMethods = array_diff($methodsWithInheritdoc, $allowedMethods);
  661. self::assertEmpty(
  662. $extraMethods,
  663. \sprintf(
  664. "Class '%s' should not have methods with '@inheritdoc' in PHPDoc that are not inheriting PHPDoc.\nViolations:\n%s",
  665. $className,
  666. implode("\n", array_map(static fn ($item): string => " * {$item}", $extraMethods))
  667. )
  668. );
  669. }
  670. public function testAllTestsForShortOpenTagAreHandled(): void
  671. {
  672. $testClassesWithShortOpenTag = array_filter(
  673. self::getTestClasses(),
  674. fn (string $className): bool => str_contains($this->getFileContentForClass($className), 'short_open_tag') && self::class !== $className
  675. );
  676. $testFilesWithShortOpenTag = array_map(
  677. fn (string $className): string => './'.$this->getFilePathForClass($className),
  678. $testClassesWithShortOpenTag
  679. );
  680. $phpunitXmlContent = file_get_contents(__DIR__.'/../../phpunit.xml.dist');
  681. $phpunitFiles = (array) simplexml_load_string($phpunitXmlContent)->xpath('testsuites/testsuite[@name="short-open-tag"]')[0]->file;
  682. sort($testFilesWithShortOpenTag);
  683. sort($phpunitFiles);
  684. self::assertSame($testFilesWithShortOpenTag, $phpunitFiles);
  685. }
  686. /**
  687. * @dataProvider provideTestClassCases
  688. *
  689. * @param class-string $className
  690. */
  691. public function testThatTestMethodsAreNotDuplicated(string $className): void
  692. {
  693. $class = new \ReflectionClass($className);
  694. $alreadyFoundMethods = [];
  695. $duplicates = [];
  696. foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
  697. if (!str_starts_with($method->getName(), 'test')) {
  698. continue;
  699. }
  700. $startLine = (int) $method->getStartLine();
  701. $length = (int) $method->getEndLine() - $startLine;
  702. if (3 === $length) { // open and closing brace are included - this checks for single line methods
  703. continue;
  704. }
  705. /** @var list<string> $source */
  706. $source = file((string) $method->getFileName());
  707. $candidateContent = implode('', \array_slice($source, $startLine, $length));
  708. if (str_contains($candidateContent, '$this->doTest(')) {
  709. continue;
  710. }
  711. $foundInDuplicates = false;
  712. foreach ($alreadyFoundMethods as $methodKey => $methodContent) {
  713. if ($candidateContent === $methodContent) {
  714. $duplicates[] = \sprintf('%s is duplicate of %s', $methodKey, $method->getName());
  715. $foundInDuplicates = true;
  716. }
  717. }
  718. if (!$foundInDuplicates) {
  719. $alreadyFoundMethods[$method->getName()] = $candidateContent;
  720. }
  721. }
  722. self::assertSame(
  723. [],
  724. $duplicates,
  725. \sprintf(
  726. "Duplicated methods found in %s:\n - %s",
  727. $className,
  728. implode("\n - ", $duplicates)
  729. )
  730. );
  731. }
  732. /**
  733. * @dataProvider provideDataProviderMethodCases
  734. *
  735. * @param class-string<TestCase> $testClassName
  736. */
  737. public function testThatDataFromDataProvidersIsNotDuplicated(string $testClassName, string $dataProviderName): void
  738. {
  739. $exceptions = [ // should only shrink
  740. 'PhpCsFixer\Tests\AutoReview\CommandTest::provideCommandHasNameConstCases',
  741. 'PhpCsFixer\Tests\AutoReview\DocumentationTest::provideFixerDocumentationFileIsUpToDateCases',
  742. 'PhpCsFixer\Tests\AutoReview\FixerFactoryTest::providePriorityIntegrationTestFilesAreListedInPriorityGraphCases',
  743. 'PhpCsFixer\Tests\Console\Command\DescribeCommandTest::provideExecuteOutputCases',
  744. 'PhpCsFixer\Tests\Console\Command\HelpCommandTest::provideGetDisplayableAllowedValuesCases',
  745. 'PhpCsFixer\Tests\Documentation\FixerDocumentGeneratorTest::provideGenerateRuleSetsDocumentationCases',
  746. 'PhpCsFixer\Tests\Fixer\Basic\EncodingFixerTest::provideFixCases',
  747. 'PhpCsFixer\Tests\UtilsTest::provideStableSortCases',
  748. ];
  749. if (\in_array($testClassName.'::'.$dataProviderName, $exceptions, true)) {
  750. $this->addToAssertionCount(1);
  751. return;
  752. }
  753. $dataProvider = new \ReflectionMethod($testClassName, $dataProviderName);
  754. $duplicates = [];
  755. $alreadyFoundCases = [];
  756. foreach ($dataProvider->invoke($dataProvider->getDeclaringClass()->newInstanceWithoutConstructor()) as $candidateKey => $candidateData) {
  757. $candidateData = serialize($candidateData);
  758. $foundInDuplicates = false;
  759. foreach ($alreadyFoundCases as $caseKey => $caseData) {
  760. if ($candidateData === $caseData) {
  761. $duplicates[] = \sprintf(
  762. 'Duplicate in %s::%s: %s and %s.'.PHP_EOL,
  763. $testClassName,
  764. $dataProviderName,
  765. \is_int($caseKey) ? '#'.$caseKey : '"'.$caseKey.'"',
  766. \is_int($candidateKey) ? '#'.$candidateKey : '"'.$candidateKey.'"',
  767. );
  768. $foundInDuplicates = true;
  769. }
  770. }
  771. if (!$foundInDuplicates) {
  772. $alreadyFoundCases[$candidateKey] = $candidateData;
  773. }
  774. }
  775. self::assertSame([], $duplicates);
  776. }
  777. /**
  778. * @return iterable<string, array{string, string}>
  779. */
  780. public static function provideDataProviderMethodCases(): iterable
  781. {
  782. if (null === self::$dataProviderMethodCases) {
  783. self::$dataProviderMethodCases = [];
  784. foreach (self::provideTestClassCases() as $testClassName) {
  785. $testClassName = reset($testClassName);
  786. $reflectionClass = new \ReflectionClass($testClassName);
  787. $dataProviderNames = array_filter(
  788. $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
  789. static fn (\ReflectionMethod $reflectionMethod): bool => $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName() && str_starts_with($reflectionMethod->getName(), 'provide')
  790. );
  791. foreach ($dataProviderNames as $dataProviderName) {
  792. self::$dataProviderMethodCases[$testClassName.'::'.$dataProviderName->getName()] = [$testClassName, $dataProviderName->getName()];
  793. }
  794. }
  795. }
  796. yield from self::$dataProviderMethodCases;
  797. }
  798. /**
  799. * @dataProvider providePhpUnitFixerExtendsAbstractPhpUnitFixerCases
  800. *
  801. * @param class-string $className
  802. */
  803. public function testPhpUnitFixerExtendsAbstractPhpUnitFixer(string $className): void
  804. {
  805. $reflection = new \ReflectionClass($className);
  806. self::assertTrue($reflection->isSubclassOf(AbstractPhpUnitFixer::class));
  807. }
  808. /**
  809. * @return iterable<array{string}>
  810. */
  811. public static function providePhpUnitFixerExtendsAbstractPhpUnitFixerCases(): iterable
  812. {
  813. $factory = new FixerFactory();
  814. $factory->registerBuiltInFixers();
  815. foreach ($factory->getFixers() as $fixer) {
  816. if (!str_starts_with($fixer->getName(), 'php_unit_')) {
  817. continue;
  818. }
  819. // this one fixes usage of PHPUnit classes
  820. if ($fixer instanceof PhpUnitNamespacedFixer) {
  821. continue;
  822. }
  823. if ($fixer instanceof AbstractProxyFixer) {
  824. continue;
  825. }
  826. yield [\get_class($fixer)];
  827. }
  828. }
  829. /**
  830. * @dataProvider provideSrcClassCases
  831. * @dataProvider provideTestClassCases
  832. *
  833. * @param class-string $className
  834. */
  835. public function testConstantsAreInUpperCase(string $className): void
  836. {
  837. $rc = new \ReflectionClass($className);
  838. $reflectionClassConstants = $rc->getReflectionConstants();
  839. if (\count($reflectionClassConstants) < 1) {
  840. $this->expectNotToPerformAssertions();
  841. return;
  842. }
  843. foreach ($reflectionClassConstants as $constant) {
  844. $constantName = $constant->getName();
  845. self::assertSame(strtoupper($constantName), $constantName, $className);
  846. }
  847. }
  848. /**
  849. * @return iterable<string, array{class-string}>
  850. */
  851. public static function provideSrcClassCases(): iterable
  852. {
  853. if (null === self::$srcClassCases) {
  854. $cases = self::getSrcClasses();
  855. self::$srcClassCases = array_combine(
  856. $cases,
  857. array_map(static fn (string $case): array => [$case], $cases),
  858. );
  859. }
  860. yield from self::$srcClassCases;
  861. }
  862. /**
  863. * @return iterable<string, array{class-string<TestCase>}>
  864. */
  865. public static function provideTestClassCases(): iterable
  866. {
  867. if (null === self::$testClassCases) {
  868. $cases = self::getTestClasses();
  869. self::$testClassCases = array_combine(
  870. $cases,
  871. array_map(static fn (string $case): array => [$case], $cases),
  872. );
  873. }
  874. yield from self::$testClassCases;
  875. }
  876. /**
  877. * @param class-string $className
  878. *
  879. * @return list<string>
  880. */
  881. private function extractFunctionNamesCalledInClass(string $className): array
  882. {
  883. $tokens = $this->createTokensForClass($className);
  884. $stringTokens = array_filter(
  885. $tokens->toArray(),
  886. static fn (Token $token): bool => $token->isGivenKind(T_STRING)
  887. );
  888. $strings = array_map(
  889. static fn (Token $token): string => $token->getContent(),
  890. $stringTokens
  891. );
  892. return array_unique($strings);
  893. }
  894. /**
  895. * @param class-string $className
  896. */
  897. private function getFilePathForClass(string $className): string
  898. {
  899. $file = $className;
  900. $file = preg_replace('#^PhpCsFixer\\\Tests\\\#', 'tests\\', $file);
  901. $file = preg_replace('#^PhpCsFixer\\\#', 'src\\', $file);
  902. return str_replace('\\', \DIRECTORY_SEPARATOR, $file).'.php';
  903. }
  904. /**
  905. * @param class-string $className
  906. */
  907. private function getFileContentForClass(string $className): string
  908. {
  909. return file_get_contents($this->getFilePathForClass($className));
  910. }
  911. /**
  912. * @param class-string $className
  913. */
  914. private function createTokensForClass(string $className): Tokens
  915. {
  916. if (!isset(self::$tokensCache[$className])) {
  917. self::$tokensCache[$className] = Tokens::fromCode(self::getFileContentForClass($className));
  918. }
  919. return self::$tokensCache[$className];
  920. }
  921. /**
  922. * @param class-string<TestCase> $testClassName
  923. *
  924. * @return iterable<string, string>
  925. */
  926. private function getUsedDataProviderMethodNames(string $testClassName): iterable
  927. {
  928. foreach ($this->getAnnotationsOfTestClass($testClassName, 'dataProvider') as $methodName => $dataProviderAnnotation) {
  929. if (1 === preg_match('/@dataProvider\s+(?P<methodName>\w+)/', $dataProviderAnnotation->getContent(), $matches)) {
  930. yield $methodName => $matches['methodName'];
  931. }
  932. }
  933. }
  934. /**
  935. * @param class-string<TestCase> $testClassName
  936. *
  937. * @return iterable<string, Annotation>
  938. */
  939. private function getAnnotationsOfTestClass(string $testClassName, string $annotation): iterable
  940. {
  941. $tokens = $this->createTokensForClass($testClassName);
  942. foreach ($tokens as $index => $token) {
  943. if (!$token->isGivenKind(T_DOC_COMMENT)) {
  944. continue;
  945. }
  946. $methodName = $tokens[$tokens->getNextTokenOfKind($index, [[T_STRING]])]->getContent();
  947. $docBlock = new DocBlock($token->getContent());
  948. $dataProviderAnnotations = $docBlock->getAnnotationsOfType($annotation);
  949. foreach ($dataProviderAnnotations as $dataProviderAnnotation) {
  950. yield $methodName => $dataProviderAnnotation;
  951. }
  952. }
  953. }
  954. /**
  955. * @return list<class-string>
  956. */
  957. private static function getSrcClasses(): array
  958. {
  959. static $classes;
  960. if (null !== $classes) {
  961. return $classes;
  962. }
  963. $finder = Finder::create()
  964. ->files()
  965. ->name('*.php')
  966. ->in(__DIR__.'/../../src')
  967. ->exclude([
  968. 'Resources',
  969. ])
  970. ;
  971. /** @var list<class-string> $classes */
  972. $classes = array_map(
  973. static fn (SplFileInfo $file): string => \sprintf(
  974. '%s\%s%s%s',
  975. 'PhpCsFixer',
  976. strtr($file->getRelativePath(), \DIRECTORY_SEPARATOR, '\\'),
  977. '' !== $file->getRelativePath() ? '\\' : '',
  978. $file->getBasename('.'.$file->getExtension())
  979. ),
  980. iterator_to_array($finder, false)
  981. );
  982. sort($classes);
  983. return $classes;
  984. }
  985. /**
  986. * @return list<class-string<TestCase>>
  987. */
  988. private static function getTestClasses(): array
  989. {
  990. static $classes;
  991. if (null !== $classes) {
  992. return $classes;
  993. }
  994. $finder = Finder::create()
  995. ->files()
  996. ->name('*Test.php')
  997. ->in(__DIR__.'/..')
  998. ->exclude([
  999. 'Fixtures',
  1000. ])
  1001. ;
  1002. /** @var list<class-string<TestCase>> $classes */
  1003. $classes = array_map(
  1004. static fn (SplFileInfo $file): string => \sprintf(
  1005. 'PhpCsFixer\Tests\%s%s%s',
  1006. strtr($file->getRelativePath(), \DIRECTORY_SEPARATOR, '\\'),
  1007. '' !== $file->getRelativePath() ? '\\' : '',
  1008. $file->getBasename('.'.$file->getExtension())
  1009. ),
  1010. iterator_to_array($finder, false)
  1011. );
  1012. sort($classes);
  1013. return $classes;
  1014. }
  1015. /**
  1016. * @param \ReflectionClass<object> $rc
  1017. *
  1018. * @return list<string>
  1019. */
  1020. private function getPublicMethodNames(\ReflectionClass $rc): array
  1021. {
  1022. return array_map(
  1023. static fn (\ReflectionMethod $rm): string => $rm->getName(),
  1024. $rc->getMethods(\ReflectionMethod::IS_PUBLIC)
  1025. );
  1026. }
  1027. }