PhpStanExtractor.php 9.5 KB

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