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