TypeExpressionTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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\DocBlock;
  13. use PhpCsFixer\DocBlock\TypeExpression;
  14. use PhpCsFixer\Tests\TestCase;
  15. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
  16. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
  17. /**
  18. * @covers \PhpCsFixer\DocBlock\TypeExpression
  19. *
  20. * @internal
  21. */
  22. final class TypeExpressionTest extends TestCase
  23. {
  24. /**
  25. * @param string[] $expectedTypes
  26. *
  27. * @dataProvider provideGetTypesCases
  28. */
  29. public function testGetTypes(string $typesExpression, array $expectedTypes): void
  30. {
  31. $expression = new TypeExpression($typesExpression, null, []);
  32. self::assertSame($expectedTypes, $expression->getTypes());
  33. }
  34. public static function provideGetTypesCases(): iterable
  35. {
  36. yield ['int', ['int']];
  37. yield ['Foo[][]', ['Foo[][]']];
  38. yield ['int[]', ['int[]']];
  39. yield ['int[]|null', ['int[]', 'null']];
  40. yield ['int[]|null|?int|array', ['int[]', 'null', '?int', 'array']];
  41. yield ['null|Foo\Bar|\Baz\Bax|int[]', ['null', 'Foo\Bar', '\Baz\Bax', 'int[]']];
  42. yield ['gen<int>', ['gen<int>']];
  43. yield ['int|gen<int>', ['int', 'gen<int>']];
  44. yield ['\int|\gen<\int, \bool>', ['\int', '\gen<\int, \bool>']];
  45. yield ['gen<int, int>', ['gen<int, int>']];
  46. yield ['gen<int, bool|string>', ['gen<int, bool|string>']];
  47. yield ['gen<int, string[]>', ['gen<int, string[]>']];
  48. yield ['gen<int, gener<string, bool>>', ['gen<int, gener<string, bool>>']];
  49. yield ['gen<int, gener<string, null|bool>>', ['gen<int, gener<string, null|bool>>']];
  50. yield ['null|gen<int, gener<string, bool>>|int|string[]', ['null', 'gen<int, gener<string, bool>>', 'int', 'string[]']];
  51. yield ['null|gen<int, gener<string, bool>>|int|array<int, string>|string[]', ['null', 'gen<int, gener<string, bool>>', 'int', 'array<int, string>', 'string[]']];
  52. yield ['this', ['this']];
  53. yield ['@this', ['@this']];
  54. yield ['$SELF|int', ['$SELF', 'int']];
  55. yield ['array<string|int, string>', ['array<string|int, string>']];
  56. yield ['Collection<Foo<Bar>, Foo<Baz>>', ['Collection<Foo<Bar>, Foo<Baz>>']];
  57. yield ['int | string', ['int', 'string']];
  58. yield ['Foo::*', ['Foo::*']];
  59. yield ['Foo::A', ['Foo::A']];
  60. yield ['Foo::A|Foo::B', ['Foo::A', 'Foo::B']];
  61. yield ['Foo::A*', ['Foo::A*']];
  62. yield ['array<Foo::A*>|null', ['array<Foo::A*>', 'null']];
  63. yield ['null|true|false|1|-1|1.5|-1.5|.5|1.|\'a\'|"b"', ['null', 'true', 'false', '1', '-1', '1.5', '-1.5', '.5', '1.', "'a'", '"b"']];
  64. yield ['int | "a" | A<B<C, D>, E<F::*|G[]>>', ['int', '"a"', 'A<B<C, D>, E<F::*|G[]>>']];
  65. yield ['class-string<Foo>', ['class-string<Foo>']];
  66. yield ['A&B', ['A', 'B']];
  67. yield ['A & B', ['A', 'B']];
  68. yield ['array{}', ['array{}']];
  69. yield ['object{ }', ['object{ }']];
  70. yield ['array{1: bool, 2: bool}', ['array{1: bool, 2: bool}']];
  71. yield ['array{a: int|string, b?: bool}', ['array{a: int|string, b?: bool}']];
  72. yield ['array{\'a\': "a", "b"?: \'b\'}', ['array{\'a\': "a", "b"?: \'b\'}']];
  73. yield ['array { a : int | string , b ? : A<B, C> }', ['array { a : int | string , b ? : A<B, C> }']];
  74. yield ['array{bool, int}', ['array{bool, int}']];
  75. yield ['object{ bool, foo2: int }', ['object{ bool, foo2: int }']];
  76. yield ['callable(string)', ['callable(string)']];
  77. yield ['callable(string): bool', ['callable(string): bool']];
  78. yield ['callable(array<int, string>, array<int, Foo>): bool', ['callable(array<int, string>, array<int, Foo>): bool']];
  79. yield ['array<int, callable(string): bool>', ['array<int, callable(string): bool>']];
  80. yield ['callable(string): callable(int)', ['callable(string): callable(int)']];
  81. yield ['callable(string) : callable(int) : bool', ['callable(string) : callable(int) : bool']];
  82. yield ['TheCollection<callable(Foo, Bar,Baz): Foo[]>|string[]|null', ['TheCollection<callable(Foo, Bar,Baz): Foo[]>', 'string[]', 'null']];
  83. yield ['Closure()', ['Closure()']];
  84. yield ['Closure(string)', ['Closure(string)']];
  85. yield ['\\Closure', ['\\Closure']];
  86. yield ['\\Closure()', ['\\Closure()']];
  87. yield ['\\Closure(string)', ['\\Closure(string)']];
  88. yield ['\\Closure(string, bool)', ['\\Closure(string, bool)']];
  89. yield ['\\Closure(string|int, bool)', ['\\Closure(string|int, bool)']];
  90. yield ['\\Closure(string):bool', ['\\Closure(string):bool']];
  91. yield ['\\Closure(string): bool', ['\\Closure(string): bool']];
  92. yield ['\\Closure(string|int, bool): bool', ['\\Closure(string|int, bool): bool']];
  93. yield ['array < int , callable ( string ) : bool >', ['array < int , callable ( string ) : bool >']];
  94. yield ['(int)', ['(int)']];
  95. yield ['(int|\\Exception)', ['(int|\\Exception)']];
  96. yield ['($foo is int ? false : true)', ['($foo is int ? false : true)']];
  97. yield ['\'\'', ['\'\'']];
  98. yield ['\'foo\'', ['\'foo\'']];
  99. yield ['\'\\\\\'', ['\'\\\\\'']];
  100. yield ['\'\\\'\'', ['\'\\\'\'']];
  101. yield ['\'a\\\'s"\\\\\n\r\t\'|"b\\"s\'\\\\\n\r\t"', ['\'a\\\'s"\\\\\n\r\t\'', '"b\\"s\'\\\\\n\r\t"']];
  102. }
  103. /**
  104. * @dataProvider provideGetTypesGlueCases
  105. */
  106. public function testGetTypesGlue(string $expectedTypesGlue, string $typesExpression): void
  107. {
  108. $expression = new TypeExpression($typesExpression, null, []);
  109. self::assertSame($expectedTypesGlue, $expression->getTypesGlue());
  110. }
  111. public static function provideGetTypesGlueCases(): iterable
  112. {
  113. yield ['|', 'string']; // for backward behaviour
  114. yield ['|', 'bool|string'];
  115. yield ['&', 'Foo&Bar'];
  116. }
  117. /**
  118. * @param NamespaceUseAnalysis[] $namespaceUses
  119. *
  120. * @dataProvider provideCommonTypeCases
  121. */
  122. public function testGetCommonType(string $typesExpression, ?string $expectedCommonType, NamespaceAnalysis $namespace = null, array $namespaceUses = []): void
  123. {
  124. $expression = new TypeExpression($typesExpression, $namespace, $namespaceUses);
  125. self::assertSame($expectedCommonType, $expression->getCommonType());
  126. }
  127. public static function provideCommonTypeCases(): iterable
  128. {
  129. $globalNamespace = new NamespaceAnalysis('', '', 0, 999, 0, 999);
  130. $appNamespace = new NamespaceAnalysis('App', 'App', 0, 999, 0, 999);
  131. $useTraversable = new NamespaceUseAnalysis('Traversable', 'Traversable', false, 0, 0, NamespaceUseAnalysis::TYPE_CLASS);
  132. $useObjectAsTraversable = new NamespaceUseAnalysis('Foo', 'Traversable', false, 0, 0, NamespaceUseAnalysis::TYPE_CLASS);
  133. yield ['true', 'bool'];
  134. yield ['false', 'bool'];
  135. yield ['bool', 'bool'];
  136. yield ['int', 'int'];
  137. yield ['float', 'float'];
  138. yield ['string', 'string'];
  139. yield ['array', 'array'];
  140. yield ['object', 'object'];
  141. yield ['self', 'self'];
  142. yield ['static', 'static'];
  143. yield ['bool[]', 'array'];
  144. yield ['int[]', 'array'];
  145. yield ['float[]', 'array'];
  146. yield ['string[]', 'array'];
  147. yield ['array[]', 'array'];
  148. yield ['bool[][]', 'array'];
  149. yield ['int[][]', 'array'];
  150. yield ['float[][]', 'array'];
  151. yield ['string[][]', 'array'];
  152. yield ['array[][]', 'array'];
  153. yield ['array|iterable', 'iterable'];
  154. yield ['iterable|array', 'iterable'];
  155. yield ['array|Traversable', 'iterable'];
  156. yield ['array|\Traversable', 'iterable'];
  157. yield ['array|Traversable', 'iterable', $globalNamespace];
  158. yield ['iterable|Traversable', 'iterable'];
  159. yield ['array<string>', 'array'];
  160. yield ['array<int, string>', 'array'];
  161. yield ['iterable<string>', 'iterable'];
  162. yield ['iterable<int, string>', 'iterable'];
  163. yield ['\Traversable<string>', '\Traversable'];
  164. yield ['Traversable<int, string>', 'Traversable'];
  165. yield ['Collection<string>', 'Collection'];
  166. yield ['Collection<int, string>', 'Collection'];
  167. yield ['array<int, string>|iterable<int, string>', 'iterable'];
  168. yield ['int[]|string[]', 'array'];
  169. yield ['int|null', 'int'];
  170. yield ['null|int', 'int'];
  171. yield ['void', 'void'];
  172. yield ['never', 'never'];
  173. yield ['array|Traversable', 'iterable', null, [$useTraversable]];
  174. yield ['array|Traversable', 'iterable', $globalNamespace, [$useTraversable]];
  175. yield ['array|Traversable', 'iterable', $appNamespace, [$useTraversable]];
  176. yield ['self|static', 'self'];
  177. yield ['array|Traversable', null, null, [$useObjectAsTraversable]];
  178. yield ['array|Traversable', null, $globalNamespace, [$useObjectAsTraversable]];
  179. yield ['array|Traversable', null, $appNamespace, [$useObjectAsTraversable]];
  180. yield ['bool|int', null];
  181. yield ['string|bool', null];
  182. yield ['array<int, string>|Collection<int, string>', null];
  183. }
  184. /**
  185. * @dataProvider provideAllowsNullCases
  186. */
  187. public function testAllowsNull(string $typesExpression, bool $expectNullAllowed): void
  188. {
  189. $expression = new TypeExpression($typesExpression, null, []);
  190. self::assertSame($expectNullAllowed, $expression->allowsNull());
  191. }
  192. public static function provideAllowsNullCases(): iterable
  193. {
  194. yield ['null', true];
  195. yield ['mixed', true];
  196. yield ['null|mixed', true];
  197. yield ['int|bool|null', true];
  198. yield ['int|bool|mixed', true];
  199. yield ['int', false];
  200. yield ['bool', false];
  201. yield ['string', false];
  202. }
  203. /**
  204. * @dataProvider provideSortTypesCases
  205. */
  206. public function testSortTypes(string $typesExpression, string $expectResult): void
  207. {
  208. $expression = new TypeExpression($typesExpression, null, []);
  209. $expression->sortTypes(static function (TypeExpression $a, TypeExpression $b): int {
  210. return strcasecmp($a->toString(), $b->toString());
  211. });
  212. self::assertSame($expectResult, $expression->toString());
  213. }
  214. public static function provideSortTypesCases(): iterable
  215. {
  216. yield 'not a union type' => [
  217. 'int',
  218. 'int',
  219. ];
  220. yield 'simple' => [
  221. 'int|bool',
  222. 'bool|int',
  223. ];
  224. yield 'simple in generic' => [
  225. 'array<int|bool>',
  226. 'array<bool|int>',
  227. ];
  228. yield 'generic with multiple types' => [
  229. 'array<int|bool, string|float>',
  230. 'array<bool|int, float|string>',
  231. ];
  232. yield 'simple in array shape with int key' => [
  233. 'array{0: int|bool}',
  234. 'array{0: bool|int}',
  235. ];
  236. yield 'simple in array shape with string key' => [
  237. 'array{"foo": int|bool}',
  238. 'array{"foo": bool|int}',
  239. ];
  240. yield 'simple in array shape with multiple keys' => [
  241. 'array{0: int|bool, "foo": int|bool}',
  242. 'array{0: bool|int, "foo": bool|int}',
  243. ];
  244. yield 'simple in array shape with implicit key' => [
  245. 'array{int|bool}',
  246. 'array{bool|int}',
  247. ];
  248. yield 'array shape with multiple colons - array shape' => [
  249. 'array{array{x:int|bool}, a:array{x:int|bool}}',
  250. 'array{array{x:bool|int}, a:array{x:bool|int}}',
  251. ];
  252. yield 'array shape with multiple colons - callable' => [
  253. 'array{array{x:int|bool}, int|bool, callable(): void}',
  254. 'array{array{x:bool|int}, bool|int, callable(): void}',
  255. ];
  256. yield 'simple in callable argument' => [
  257. 'callable(int|bool)',
  258. 'callable(bool|int)',
  259. ];
  260. yield 'callable with multiple arguments' => [
  261. 'callable(int|bool, null|array)',
  262. 'callable(bool|int, array|null)',
  263. ];
  264. yield 'simple in callable return type' => [
  265. 'callable(): string|float',
  266. 'callable(): float|string',
  267. ];
  268. yield 'simple in closure argument' => [
  269. 'Closure(int|bool)',
  270. 'Closure(bool|int)',
  271. ];
  272. yield 'closure with multiple arguments' => [
  273. 'Closure(int|bool, null|array)',
  274. 'Closure(bool|int, array|null)',
  275. ];
  276. yield 'simple in closure return type' => [
  277. 'Closure(): string|float',
  278. 'Closure(): float|string',
  279. ];
  280. yield 'with multiple nesting levels' => [
  281. 'array{0: Foo<int|bool>|Bar<callable(string|float|array<int|bool>): Foo|Bar>}',
  282. 'array{0: Bar<callable(array<bool|int>|float|string): Bar|Foo>|Foo<bool|int>}',
  283. ];
  284. yield 'complex type with Closure with $this' => [
  285. 'array<string, string|array{ string|\Closure(mixed, string, $this): (int|float) }>|false',
  286. 'array<string, array{ \Closure(mixed, string, $this): (float|int)|string }|string>|false',
  287. ];
  288. yield 'nullable generic' => [
  289. '?array<Foo|Bar>',
  290. '?array<Bar|Foo>',
  291. ];
  292. yield 'nullable callable' => [
  293. '?callable(Foo|Bar): Foo|Bar',
  294. '?callable(Bar|Foo): Bar|Foo',
  295. ];
  296. yield 'nullable array shape' => [
  297. '?array{0: Foo|Bar}',
  298. '?array{0: Bar|Foo}',
  299. ];
  300. yield 'simple types alternation' => [
  301. 'array<Foo&Bar>',
  302. 'array<Bar&Foo>',
  303. ];
  304. yield 'nesty stuff' => [
  305. 'array<Level11&array<Level2|array<Level31&Level32>>>',
  306. 'array<array<array<Level31&Level32>|Level2>&Level11>',
  307. ];
  308. yield 'parenthesized' => [
  309. '(Foo|Bar)',
  310. '(Bar|Foo)',
  311. ];
  312. yield 'parenthesized intersect' => [
  313. '(Foo&Bar)',
  314. '(Bar&Foo)',
  315. ];
  316. yield 'parenthesized in closure return type' => [
  317. 'Closure(): (string|float)',
  318. 'Closure(): (float|string)',
  319. ];
  320. yield 'conditional with variable' => [
  321. '($x is (CFoo|(CBaz&CBar)) ? (TFoo|(TBaz&TBar)) : (FFoo|(FBaz&FBar)))',
  322. '($x is ((CBar&CBaz)|CFoo) ? ((TBar&TBaz)|TFoo) : ((FBar&FBaz)|FFoo))',
  323. ];
  324. yield 'conditional with type' => [
  325. '((Foo|Bar) is x ? y : z)',
  326. '((Bar|Foo) is x ? y : z)',
  327. ];
  328. yield 'conditional in conditional' => [
  329. '((Foo|Bar) is x ? ($x is (CFoo|CBar) ? (TFoo|TBar) : (FFoo|FBar)) : z)',
  330. '((Bar|Foo) is x ? ($x is (CBar|CFoo) ? (TBar|TFoo) : (FBar|FFoo)) : z)',
  331. ];
  332. }
  333. }