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