UtilsTest.php 12 KB

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