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