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