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