Annotation.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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. /**
  17. * This represents an entire annotation from a docblock.
  18. *
  19. * @author Graham Campbell <hello@gjcampbell.co.uk>
  20. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  21. */
  22. final class Annotation
  23. {
  24. /**
  25. * All the annotation tag names with types.
  26. *
  27. * @var list<string>
  28. */
  29. private static array $tags = [
  30. 'method',
  31. 'param',
  32. 'property',
  33. 'property-read',
  34. 'property-write',
  35. 'return',
  36. 'throws',
  37. 'type',
  38. 'var',
  39. ];
  40. /**
  41. * The lines that make up the annotation.
  42. *
  43. * @var array<int, Line>
  44. */
  45. private array $lines;
  46. /**
  47. * The position of the first line of the annotation in the docblock.
  48. *
  49. * @var int
  50. */
  51. private $start;
  52. /**
  53. * The position of the last line of the annotation in the docblock.
  54. *
  55. * @var int
  56. */
  57. private $end;
  58. /**
  59. * The associated tag.
  60. *
  61. * @var null|Tag
  62. */
  63. private $tag;
  64. /**
  65. * Lazy loaded, cached types content.
  66. *
  67. * @var null|string
  68. */
  69. private $typesContent;
  70. /**
  71. * The cached types.
  72. *
  73. * @var null|list<string>
  74. */
  75. private $types;
  76. /**
  77. * @var null|NamespaceAnalysis
  78. */
  79. private $namespace;
  80. /**
  81. * @var list<NamespaceUseAnalysis>
  82. */
  83. private array $namespaceUses;
  84. /**
  85. * Create a new line instance.
  86. *
  87. * @param array<int, Line> $lines
  88. * @param null|NamespaceAnalysis $namespace
  89. * @param list<NamespaceUseAnalysis> $namespaceUses
  90. */
  91. public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
  92. {
  93. $this->lines = array_values($lines);
  94. $this->namespace = $namespace;
  95. $this->namespaceUses = $namespaceUses;
  96. $this->start = array_key_first($lines);
  97. $this->end = array_key_last($lines);
  98. }
  99. /**
  100. * Get the string representation of object.
  101. */
  102. public function __toString(): string
  103. {
  104. return $this->getContent();
  105. }
  106. /**
  107. * Get all the annotation tag names with types.
  108. *
  109. * @return list<string>
  110. */
  111. public static function getTagsWithTypes(): array
  112. {
  113. return self::$tags;
  114. }
  115. /**
  116. * Get the start position of this annotation.
  117. */
  118. public function getStart(): int
  119. {
  120. return $this->start;
  121. }
  122. /**
  123. * Get the end position of this annotation.
  124. */
  125. public function getEnd(): int
  126. {
  127. return $this->end;
  128. }
  129. /**
  130. * Get the associated tag.
  131. */
  132. public function getTag(): Tag
  133. {
  134. if (null === $this->tag) {
  135. $this->tag = new Tag($this->lines[0]);
  136. }
  137. return $this->tag;
  138. }
  139. /**
  140. * @internal
  141. */
  142. public function getTypeExpression(): ?TypeExpression
  143. {
  144. $typesContent = $this->getTypesContent();
  145. return null === $typesContent
  146. ? null
  147. : new TypeExpression($typesContent, $this->namespace, $this->namespaceUses);
  148. }
  149. /**
  150. * @internal
  151. */
  152. public function getVariableName(): ?string
  153. {
  154. $type = preg_quote($this->getTypesContent() ?? '', '/');
  155. $regex = \sprintf(
  156. '/@%s\s+(%s\s*)?(&\s*)?(\.{3}\s*)?(?<variable>\$%s)(?:.*|$)/',
  157. $this->tag->getName(),
  158. $type,
  159. TypeExpression::REGEX_IDENTIFIER
  160. );
  161. if (Preg::match($regex, $this->lines[0]->getContent(), $matches)) {
  162. return $matches['variable'];
  163. }
  164. return null;
  165. }
  166. /**
  167. * Get the types associated with this annotation.
  168. *
  169. * @return list<string>
  170. */
  171. public function getTypes(): array
  172. {
  173. if (null === $this->types) {
  174. $typeExpression = $this->getTypeExpression();
  175. $this->types = null === $typeExpression
  176. ? []
  177. : $typeExpression->getTypes();
  178. }
  179. return $this->types;
  180. }
  181. /**
  182. * Set the types associated with this annotation.
  183. *
  184. * @param list<string> $types
  185. */
  186. public function setTypes(array $types): void
  187. {
  188. $origTypesContent = $this->getTypesContent();
  189. $newTypesContent = implode(
  190. // Fallback to union type is provided for backward compatibility (previously glue was set to `|` by default even when type was not composite)
  191. // @TODO Better handling for cases where type is fixed (original type is not composite, but was made composite during fix)
  192. $this->getTypeExpression()->getTypesGlue() ?? '|',
  193. $types
  194. );
  195. if ($origTypesContent === $newTypesContent) {
  196. return;
  197. }
  198. $pattern = '/'.preg_quote($origTypesContent, '/').'/';
  199. $this->lines[0]->setContent(Preg::replace($pattern, $newTypesContent, $this->lines[0]->getContent(), 1));
  200. $this->clearCache();
  201. }
  202. /**
  203. * Get the normalized types associated with this annotation, so they can easily be compared.
  204. *
  205. * @return list<string>
  206. */
  207. public function getNormalizedTypes(): array
  208. {
  209. $typeExpression = $this->getTypeExpression();
  210. if (null === $typeExpression) {
  211. return [];
  212. }
  213. $normalizedTypeExpression = $typeExpression
  214. ->mapTypes(static fn (TypeExpression $v) => new TypeExpression(strtolower($v->toString()), null, []))
  215. ->sortTypes(static fn (TypeExpression $a, TypeExpression $b) => $a->toString() <=> $b->toString())
  216. ;
  217. return $normalizedTypeExpression->getTypes();
  218. }
  219. /**
  220. * Remove this annotation by removing all its lines.
  221. */
  222. public function remove(): void
  223. {
  224. foreach ($this->lines as $line) {
  225. if ($line->isTheStart() && $line->isTheEnd()) {
  226. // Single line doc block, remove entirely
  227. $line->remove();
  228. } elseif ($line->isTheStart()) {
  229. // Multi line doc block, but start is on the same line as the first annotation, keep only the start
  230. $content = Preg::replace('#(\s*/\*\*).*#', '$1', $line->getContent());
  231. $line->setContent($content);
  232. } elseif ($line->isTheEnd()) {
  233. // Multi line doc block, but end is on the same line as the last annotation, keep only the end
  234. $content = Preg::replace('#(\s*)\S.*(\*/.*)#', '$1$2', $line->getContent());
  235. $line->setContent($content);
  236. } else {
  237. // Multi line doc block, neither start nor end on this line, can be removed safely
  238. $line->remove();
  239. }
  240. }
  241. $this->clearCache();
  242. }
  243. /**
  244. * Get the annotation content.
  245. */
  246. public function getContent(): string
  247. {
  248. return implode('', $this->lines);
  249. }
  250. public function supportTypes(): bool
  251. {
  252. return \in_array($this->getTag()->getName(), self::$tags, true);
  253. }
  254. /**
  255. * Get the current types content.
  256. *
  257. * Be careful modifying the underlying line as that won't flush the cache.
  258. */
  259. private function getTypesContent(): ?string
  260. {
  261. if (null === $this->typesContent) {
  262. $name = $this->getTag()->getName();
  263. if (!$this->supportTypes()) {
  264. throw new \RuntimeException('This tag does not support types.');
  265. }
  266. $matchingResult = Preg::match(
  267. '{^(?:\h*\*|/\*\*)[\h*]*@'.$name.'\h+'.TypeExpression::REGEX_TYPES.'(?:(?:[*\h\v]|\&?[\.\$]).*)?\r?$}is',
  268. $this->lines[0]->getContent(),
  269. $matches
  270. );
  271. $this->typesContent = $matchingResult
  272. ? $matches['types']
  273. : null;
  274. }
  275. return $this->typesContent;
  276. }
  277. private function clearCache(): void
  278. {
  279. $this->types = null;
  280. $this->typesContent = null;
  281. }
  282. }