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