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