RuleSetTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  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\RuleSet;
  13. use PhpCsFixer\ConfigurationException\InvalidForEnvFixerConfigurationException;
  14. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  15. use PhpCsFixer\Fixer\DeprecatedFixerInterface;
  16. use PhpCsFixer\Fixer\PhpUnit\PhpUnitTargetVersion;
  17. use PhpCsFixer\FixerConfiguration\DeprecatedFixerOptionInterface;
  18. use PhpCsFixer\FixerFactory;
  19. use PhpCsFixer\RuleSet\RuleSet;
  20. use PhpCsFixer\RuleSet\RuleSets;
  21. use PhpCsFixer\Tests\TestCase;
  22. /**
  23. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  24. *
  25. * @internal
  26. *
  27. * @covers \PhpCsFixer\RuleSet\RuleSet
  28. */
  29. final class RuleSetTest extends TestCase
  30. {
  31. /**
  32. * @param array|bool $ruleConfig
  33. *
  34. * @dataProvider provideAllRulesFromSetsCases
  35. */
  36. public function testIfAllRulesInSetsExists(string $setName, string $ruleName, $ruleConfig): void
  37. {
  38. $factory = new FixerFactory();
  39. $factory->registerBuiltInFixers();
  40. $fixers = [];
  41. foreach ($factory->getFixers() as $fixer) {
  42. $fixers[$fixer->getName()] = $fixer;
  43. }
  44. static::assertArrayHasKey($ruleName, $fixers, sprintf('RuleSet "%s" contains unknown rule.', $setName));
  45. if (true === $ruleConfig) {
  46. return; // rule doesn't need configuration.
  47. }
  48. $fixer = $fixers[$ruleName];
  49. static::assertInstanceOf(ConfigurableFixerInterface::class, $fixer, sprintf('RuleSet "%s" contains configuration for rule "%s" which cannot be configured.', $setName, $ruleName));
  50. try {
  51. $fixer->configure($ruleConfig); // test fixer accepts the configuration
  52. } catch (InvalidForEnvFixerConfigurationException $exception) {
  53. // ignore
  54. }
  55. }
  56. /**
  57. * @param array|bool $ruleConfig
  58. *
  59. * @dataProvider provideAllRulesFromSetsCases
  60. */
  61. public function testThatDefaultConfigIsNotPassed(string $setName, string $ruleName, $ruleConfig): void
  62. {
  63. $factory = new FixerFactory();
  64. $factory->registerBuiltInFixers();
  65. $factory->useRuleSet(new RuleSet([$ruleName => true]));
  66. $fixer = current($factory->getFixers());
  67. if (!$fixer instanceof ConfigurableFixerInterface || \is_bool($ruleConfig)) {
  68. $this->addToAssertionCount(1);
  69. return;
  70. }
  71. $defaultConfig = [];
  72. foreach ($fixer->getConfigurationDefinition()->getOptions() as $option) {
  73. if ($option instanceof DeprecatedFixerOptionInterface) {
  74. continue;
  75. }
  76. $defaultConfig[$option->getName()] = $option->getDefault();
  77. }
  78. static::assertNotSame(
  79. $this->sortNestedArray($defaultConfig),
  80. $this->sortNestedArray($ruleConfig),
  81. sprintf('Rule "%s" (in RuleSet "%s") has default config passed.', $ruleName, $setName)
  82. );
  83. }
  84. /**
  85. * @dataProvider provideAllRulesFromSetsCases
  86. */
  87. public function testThatThereIsNoDeprecatedFixerInRuleSet(string $setName, string $ruleName): void
  88. {
  89. $factory = new FixerFactory();
  90. $factory->registerBuiltInFixers();
  91. $factory->useRuleSet(new RuleSet([$ruleName => true]));
  92. $fixer = current($factory->getFixers());
  93. static::assertNotInstanceOf(DeprecatedFixerInterface::class, $fixer, sprintf('RuleSet "%s" contains deprecated rule "%s".', $setName, $ruleName));
  94. }
  95. public function provideAllRulesFromSetsCases(): \Generator
  96. {
  97. foreach (RuleSets::getSetDefinitionNames() as $setName) {
  98. $ruleSet = new RuleSet([$setName => true]);
  99. foreach ($ruleSet->getRules() as $rule => $config) {
  100. yield $setName.':'.$rule => [
  101. $setName,
  102. $rule,
  103. $config,
  104. ];
  105. }
  106. }
  107. }
  108. public function testGetBuildInSetDefinitionNames(): void
  109. {
  110. $setNames = RuleSets::getSetDefinitionNames();
  111. static::assertIsArray($setNames);
  112. static::assertNotEmpty($setNames);
  113. }
  114. public function testResolveRulesWithInvalidSet(): void
  115. {
  116. $this->expectException(\InvalidArgumentException::class);
  117. $this->expectExceptionMessage('Set "@foo" does not exist.');
  118. new RuleSet(['@foo' => true]);
  119. }
  120. public function testResolveRulesWithMissingRuleValue(): void
  121. {
  122. $this->expectException(\InvalidArgumentException::class);
  123. $this->expectExceptionMessage('Missing value for "braces" rule/set.');
  124. new RuleSet(['braces']);
  125. }
  126. public function testResolveRulesWithSet(): void
  127. {
  128. $ruleSet = new RuleSet([
  129. '@PSR1' => true,
  130. 'braces' => true,
  131. 'encoding' => false,
  132. 'line_ending' => true,
  133. 'strict_comparison' => true,
  134. ]);
  135. static::assertSameRules(
  136. [
  137. 'braces' => true,
  138. 'full_opening_tag' => true,
  139. 'line_ending' => true,
  140. 'strict_comparison' => true,
  141. ],
  142. $ruleSet->getRules()
  143. );
  144. }
  145. public function testResolveRulesWithNestedSet(): void
  146. {
  147. $ruleSet = new RuleSet([
  148. '@PSR2' => true,
  149. 'strict_comparison' => true,
  150. ]);
  151. static::assertSameRules(
  152. [
  153. 'blank_line_after_namespace' => true,
  154. 'braces' => true,
  155. 'class_definition' => true,
  156. 'constant_case' => true,
  157. 'elseif' => true,
  158. 'encoding' => true,
  159. 'full_opening_tag' => true,
  160. 'function_declaration' => true,
  161. 'indentation_type' => true,
  162. 'line_ending' => true,
  163. 'lowercase_keywords' => true,
  164. 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
  165. 'no_break_comment' => true,
  166. 'no_closing_tag' => true,
  167. 'no_space_around_double_colon' => true,
  168. 'no_spaces_after_function_name' => true,
  169. 'no_spaces_inside_parenthesis' => true,
  170. 'no_trailing_whitespace' => true,
  171. 'no_trailing_whitespace_in_comment' => true,
  172. 'single_blank_line_at_eof' => true,
  173. 'single_class_element_per_statement' => ['elements' => ['property']],
  174. 'single_import_per_statement' => true,
  175. 'single_line_after_imports' => true,
  176. 'strict_comparison' => true,
  177. 'switch_case_semicolon_to_colon' => true,
  178. 'switch_case_space' => true,
  179. 'visibility_required' => ['elements' => ['method', 'property']],
  180. ],
  181. $ruleSet->getRules()
  182. );
  183. }
  184. public function testResolveRulesWithDisabledSet(): void
  185. {
  186. $ruleSet = new RuleSet([
  187. '@PSR2' => true,
  188. '@PSR1' => false,
  189. 'encoding' => true,
  190. ]);
  191. static::assertSameRules(
  192. [
  193. 'blank_line_after_namespace' => true,
  194. 'braces' => true,
  195. 'constant_case' => true,
  196. 'class_definition' => true,
  197. 'elseif' => true,
  198. 'encoding' => true,
  199. 'function_declaration' => true,
  200. 'indentation_type' => true,
  201. 'line_ending' => true,
  202. 'lowercase_keywords' => true,
  203. 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
  204. 'no_break_comment' => true,
  205. 'no_closing_tag' => true,
  206. 'no_spaces_after_function_name' => true,
  207. 'no_space_around_double_colon' => true,
  208. 'no_spaces_inside_parenthesis' => true,
  209. 'no_trailing_whitespace' => true,
  210. 'no_trailing_whitespace_in_comment' => true,
  211. 'single_blank_line_at_eof' => true,
  212. 'single_class_element_per_statement' => ['elements' => ['property']],
  213. 'single_import_per_statement' => true,
  214. 'single_line_after_imports' => true,
  215. 'switch_case_semicolon_to_colon' => true,
  216. 'switch_case_space' => true,
  217. 'visibility_required' => ['elements' => ['method', 'property']],
  218. ],
  219. $ruleSet->getRules()
  220. );
  221. }
  222. /**
  223. * @dataProvider provideSafeSetCases
  224. */
  225. public function testRiskyRulesInSet(array $set, bool $safe): void
  226. {
  227. try {
  228. $fixers = (new FixerFactory())
  229. ->registerBuiltInFixers()
  230. ->useRuleSet(new RuleSet($set))
  231. ->getFixers()
  232. ;
  233. } catch (InvalidForEnvFixerConfigurationException $exception) {
  234. static::markTestSkipped($exception->getMessage());
  235. }
  236. $fixerNames = [];
  237. foreach ($fixers as $fixer) {
  238. if ($safe === $fixer->isRisky()) {
  239. $fixerNames[] = $fixer->getName();
  240. }
  241. }
  242. static::assertCount(
  243. 0,
  244. $fixerNames,
  245. sprintf(
  246. 'Set should only contain %s fixers, got: \'%s\'.',
  247. $safe ? 'safe' : 'risky',
  248. implode('\', \'', $fixerNames)
  249. )
  250. );
  251. }
  252. public function provideSafeSetCases(): \Generator
  253. {
  254. foreach (RuleSets::getSetDefinitionNames() as $name) {
  255. yield $name => [
  256. [$name => true],
  257. !str_contains($name, ':risky'),
  258. ];
  259. }
  260. yield '@Symfony:risky_and_@Symfony' => [
  261. [
  262. '@Symfony:risky' => true,
  263. '@Symfony' => false,
  264. ],
  265. false,
  266. ];
  267. }
  268. public function testInvalidConfigNestedSets(): void
  269. {
  270. $this->expectException(\UnexpectedValueException::class);
  271. $this->expectExceptionMessageMatches('#^Nested rule set "@PSR1" configuration must be a boolean\.$#');
  272. new RuleSet(
  273. ['@PSR1' => ['@PSR2' => 'no']]
  274. );
  275. }
  276. public function testGetMissingRuleConfiguration(): void
  277. {
  278. $ruleSet = new RuleSet();
  279. $this->expectException(\InvalidArgumentException::class);
  280. $this->expectExceptionMessageMatches('#^Rule "_not_exists" is not in the set\.$#');
  281. $ruleSet->getRuleConfiguration('_not_exists');
  282. }
  283. public function testDuplicateRuleConfigurationInSetDefinitions(): void
  284. {
  285. $resolvedSets = [];
  286. $setDefinitions = RuleSets::getSetDefinitions();
  287. foreach ($setDefinitions as $setName => $setDefinition) {
  288. $resolvedSets[$setName] = ['rules' => [], 'sets' => []];
  289. foreach ($setDefinition->getRules() as $name => $value) {
  290. if (str_starts_with($name, '@')) {
  291. $resolvedSets[$setName]['sets'][$name] = $this->expendSet($setDefinitions, $resolvedSets, $name, $value);
  292. } else {
  293. $resolvedSets[$setName]['rules'][$name] = $value;
  294. }
  295. }
  296. }
  297. $duplicates = [];
  298. foreach ($resolvedSets as $name => $resolvedSet) {
  299. foreach ($resolvedSet['rules'] as $ruleName => $config) {
  300. if (\count($resolvedSet['sets']) < 1) {
  301. continue;
  302. }
  303. $setDuplicates = $this->findInSets($resolvedSet['sets'], $ruleName, $config);
  304. if (\count($setDuplicates) > 0) {
  305. if (!isset($duplicates[$name])) {
  306. $duplicates[$name] = [];
  307. }
  308. $duplicates[$name][$ruleName] = $setDuplicates;
  309. }
  310. }
  311. }
  312. if (\count($duplicates) > 0) {
  313. $message = '';
  314. foreach ($duplicates as $setName => $r) {
  315. $message .= sprintf("\n\"%s\" defines rules the same as it extends from:", $setName);
  316. foreach ($duplicates[$setName] as $ruleName => $otherSets) {
  317. $message .= sprintf("\n- \"%s\" is also in \"%s\"", $ruleName, implode(', ', $otherSets));
  318. }
  319. }
  320. static::fail($message);
  321. } else {
  322. $this->addToAssertionCount(1);
  323. }
  324. }
  325. /**
  326. * @dataProvider providePhpUnitTargetVersionHasSetCases
  327. */
  328. public function testPhpUnitTargetVersionHasSet(string $version): void
  329. {
  330. static::assertContains(
  331. sprintf('@PHPUnit%sMigration:risky', str_replace('.', '', $version)),
  332. RuleSets::getSetDefinitionNames(),
  333. sprintf('PHPUnit target version %s is missing its set in %s.', $version, RuleSet::class)
  334. );
  335. }
  336. public static function providePhpUnitTargetVersionHasSetCases(): \Generator
  337. {
  338. foreach ((new \ReflectionClass(PhpUnitTargetVersion::class))->getConstants() as $constant) {
  339. if ('newest' === $constant) {
  340. continue;
  341. }
  342. yield [$constant];
  343. }
  344. }
  345. public function testEmptyName(): void
  346. {
  347. $this->expectException(\InvalidArgumentException::class);
  348. $this->expectExceptionMessage('Rule/set name must not be empty.');
  349. new RuleSet(['' => 'foo']);
  350. }
  351. public function testInvalidConfig(): void
  352. {
  353. $this->expectException(\InvalidArgumentException::class);
  354. $this->expectExceptionMessage('[@Symfony:risky] Set must be enabled (true) or disabled (false). Other values are not allowed. To disable the set, use "FALSE" instead of "NULL".');
  355. new RuleSet(['@Symfony:risky' => null]);
  356. }
  357. private function sortNestedArray(array $array): array
  358. {
  359. foreach ($array as $key => $element) {
  360. if (!\is_array($element)) {
  361. continue;
  362. }
  363. $array[$key] = $this->sortNestedArray($element);
  364. }
  365. // sort by key if associative, by values otherwise
  366. if (array_keys($array) === range(0, \count($array) - 1)) {
  367. sort($array);
  368. } else {
  369. ksort($array);
  370. }
  371. return $array;
  372. }
  373. private function findInSets(array $sets, string $ruleName, $config): array
  374. {
  375. $duplicates = [];
  376. foreach ($sets as $setName => $setRules) {
  377. if (\array_key_exists($ruleName, $setRules['rules'])) {
  378. if ($config === $setRules['rules'][$ruleName]) {
  379. $duplicates[] = $setName;
  380. }
  381. break; // do not check below, config for the rule has been changed
  382. }
  383. if (isset($setRules['sets']) && \count($setRules['sets']) > 0) {
  384. $subSetDuplicates = $this->findInSets($setRules['sets'], $ruleName, $config);
  385. if (\count($subSetDuplicates) > 0) {
  386. $duplicates = array_merge($duplicates, $subSetDuplicates);
  387. }
  388. }
  389. }
  390. return $duplicates;
  391. }
  392. /**
  393. * @param array|bool $setValue
  394. *
  395. * @return mixed
  396. */
  397. private function expendSet(array $setDefinitions, array $resolvedSets, string $setName, $setValue)
  398. {
  399. $rules = $setDefinitions[$setName]->getRules();
  400. foreach ($rules as $name => $value) {
  401. if (str_starts_with($name, '@')) {
  402. $resolvedSets[$setName]['sets'][$name] = $this->expendSet($setDefinitions, $resolvedSets, $name, $setValue);
  403. } elseif (false === $setValue) {
  404. $resolvedSets[$setName]['rules'][$name] = false;
  405. } else {
  406. $resolvedSets[$setName]['rules'][$name] = $value;
  407. }
  408. }
  409. return $resolvedSets[$setName];
  410. }
  411. private static function assertSameRules(array $expected, array $actual): void
  412. {
  413. ksort($expected);
  414. ksort($actual);
  415. static::assertSame($expected, $actual);
  416. }
  417. }