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