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