UtilsTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  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. }
  107. /**
  108. * @param array{int, string}|string $input token prototype
  109. *
  110. * @dataProvider provideCalculateTrailingWhitespaceIndentCases
  111. */
  112. public function testCalculateTrailingWhitespaceIndent(string $spaces, $input): void
  113. {
  114. $token = new Token($input);
  115. self::assertSame($spaces, Utils::calculateTrailingWhitespaceIndent($token));
  116. }
  117. /**
  118. * @return iterable<array{string, array{int, string}|string}>
  119. */
  120. public static function provideCalculateTrailingWhitespaceIndentCases(): iterable
  121. {
  122. yield [' ', [T_WHITESPACE, "\n\n "]];
  123. yield [' ', [T_WHITESPACE, "\r\n\r\r\r "]];
  124. yield ["\t", [T_WHITESPACE, "\r\n\t"]];
  125. yield ['', [T_WHITESPACE, "\t\n\r"]];
  126. yield ['', [T_WHITESPACE, "\n"]];
  127. yield ['', ''];
  128. }
  129. public function testCalculateTrailingWhitespaceIndentFail(): void
  130. {
  131. $this->expectException(\InvalidArgumentException::class);
  132. $this->expectExceptionMessage('The given token must be whitespace, got "T_STRING".');
  133. $token = new Token([T_STRING, 'foo']);
  134. Utils::calculateTrailingWhitespaceIndent($token);
  135. }
  136. /**
  137. * @param list<mixed> $expected
  138. * @param list<mixed> $elements
  139. *
  140. * @dataProvider provideStableSortCases
  141. */
  142. public function testStableSort(
  143. array $expected,
  144. array $elements,
  145. callable $getComparableValueCallback,
  146. callable $compareValuesCallback
  147. ): void {
  148. self::assertSame(
  149. $expected,
  150. Utils::stableSort($elements, $getComparableValueCallback, $compareValuesCallback)
  151. );
  152. }
  153. /**
  154. * @return iterable<array{list<mixed>, list<mixed>, callable, callable}>
  155. */
  156. public static function provideStableSortCases(): iterable
  157. {
  158. yield [
  159. ['a', 'b', 'c', 'd', 'e'],
  160. ['b', 'd', 'e', 'a', 'c'],
  161. static fn ($element) => $element,
  162. 'strcmp',
  163. ];
  164. yield [
  165. ['b', 'd', 'e', 'a', 'c'],
  166. ['b', 'd', 'e', 'a', 'c'],
  167. static fn (): string => 'foo',
  168. 'strcmp',
  169. ];
  170. yield [
  171. ['b', 'd', 'e', 'a', 'c'],
  172. ['b', 'd', 'e', 'a', 'c'],
  173. static fn ($element) => $element,
  174. static fn (): int => 0,
  175. ];
  176. yield [
  177. ['bar1', 'baz1', 'foo1', 'bar2', 'baz2', 'foo2'],
  178. ['foo1', 'foo2', 'bar1', 'bar2', 'baz1', 'baz2'],
  179. static fn ($element) => preg_replace('/([a-z]+)(\d+)/', '$2$1', $element),
  180. 'strcmp',
  181. ];
  182. }
  183. public function testSortFixers(): void
  184. {
  185. $fixers = [
  186. $this->createFixerDouble('f1', 0),
  187. $this->createFixerDouble('f2', -10),
  188. $this->createFixerDouble('f3', 10),
  189. $this->createFixerDouble('f4', -10),
  190. ];
  191. self::assertSame(
  192. [
  193. $fixers[2],
  194. $fixers[0],
  195. $fixers[1],
  196. $fixers[3],
  197. ],
  198. Utils::sortFixers($fixers)
  199. );
  200. }
  201. public function testNaturalLanguageJoinThrowsInvalidArgumentExceptionForEmptyArray(): void
  202. {
  203. $this->expectException(\InvalidArgumentException::class);
  204. $this->expectExceptionMessage('Array of names cannot be empty.');
  205. Utils::naturalLanguageJoin([]);
  206. }
  207. public function testNaturalLanguageJoinThrowsInvalidArgumentExceptionForMoreThanOneCharWrapper(): void
  208. {
  209. $this->expectException(\InvalidArgumentException::class);
  210. $this->expectExceptionMessage('Wrapper should be a single-char string or empty.');
  211. Utils::naturalLanguageJoin(['a', 'b'], 'foo');
  212. }
  213. /**
  214. * @dataProvider provideNaturalLanguageJoinCases
  215. *
  216. * @param list<string> $names
  217. */
  218. public function testNaturalLanguageJoin(string $joined, array $names, string $wrapper = '"'): void
  219. {
  220. self::assertSame($joined, Utils::naturalLanguageJoin($names, $wrapper));
  221. }
  222. /**
  223. * @return iterable<array{0: string, 1: list<string>, 2?: string}>
  224. */
  225. public static function provideNaturalLanguageJoinCases(): iterable
  226. {
  227. yield [
  228. '"a"',
  229. ['a'],
  230. ];
  231. yield [
  232. '"a" and "b"',
  233. ['a', 'b'],
  234. ];
  235. yield [
  236. '"a", "b" and "c"',
  237. ['a', 'b', 'c'],
  238. ];
  239. yield [
  240. '\'a\'',
  241. ['a'],
  242. '\'',
  243. ];
  244. yield [
  245. '\'a\' and \'b\'',
  246. ['a', 'b'],
  247. '\'',
  248. ];
  249. yield [
  250. '\'a\', \'b\' and \'c\'',
  251. ['a', 'b', 'c'],
  252. '\'',
  253. ];
  254. yield [
  255. '?a?',
  256. ['a'],
  257. '?',
  258. ];
  259. yield [
  260. '?a? and ?b?',
  261. ['a', 'b'],
  262. '?',
  263. ];
  264. yield [
  265. '?a?, ?b? and ?c?',
  266. ['a', 'b', 'c'],
  267. '?',
  268. ];
  269. yield [
  270. 'a',
  271. ['a'],
  272. '',
  273. ];
  274. yield [
  275. 'a and b',
  276. ['a', 'b'],
  277. '',
  278. ];
  279. yield [
  280. 'a, b and c',
  281. ['a', 'b', 'c'],
  282. '',
  283. ];
  284. }
  285. public function testNaturalLanguageJoinWithBackticksThrowsInvalidArgumentExceptionForEmptyArray(): void
  286. {
  287. $this->expectException(\InvalidArgumentException::class);
  288. Utils::naturalLanguageJoinWithBackticks([]);
  289. }
  290. /**
  291. * @param list<string> $names
  292. *
  293. * @dataProvider provideNaturalLanguageJoinWithBackticksCases
  294. */
  295. public function testNaturalLanguageJoinWithBackticks(string $joined, array $names): void
  296. {
  297. self::assertSame($joined, Utils::naturalLanguageJoinWithBackticks($names));
  298. }
  299. /**
  300. * @return iterable<array{string, list<string>}>
  301. */
  302. public static function provideNaturalLanguageJoinWithBackticksCases(): iterable
  303. {
  304. yield [
  305. '`a`',
  306. ['a'],
  307. ];
  308. yield [
  309. '`a` and `b`',
  310. ['a', 'b'],
  311. ];
  312. yield [
  313. '`a`, `b` and `c`',
  314. ['a', 'b', 'c'],
  315. ];
  316. }
  317. /**
  318. * @group legacy
  319. */
  320. public function testTriggerDeprecationWhenFutureModeIsOff(): void
  321. {
  322. putenv('PHP_CS_FIXER_FUTURE_MODE=0');
  323. $message = __METHOD__.'::The message';
  324. $this->expectDeprecation($message);
  325. Utils::triggerDeprecation(new \DomainException($message));
  326. $triggered = Utils::getTriggeredDeprecations();
  327. self::assertContains($message, $triggered);
  328. }
  329. public function testTriggerDeprecationWhenFutureModeIsOn(): void
  330. {
  331. putenv('PHP_CS_FIXER_FUTURE_MODE=1');
  332. $message = __METHOD__.'::The message';
  333. $exception = new \DomainException($message);
  334. $futureModeException = null;
  335. try {
  336. Utils::triggerDeprecation($exception);
  337. } catch (\Exception $futureModeException) {
  338. }
  339. self::assertInstanceOf(\RuntimeException::class, $futureModeException);
  340. self::assertSame($exception, $futureModeException->getPrevious());
  341. $triggered = Utils::getTriggeredDeprecations();
  342. self::assertNotContains($message, $triggered);
  343. }
  344. /**
  345. * @param mixed $input
  346. *
  347. * @dataProvider provideToStringCases
  348. */
  349. public function testToString(string $expected, $input): void
  350. {
  351. self::assertSame($expected, Utils::toString($input));
  352. }
  353. /**
  354. * @return iterable<array{string, mixed}>
  355. */
  356. public static function provideToStringCases(): iterable
  357. {
  358. yield ["['a' => 3, 'b' => 'c']", ['a' => 3, 'b' => 'c']];
  359. yield ['[[1], [2]]', [[1], [2]]];
  360. yield ['[0 => [1], \'a\' => [2]]', [[1], 'a' => [2]]];
  361. yield ['[1, 2, \'foo\', null]', [1, 2, 'foo', null]];
  362. yield ['[1, 2]', [1, 2]];
  363. yield ['[]', []];
  364. yield ['1.5', 1.5];
  365. yield ['false', false];
  366. yield ['true', true];
  367. yield ['1', 1];
  368. yield ["'foo'", 'foo'];
  369. }
  370. private function createFixerDouble(string $name, int $priority): FixerInterface
  371. {
  372. return new class($name, $priority) implements FixerInterface {
  373. private string $name;
  374. private int $priority;
  375. public function __construct(string $name, int $priority)
  376. {
  377. $this->name = $name;
  378. $this->priority = $priority;
  379. }
  380. public function isCandidate(Tokens $tokens): bool
  381. {
  382. throw new \LogicException('Not implemented.');
  383. }
  384. public function isRisky(): bool
  385. {
  386. throw new \LogicException('Not implemented.');
  387. }
  388. public function fix(\SplFileInfo $file, Tokens $tokens): void
  389. {
  390. throw new \LogicException('Not implemented.');
  391. }
  392. public function getDefinition(): FixerDefinitionInterface
  393. {
  394. throw new \LogicException('Not implemented.');
  395. }
  396. public function getName(): string
  397. {
  398. return $this->name;
  399. }
  400. public function getPriority(): int
  401. {
  402. return $this->priority;
  403. }
  404. public function supports(\SplFileInfo $file): bool
  405. {
  406. throw new \LogicException('Not implemented.');
  407. }
  408. };
  409. }
  410. }