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