1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Reflection\Php;
4:
5: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod;
6: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter;
7: use PHPStan\Internal\DeprecatedAttributeHelper;
8: use PHPStan\Parser\Parser;
9: use PHPStan\Parser\VariadicMethodsVisitor;
10: use PHPStan\Reflection\Assertions;
11: use PHPStan\Reflection\AttributeReflection;
12: use PHPStan\Reflection\AttributeReflectionFactory;
13: use PHPStan\Reflection\ClassMemberReflection;
14: use PHPStan\Reflection\ClassReflection;
15: use PHPStan\Reflection\ExtendedFunctionVariant;
16: use PHPStan\Reflection\ExtendedMethodReflection;
17: use PHPStan\Reflection\ExtendedParameterReflection;
18: use PHPStan\Reflection\ExtendedParametersAcceptor;
19: use PHPStan\Reflection\InitializerExprContext;
20: use PHPStan\Reflection\InitializerExprTypeResolver;
21: use PHPStan\Reflection\MethodPrototypeReflection;
22: use PHPStan\Reflection\ReflectionProvider;
23: use PHPStan\TrinaryLogic;
24: use PHPStan\Type\ArrayType;
25: use PHPStan\Type\BooleanType;
26: use PHPStan\Type\Generic\TemplateTypeMap;
27: use PHPStan\Type\IntegerType;
28: use PHPStan\Type\MixedType;
29: use PHPStan\Type\ObjectWithoutClassType;
30: use PHPStan\Type\StringType;
31: use PHPStan\Type\ThisType;
32: use PHPStan\Type\Type;
33: use PHPStan\Type\TypehintHelper;
34: use PHPStan\Type\VoidType;
35: use ReflectionException;
36: use function array_key_exists;
37: use function array_map;
38: use function count;
39: use function explode;
40: use function in_array;
41: use function is_array;
42: use function sprintf;
43: use function strtolower;
44: use const PHP_VERSION_ID;
45:
46: /**
47: * @api
48: */
49: final class PhpMethodReflection implements ExtendedMethodReflection
50: {
51:
52: /** @var list<PhpParameterReflection>|null */
53: private ?array $parameters = null;
54:
55: private ?Type $returnType = null;
56:
57: private ?Type $nativeReturnType = null;
58:
59: /** @var list<ExtendedFunctionVariant>|null */
60: private ?array $variants = null;
61:
62: private ?bool $containsVariadicCalls = null;
63:
64: /**
65: * @param Type[] $phpDocParameterTypes
66: * @param Type[] $phpDocParameterOutTypes
67: * @param array<string, TrinaryLogic> $immediatelyInvokedCallableParameters
68: * @param array<string, Type> $phpDocClosureThisTypeParameters
69: * @param list<AttributeReflection> $attributes
70: */
71: public function __construct(
72: private InitializerExprTypeResolver $initializerExprTypeResolver,
73: private ClassReflection $declaringClass,
74: private ?ClassReflection $declaringTrait,
75: private ReflectionMethod $reflection,
76: private ReflectionProvider $reflectionProvider,
77: private AttributeReflectionFactory $attributeReflectionFactory,
78: private Parser $parser,
79: private TemplateTypeMap $templateTypeMap,
80: private array $phpDocParameterTypes,
81: private ?Type $phpDocReturnType,
82: private ?Type $phpDocThrowType,
83: private ?string $deprecatedDescription,
84: private bool $isDeprecated,
85: private bool $isInternal,
86: private bool $isFinal,
87: private ?bool $isPure,
88: private Assertions $asserts,
89: private bool $acceptsNamedArguments,
90: private ?Type $selfOutType,
91: private ?string $phpDocComment,
92: private array $phpDocParameterOutTypes,
93: private array $immediatelyInvokedCallableParameters,
94: private array $phpDocClosureThisTypeParameters,
95: private array $attributes,
96: )
97: {
98: }
99:
100: public function getDeclaringClass(): ClassReflection
101: {
102: return $this->declaringClass;
103: }
104:
105: public function getDeclaringTrait(): ?ClassReflection
106: {
107: return $this->declaringTrait;
108: }
109:
110: /**
111: * @return self|MethodPrototypeReflection
112: */
113: public function getPrototype(): ClassMemberReflection
114: {
115: try {
116: $prototypeMethod = $this->reflection->getPrototype();
117: $prototypeDeclaringClass = $this->declaringClass->getAncestorWithClassName($prototypeMethod->getDeclaringClass()->getName());
118: if ($prototypeDeclaringClass === null) {
119: $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName());
120: }
121:
122: if (!$prototypeDeclaringClass->hasNativeMethod($prototypeMethod->getName())) {
123: return $this;
124: }
125:
126: $tentativeReturnType = null;
127: if ($prototypeMethod->getTentativeReturnType() !== null) {
128: $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType(), selfClass: $prototypeDeclaringClass);
129: }
130:
131: return new MethodPrototypeReflection(
132: $prototypeMethod->getName(),
133: $prototypeDeclaringClass,
134: $prototypeMethod->isStatic(),
135: $prototypeMethod->isPrivate(),
136: $prototypeMethod->isPublic(),
137: $prototypeMethod->isAbstract(),
138: $prototypeMethod->isInternal(),
139: $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(),
140: $tentativeReturnType,
141: );
142: } catch (ReflectionException) {
143: return $this;
144: }
145: }
146:
147: public function isStatic(): bool
148: {
149: return $this->reflection->isStatic();
150: }
151:
152: public function getName(): string
153: {
154: $name = $this->reflection->getName();
155: $lowercaseName = strtolower($name);
156: if ($lowercaseName === $name) {
157: if (PHP_VERSION_ID >= 80000) {
158: return $name;
159: }
160:
161: // fix for https://bugs.php.net/bug.php?id=74939
162: foreach ($this->getDeclaringClass()->getNativeReflection()->getTraitAliases() as $traitTarget) {
163: $correctName = $this->getMethodNameWithCorrectCase($name, $traitTarget);
164: if ($correctName !== null) {
165: $name = $correctName;
166: break;
167: }
168: }
169: }
170:
171: return $name;
172: }
173:
174: private function getMethodNameWithCorrectCase(string $lowercaseMethodName, string $traitTarget): ?string
175: {
176: $trait = explode('::', $traitTarget)[0];
177: $traitReflection = $this->reflectionProvider->getClass($trait)->getNativeReflection();
178: foreach ($traitReflection->getTraitAliases() as $methodAlias => $aliasTraitTarget) {
179: if ($lowercaseMethodName === strtolower($methodAlias)) {
180: return $methodAlias;
181: }
182:
183: $correctName = $this->getMethodNameWithCorrectCase($lowercaseMethodName, $aliasTraitTarget);
184: if ($correctName !== null) {
185: return $correctName;
186: }
187: }
188:
189: return null;
190: }
191:
192: /**
193: * @return list<ExtendedParametersAcceptor>
194: */
195: public function getVariants(): array
196: {
197: if ($this->variants === null) {
198: $this->variants = [
199: new ExtendedFunctionVariant(
200: $this->templateTypeMap,
201: null,
202: $this->getParameters(),
203: $this->isVariadic(),
204: $this->getReturnType(),
205: $this->getPhpDocReturnType(),
206: $this->getNativeReturnType(),
207: ),
208: ];
209: }
210:
211: return $this->variants;
212: }
213:
214: public function getOnlyVariant(): ExtendedParametersAcceptor
215: {
216: return $this->getVariants()[0];
217: }
218:
219: public function getNamedArgumentsVariants(): ?array
220: {
221: return null;
222: }
223:
224: /**
225: * @return list<ExtendedParameterReflection>
226: */
227: private function getParameters(): array
228: {
229: if ($this->parameters === null) {
230: $this->parameters = array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection(
231: $this->initializerExprTypeResolver,
232: $reflection,
233: $this->phpDocParameterTypes[$reflection->getName()] ?? null,
234: $this->getDeclaringClass(),
235: $this->phpDocParameterOutTypes[$reflection->getName()] ?? null,
236: $this->immediatelyInvokedCallableParameters[$reflection->getName()] ?? TrinaryLogic::createMaybe(),
237: $this->phpDocClosureThisTypeParameters[$reflection->getName()] ?? null,
238: $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)),
239: ), $this->reflection->getParameters());
240: }
241:
242: return $this->parameters;
243: }
244:
245: private function isVariadic(): bool
246: {
247: $isNativelyVariadic = $this->reflection->isVariadic();
248: $declaringClass = $this->declaringClass;
249: $filename = $this->declaringClass->getFileName();
250: if ($this->declaringTrait !== null) {
251: $declaringClass = $this->declaringTrait;
252: $filename = $this->declaringTrait->getFileName();
253: }
254:
255: if (!$isNativelyVariadic && $filename !== null && !$this->declaringClass->isBuiltin()) {
256: if ($this->containsVariadicCalls !== null) {
257: return $this->containsVariadicCalls;
258: }
259:
260: $className = $declaringClass->getName();
261: if ($declaringClass->isAnonymous()) {
262: $className = sprintf('%s:%s:%s', VariadicMethodsVisitor::ANONYMOUS_CLASS_PREFIX, $declaringClass->getNativeReflection()->getStartLine(), $declaringClass->getNativeReflection()->getEndLine());
263: }
264: if (array_key_exists($className, VariadicMethodsVisitor::$cache)) {
265: if (array_key_exists($this->reflection->getName(), VariadicMethodsVisitor::$cache[$className])) {
266: return $this->containsVariadicCalls = VariadicMethodsVisitor::$cache[$className][$this->reflection->getName()];
267: }
268:
269: return $this->containsVariadicCalls = false;
270: }
271:
272: $nodes = $this->parser->parseFile($filename);
273: if (count($nodes) > 0) {
274: $variadicMethods = $nodes[0]->getAttribute(VariadicMethodsVisitor::ATTRIBUTE_NAME);
275:
276: if (
277: is_array($variadicMethods)
278: && array_key_exists($className, $variadicMethods)
279: && array_key_exists($this->reflection->getName(), $variadicMethods[$className])
280: ) {
281: return $this->containsVariadicCalls = $variadicMethods[$className][$this->reflection->getName()];
282: }
283: }
284:
285: return $this->containsVariadicCalls = false;
286: }
287:
288: return $isNativelyVariadic;
289: }
290:
291: public function isPrivate(): bool
292: {
293: return $this->reflection->isPrivate();
294: }
295:
296: public function isPublic(): bool
297: {
298: return $this->reflection->isPublic();
299: }
300:
301: private function getReturnType(): Type
302: {
303: if ($this->returnType === null) {
304: $name = strtolower($this->getName());
305: $returnType = $this->reflection->getReturnType();
306: if ($returnType === null) {
307: if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) {
308: return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType);
309: }
310: if ($name === '__tostring') {
311: return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType);
312: }
313: if ($name === '__isset') {
314: return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType);
315: }
316: if ($name === '__sleep') {
317: return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType);
318: }
319: if ($name === '__set_state') {
320: return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType);
321: }
322: }
323:
324: $this->returnType = TypehintHelper::decideTypeFromReflection(
325: $returnType,
326: $this->phpDocReturnType,
327: $this->declaringClass,
328: );
329: }
330:
331: return $this->returnType;
332: }
333:
334: private function getPhpDocReturnType(): Type
335: {
336: if ($this->phpDocReturnType !== null) {
337: return $this->phpDocReturnType;
338: }
339:
340: return new MixedType();
341: }
342:
343: private function getNativeReturnType(): Type
344: {
345: if ($this->nativeReturnType === null) {
346: $this->nativeReturnType = TypehintHelper::decideTypeFromReflection(
347: $this->reflection->getReturnType(),
348: selfClass: $this->declaringClass,
349: );
350: }
351:
352: return $this->nativeReturnType;
353: }
354:
355: public function getDeprecatedDescription(): ?string
356: {
357: if ($this->isDeprecated) {
358: return $this->deprecatedDescription;
359: }
360:
361: if ($this->reflection->isDeprecated()) {
362: $attributes = $this->reflection->getBetterReflection()->getAttributes();
363: return DeprecatedAttributeHelper::getDeprecatedDescription($attributes);
364: }
365:
366: return null;
367: }
368:
369: public function isDeprecated(): TrinaryLogic
370: {
371: if ($this->isDeprecated) {
372: return TrinaryLogic::createYes();
373: }
374:
375: return TrinaryLogic::createFromBoolean($this->reflection->isDeprecated());
376: }
377:
378: public function isInternal(): TrinaryLogic
379: {
380: return TrinaryLogic::createFromBoolean($this->isInternal);
381: }
382:
383: public function isBuiltin(): TrinaryLogic
384: {
385: return TrinaryLogic::createFromBoolean($this->reflection->isInternal());
386: }
387:
388: public function isFinal(): TrinaryLogic
389: {
390: return TrinaryLogic::createFromBoolean($this->isFinal || $this->reflection->isFinal());
391: }
392:
393: public function isFinalByKeyword(): TrinaryLogic
394: {
395: return TrinaryLogic::createFromBoolean($this->reflection->isFinal());
396: }
397:
398: public function isAbstract(): bool
399: {
400: return $this->reflection->isAbstract();
401: }
402:
403: public function getThrowType(): ?Type
404: {
405: return $this->phpDocThrowType;
406: }
407:
408: public function hasSideEffects(): TrinaryLogic
409: {
410: if (
411: strtolower($this->getName()) !== '__construct'
412: && $this->getReturnType()->isVoid()->yes()
413: ) {
414: return TrinaryLogic::createYes();
415: }
416: if ($this->isPure !== null) {
417: return TrinaryLogic::createFromBoolean(!$this->isPure);
418: }
419:
420: if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->getReturnType())->yes()) {
421: return TrinaryLogic::createYes();
422: }
423:
424: return TrinaryLogic::createMaybe();
425: }
426:
427: public function getAsserts(): Assertions
428: {
429: return $this->asserts;
430: }
431:
432: public function acceptsNamedArguments(): TrinaryLogic
433: {
434: return TrinaryLogic::createFromBoolean(
435: $this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments,
436: );
437: }
438:
439: public function getSelfOutType(): ?Type
440: {
441: return $this->selfOutType;
442: }
443:
444: public function getDocComment(): ?string
445: {
446: return $this->phpDocComment;
447: }
448:
449: public function returnsByReference(): TrinaryLogic
450: {
451: return TrinaryLogic::createFromBoolean($this->reflection->returnsReference());
452: }
453:
454: public function isPure(): TrinaryLogic
455: {
456: if ($this->isPure === null) {
457: return TrinaryLogic::createMaybe();
458: }
459:
460: return TrinaryLogic::createFromBoolean($this->isPure);
461: }
462:
463: public function changePropertyGetHookPhpDocType(Type $phpDocType): self
464: {
465: return new self(
466: $this->initializerExprTypeResolver,
467: $this->declaringClass,
468: $this->declaringTrait,
469: $this->reflection,
470: $this->reflectionProvider,
471: $this->attributeReflectionFactory,
472: $this->parser,
473: $this->templateTypeMap,
474: $this->phpDocParameterTypes,
475: $phpDocType,
476: $this->phpDocThrowType,
477: $this->deprecatedDescription,
478: $this->isDeprecated,
479: $this->isInternal,
480: $this->isFinal,
481: $this->isPure,
482: $this->asserts,
483: $this->acceptsNamedArguments,
484: $this->selfOutType,
485: $this->phpDocComment,
486: $this->phpDocParameterOutTypes,
487: $this->immediatelyInvokedCallableParameters,
488: $this->phpDocClosureThisTypeParameters,
489: $this->attributes,
490: );
491: }
492:
493: public function changePropertySetHookPhpDocType(string $parameterName, Type $phpDocType): self
494: {
495: $phpDocParameterTypes = $this->phpDocParameterTypes;
496: $phpDocParameterTypes[$parameterName] = $phpDocType;
497:
498: return new self(
499: $this->initializerExprTypeResolver,
500: $this->declaringClass,
501: $this->declaringTrait,
502: $this->reflection,
503: $this->reflectionProvider,
504: $this->attributeReflectionFactory,
505: $this->parser,
506: $this->templateTypeMap,
507: $phpDocParameterTypes,
508: $this->phpDocReturnType,
509: $this->phpDocThrowType,
510: $this->deprecatedDescription,
511: $this->isDeprecated,
512: $this->isInternal,
513: $this->isFinal,
514: $this->isPure,
515: $this->asserts,
516: $this->acceptsNamedArguments,
517: $this->selfOutType,
518: $this->phpDocComment,
519: $this->phpDocParameterOutTypes,
520: $this->immediatelyInvokedCallableParameters,
521: $this->phpDocClosureThisTypeParameters,
522: $this->attributes,
523: );
524: }
525:
526: public function getAttributes(): array
527: {
528: return $this->attributes;
529: }
530:
531: }
532: