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