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