TypeExpression.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  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\DocBlock;
  13. use PhpCsFixer\Preg;
  14. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
  15. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
  16. use PhpCsFixer\Utils;
  17. /**
  18. * @internal
  19. */
  20. final class TypeExpression
  21. {
  22. /**
  23. * Regex to match any PHP identifier.
  24. *
  25. * @internal
  26. */
  27. public const REGEX_IDENTIFIER = '(?:(?!(?<!\*)\d)[^\x00-\x2f\x3a-\x40\x5b-\x5e\x60\x7b-\x7f]++)';
  28. /**
  29. * Regex to match any PHPDoc type.
  30. *
  31. * @internal
  32. */
  33. public const REGEX_TYPES = '(?<types>(?x) # one or several types separated by `|` or `&`
  34. '.self::REGEX_TYPE.'
  35. (?:
  36. \h*(?<glue>[|&])\h*
  37. (?&type)
  38. )*+
  39. )';
  40. private const REGEX_TYPE = '(?<type>(?x) # single type
  41. (?<nullable>\??\h*)
  42. (?:
  43. (?<array_shape>
  44. (?<array_shape_start>(?i)(?:array|list|object)(?-i)\h*\{\h*)
  45. (?<array_shape_inners>
  46. (?<array_shape_inner>
  47. (?<array_shape_inner_key>(?:(?&constant)|(?&identifier))\h*\??\h*:\h*|)
  48. (?<array_shape_inner_value>(?&types_inner))
  49. )
  50. (?:
  51. \h*,\h*
  52. (?&array_shape_inner)
  53. )*
  54. (?:\h*,\h*)?
  55. |)
  56. \h*\}
  57. )
  58. |
  59. (?<callable> # callable syntax, e.g. `callable(string, int...): bool`
  60. (?<callable_start>(?&name)\h*\(\h*)
  61. (?<callable_arguments>
  62. (?<callable_argument>
  63. (?<callable_argument_type>(?&types_inner))
  64. (?<callable_argument_is_reference>\h*&|)
  65. (?<callable_argument_is_variadic>\h*\.\.\.|)
  66. (?<callable_argument_name>\h*\$(?&identifier)|)
  67. (?<callable_argument_is_optional>\h*=|)
  68. )
  69. (?:
  70. \h*,\h*
  71. (?&callable_argument)
  72. )*
  73. (?:\h*,\h*)?
  74. |)
  75. \h*\)
  76. (?:
  77. \h*\:\h*
  78. (?<callable_return>(?&type))
  79. )?
  80. )
  81. |
  82. (?<generic> # generic syntax, e.g.: `array<int, \Foo\Bar>`
  83. (?<generic_start>(?&name)\h*<\h*)
  84. (?<generic_types>
  85. (?&types_inner)
  86. (?:
  87. \h*,\h*
  88. (?&types_inner)
  89. )*
  90. (?:\h*,\h*)?
  91. )
  92. \h*>
  93. )
  94. |
  95. (?<class_constant> # class constants with optional wildcard, e.g.: `Foo::*`, `Foo::CONST_A`, `FOO::CONST_*`
  96. (?&name)::\*?(?:(?&identifier)\*?)*
  97. )
  98. |
  99. (?<constant> # single constant value (case insensitive), e.g.: 1, -1.8E+6, `\'a\'`
  100. (?i)
  101. # all sorts of numbers: with or without sign, supports literal separator and several numeric systems,
  102. # e.g.: 1, +1.1, 1., .1, -1, 123E+8, 123_456_789, 0x7Fb4, 0b0110, 0o777
  103. [+-]?(?:
  104. (?:0b[01]++(?:_[01]++)*+)
  105. | (?:0o[0-7]++(?:_[0-7]++)*+)
  106. | (?:0x[\da-f]++(?:_[\da-f]++)*+)
  107. | (?:(?<constant_digits>\d++(?:_\d++)*+)|(?=\.\d))
  108. (?:\.(?&constant_digits)|(?<=\d)\.)?+
  109. (?:e[+-]?(?&constant_digits))?+
  110. )
  111. | \'(?:[^\'\\\\]|\\\\.)*+\'
  112. | "(?:[^"\\\\]|\\\\.)*+"
  113. (?-i)
  114. )
  115. |
  116. (?<this> # self reference, e.g.: $this, $self, @static
  117. (?i)
  118. [@$](?:this | self | static)
  119. (?-i)
  120. )
  121. |
  122. (?<name> # full name, e.g.: `int`, `\DateTime`, `\Foo\Bar`, `positive-int`
  123. \\\\?+
  124. (?<identifier>'.self::REGEX_IDENTIFIER.')
  125. (?:[\\\\\-](?&identifier))*+
  126. )
  127. |
  128. (?<parenthesized> # parenthesized type, e.g.: `(int)`, `(int|\stdClass)`
  129. (?<parenthesized_start>
  130. \(\h*
  131. )
  132. (?:
  133. (?<parenthesized_types>
  134. (?&types_inner)
  135. )
  136. |
  137. (?<conditional> # conditional type, e.g.: `$foo is \Throwable ? false : $foo`
  138. (?<conditional_cond_left>
  139. (?:\$(?&identifier))
  140. |
  141. (?<conditional_cond_left_types>(?&types_inner))
  142. )
  143. (?<conditional_cond_middle>
  144. \h+(?i)is(?:\h+not)?(?-i)\h+
  145. )
  146. (?<conditional_cond_right_types>(?&types_inner))
  147. (?<conditional_true_start>\h*\?\h*)
  148. (?<conditional_true_types>(?&types_inner))
  149. (?<conditional_false_start>\h*:\h*)
  150. (?<conditional_false_types>(?&types_inner))
  151. )
  152. )
  153. \h*\)
  154. )
  155. )
  156. (?<array> # array, e.g.: `string[]`, `array<int, string>[][]`
  157. (\h*\[\h*\])*
  158. )
  159. (?:(?=1)0
  160. (?<types_inner>
  161. (?&type)
  162. (?:
  163. \h*[|&]\h*
  164. (?&type)
  165. )*+
  166. )
  167. |)
  168. )';
  169. private string $value;
  170. private bool $isUnionType = false;
  171. private string $typesGlue = '|';
  172. /**
  173. * @var list<array{start_index: int, expression: self}>
  174. */
  175. private array $innerTypeExpressions = [];
  176. private ?NamespaceAnalysis $namespace;
  177. /**
  178. * @var NamespaceUseAnalysis[]
  179. */
  180. private array $namespaceUses;
  181. /**
  182. * @param NamespaceUseAnalysis[] $namespaceUses
  183. */
  184. public function __construct(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses)
  185. {
  186. $this->value = $value;
  187. $this->namespace = $namespace;
  188. $this->namespaceUses = $namespaceUses;
  189. $this->parse();
  190. }
  191. public function toString(): string
  192. {
  193. return $this->value;
  194. }
  195. /**
  196. * @return string[]
  197. */
  198. public function getTypes(): array
  199. {
  200. if ($this->isUnionType) {
  201. return array_map(
  202. static fn (array $type) => $type['expression']->toString(),
  203. $this->innerTypeExpressions,
  204. );
  205. }
  206. return [$this->value];
  207. }
  208. public function isUnionType(): bool
  209. {
  210. return $this->isUnionType;
  211. }
  212. public function getTypesGlue(): string
  213. {
  214. return $this->typesGlue;
  215. }
  216. /**
  217. * @param \Closure(self): void $callback
  218. */
  219. public function walkTypes(\Closure $callback): void
  220. {
  221. foreach ($this->innerTypeExpressions as [
  222. 'start_index' => $startIndex,
  223. 'expression' => $inner,
  224. ]) {
  225. $initialValueLength = \strlen($inner->toString());
  226. $inner->walkTypes($callback);
  227. $this->value = substr_replace(
  228. $this->value,
  229. $inner->toString(),
  230. $startIndex,
  231. $initialValueLength
  232. );
  233. }
  234. $callback($this);
  235. }
  236. /**
  237. * @param \Closure(self, self): (-1|0|1) $compareCallback
  238. */
  239. public function sortTypes(\Closure $compareCallback): void
  240. {
  241. $this->walkTypes(static function (self $type) use ($compareCallback): void {
  242. if ($type->isUnionType) {
  243. $type->innerTypeExpressions = Utils::stableSort(
  244. $type->innerTypeExpressions,
  245. static fn (array $type): self => $type['expression'],
  246. $compareCallback,
  247. );
  248. $type->value = implode($type->getTypesGlue(), $type->getTypes());
  249. }
  250. });
  251. }
  252. public function getCommonType(): ?string
  253. {
  254. $aliases = $this->getAliases();
  255. $mainType = null;
  256. foreach ($this->getTypes() as $type) {
  257. if ('null' === $type) {
  258. continue;
  259. }
  260. if (str_starts_with($type, '?')) {
  261. $type = substr($type, 1);
  262. }
  263. if (Preg::match('/\[\h*\]$/', $type)) {
  264. $type = 'array';
  265. } elseif (Preg::match('/^(.+?)\h*[<{(]/', $type, $matches)) {
  266. $type = $matches[1];
  267. }
  268. if (isset($aliases[$type])) {
  269. $type = $aliases[$type];
  270. }
  271. if (null === $mainType || $type === $mainType) {
  272. $mainType = $type;
  273. continue;
  274. }
  275. $mainType = $this->getParentType($type, $mainType);
  276. if (null === $mainType) {
  277. return null;
  278. }
  279. }
  280. return $mainType;
  281. }
  282. public function allowsNull(): bool
  283. {
  284. foreach ($this->getTypes() as $type) {
  285. if (\in_array($type, ['null', 'mixed'], true) || str_starts_with($type, '?')) {
  286. return true;
  287. }
  288. }
  289. return false;
  290. }
  291. private function parse(): void
  292. {
  293. $index = 0;
  294. while (true) {
  295. Preg::match(
  296. '{\G'.self::REGEX_TYPE.'(?:\h*(?<glue>[|&])\h*|$)}',
  297. $this->value,
  298. $matches,
  299. PREG_OFFSET_CAPTURE,
  300. $index
  301. );
  302. if ([] === $matches) {
  303. throw new \Exception('Unable to parse phpdoc type '.var_export($this->value, true));
  304. }
  305. if (!$this->isUnionType) {
  306. if (($matches['glue'][0] ?? '') === '') {
  307. break;
  308. }
  309. $this->isUnionType = true;
  310. $this->typesGlue = $matches['glue'][0];
  311. }
  312. $this->innerTypeExpressions[] = [
  313. 'start_index' => $index,
  314. 'expression' => $this->inner($matches['type'][0]),
  315. ];
  316. $consumedValueLength = \strlen($matches[0][0]);
  317. $index += $consumedValueLength;
  318. if (\strlen($this->value) === $index) {
  319. return;
  320. }
  321. }
  322. $nullableLength = \strlen($matches['nullable'][0]);
  323. $index = $nullableLength;
  324. if ('' !== ($matches['generic'][0] ?? '') && $matches['generic'][1] === $nullableLength) {
  325. $this->parseCommaSeparatedInnerTypes(
  326. $index + \strlen($matches['generic_start'][0]),
  327. $matches['generic_types'][0]
  328. );
  329. } elseif ('' !== ($matches['callable'][0] ?? '') && $matches['callable'][1] === $nullableLength) {
  330. $this->parseCallableArgumentTypes(
  331. $index + \strlen($matches['callable_start'][0]),
  332. $matches['callable_arguments'][0]
  333. );
  334. if ('' !== ($matches['callable_return'][0] ?? '')) {
  335. $this->innerTypeExpressions[] = [
  336. 'start_index' => \strlen($this->value) - \strlen($matches['callable_return'][0]),
  337. 'expression' => $this->inner($matches['callable_return'][0]),
  338. ];
  339. }
  340. } elseif ('' !== ($matches['array_shape'][0] ?? '') && $matches['array_shape'][1] === $nullableLength) {
  341. $this->parseArrayShapeInnerTypes(
  342. $index + \strlen($matches['array_shape_start'][0]),
  343. $matches['array_shape_inners'][0]
  344. );
  345. } elseif ('' !== ($matches['parenthesized'][0] ?? '') && $matches['parenthesized'][1] === $nullableLength) {
  346. $index += \strlen($matches['parenthesized_start'][0]);
  347. if ('' !== ($matches['conditional'][0] ?? '')) {
  348. if ('' !== ($matches['conditional_cond_left_types'][0] ?? '')) {
  349. $this->innerTypeExpressions[] = [
  350. 'start_index' => $index,
  351. 'expression' => $this->inner($matches['conditional_cond_left_types'][0]),
  352. ];
  353. }
  354. $index += \strlen($matches['conditional_cond_left'][0]) + \strlen($matches['conditional_cond_middle'][0]);
  355. $this->innerTypeExpressions[] = [
  356. 'start_index' => $index,
  357. 'expression' => $this->inner($matches['conditional_cond_right_types'][0]),
  358. ];
  359. $index += \strlen($matches['conditional_cond_right_types'][0]) + \strlen($matches['conditional_true_start'][0]);
  360. $this->innerTypeExpressions[] = [
  361. 'start_index' => $index,
  362. 'expression' => $this->inner($matches['conditional_true_types'][0]),
  363. ];
  364. $index += \strlen($matches['conditional_true_types'][0]) + \strlen($matches['conditional_false_start'][0]);
  365. $this->innerTypeExpressions[] = [
  366. 'start_index' => $index,
  367. 'expression' => $this->inner($matches['conditional_false_types'][0]),
  368. ];
  369. } else {
  370. $this->innerTypeExpressions[] = [
  371. 'start_index' => $index,
  372. 'expression' => $this->inner($matches['parenthesized_types'][0]),
  373. ];
  374. }
  375. }
  376. }
  377. private function parseCommaSeparatedInnerTypes(int $startIndex, string $value): void
  378. {
  379. $index = 0;
  380. while (\strlen($value) !== $index) {
  381. Preg::match(
  382. '{\G'.self::REGEX_TYPES.'(?:\h*,\h*|$)}',
  383. $value,
  384. $matches,
  385. 0,
  386. $index
  387. );
  388. $this->innerTypeExpressions[] = [
  389. 'start_index' => $startIndex + $index,
  390. 'expression' => $this->inner($matches['types']),
  391. ];
  392. $index += \strlen($matches[0]);
  393. }
  394. }
  395. private function parseCallableArgumentTypes(int $startIndex, string $value): void
  396. {
  397. $index = 0;
  398. while (\strlen($value) !== $index) {
  399. Preg::match(
  400. '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_argument>(?&callable_argument))(?:\h*,\h*|$))}',
  401. $value,
  402. $prematches,
  403. 0,
  404. $index
  405. );
  406. $consumedValue = $prematches['_callable_argument'];
  407. $consumedValueLength = \strlen($consumedValue);
  408. $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
  409. $addedPrefix = 'Closure(';
  410. Preg::match(
  411. '{^'.self::REGEX_TYPES.'$}',
  412. $addedPrefix.$consumedValue.'): void',
  413. $matches,
  414. PREG_OFFSET_CAPTURE
  415. );
  416. $this->innerTypeExpressions[] = [
  417. 'start_index' => $startIndex + $index,
  418. 'expression' => $this->inner($matches['callable_argument_type'][0]),
  419. ];
  420. $index += $consumedValueLength + $consumedCommaLength;
  421. }
  422. }
  423. private function parseArrayShapeInnerTypes(int $startIndex, string $value): void
  424. {
  425. $index = 0;
  426. while (\strlen($value) !== $index) {
  427. Preg::match(
  428. '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_array_shape_inner>(?&array_shape_inner))(?:\h*,\h*|$))}',
  429. $value,
  430. $prematches,
  431. 0,
  432. $index
  433. );
  434. $consumedValue = $prematches['_array_shape_inner'];
  435. $consumedValueLength = \strlen($consumedValue);
  436. $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
  437. $addedPrefix = 'array{';
  438. Preg::match(
  439. '{^'.self::REGEX_TYPES.'$}',
  440. $addedPrefix.$consumedValue.'}',
  441. $matches,
  442. PREG_OFFSET_CAPTURE
  443. );
  444. $this->innerTypeExpressions[] = [
  445. 'start_index' => $startIndex + $index + $matches['array_shape_inner_value'][1] - \strlen($addedPrefix),
  446. 'expression' => $this->inner($matches['array_shape_inner_value'][0]),
  447. ];
  448. $index += $consumedValueLength + $consumedCommaLength;
  449. }
  450. }
  451. private function inner(string $value): self
  452. {
  453. return new self($value, $this->namespace, $this->namespaceUses);
  454. }
  455. private function getParentType(string $type1, string $type2): ?string
  456. {
  457. $types = [
  458. $this->normalize($type1),
  459. $this->normalize($type2),
  460. ];
  461. natcasesort($types);
  462. $types = implode('|', $types);
  463. $parents = [
  464. 'array|Traversable' => 'iterable',
  465. 'array|iterable' => 'iterable',
  466. 'iterable|Traversable' => 'iterable',
  467. 'self|static' => 'self',
  468. ];
  469. return $parents[$types] ?? null;
  470. }
  471. private function normalize(string $type): string
  472. {
  473. $aliases = $this->getAliases();
  474. if (isset($aliases[$type])) {
  475. return $aliases[$type];
  476. }
  477. if (\in_array($type, [
  478. 'array',
  479. 'bool',
  480. 'callable',
  481. 'false',
  482. 'float',
  483. 'int',
  484. 'iterable',
  485. 'mixed',
  486. 'never',
  487. 'null',
  488. 'object',
  489. 'resource',
  490. 'string',
  491. 'true',
  492. 'void',
  493. ], true)) {
  494. return $type;
  495. }
  496. if (Preg::match('/\[\]$/', $type)) {
  497. return 'array';
  498. }
  499. if (Preg::match('/^(.+?)</', $type, $matches)) {
  500. return $matches[1];
  501. }
  502. if (str_starts_with($type, '\\')) {
  503. return substr($type, 1);
  504. }
  505. foreach ($this->namespaceUses as $namespaceUse) {
  506. if ($namespaceUse->getShortName() === $type) {
  507. return $namespaceUse->getFullName();
  508. }
  509. }
  510. if (null === $this->namespace || $this->namespace->isGlobalNamespace()) {
  511. return $type;
  512. }
  513. return "{$this->namespace->getFullName()}\\{$type}";
  514. }
  515. /**
  516. * @return array<string, string>
  517. */
  518. private function getAliases(): array
  519. {
  520. return [
  521. 'boolean' => 'bool',
  522. 'callback' => 'callable',
  523. 'double' => 'float',
  524. 'false' => 'bool',
  525. 'integer' => 'int',
  526. 'list' => 'array',
  527. 'real' => 'float',
  528. 'true' => 'bool',
  529. ];
  530. }
  531. }