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