RuleSetTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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->expectNotToPerformAssertions();
  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::assertNotEmpty($setNames);
  112. }
  113. public function testResolveRulesWithInvalidSet(): void
  114. {
  115. $this->expectException(\InvalidArgumentException::class);
  116. $this->expectExceptionMessage('Set "@foo" does not exist.');
  117. new RuleSet(['@foo' => true]);
  118. }
  119. public function testResolveRulesWithMissingRuleValue(): void
  120. {
  121. $this->expectException(\InvalidArgumentException::class);
  122. $this->expectExceptionMessage('Missing value for "braces" rule/set.');
  123. new RuleSet(['braces']);
  124. }
  125. public function testResolveRulesWithSet(): void
  126. {
  127. $ruleSet = new RuleSet([
  128. '@PSR1' => true,
  129. 'braces' => true,
  130. 'encoding' => false,
  131. 'line_ending' => true,
  132. 'strict_comparison' => true,
  133. ]);
  134. static::assertSameRules(
  135. [
  136. 'braces' => true,
  137. 'full_opening_tag' => true,
  138. 'line_ending' => true,
  139. 'strict_comparison' => true,
  140. ],
  141. $ruleSet->getRules()
  142. );
  143. }
  144. public function testResolveRulesWithNestedSet(): void
  145. {
  146. $ruleSet = new RuleSet([
  147. '@PSR2' => true,
  148. 'strict_comparison' => true,
  149. ]);
  150. static::assertSameRules(
  151. [
  152. 'blank_line_after_namespace' => true,
  153. 'braces' => true,
  154. 'class_definition' => true,
  155. 'constant_case' => true,
  156. 'elseif' => true,
  157. 'encoding' => true,
  158. 'full_opening_tag' => true,
  159. 'function_declaration' => true,
  160. 'indentation_type' => true,
  161. 'line_ending' => true,
  162. 'lowercase_keywords' => true,
  163. 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
  164. 'no_break_comment' => true,
  165. 'no_closing_tag' => true,
  166. 'no_space_around_double_colon' => true,
  167. 'no_spaces_after_function_name' => true,
  168. 'no_spaces_inside_parenthesis' => true,
  169. 'no_trailing_whitespace' => true,
  170. 'no_trailing_whitespace_in_comment' => true,
  171. 'single_blank_line_at_eof' => true,
  172. 'single_class_element_per_statement' => ['elements' => ['property']],
  173. 'single_import_per_statement' => true,
  174. 'single_line_after_imports' => true,
  175. 'strict_comparison' => true,
  176. 'switch_case_semicolon_to_colon' => true,
  177. 'switch_case_space' => true,
  178. 'visibility_required' => ['elements' => ['method', 'property']],
  179. ],
  180. $ruleSet->getRules()
  181. );
  182. }
  183. public function testResolveRulesWithDisabledSet(): void
  184. {
  185. $ruleSet = new RuleSet([
  186. '@PSR2' => true,
  187. '@PSR1' => false,
  188. 'encoding' => true,
  189. ]);
  190. static::assertSameRules(
  191. [
  192. 'blank_line_after_namespace' => true,
  193. 'braces' => true,
  194. 'constant_case' => true,
  195. 'class_definition' => true,
  196. 'elseif' => true,
  197. 'encoding' => true,
  198. 'function_declaration' => true,
  199. 'indentation_type' => true,
  200. 'line_ending' => true,
  201. 'lowercase_keywords' => true,
  202. 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
  203. 'no_break_comment' => true,
  204. 'no_closing_tag' => true,
  205. 'no_spaces_after_function_name' => true,
  206. 'no_space_around_double_colon' => true,
  207. 'no_spaces_inside_parenthesis' => true,
  208. 'no_trailing_whitespace' => true,
  209. 'no_trailing_whitespace_in_comment' => true,
  210. 'single_blank_line_at_eof' => true,
  211. 'single_class_element_per_statement' => ['elements' => ['property']],
  212. 'single_import_per_statement' => true,
  213. 'single_line_after_imports' => true,
  214. 'switch_case_semicolon_to_colon' => true,
  215. 'switch_case_space' => true,
  216. 'visibility_required' => ['elements' => ['method', 'property']],
  217. ],
  218. $ruleSet->getRules()
  219. );
  220. }
  221. /**
  222. * @dataProvider provideSafeSetCases
  223. */
  224. public function testRiskyRulesInSet(array $set, bool $safe): void
  225. {
  226. try {
  227. $fixers = (new FixerFactory())
  228. ->registerBuiltInFixers()
  229. ->useRuleSet(new RuleSet($set))
  230. ->getFixers()
  231. ;
  232. } catch (InvalidForEnvFixerConfigurationException $exception) {
  233. static::markTestSkipped($exception->getMessage());
  234. }
  235. $fixerNames = [];
  236. foreach ($fixers as $fixer) {
  237. if ($safe === $fixer->isRisky()) {
  238. $fixerNames[] = $fixer->getName();
  239. }
  240. }
  241. static::assertCount(
  242. 0,
  243. $fixerNames,
  244. sprintf(
  245. 'Set should only contain %s fixers, got: \'%s\'.',
  246. $safe ? 'safe' : 'risky',
  247. implode('\', \'', $fixerNames)
  248. )
  249. );
  250. }
  251. public function provideSafeSetCases(): \Generator
  252. {
  253. foreach (RuleSets::getSetDefinitionNames() as $name) {
  254. yield $name => [
  255. [$name => true],
  256. !str_contains($name, ':risky'),
  257. ];
  258. }
  259. yield '@Symfony:risky_and_@Symfony' => [
  260. [
  261. '@Symfony:risky' => true,
  262. '@Symfony' => false,
  263. ],
  264. false,
  265. ];
  266. }
  267. public function testInvalidConfigNestedSets(): void
  268. {
  269. $this->expectException(\UnexpectedValueException::class);
  270. $this->expectExceptionMessageMatches('#^Nested rule set "@PSR1" configuration must be a boolean\.$#');
  271. new RuleSet(
  272. ['@PSR1' => ['@PSR2' => 'no']]
  273. );
  274. }
  275. public function testGetMissingRuleConfiguration(): void
  276. {
  277. $ruleSet = new RuleSet();
  278. $this->expectException(\InvalidArgumentException::class);
  279. $this->expectExceptionMessageMatches('#^Rule "_not_exists" is not in the set\.$#');
  280. $ruleSet->getRuleConfiguration('_not_exists');
  281. }
  282. public function testDuplicateRuleConfigurationInSetDefinitions(): void
  283. {
  284. $resolvedSets = [];
  285. $setDefinitions = RuleSets::getSetDefinitions();
  286. foreach ($setDefinitions as $setName => $setDefinition) {
  287. $resolvedSets[$setName] = ['rules' => [], 'sets' => []];
  288. foreach ($setDefinition->getRules() as $name => $value) {
  289. if (str_starts_with($name, '@')) {
  290. $resolvedSets[$setName]['sets'][$name] = $this->expendSet($setDefinitions, $resolvedSets, $name, $value);
  291. } else {
  292. $resolvedSets[$setName]['rules'][$name] = $value;
  293. }
  294. }
  295. }
  296. $duplicates = [];
  297. foreach ($resolvedSets as $name => $resolvedSet) {
  298. foreach ($resolvedSet['rules'] as $ruleName => $config) {
  299. if (\count($resolvedSet['sets']) < 1) {
  300. continue;
  301. }
  302. $setDuplicates = $this->findInSets($resolvedSet['sets'], $ruleName, $config);
  303. if (\count($setDuplicates) > 0) {
  304. if (!isset($duplicates[$name])) {
  305. $duplicates[$name] = [];
  306. }
  307. $duplicates[$name][$ruleName] = $setDuplicates;
  308. }
  309. }
  310. }
  311. if (\count($duplicates) > 0) {
  312. $message = '';
  313. foreach ($duplicates as $setName => $rules) {
  314. $message .= sprintf("\n\"%s\" defines rules the same as it extends from:", $setName);
  315. foreach ($rules as $ruleName => $otherSets) {
  316. $message .= sprintf("\n- \"%s\" is also in \"%s\"", $ruleName, implode(', ', $otherSets));
  317. }
  318. }
  319. static::fail($message);
  320. } else {
  321. $this->addToAssertionCount(1);
  322. }
  323. }
  324. /**
  325. * @dataProvider providePhpUnitTargetVersionHasSetCases
  326. */
  327. public function testPhpUnitTargetVersionHasSet(string $version): void
  328. {
  329. static::assertContains(
  330. sprintf('@PHPUnit%sMigration:risky', str_replace('.', '', $version)),
  331. RuleSets::getSetDefinitionNames(),
  332. sprintf('PHPUnit target version %s is missing its set in %s.', $version, RuleSet::class)
  333. );
  334. }
  335. public static function providePhpUnitTargetVersionHasSetCases(): \Generator
  336. {
  337. foreach ((new \ReflectionClass(PhpUnitTargetVersion::class))->getConstants() as $constant) {
  338. if ('newest' === $constant) {
  339. continue;
  340. }
  341. yield [$constant];
  342. }
  343. }
  344. public function testEmptyName(): void
  345. {
  346. $this->expectException(\InvalidArgumentException::class);
  347. $this->expectExceptionMessage('Rule/set name must not be empty.');
  348. new RuleSet(['' => 'foo']);
  349. }
  350. public function testInvalidConfig(): void
  351. {
  352. $this->expectException(\InvalidArgumentException::class);
  353. $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".');
  354. new RuleSet(['@Symfony:risky' => null]);
  355. }
  356. private function sortNestedArray(array $array): array
  357. {
  358. foreach ($array as $key => $element) {
  359. if (!\is_array($element)) {
  360. continue;
  361. }
  362. $array[$key] = $this->sortNestedArray($element);
  363. }
  364. // sort by key if associative, by values otherwise
  365. if (array_keys($array) === range(0, \count($array) - 1)) {
  366. sort($array);
  367. } else {
  368. ksort($array);
  369. }
  370. return $array;
  371. }
  372. private function findInSets(array $sets, string $ruleName, $config): array
  373. {
  374. $duplicates = [];
  375. foreach ($sets as $setName => $setRules) {
  376. if (\array_key_exists($ruleName, $setRules['rules'])) {
  377. if ($config === $setRules['rules'][$ruleName]) {
  378. $duplicates[] = $setName;
  379. }
  380. break; // do not check below, config for the rule has been changed
  381. }
  382. if (isset($setRules['sets']) && \count($setRules['sets']) > 0) {
  383. $subSetDuplicates = $this->findInSets($setRules['sets'], $ruleName, $config);
  384. if (\count($subSetDuplicates) > 0) {
  385. $duplicates = array_merge($duplicates, $subSetDuplicates);
  386. }
  387. }
  388. }
  389. return $duplicates;
  390. }
  391. /**
  392. * @param array|bool $setValue
  393. *
  394. * @return mixed
  395. */
  396. private function expendSet(array $setDefinitions, array $resolvedSets, string $setName, $setValue)
  397. {
  398. $rules = $setDefinitions[$setName]->getRules();
  399. foreach ($rules as $name => $value) {
  400. if (str_starts_with($name, '@')) {
  401. $resolvedSets[$setName]['sets'][$name] = $this->expendSet($setDefinitions, $resolvedSets, $name, $setValue);
  402. } elseif (false === $setValue) {
  403. $resolvedSets[$setName]['rules'][$name] = false;
  404. } else {
  405. $resolvedSets[$setName]['rules'][$name] = $value;
  406. }
  407. }
  408. return $resolvedSets[$setName];
  409. }
  410. private static function assertSameRules(array $expected, array $actual): void
  411. {
  412. ksort($expected);
  413. ksort($actual);
  414. static::assertSame($expected, $actual);
  415. }
  416. }