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