UtilsTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  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;
  13. use PhpCsFixer\Fixer\FixerInterface;
  14. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  15. use PhpCsFixer\Tokenizer\Token;
  16. use PhpCsFixer\Tokenizer\Tokens;
  17. use PhpCsFixer\Utils;
  18. /**
  19. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  20. * @author Graham Campbell <hello@gjcampbell.co.uk>
  21. * @author Odín del Río <odin.drp@gmail.com>
  22. *
  23. * @internal
  24. *
  25. * @covers \PhpCsFixer\Utils
  26. */
  27. final class UtilsTest extends TestCase
  28. {
  29. /**
  30. * @var null|false|string
  31. */
  32. private $originalValueOfFutureMode;
  33. protected function setUp(): void
  34. {
  35. $this->originalValueOfFutureMode = getenv('PHP_CS_FIXER_FUTURE_MODE');
  36. }
  37. protected function tearDown(): void
  38. {
  39. putenv("PHP_CS_FIXER_FUTURE_MODE={$this->originalValueOfFutureMode}");
  40. parent::tearDown();
  41. }
  42. /**
  43. * @param string $expected Camel case string
  44. *
  45. * @dataProvider provideCamelCaseToUnderscoreCases
  46. */
  47. public function testCamelCaseToUnderscore(string $expected, ?string $input = null): void
  48. {
  49. if (null !== $input) {
  50. self::assertSame($expected, Utils::camelCaseToUnderscore($input));
  51. }
  52. self::assertSame($expected, Utils::camelCaseToUnderscore($expected));
  53. }
  54. /**
  55. * @return iterable<array{0: string, 1?: string}>
  56. */
  57. public static function provideCamelCaseToUnderscoreCases(): iterable
  58. {
  59. yield [
  60. 'dollar_close_curly_braces',
  61. 'DollarCloseCurlyBraces',
  62. ];
  63. yield [
  64. 'utf8_encoder_fixer',
  65. 'utf8EncoderFixer',
  66. ];
  67. yield [
  68. 'terminated_with_number10',
  69. 'TerminatedWithNumber10',
  70. ];
  71. yield [
  72. 'utf8_encoder_fixer',
  73. ];
  74. yield [
  75. 'a',
  76. 'A',
  77. ];
  78. yield [
  79. 'aa',
  80. 'AA',
  81. ];
  82. yield [
  83. 'foo',
  84. 'FOO',
  85. ];
  86. yield [
  87. 'foo_bar_baz',
  88. 'FooBarBAZ',
  89. ];
  90. yield [
  91. 'foo_bar_baz',
  92. 'FooBARBaz',
  93. ];
  94. yield [
  95. 'foo_bar_baz',
  96. 'FOOBarBaz',
  97. ];
  98. yield [
  99. 'mr_t',
  100. 'MrT',
  101. ];
  102. yield [
  103. 'voyage_éclair',
  104. 'VoyageÉclair',
  105. ];
  106. yield [
  107. 'i_want_to_fully_be_a_snake',
  108. 'i_wantTo_fully_be_A_Snake',
  109. ];
  110. }
  111. /**
  112. * @param array{int, string}|string $input token prototype
  113. *
  114. * @dataProvider provideCalculateTrailingWhitespaceIndentCases
  115. */
  116. public function testCalculateTrailingWhitespaceIndent(string $spaces, $input): void
  117. {
  118. $token = new Token($input);
  119. self::assertSame($spaces, Utils::calculateTrailingWhitespaceIndent($token));
  120. }
  121. /**
  122. * @return iterable<array{string, array{int, string}|string}>
  123. */
  124. public static function provideCalculateTrailingWhitespaceIndentCases(): iterable
  125. {
  126. yield [' ', [T_WHITESPACE, "\n\n "]];
  127. yield [' ', [T_WHITESPACE, "\r\n\r\r\r "]];
  128. yield ["\t", [T_WHITESPACE, "\r\n\t"]];
  129. yield ['', [T_WHITESPACE, "\t\n\r"]];
  130. yield ['', [T_WHITESPACE, "\n"]];
  131. yield ['', ''];
  132. }
  133. public function testCalculateTrailingWhitespaceIndentFail(): void
  134. {
  135. $this->expectException(\InvalidArgumentException::class);
  136. $this->expectExceptionMessage('The given token must be whitespace, got "T_STRING".');
  137. $token = new Token([T_STRING, 'foo']);
  138. Utils::calculateTrailingWhitespaceIndent($token);
  139. }
  140. /**
  141. * @param list<mixed> $expected
  142. * @param list<mixed> $elements
  143. *
  144. * @dataProvider provideStableSortCases
  145. */
  146. public function testStableSort(
  147. array $expected,
  148. array $elements,
  149. callable $getComparableValueCallback,
  150. callable $compareValuesCallback
  151. ): void {
  152. self::assertSame(
  153. $expected,
  154. Utils::stableSort($elements, $getComparableValueCallback, $compareValuesCallback)
  155. );
  156. }
  157. /**
  158. * @return iterable<array{list<mixed>, list<mixed>, callable, callable}>
  159. */
  160. public static function provideStableSortCases(): iterable
  161. {
  162. yield [
  163. ['a', 'b', 'c', 'd', 'e'],
  164. ['b', 'd', 'e', 'a', 'c'],
  165. static fn ($element) => $element,
  166. 'strcmp',
  167. ];
  168. yield [
  169. ['b', 'd', 'e', 'a', 'c'],
  170. ['b', 'd', 'e', 'a', 'c'],
  171. static fn (): string => 'foo',
  172. 'strcmp',
  173. ];
  174. yield [
  175. ['b', 'd', 'e', 'a', 'c'],
  176. ['b', 'd', 'e', 'a', 'c'],
  177. static fn ($element) => $element,
  178. static fn (): int => 0,
  179. ];
  180. yield [
  181. ['bar1', 'baz1', 'foo1', 'bar2', 'baz2', 'foo2'],
  182. ['foo1', 'foo2', 'bar1', 'bar2', 'baz1', 'baz2'],
  183. static fn ($element) => preg_replace('/([a-z]+)(\d+)/', '$2$1', $element),
  184. 'strcmp',
  185. ];
  186. }
  187. public function testSortFixers(): void
  188. {
  189. $fixers = [
  190. $this->createFixerDouble('f1', 0),
  191. $this->createFixerDouble('f2', -10),
  192. $this->createFixerDouble('f3', 10),
  193. $this->createFixerDouble('f4', -10),
  194. ];
  195. self::assertSame(
  196. [
  197. $fixers[2],
  198. $fixers[0],
  199. $fixers[1],
  200. $fixers[3],
  201. ],
  202. Utils::sortFixers($fixers)
  203. );
  204. }
  205. public function testNaturalLanguageJoinThrowsInvalidArgumentExceptionForEmptyArray(): void
  206. {
  207. $this->expectException(\InvalidArgumentException::class);
  208. $this->expectExceptionMessage('Array of names cannot be empty.');
  209. Utils::naturalLanguageJoin([]);
  210. }
  211. public function testNaturalLanguageJoinThrowsInvalidArgumentExceptionForMoreThanOneCharWrapper(): void
  212. {
  213. $this->expectException(\InvalidArgumentException::class);
  214. $this->expectExceptionMessage('Wrapper should be a single-char string or empty.');
  215. Utils::naturalLanguageJoin(['a', 'b'], 'foo');
  216. }
  217. /**
  218. * @dataProvider provideNaturalLanguageJoinCases
  219. *
  220. * @param list<string> $names
  221. */
  222. public function testNaturalLanguageJoin(string $joined, array $names, string $wrapper = '"'): void
  223. {
  224. self::assertSame($joined, Utils::naturalLanguageJoin($names, $wrapper));
  225. }
  226. /**
  227. * @return iterable<array{0: string, 1: list<string>, 2?: string}>
  228. */
  229. public static function provideNaturalLanguageJoinCases(): iterable
  230. {
  231. yield [
  232. '"a"',
  233. ['a'],
  234. ];
  235. yield [
  236. '"a" and "b"',
  237. ['a', 'b'],
  238. ];
  239. yield [
  240. '"a", "b" and "c"',
  241. ['a', 'b', 'c'],
  242. ];
  243. yield [
  244. '\'a\'',
  245. ['a'],
  246. '\'',
  247. ];
  248. yield [
  249. '\'a\' and \'b\'',
  250. ['a', 'b'],
  251. '\'',
  252. ];
  253. yield [
  254. '\'a\', \'b\' and \'c\'',
  255. ['a', 'b', 'c'],
  256. '\'',
  257. ];
  258. yield [
  259. '?a?',
  260. ['a'],
  261. '?',
  262. ];
  263. yield [
  264. '?a? and ?b?',
  265. ['a', 'b'],
  266. '?',
  267. ];
  268. yield [
  269. '?a?, ?b? and ?c?',
  270. ['a', 'b', 'c'],
  271. '?',
  272. ];
  273. yield [
  274. 'a',
  275. ['a'],
  276. '',
  277. ];
  278. yield [
  279. 'a and b',
  280. ['a', 'b'],
  281. '',
  282. ];
  283. yield [
  284. 'a, b and c',
  285. ['a', 'b', 'c'],
  286. '',
  287. ];
  288. }
  289. public function testNaturalLanguageJoinWithBackticksThrowsInvalidArgumentExceptionForEmptyArray(): void
  290. {
  291. $this->expectException(\InvalidArgumentException::class);
  292. Utils::naturalLanguageJoinWithBackticks([]);
  293. }
  294. /**
  295. * @param list<string> $names
  296. *
  297. * @dataProvider provideNaturalLanguageJoinWithBackticksCases
  298. */
  299. public function testNaturalLanguageJoinWithBackticks(string $joined, array $names): void
  300. {
  301. self::assertSame($joined, Utils::naturalLanguageJoinWithBackticks($names));
  302. }
  303. /**
  304. * @return iterable<array{string, list<string>}>
  305. */
  306. public static function provideNaturalLanguageJoinWithBackticksCases(): iterable
  307. {
  308. yield [
  309. '`a`',
  310. ['a'],
  311. ];
  312. yield [
  313. '`a` and `b`',
  314. ['a', 'b'],
  315. ];
  316. yield [
  317. '`a`, `b` and `c`',
  318. ['a', 'b', 'c'],
  319. ];
  320. }
  321. /**
  322. * @group legacy
  323. */
  324. public function testTriggerDeprecationWhenFutureModeIsOff(): void
  325. {
  326. putenv('PHP_CS_FIXER_FUTURE_MODE=0');
  327. $message = __METHOD__.'::The message';
  328. $this->expectDeprecation($message);
  329. Utils::triggerDeprecation(new \DomainException($message));
  330. $triggered = Utils::getTriggeredDeprecations();
  331. self::assertContains($message, $triggered);
  332. }
  333. public function testTriggerDeprecationWhenFutureModeIsOn(): void
  334. {
  335. putenv('PHP_CS_FIXER_FUTURE_MODE=1');
  336. $message = __METHOD__.'::The message';
  337. $exception = new \DomainException($message);
  338. $futureModeException = null;
  339. try {
  340. Utils::triggerDeprecation($exception);
  341. } catch (\Exception $futureModeException) {
  342. }
  343. self::assertInstanceOf(\RuntimeException::class, $futureModeException);
  344. self::assertSame($exception, $futureModeException->getPrevious());
  345. $triggered = Utils::getTriggeredDeprecations();
  346. self::assertNotContains($message, $triggered);
  347. }
  348. /**
  349. * @param mixed $input
  350. *
  351. * @dataProvider provideToStringCases
  352. */
  353. public function testToString(string $expected, $input): void
  354. {
  355. self::assertSame($expected, Utils::toString($input));
  356. }
  357. /**
  358. * @return iterable<array{string, mixed}>
  359. */
  360. public static function provideToStringCases(): iterable
  361. {
  362. yield ["['a' => 3, 'b' => 'c']", ['a' => 3, 'b' => 'c']];
  363. yield ['[[1], [2]]', [[1], [2]]];
  364. yield ['[0 => [1], \'a\' => [2]]', [[1], 'a' => [2]]];
  365. yield ['[1, 2, \'foo\', null]', [1, 2, 'foo', null]];
  366. yield ['[1, 2]', [1, 2]];
  367. yield ['[]', []];
  368. yield ['1.5', 1.5];
  369. yield ['false', false];
  370. yield ['true', true];
  371. yield ['1', 1];
  372. yield ["'foo'", 'foo'];
  373. }
  374. private function createFixerDouble(string $name, int $priority): FixerInterface
  375. {
  376. return new class($name, $priority) implements FixerInterface {
  377. private string $name;
  378. private int $priority;
  379. public function __construct(string $name, int $priority)
  380. {
  381. $this->name = $name;
  382. $this->priority = $priority;
  383. }
  384. public function isCandidate(Tokens $tokens): bool
  385. {
  386. throw new \LogicException('Not implemented.');
  387. }
  388. public function isRisky(): bool
  389. {
  390. throw new \LogicException('Not implemented.');
  391. }
  392. public function fix(\SplFileInfo $file, Tokens $tokens): void
  393. {
  394. throw new \LogicException('Not implemented.');
  395. }
  396. public function getDefinition(): FixerDefinitionInterface
  397. {
  398. throw new \LogicException('Not implemented.');
  399. }
  400. public function getName(): string
  401. {
  402. return $this->name;
  403. }
  404. public function getPriority(): int
  405. {
  406. return $this->priority;
  407. }
  408. public function supports(\SplFileInfo $file): bool
  409. {
  410. throw new \LogicException('Not implemented.');
  411. }
  412. };
  413. }
  414. }