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\ClassMemberReflection;
15: use PHPStan\Reflection\ClassReflection;
16: use PHPStan\Reflection\ExtendedMethodReflection;
17: use PHPStan\Reflection\FunctionVariantWithPhpDocs;
18: use PHPStan\Reflection\InitializerExprTypeResolver;
19: use PHPStan\Reflection\MethodPrototypeReflection;
20: use PHPStan\Reflection\ParameterReflectionWithPhpDocs;
21: use PHPStan\Reflection\ParametersAcceptor;
22: use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
23: use PHPStan\Reflection\ReflectionProvider;
24: use PHPStan\TrinaryLogic;
25: use PHPStan\Type\ArrayType;
26: use PHPStan\Type\BooleanType;
27: use PHPStan\Type\Generic\TemplateTypeMap;
28: use PHPStan\Type\IntegerType;
29: use PHPStan\Type\MixedType;
30: use PHPStan\Type\ObjectWithoutClassType;
31: use PHPStan\Type\StringType;
32: use PHPStan\Type\Type;
33: use PHPStan\Type\TypehintHelper;
34: use PHPStan\Type\VoidType;
35: use ReflectionException;
36: use function array_map;
37: use function explode;
38: use function filemtime;
39: use function is_bool;
40: use function is_file;
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: */
63: public function __construct(
64: private InitializerExprTypeResolver $initializerExprTypeResolver,
65: private ClassReflection $declaringClass,
66: private ?ClassReflection $declaringTrait,
67: private BuiltinMethodReflection $reflection,
68: private ReflectionProvider $reflectionProvider,
69: private Parser $parser,
70: private FunctionCallStatementFinder $functionCallStatementFinder,
71: private Cache $cache,
72: private TemplateTypeMap $templateTypeMap,
73: private array $phpDocParameterTypes,
74: private ?Type $phpDocReturnType,
75: private ?Type $phpDocThrowType,
76: private ?string $deprecatedDescription,
77: private bool $isDeprecated,
78: private bool $isInternal,
79: private bool $isFinal,
80: private ?bool $isPure,
81: )
82: {
83: }
84:
85: public function getDeclaringClass(): ClassReflection
86: {
87: return $this->declaringClass;
88: }
89:
90: public function getDeclaringTrait(): ?ClassReflection
91: {
92: return $this->declaringTrait;
93: }
94:
95: public function getDocComment(): ?string
96: {
97: return $this->reflection->getDocComment();
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->reflection->getParameters());
209: }
210:
211: return $this->parameters;
212: }
213:
214: private function isVariadic(): bool
215: {
216: $isNativelyVariadic = $this->reflection->isVariadic();
217: $declaringClass = $this->declaringClass;
218: $filename = $this->declaringClass->getFileName();
219: if ($this->declaringTrait !== null) {
220: $declaringClass = $this->declaringTrait;
221: $filename = $this->declaringTrait->getFileName();
222: }
223:
224: if (!$isNativelyVariadic && $filename !== null && is_file($filename)) {
225: $modifiedTime = filemtime($filename);
226: if ($modifiedTime === false) {
227: $modifiedTime = time();
228: }
229: $key = sprintf('variadic-method-%s-%s-%s', $declaringClass->getName(), $this->reflection->getName(), $filename);
230: $variableCacheKey = sprintf('%d-v4', $modifiedTime);
231: $cachedResult = $this->cache->load($key, $variableCacheKey);
232: if ($cachedResult === null || !is_bool($cachedResult)) {
233: $nodes = $this->parser->parseFile($filename);
234: $result = $this->callsFuncGetArgs($declaringClass, $nodes);
235: $this->cache->save($key, $variableCacheKey, $result);
236: return $result;
237: }
238:
239: return $cachedResult;
240: }
241:
242: return $isNativelyVariadic;
243: }
244:
245: /**
246: * @param Node[] $nodes
247: */
248: private function callsFuncGetArgs(ClassReflection $declaringClass, array $nodes): bool
249: {
250: foreach ($nodes as $node) {
251: if (
252: $node instanceof Node\Stmt\ClassLike
253: ) {
254: if (!isset($node->namespacedName)) {
255: continue;
256: }
257: if ($declaringClass->getName() !== (string) $node->namespacedName) {
258: continue;
259: }
260: if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) {
261: return true;
262: }
263: continue;
264: }
265:
266: if ($node instanceof ClassMethod) {
267: if ($node->getStmts() === null) {
268: continue; // interface
269: }
270:
271: $methodName = $node->name->name;
272: if ($methodName === $this->reflection->getName()) {
273: return $this->functionCallStatementFinder->findFunctionCallInStatements(ParametersAcceptor::VARIADIC_FUNCTIONS, $node->getStmts()) !== null;
274: }
275:
276: continue;
277: }
278:
279: if ($node instanceof Function_) {
280: continue;
281: }
282:
283: if ($node instanceof Namespace_) {
284: if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) {
285: return true;
286: }
287: continue;
288: }
289:
290: if (!$node instanceof Declare_ || $node->stmts === null) {
291: continue;
292: }
293:
294: if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) {
295: return true;
296: }
297: }
298:
299: return false;
300: }
301:
302: public function isPrivate(): bool
303: {
304: return $this->reflection->isPrivate();
305: }
306:
307: public function isPublic(): bool
308: {
309: return $this->reflection->isPublic();
310: }
311:
312: private function getReturnType(): Type
313: {
314: if ($this->returnType === null) {
315: $name = strtolower($this->getName());
316: if (
317: $name === '__construct'
318: || $name === '__destruct'
319: || $name === '__unset'
320: || $name === '__wakeup'
321: || $name === '__clone'
322: ) {
323: return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType);
324: }
325: if ($name === '__tostring') {
326: return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType);
327: }
328: if ($name === '__isset') {
329: return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType);
330: }
331: if ($name === '__sleep') {
332: return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType);
333: }
334: if ($name === '__set_state') {
335: return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType);
336: }
337:
338: $this->returnType = TypehintHelper::decideTypeFromReflection(
339: $this->reflection->getReturnType(),
340: $this->phpDocReturnType,
341: $this->declaringClass->getName(),
342: );
343: }
344:
345: return $this->returnType;
346: }
347:
348: private function getPhpDocReturnType(): Type
349: {
350: if ($this->phpDocReturnType !== null) {
351: return $this->phpDocReturnType;
352: }
353:
354: return new MixedType();
355: }
356:
357: private function getNativeReturnType(): Type
358: {
359: if ($this->nativeReturnType === null) {
360: $this->nativeReturnType = TypehintHelper::decideTypeFromReflection(
361: $this->reflection->getReturnType(),
362: null,
363: $this->declaringClass->getName(),
364: );
365: }
366:
367: return $this->nativeReturnType;
368: }
369:
370: public function getDeprecatedDescription(): ?string
371: {
372: if ($this->isDeprecated) {
373: return $this->deprecatedDescription;
374: }
375:
376: return null;
377: }
378:
379: public function isDeprecated(): TrinaryLogic
380: {
381: return $this->reflection->isDeprecated()->or(TrinaryLogic::createFromBoolean($this->isDeprecated));
382: }
383:
384: public function isInternal(): TrinaryLogic
385: {
386: return TrinaryLogic::createFromBoolean($this->reflection->isInternal() || $this->isInternal);
387: }
388:
389: public function isFinal(): TrinaryLogic
390: {
391: return TrinaryLogic::createFromBoolean($this->reflection->isFinal() || $this->isFinal);
392: }
393:
394: public function isAbstract(): bool
395: {
396: return $this->reflection->isAbstract();
397: }
398:
399: public function getThrowType(): ?Type
400: {
401: return $this->phpDocThrowType;
402: }
403:
404: public function hasSideEffects(): TrinaryLogic
405: {
406: $name = strtolower($this->getName());
407: $isVoid = $this->getReturnType() instanceof VoidType;
408:
409: if (
410: $name !== '__construct'
411: && $isVoid
412: ) {
413: return TrinaryLogic::createYes();
414: }
415: if ($this->isPure !== null) {
416: return TrinaryLogic::createFromBoolean(!$this->isPure);
417: }
418:
419: return TrinaryLogic::createMaybe();
420: }
421:
422: }
423: