ConfigurationResolverTest.php 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268
  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\Console;
  13. use PhpCsFixer\Cache\NullCacheManager;
  14. use PhpCsFixer\Config;
  15. use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
  16. use PhpCsFixer\Console\Command\FixCommand;
  17. use PhpCsFixer\Console\ConfigurationResolver;
  18. use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
  19. use PhpCsFixer\Finder;
  20. use PhpCsFixer\Linter\LinterInterface;
  21. use PhpCsFixer\Tests\Fixtures\DeprecatedFixer;
  22. use PhpCsFixer\Tests\TestCase;
  23. use PhpCsFixer\ToolInfo;
  24. use Symfony\Component\Console\Output\OutputInterface;
  25. /**
  26. * @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
  27. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  28. *
  29. * @internal
  30. *
  31. * @covers \PhpCsFixer\Console\ConfigurationResolver
  32. */
  33. final class ConfigurationResolverTest extends TestCase
  34. {
  35. public function testSetOptionWithUndefinedOption(): void
  36. {
  37. $this->expectException(InvalidConfigurationException::class);
  38. $this->expectExceptionMessageMatches('/^Unknown option name: "foo"\.$/');
  39. $this->createConfigurationResolver(['foo' => 'bar']);
  40. }
  41. public function testResolveProgressWithPositiveConfigAndPositiveOption(): void
  42. {
  43. $config = new Config();
  44. $config->setHideProgress(true);
  45. $resolver = $this->createConfigurationResolver([
  46. 'format' => 'txt',
  47. 'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
  48. ], $config);
  49. self::assertSame('none', $resolver->getProgressType());
  50. }
  51. public function testResolveProgressWithPositiveConfigAndNegativeOption(): void
  52. {
  53. $config = new Config();
  54. $config->setHideProgress(true);
  55. $resolver = $this->createConfigurationResolver([
  56. 'format' => 'txt',
  57. 'verbosity' => OutputInterface::VERBOSITY_NORMAL,
  58. ], $config);
  59. self::assertSame('none', $resolver->getProgressType());
  60. }
  61. public function testResolveProgressWithNegativeConfigAndPositiveOption(): void
  62. {
  63. $config = new Config();
  64. $config->setHideProgress(false);
  65. $resolver = $this->createConfigurationResolver([
  66. 'format' => 'txt',
  67. 'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
  68. ], $config);
  69. self::assertSame('dots', $resolver->getProgressType());
  70. }
  71. public function testResolveProgressWithNegativeConfigAndNegativeOption(): void
  72. {
  73. $config = new Config();
  74. $config->setHideProgress(false);
  75. $resolver = $this->createConfigurationResolver([
  76. 'format' => 'txt',
  77. 'verbosity' => OutputInterface::VERBOSITY_NORMAL,
  78. ], $config);
  79. self::assertSame('none', $resolver->getProgressType());
  80. }
  81. /**
  82. * @dataProvider provideProgressTypeCases
  83. */
  84. public function testResolveProgressWithPositiveConfigAndExplicitProgress(string $progressType): void
  85. {
  86. $config = new Config();
  87. $config->setHideProgress(true);
  88. $resolver = $this->createConfigurationResolver([
  89. 'format' => 'txt',
  90. 'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
  91. 'show-progress' => $progressType,
  92. ], $config);
  93. self::assertSame($progressType, $resolver->getProgressType());
  94. }
  95. /**
  96. * @dataProvider provideProgressTypeCases
  97. */
  98. public function testResolveProgressWithNegativeConfigAndExplicitProgress(string $progressType): void
  99. {
  100. $config = new Config();
  101. $config->setHideProgress(false);
  102. $resolver = $this->createConfigurationResolver([
  103. 'format' => 'txt',
  104. 'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
  105. 'show-progress' => $progressType,
  106. ], $config);
  107. self::assertSame($progressType, $resolver->getProgressType());
  108. }
  109. public static function provideProgressTypeCases(): iterable
  110. {
  111. foreach (ProgressOutputType::AVAILABLE as $outputType) {
  112. yield $outputType => [$outputType];
  113. }
  114. }
  115. public function testResolveProgressWithInvalidExplicitProgress(): void
  116. {
  117. $resolver = $this->createConfigurationResolver([
  118. 'format' => 'txt',
  119. 'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
  120. 'show-progress' => 'foo',
  121. ]);
  122. $this->expectException(InvalidConfigurationException::class);
  123. $this->expectExceptionMessage('The progress type "foo" is not defined, supported are "none" and "dots".');
  124. $resolver->getProgressType();
  125. }
  126. public function testResolveConfigFileDefault(): void
  127. {
  128. $resolver = $this->createConfigurationResolver([]);
  129. self::assertNull($resolver->getConfigFile());
  130. self::assertInstanceOf(\PhpCsFixer\ConfigInterface::class, $resolver->getConfig());
  131. }
  132. public function testResolveConfigFileByPathOfFile(): void
  133. {
  134. $dir = __DIR__.'/../Fixtures/ConfigurationResolverConfigFile/case_1';
  135. $resolver = $this->createConfigurationResolver(['path' => [$dir.\DIRECTORY_SEPARATOR.'foo.php']]);
  136. self::assertSame($dir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php', $resolver->getConfigFile());
  137. self::assertInstanceOf(\Test1Config::class, $resolver->getConfig()); // @phpstan-ignore-line to avoid `Class Test1Config not found.`
  138. }
  139. public function testResolveConfigFileSpecified(): void
  140. {
  141. $file = __DIR__.'/../Fixtures/ConfigurationResolverConfigFile/case_4/my.php-cs-fixer.php';
  142. $resolver = $this->createConfigurationResolver(['config' => $file]);
  143. self::assertSame($file, $resolver->getConfigFile());
  144. self::assertInstanceOf(\Test4Config::class, $resolver->getConfig()); // @phpstan-ignore-line to avoid `Class Test4Config not found.`
  145. }
  146. /**
  147. * @dataProvider provideResolveConfigFileChooseFileCases
  148. */
  149. public function testResolveConfigFileChooseFile(string $expectedFile, string $expectedClass, string $path, ?string $cwdPath = null): void
  150. {
  151. $resolver = $this->createConfigurationResolver(
  152. ['path' => [$path]],
  153. null,
  154. $cwdPath ?? ''
  155. );
  156. self::assertSame($expectedFile, $resolver->getConfigFile());
  157. self::assertInstanceOf($expectedClass, $resolver->getConfig());
  158. }
  159. public static function provideResolveConfigFileChooseFileCases(): iterable
  160. {
  161. $dirBase = self::getFixtureDir();
  162. yield [
  163. $dirBase.'case_1'.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
  164. 'Test1Config',
  165. $dirBase.'case_1',
  166. ];
  167. yield [
  168. $dirBase.'case_2'.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
  169. 'Test2Config',
  170. $dirBase.'case_2',
  171. ];
  172. yield [
  173. $dirBase.'case_3'.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
  174. 'Test3Config',
  175. $dirBase.'case_3',
  176. ];
  177. yield [
  178. $dirBase.'case_6'.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
  179. 'Test6Config',
  180. $dirBase.'case_6'.\DIRECTORY_SEPARATOR.'subdir',
  181. $dirBase.'case_6',
  182. ];
  183. yield [
  184. $dirBase.'case_6'.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
  185. 'Test6Config',
  186. $dirBase.'case_6'.\DIRECTORY_SEPARATOR.'subdir/empty_file.php',
  187. $dirBase.'case_6',
  188. ];
  189. }
  190. public function testResolveConfigFileChooseFileWithInvalidFile(): void
  191. {
  192. $this->expectException(InvalidConfigurationException::class);
  193. $this->expectExceptionMessageMatches(
  194. '#^The config file: ".+[\/\\\]Fixtures[\/\\\]ConfigurationResolverConfigFile[\/\\\]case_5[\/\\\]\.php-cs-fixer\.dist\.php" does not return a "PhpCsFixer\\\ConfigInterface" instance\. Got: "string"\.$#'
  195. );
  196. $dirBase = self::getFixtureDir();
  197. $resolver = $this->createConfigurationResolver(['path' => [$dirBase.'case_5']]);
  198. $resolver->getConfig();
  199. }
  200. public function testResolveConfigFileChooseFileWithInvalidFormat(): void
  201. {
  202. $this->expectException(InvalidConfigurationException::class);
  203. $this->expectExceptionMessageMatches('/^The format "xls" is not defined, supported are "checkstyle", "gitlab", "json", "junit", "txt" and "xml"\.$/');
  204. $dirBase = self::getFixtureDir();
  205. $resolver = $this->createConfigurationResolver(['path' => [$dirBase.'case_7']]);
  206. $resolver->getReporter();
  207. }
  208. public function testResolveConfigFileChooseFileWithPathArrayWithoutConfig(): void
  209. {
  210. $this->expectException(InvalidConfigurationException::class);
  211. $this->expectExceptionMessageMatches('/^For multiple paths config parameter is required\.$/');
  212. $dirBase = self::getFixtureDir();
  213. $resolver = $this->createConfigurationResolver(['path' => [$dirBase.'case_1/.php-cs-fixer.dist.php', $dirBase.'case_1/foo.php']]);
  214. $resolver->getConfig();
  215. }
  216. public function testResolveConfigFileChooseFileWithPathArrayAndConfig(): void
  217. {
  218. $dirBase = self::getFixtureDir();
  219. $configFile = $dirBase.'case_1/.php-cs-fixer.dist.php';
  220. $resolver = $this->createConfigurationResolver([
  221. 'config' => $configFile,
  222. 'path' => [$configFile, $dirBase.'case_1/foo.php'],
  223. ]);
  224. self::assertSame($configFile, $resolver->getConfigFile());
  225. }
  226. /**
  227. * @param array<int, string> $paths
  228. * @param array<int, string> $expectedPaths
  229. *
  230. * @dataProvider provideResolvePathCases
  231. */
  232. public function testResolvePath(array $paths, string $cwd, array $expectedPaths): void
  233. {
  234. $resolver = $this->createConfigurationResolver(
  235. ['path' => $paths],
  236. null,
  237. $cwd
  238. );
  239. self::assertSame($expectedPaths, $resolver->getPath());
  240. }
  241. public static function provideResolvePathCases(): iterable
  242. {
  243. yield [
  244. ['Command'],
  245. __DIR__,
  246. [__DIR__.\DIRECTORY_SEPARATOR.'Command'],
  247. ];
  248. yield [
  249. [basename(__DIR__)],
  250. \dirname(__DIR__),
  251. [__DIR__],
  252. ];
  253. yield [
  254. [' Command'],
  255. __DIR__,
  256. [__DIR__.\DIRECTORY_SEPARATOR.'Command'],
  257. ];
  258. yield [
  259. ['Command '],
  260. __DIR__,
  261. [__DIR__.\DIRECTORY_SEPARATOR.'Command'],
  262. ];
  263. }
  264. /**
  265. * @param array<string> $paths
  266. *
  267. * @dataProvider provideRejectInvalidPathCases
  268. */
  269. public function testRejectInvalidPath(array $paths, string $expectedMessage): void
  270. {
  271. $resolver = $this->createConfigurationResolver(
  272. ['path' => $paths],
  273. null,
  274. \dirname(__DIR__)
  275. );
  276. $this->expectException(InvalidConfigurationException::class);
  277. $this->expectExceptionMessage($expectedMessage);
  278. $resolver->getPath();
  279. }
  280. public static function provideRejectInvalidPathCases(): iterable
  281. {
  282. yield [
  283. [''],
  284. 'Invalid path: "".',
  285. ];
  286. yield [
  287. [__DIR__, ''],
  288. 'Invalid path: "".',
  289. ];
  290. yield [
  291. ['', __DIR__],
  292. 'Invalid path: "".',
  293. ];
  294. yield [
  295. [' '],
  296. 'Invalid path: " ".',
  297. ];
  298. yield [
  299. [__DIR__, ' '],
  300. 'Invalid path: " ".',
  301. ];
  302. yield [
  303. [' ', __DIR__],
  304. 'Invalid path: " ".',
  305. ];
  306. }
  307. public function testResolvePathWithFileThatIsExcludedDirectlyOverridePathMode(): void
  308. {
  309. $config = new Config();
  310. $config->getFinder()
  311. ->in(__DIR__)
  312. ->notPath(basename(__FILE__))
  313. ;
  314. $resolver = $this->createConfigurationResolver(
  315. ['path' => [__FILE__]],
  316. $config
  317. );
  318. self::assertCount(1, $resolver->getFinder());
  319. }
  320. public function testResolvePathWithFileThatIsExcludedDirectlyIntersectionPathMode(): void
  321. {
  322. $config = new Config();
  323. $config->getFinder()
  324. ->in(__DIR__)
  325. ->notPath(basename(__FILE__))
  326. ;
  327. $resolver = $this->createConfigurationResolver([
  328. 'path' => [__FILE__],
  329. 'path-mode' => 'intersection',
  330. ], $config);
  331. self::assertCount(0, $resolver->getFinder());
  332. }
  333. public function testResolvePathWithFileThatIsExcludedByDirOverridePathMode(): void
  334. {
  335. $dir = \dirname(__DIR__);
  336. $config = new Config();
  337. $config->getFinder()
  338. ->in($dir)
  339. ->exclude(basename(__DIR__))
  340. ;
  341. $resolver = $this->createConfigurationResolver(
  342. ['path' => [__FILE__]],
  343. $config
  344. );
  345. self::assertCount(1, $resolver->getFinder());
  346. }
  347. public function testResolvePathWithFileThatIsExcludedByDirIntersectionPathMode(): void
  348. {
  349. $dir = \dirname(__DIR__);
  350. $config = new Config();
  351. $config->getFinder()
  352. ->in($dir)
  353. ->exclude(basename(__DIR__))
  354. ;
  355. $resolver = $this->createConfigurationResolver([
  356. 'path-mode' => 'intersection',
  357. 'path' => [__FILE__],
  358. ], $config);
  359. self::assertCount(0, $resolver->getFinder());
  360. }
  361. public function testResolvePathWithFileThatIsNotExcluded(): void
  362. {
  363. $dir = __DIR__;
  364. $config = new Config();
  365. $config->getFinder()
  366. ->in($dir)
  367. ->notPath('foo-'.basename(__FILE__))
  368. ;
  369. $resolver = $this->createConfigurationResolver(
  370. ['path' => [__FILE__]],
  371. $config
  372. );
  373. self::assertCount(1, $resolver->getFinder());
  374. }
  375. /**
  376. * @param \Exception|list<string> $expected
  377. * @param list<string> $path
  378. *
  379. * @dataProvider provideResolveIntersectionOfPathsCases
  380. */
  381. public function testResolveIntersectionOfPaths($expected, ?Finder $configFinder, array $path, string $pathMode, ?string $configOption = null): void
  382. {
  383. if ($expected instanceof \Exception) {
  384. $this->expectException(\get_class($expected));
  385. }
  386. if (null !== $configFinder) {
  387. $config = new Config();
  388. $config->setFinder($configFinder);
  389. } else {
  390. $config = null;
  391. }
  392. $resolver = $this->createConfigurationResolver([
  393. 'config' => $configOption,
  394. 'path' => $path,
  395. 'path-mode' => $pathMode,
  396. ], $config);
  397. $intersectionItems = array_map(
  398. static fn (\SplFileInfo $file): string => $file->getRealPath(),
  399. iterator_to_array($resolver->getFinder(), false)
  400. );
  401. sort($expected);
  402. sort($intersectionItems);
  403. self::assertSame($expected, $intersectionItems);
  404. }
  405. public static function provideResolveIntersectionOfPathsCases(): iterable
  406. {
  407. $dir = __DIR__.'/../Fixtures/ConfigurationResolverPathsIntersection';
  408. $cb = static fn (array $items): array => array_map(
  409. static fn (string $item): string => realpath($dir.'/'.$item),
  410. $items
  411. );
  412. yield 'no path at all' => [
  413. new \LogicException(),
  414. Finder::create(),
  415. [],
  416. 'override',
  417. ];
  418. yield 'configured only by finder' => [
  419. // don't override if the argument is empty
  420. $cb(['a1.php', 'a2.php', 'b/b1.php', 'b/b2.php', 'b_b/b_b1.php', 'c/c1.php', 'c/d/cd1.php', 'd/d1.php', 'd/d2.php', 'd/e/de1.php', 'd/f/df1.php']),
  421. Finder::create()
  422. ->in($dir),
  423. [],
  424. 'override',
  425. ];
  426. yield 'configured only by argument' => [
  427. $cb(['a1.php', 'a2.php', 'b/b1.php', 'b/b2.php', 'b_b/b_b1.php', 'c/c1.php', 'c/d/cd1.php', 'd/d1.php', 'd/d2.php', 'd/e/de1.php', 'd/f/df1.php']),
  428. Finder::create(),
  429. [$dir],
  430. 'override',
  431. ];
  432. yield 'configured by finder, intersected with empty argument' => [
  433. [],
  434. Finder::create()
  435. ->in($dir),
  436. [],
  437. 'intersection',
  438. ];
  439. yield 'configured by finder, intersected with dir' => [
  440. $cb(['c/c1.php', 'c/d/cd1.php']),
  441. Finder::create()
  442. ->in($dir),
  443. [$dir.'/c'],
  444. 'intersection',
  445. ];
  446. yield 'configured by finder, intersected with file' => [
  447. $cb(['c/c1.php']),
  448. Finder::create()
  449. ->in($dir),
  450. [$dir.'/c/c1.php'],
  451. 'intersection',
  452. ];
  453. yield 'finder points to one dir while argument to another, not connected' => [
  454. [],
  455. Finder::create()
  456. ->in($dir.'/b'),
  457. [$dir.'/c'],
  458. 'intersection',
  459. ];
  460. yield 'finder with excluded dir, intersected with excluded file' => [
  461. [],
  462. Finder::create()
  463. ->in($dir)
  464. ->exclude('c'),
  465. [$dir.'/c/d/cd1.php'],
  466. 'intersection',
  467. ];
  468. yield 'finder with excluded dir, intersected with dir containing excluded one' => [
  469. $cb(['c/c1.php']),
  470. Finder::create()
  471. ->in($dir)
  472. ->exclude('c/d'),
  473. [$dir.'/c'],
  474. 'intersection',
  475. ];
  476. yield 'finder with excluded file, intersected with dir containing excluded one' => [
  477. $cb(['c/d/cd1.php']),
  478. Finder::create()
  479. ->in($dir)
  480. ->notPath('c/c1.php'),
  481. [$dir.'/c'],
  482. 'intersection',
  483. ];
  484. yield 'configured by finder, intersected with non-existing path' => [
  485. new \LogicException(),
  486. Finder::create()
  487. ->in($dir),
  488. ['non_existing_dir'],
  489. 'intersection',
  490. ];
  491. yield 'configured by config file, overridden by multiple files' => [
  492. $cb(['d/d1.php', 'd/d2.php']),
  493. null,
  494. [$dir.'/d/d1.php', $dir.'/d/d2.php'],
  495. 'override',
  496. $dir.'/d/.php-cs-fixer.php',
  497. ];
  498. yield 'configured by config file, intersected with multiple files' => [
  499. $cb(['d/d1.php', 'd/d2.php']),
  500. null,
  501. [$dir.'/d/d1.php', $dir.'/d/d2.php'],
  502. 'intersection',
  503. $dir.'/d/.php-cs-fixer.php',
  504. ];
  505. yield 'configured by config file, overridden by non-existing dir' => [
  506. new \LogicException(),
  507. null,
  508. [$dir.'/d/fff'],
  509. 'override',
  510. $dir.'/d/.php-cs-fixer.php',
  511. ];
  512. yield 'configured by config file, intersected with non-existing dir' => [
  513. new \LogicException(),
  514. null,
  515. [$dir.'/d/fff'],
  516. 'intersection',
  517. $dir.'/d/.php-cs-fixer.php',
  518. ];
  519. yield 'configured by config file, overridden by non-existing file' => [
  520. new \LogicException(),
  521. null,
  522. [$dir.'/d/fff.php'],
  523. 'override',
  524. $dir.'/d/.php-cs-fixer.php',
  525. ];
  526. yield 'configured by config file, intersected with non-existing file' => [
  527. new \LogicException(),
  528. null,
  529. [$dir.'/d/fff.php'],
  530. 'intersection',
  531. $dir.'/d/.php-cs-fixer.php',
  532. ];
  533. yield 'configured by config file, overridden by multiple files and dirs' => [
  534. $cb(['d/d1.php', 'd/e/de1.php', 'd/f/df1.php']),
  535. null,
  536. [$dir.'/d/d1.php', $dir.'/d/e', $dir.'/d/f/'],
  537. 'override',
  538. $dir.'/d/.php-cs-fixer.php',
  539. ];
  540. yield 'configured by config file, intersected with multiple files and dirs' => [
  541. $cb(['d/d1.php', 'd/e/de1.php', 'd/f/df1.php']),
  542. null,
  543. [$dir.'/d/d1.php', $dir.'/d/e', $dir.'/d/f/'],
  544. 'intersection',
  545. $dir.'/d/.php-cs-fixer.php',
  546. ];
  547. }
  548. /**
  549. * @param array<string, mixed> $options
  550. *
  551. * @dataProvider provideConfigFinderIsOverriddenCases
  552. */
  553. public function testConfigFinderIsOverridden(array $options, bool $expectedResult): void
  554. {
  555. $resolver = $this->createConfigurationResolver($options);
  556. self::assertSame($expectedResult, $resolver->configFinderIsOverridden());
  557. $resolver = $this->createConfigurationResolver($options);
  558. $resolver->getFinder();
  559. self::assertSame($expectedResult, $resolver->configFinderIsOverridden());
  560. }
  561. public static function provideConfigFinderIsOverriddenCases(): iterable
  562. {
  563. $root = __DIR__.'/../..';
  564. yield [
  565. [
  566. 'config' => $root.'/.php-cs-fixer.dist.php',
  567. ],
  568. false,
  569. ];
  570. yield [
  571. [
  572. 'config' => $root.'/.php-cs-fixer.dist.php',
  573. 'path' => [$root.'/src'],
  574. ],
  575. true,
  576. ];
  577. yield [
  578. [],
  579. false,
  580. ];
  581. yield [
  582. [
  583. 'path' => [$root.'/src'],
  584. ],
  585. false,
  586. ];
  587. yield [
  588. [
  589. 'config' => $root.'/.php-cs-fixer.dist.php',
  590. 'path' => [$root.'/src'],
  591. 'path-mode' => ConfigurationResolver::PATH_MODE_INTERSECTION,
  592. ],
  593. false,
  594. ];
  595. // scenario when loaded config is not setting custom finder
  596. yield [
  597. [
  598. 'config' => $root.'/tests/Fixtures/ConfigurationResolverConfigFile/case_3/.php-cs-fixer.dist.php',
  599. 'path' => [$root.'/src'],
  600. ],
  601. false,
  602. ];
  603. // scenario when loaded config contains not fully defined finder
  604. yield [
  605. [
  606. 'config' => $root.'/tests/Fixtures/ConfigurationResolverConfigFile/case_9/.php-cs-fixer.php',
  607. 'path' => [$root.'/src'],
  608. ],
  609. false,
  610. ];
  611. }
  612. public function testResolveIsDryRunViaStdIn(): void
  613. {
  614. $resolver = $this->createConfigurationResolver([
  615. 'dry-run' => false,
  616. 'path' => ['-'],
  617. ]);
  618. self::assertTrue($resolver->isDryRun());
  619. }
  620. public function testResolveIsDryRunViaNegativeOption(): void
  621. {
  622. $resolver = $this->createConfigurationResolver(['dry-run' => false]);
  623. self::assertFalse($resolver->isDryRun());
  624. }
  625. public function testResolveIsDryRunViaPositiveOption(): void
  626. {
  627. $resolver = $this->createConfigurationResolver(['dry-run' => true]);
  628. self::assertTrue($resolver->isDryRun());
  629. }
  630. /**
  631. * @dataProvider provideResolveBooleanOptionCases
  632. */
  633. public function testResolveUsingCacheWithConfigOption(bool $expected, bool $configValue, ?string $passed): void
  634. {
  635. $config = new Config();
  636. $config->setUsingCache($configValue);
  637. $resolver = $this->createConfigurationResolver(
  638. ['using-cache' => $passed],
  639. $config
  640. );
  641. self::assertSame($expected, $resolver->getUsingCache());
  642. }
  643. public function testResolveUsingCacheWithPositiveConfigAndNoOption(): void
  644. {
  645. $config = new Config();
  646. $config->setUsingCache(true);
  647. $resolver = $this->createConfigurationResolver(
  648. [],
  649. $config
  650. );
  651. self::assertTrue($resolver->getUsingCache());
  652. }
  653. public function testResolveUsingCacheWithNegativeConfigAndNoOption(): void
  654. {
  655. $config = new Config();
  656. $config->setUsingCache(false);
  657. $resolver = $this->createConfigurationResolver(
  658. [],
  659. $config
  660. );
  661. self::assertFalse($resolver->getUsingCache());
  662. }
  663. public function testResolveCacheFileWithoutConfigAndOption(): void
  664. {
  665. $config = new Config();
  666. $default = $config->getCacheFile();
  667. $resolver = $this->createConfigurationResolver(
  668. [],
  669. $config
  670. );
  671. self::assertSame($default, $resolver->getCacheFile());
  672. }
  673. public function testResolveCacheFileWithConfig(): void
  674. {
  675. $cacheFile = 'foo/bar.baz';
  676. $config = new Config();
  677. $config
  678. ->setUsingCache(false)
  679. ->setCacheFile($cacheFile)
  680. ;
  681. $resolver = $this->createConfigurationResolver(
  682. [],
  683. $config
  684. );
  685. self::assertNull($resolver->getCacheFile());
  686. $cacheManager = $resolver->getCacheManager();
  687. self::assertInstanceOf(NullCacheManager::class, $cacheManager);
  688. $linter = $resolver->getLinter();
  689. self::assertInstanceOf(LinterInterface::class, $linter);
  690. }
  691. public function testResolveCacheFileWithOption(): void
  692. {
  693. $cacheFile = 'bar.baz';
  694. $config = new Config();
  695. $config->setCacheFile($cacheFile);
  696. $resolver = $this->createConfigurationResolver(
  697. ['cache-file' => $cacheFile],
  698. $config
  699. );
  700. self::assertSame($cacheFile, $resolver->getCacheFile());
  701. }
  702. public function testResolveCacheFileWithConfigAndOption(): void
  703. {
  704. $configCacheFile = 'foo/bar.baz';
  705. $optionCacheFile = 'bar.baz';
  706. $config = new Config();
  707. $config->setCacheFile($configCacheFile);
  708. $resolver = $this->createConfigurationResolver(
  709. ['cache-file' => $optionCacheFile],
  710. $config
  711. );
  712. self::assertSame($optionCacheFile, $resolver->getCacheFile());
  713. }
  714. /**
  715. * @dataProvider provideResolveBooleanOptionCases
  716. */
  717. public function testResolveAllowRiskyWithConfigOption(bool $expected, bool $configValue, ?string $passed): void
  718. {
  719. $config = new Config();
  720. $config->setRiskyAllowed($configValue);
  721. $resolver = $this->createConfigurationResolver(
  722. ['allow-risky' => $passed],
  723. $config
  724. );
  725. self::assertSame($expected, $resolver->getRiskyAllowed());
  726. }
  727. public function testResolveAllowRiskyWithNegativeConfigAndPositiveOption(): void
  728. {
  729. $config = new Config();
  730. $config->setRiskyAllowed(false);
  731. $resolver = $this->createConfigurationResolver(
  732. ['allow-risky' => 'yes'],
  733. $config
  734. );
  735. self::assertTrue($resolver->getRiskyAllowed());
  736. }
  737. public function testResolveAllowRiskyWithNegativeConfigAndNegativeOption(): void
  738. {
  739. $config = new Config();
  740. $config->setRiskyAllowed(false);
  741. $resolver = $this->createConfigurationResolver(
  742. ['allow-risky' => 'no'],
  743. $config
  744. );
  745. self::assertFalse($resolver->getRiskyAllowed());
  746. }
  747. public function testResolveAllowRiskyWithPositiveConfigAndNoOption(): void
  748. {
  749. $config = new Config();
  750. $config->setRiskyAllowed(true);
  751. $resolver = $this->createConfigurationResolver(
  752. [],
  753. $config
  754. );
  755. self::assertTrue($resolver->getRiskyAllowed());
  756. }
  757. public function testResolveAllowRiskyWithNegativeConfigAndNoOption(): void
  758. {
  759. $config = new Config();
  760. $config->setRiskyAllowed(false);
  761. $resolver = $this->createConfigurationResolver(
  762. [],
  763. $config
  764. );
  765. self::assertFalse($resolver->getRiskyAllowed());
  766. }
  767. public function testResolveRulesWithConfig(): void
  768. {
  769. $config = new Config();
  770. $config->setRules([
  771. 'statement_indentation' => true,
  772. 'strict_comparison' => false,
  773. ]);
  774. $resolver = $this->createConfigurationResolver(
  775. [],
  776. $config
  777. );
  778. self::assertSameRules(
  779. [
  780. 'statement_indentation' => true,
  781. ],
  782. $resolver->getRules()
  783. );
  784. }
  785. public function testResolveRulesWithOption(): void
  786. {
  787. $resolver = $this->createConfigurationResolver(['rules' => 'statement_indentation,-strict_comparison']);
  788. self::assertSameRules(
  789. [
  790. 'statement_indentation' => true,
  791. ],
  792. $resolver->getRules()
  793. );
  794. }
  795. /**
  796. * @param string[] $rules
  797. *
  798. * @dataProvider provideResolveRenamedRulesWithUnknownRulesCases
  799. */
  800. public function testResolveRenamedRulesWithUnknownRules(string $expectedMessage, array $rules): void
  801. {
  802. $this->expectException(InvalidConfigurationException::class);
  803. $this->expectExceptionMessage($expectedMessage);
  804. $resolver = $this->createConfigurationResolver(['rules' => implode(',', $rules)]);
  805. $resolver->getRules();
  806. }
  807. public static function provideResolveRenamedRulesWithUnknownRulesCases(): iterable
  808. {
  809. yield 'with config' => [
  810. 'The rules contain unknown fixers: "blank_line_before_return" is renamed (did you mean "blank_line_before_statement"? (note: use configuration "[\'statements\' => [\'return\']]")).
  811. For more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.0.0/UPGRADE-v3.md#renamed-ruless.',
  812. ['blank_line_before_return'],
  813. ];
  814. yield 'without config' => [
  815. 'The rules contain unknown fixers: "final_static_access" is renamed (did you mean "self_static_accessor"?).
  816. For more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.0.0/UPGRADE-v3.md#renamed-ruless.',
  817. ['final_static_access'],
  818. ];
  819. yield [
  820. 'The rules contain unknown fixers: "hash_to_slash_comment" is renamed (did you mean "single_line_comment_style"? (note: use configuration "[\'comment_types\' => [\'hash\']]")).
  821. For more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.0.0/UPGRADE-v3.md#renamed-ruless.',
  822. ['hash_to_slash_comment'],
  823. ];
  824. yield 'both renamed and unknown' => [
  825. 'The rules contain unknown fixers: "final_static_access" is renamed (did you mean "self_static_accessor"?), "binary_operator_space" (did you mean "binary_operator_spaces"?).
  826. For more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.0.0/UPGRADE-v3.md#renamed-ruless.',
  827. ['final_static_access', 'binary_operator_space'],
  828. ];
  829. }
  830. public function testResolveRulesWithUnknownRules(): void
  831. {
  832. $this->expectException(InvalidConfigurationException::class);
  833. $this->expectExceptionMessage('The rules contain unknown fixers: "bar", "binary_operator_space" (did you mean "binary_operator_spaces"?).');
  834. $resolver = $this->createConfigurationResolver(['rules' => 'statement_indentation,-bar,binary_operator_space']);
  835. $resolver->getRules();
  836. }
  837. public function testResolveRulesWithConfigAndOption(): void
  838. {
  839. $config = new Config();
  840. $config->setRules([
  841. 'statement_indentation' => true,
  842. 'strict_comparison' => false,
  843. ]);
  844. $resolver = $this->createConfigurationResolver(
  845. ['rules' => 'blank_line_before_statement'],
  846. $config
  847. );
  848. self::assertSameRules(
  849. [
  850. 'blank_line_before_statement' => true,
  851. ],
  852. $resolver->getRules()
  853. );
  854. }
  855. public function testResolveCommandLineInputOverridesDefault(): void
  856. {
  857. $command = new FixCommand(new ToolInfo());
  858. $definition = $command->getDefinition();
  859. $arguments = $definition->getArguments();
  860. self::assertCount(1, $arguments, 'Expected one argument, possibly test needs updating.');
  861. self::assertArrayHasKey('path', $arguments);
  862. $options = $definition->getOptions();
  863. self::assertSame(
  864. ['path-mode', 'allow-risky', 'config', 'dry-run', 'rules', 'using-cache', 'cache-file', 'diff', 'format', 'stop-on-violation', 'show-progress'],
  865. array_keys($options),
  866. 'Expected options mismatch, possibly test needs updating.'
  867. );
  868. $resolver = $this->createConfigurationResolver([
  869. 'path-mode' => 'intersection',
  870. 'allow-risky' => 'yes',
  871. 'config' => null,
  872. 'dry-run' => true,
  873. 'rules' => 'php_unit_construct',
  874. 'using-cache' => 'no',
  875. 'diff' => true,
  876. 'format' => 'json',
  877. 'stop-on-violation' => true,
  878. ]);
  879. self::assertTrue($resolver->shouldStopOnViolation());
  880. self::assertTrue($resolver->getRiskyAllowed());
  881. self::assertTrue($resolver->isDryRun());
  882. self::assertSame(['php_unit_construct' => true], $resolver->getRules());
  883. self::assertFalse($resolver->getUsingCache());
  884. self::assertNull($resolver->getCacheFile());
  885. self::assertInstanceOf(\PhpCsFixer\Differ\UnifiedDiffer::class, $resolver->getDiffer());
  886. self::assertSame('json', $resolver->getReporter()->getFormat());
  887. }
  888. /**
  889. * @param null|bool|string $diffConfig
  890. *
  891. * @dataProvider provideResolveDifferCases
  892. */
  893. public function testResolveDiffer(string $expected, $diffConfig): void
  894. {
  895. $resolver = $this->createConfigurationResolver([
  896. 'diff' => $diffConfig,
  897. ]);
  898. self::assertInstanceOf($expected, $resolver->getDiffer());
  899. }
  900. public static function provideResolveDifferCases(): iterable
  901. {
  902. yield [
  903. \PhpCsFixer\Differ\NullDiffer::class,
  904. false,
  905. ];
  906. yield [
  907. \PhpCsFixer\Differ\NullDiffer::class,
  908. null,
  909. ];
  910. yield [
  911. \PhpCsFixer\Differ\UnifiedDiffer::class,
  912. true,
  913. ];
  914. }
  915. public function testResolveConfigFileOverridesDefault(): void
  916. {
  917. $dir = __DIR__.'/../Fixtures/ConfigurationResolverConfigFile/case_8';
  918. $resolver = $this->createConfigurationResolver(['path' => [$dir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php']]);
  919. self::assertTrue($resolver->getRiskyAllowed());
  920. self::assertSame(['php_unit_construct' => true], $resolver->getRules());
  921. self::assertFalse($resolver->getUsingCache());
  922. self::assertNull($resolver->getCacheFile());
  923. self::assertSame('xml', $resolver->getReporter()->getFormat());
  924. }
  925. public function testDeprecationOfPassingOtherThanNoOrYes(): void
  926. {
  927. $this->expectException(InvalidConfigurationException::class);
  928. $this->expectExceptionMessage('Expected "yes" or "no" for option "allow-risky", got "yes please".');
  929. $resolver = $this->createConfigurationResolver(['allow-risky' => 'yes please']);
  930. $resolver->getRiskyAllowed();
  931. }
  932. public static function provideResolveBooleanOptionCases(): iterable
  933. {
  934. yield [true, true, 'yes'];
  935. yield [true, false, 'yes'];
  936. yield [false, true, 'no'];
  937. yield [false, false, 'no'];
  938. yield [true, true, null];
  939. yield [false, false, null];
  940. }
  941. public function testWithEmptyRules(): void
  942. {
  943. $resolver = $this->createConfigurationResolver(['rules' => '']);
  944. $this->expectException(InvalidConfigurationException::class);
  945. $this->expectExceptionMessageMatches('/^Empty rules value is not allowed\.$/');
  946. $resolver->getRules();
  947. }
  948. /**
  949. * @param array<string, mixed>|bool $ruleConfig
  950. *
  951. * @dataProvider provideDeprecatedFixerConfiguredCases
  952. *
  953. * @group legacy
  954. */
  955. public function testDeprecatedFixerConfigured($ruleConfig): void
  956. {
  957. $this->expectDeprecation('Rule "Vendor4/foo" is deprecated. Use "testA" and "testB" instead.');
  958. $fixer = new DeprecatedFixer();
  959. $config = new Config();
  960. $config->registerCustomFixers([$fixer]);
  961. $config->setRules([$fixer->getName() => $ruleConfig]);
  962. $resolver = $this->createConfigurationResolver([], $config);
  963. $resolver->getFixers();
  964. }
  965. public static function provideDeprecatedFixerConfiguredCases(): iterable
  966. {
  967. yield [true];
  968. yield [['foo' => true]];
  969. yield [false];
  970. }
  971. public static function provideGetDirectoryCases(): iterable
  972. {
  973. yield [null, '/my/path/my/file', 'my/file'];
  974. yield ['/my/path/.php-cs-fixer.cache', '/my/path/my/file', 'my/file'];
  975. yield ['/my/path2/dir/.php-cs-fixer.cache', '/my/path2/dir/dir2/file', 'dir2/file'];
  976. yield ['dir/.php-cs-fixer.cache', '/my/path/dir/dir3/file', 'dir3/file'];
  977. }
  978. /**
  979. * @dataProvider provideGetDirectoryCases
  980. */
  981. public function testGetDirectory(?string $cacheFile, string $file, string $expectedPathRelativeToFile): void
  982. {
  983. if (null !== $cacheFile) {
  984. $cacheFile = $this->normalizePath($cacheFile);
  985. }
  986. $file = $this->normalizePath($file);
  987. $expectedPathRelativeToFile = $this->normalizePath($expectedPathRelativeToFile);
  988. $config = new Config();
  989. if (null === $cacheFile) {
  990. $config->setUsingCache(false);
  991. } else {
  992. $config->setCacheFile($cacheFile);
  993. }
  994. $resolver = new ConfigurationResolver($config, [], $this->normalizePath('/my/path'), new TestToolInfo());
  995. $directory = $resolver->getDirectory();
  996. self::assertSame($expectedPathRelativeToFile, $directory->getRelativePathTo($file));
  997. }
  998. private function normalizePath(string $path): string
  999. {
  1000. return str_replace('/', \DIRECTORY_SEPARATOR, $path);
  1001. }
  1002. /**
  1003. * @param array<string, array<string, mixed>|bool> $expected
  1004. * @param array<string, array<string, mixed>|bool> $actual
  1005. */
  1006. private static function assertSameRules(array $expected, array $actual): void
  1007. {
  1008. ksort($expected);
  1009. ksort($actual);
  1010. self::assertSame($expected, $actual);
  1011. }
  1012. private static function getFixtureDir(): string
  1013. {
  1014. return realpath(__DIR__.\DIRECTORY_SEPARATOR.'..'.\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR.'ConfigurationResolverConfigFile'.\DIRECTORY_SEPARATOR).'/';
  1015. }
  1016. /**
  1017. * @param array<string, mixed> $options
  1018. */
  1019. private function createConfigurationResolver(array $options, Config $config = null, string $cwdPath = ''): ConfigurationResolver
  1020. {
  1021. if (null === $config) {
  1022. $config = new Config();
  1023. }
  1024. return new ConfigurationResolver(
  1025. $config,
  1026. $options,
  1027. $cwdPath,
  1028. new TestToolInfo()
  1029. );
  1030. }
  1031. }