No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PhpStanExtractor.php 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\PropertyInfo\Extractor;
  11. use phpDocumentor\Reflection\Types\ContextFactory;
  12. use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
  13. use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
  14. use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
  15. use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
  16. use PHPStan\PhpDocParser\Lexer\Lexer;
  17. use PHPStan\PhpDocParser\Parser\ConstExprParser;
  18. use PHPStan\PhpDocParser\Parser\PhpDocParser;
  19. use PHPStan\PhpDocParser\Parser\TokenIterator;
  20. use PHPStan\PhpDocParser\Parser\TypeParser;
  21. use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory;
  22. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  23. use Symfony\Component\PropertyInfo\Type;
  24. use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
  25. /**
  26. * Extracts data using PHPStan parser.
  27. *
  28. * @author Baptiste Leduc <baptiste.leduc@gmail.com>
  29. */
  30. final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
  31. {
  32. private const PROPERTY = 0;
  33. private const ACCESSOR = 1;
  34. private const MUTATOR = 2;
  35. /** @var PhpDocParser */
  36. private $phpDocParser;
  37. /** @var Lexer */
  38. private $lexer;
  39. /** @var NameScopeFactory */
  40. private $nameScopeFactory;
  41. /** @var array<string, array{PhpDocNode|null, int|null, string|null, string|null}> */
  42. private $docBlocks = [];
  43. private $phpStanTypeHelper;
  44. private $mutatorPrefixes;
  45. private $accessorPrefixes;
  46. private $arrayMutatorPrefixes;
  47. /**
  48. * @param list<string>|null $mutatorPrefixes
  49. * @param list<string>|null $accessorPrefixes
  50. * @param list<string>|null $arrayMutatorPrefixes
  51. */
  52. public function __construct(?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null)
  53. {
  54. if (!class_exists(ContextFactory::class)) {
  55. throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpdocumentor/type-resolver" package is not installed. Try running composer require "phpdocumentor/type-resolver".', __CLASS__));
  56. }
  57. if (!class_exists(PhpDocParser::class)) {
  58. throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpstan/phpdoc-parser" package is not installed. Try running composer require "phpstan/phpdoc-parser".', __CLASS__));
  59. }
  60. $this->phpStanTypeHelper = new PhpStanTypeHelper();
  61. $this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes;
  62. $this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
  63. $this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes;
  64. $this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
  65. $this->lexer = new Lexer();
  66. $this->nameScopeFactory = new NameScopeFactory();
  67. }
  68. public function getTypes(string $class, string $property, array $context = []): ?array
  69. {
  70. /** @var PhpDocNode|null $docNode */
  71. [$docNode, $source, $prefix, $declaringClass] = $this->getDocBlock($class, $property);
  72. $nameScope = $this->nameScopeFactory->create($class, $declaringClass);
  73. if (null === $docNode) {
  74. return null;
  75. }
  76. switch ($source) {
  77. case self::PROPERTY:
  78. $tag = '@var';
  79. break;
  80. case self::ACCESSOR:
  81. $tag = '@return';
  82. break;
  83. case self::MUTATOR:
  84. $tag = '@param';
  85. break;
  86. }
  87. $parentClass = null;
  88. $types = [];
  89. foreach ($docNode->getTagsByName($tag) as $tagDocNode) {
  90. if ($tagDocNode->value instanceof InvalidTagValueNode) {
  91. continue;
  92. }
  93. foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value, $nameScope) as $type) {
  94. switch ($type->getClassName()) {
  95. case 'self':
  96. case 'static':
  97. $resolvedClass = $class;
  98. break;
  99. case 'parent':
  100. if (false !== $resolvedClass = $parentClass ?? $parentClass = get_parent_class($class)) {
  101. break;
  102. }
  103. // no break
  104. default:
  105. $types[] = $type;
  106. continue 2;
  107. }
  108. $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
  109. }
  110. }
  111. if (!isset($types[0])) {
  112. return null;
  113. }
  114. if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) {
  115. return $types;
  116. }
  117. return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
  118. }
  119. public function getTypesFromConstructor(string $class, string $property): ?array
  120. {
  121. if (null === $tagDocNode = $this->getDocBlockFromConstructor($class, $property)) {
  122. return null;
  123. }
  124. $types = [];
  125. foreach ($this->phpStanTypeHelper->getTypes($tagDocNode, $this->nameScopeFactory->create($class)) as $type) {
  126. $types[] = $type;
  127. }
  128. if (!isset($types[0])) {
  129. return null;
  130. }
  131. return $types;
  132. }
  133. private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode
  134. {
  135. try {
  136. $reflectionClass = new \ReflectionClass($class);
  137. } catch (\ReflectionException $e) {
  138. return null;
  139. }
  140. if (null === $reflectionConstructor = $reflectionClass->getConstructor()) {
  141. return null;
  142. }
  143. if (!$rawDocNode = $reflectionConstructor->getDocComment()) {
  144. return null;
  145. }
  146. $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  147. $phpDocNode = $this->phpDocParser->parse($tokens);
  148. $tokens->consumeTokenType(Lexer::TOKEN_END);
  149. return $this->filterDocBlockParams($phpDocNode, $property);
  150. }
  151. private function filterDocBlockParams(PhpDocNode $docNode, string $allowedParam): ?ParamTagValueNode
  152. {
  153. $tags = array_values(array_filter($docNode->getTagsByName('@param'), function ($tagNode) use ($allowedParam) {
  154. return $tagNode instanceof PhpDocTagNode && ('$'.$allowedParam) === $tagNode->value->parameterName;
  155. }));
  156. if (!$tags) {
  157. return null;
  158. }
  159. return $tags[0]->value;
  160. }
  161. /**
  162. * @return array{PhpDocNode|null, int|null, string|null, string|null}
  163. */
  164. private function getDocBlock(string $class, string $property): array
  165. {
  166. $propertyHash = $class.'::'.$property;
  167. if (isset($this->docBlocks[$propertyHash])) {
  168. return $this->docBlocks[$propertyHash];
  169. }
  170. $ucFirstProperty = ucfirst($property);
  171. if ([$docBlock, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
  172. $data = [$docBlock, self::PROPERTY, null, $declaringClass];
  173. } elseif ([$docBlock, $_, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR)) {
  174. $data = [$docBlock, self::ACCESSOR, null, $declaringClass];
  175. } elseif ([$docBlock, $prefix, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR)) {
  176. $data = [$docBlock, self::MUTATOR, $prefix, $declaringClass];
  177. } else {
  178. $data = [null, null, null, null];
  179. }
  180. return $this->docBlocks[$propertyHash] = $data;
  181. }
  182. /**
  183. * @return array{PhpDocNode, string}|null
  184. */
  185. private function getDocBlockFromProperty(string $class, string $property): ?array
  186. {
  187. // Use a ReflectionProperty instead of $class to get the parent class if applicable
  188. try {
  189. $reflectionProperty = new \ReflectionProperty($class, $property);
  190. } catch (\ReflectionException $e) {
  191. return null;
  192. }
  193. if (null === $rawDocNode = $reflectionProperty->getDocComment() ?: null) {
  194. return null;
  195. }
  196. $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  197. $phpDocNode = $this->phpDocParser->parse($tokens);
  198. $tokens->consumeTokenType(Lexer::TOKEN_END);
  199. return [$phpDocNode, $reflectionProperty->class];
  200. }
  201. /**
  202. * @return array{PhpDocNode, string, string}|null
  203. */
  204. private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
  205. {
  206. $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
  207. $prefix = null;
  208. foreach ($prefixes as $prefix) {
  209. $methodName = $prefix.$ucFirstProperty;
  210. try {
  211. $reflectionMethod = new \ReflectionMethod($class, $methodName);
  212. if ($reflectionMethod->isStatic()) {
  213. continue;
  214. }
  215. if (
  216. (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters())
  217. || (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  218. ) {
  219. break;
  220. }
  221. } catch (\ReflectionException $e) {
  222. // Try the next prefix if the method doesn't exist
  223. }
  224. }
  225. if (!isset($reflectionMethod)) {
  226. return null;
  227. }
  228. if (null === $rawDocNode = $reflectionMethod->getDocComment() ?: null) {
  229. return null;
  230. }
  231. $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  232. $phpDocNode = $this->phpDocParser->parse($tokens);
  233. $tokens->consumeTokenType(Lexer::TOKEN_END);
  234. return [$phpDocNode, $prefix, $reflectionMethod->class];
  235. }
  236. }