ProjectCodeTest.php 42 KB

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