1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use Closure;
8: use Error;
9: use OutOfBoundsException;
10: use PhpParser\Node;
11: use PhpParser\Node\Stmt\Property as PropertyNode;
12: use PhpParser\NodeTraverser;
13: use PhpParser\NodeVisitor\FindingVisitor;
14: use ReflectionClass as CoreReflectionClass;
15: use ReflectionException;
16: use ReflectionProperty as CoreReflectionProperty;
17: use PHPStan\BetterReflection\NodeCompiler\CompiledValue;
18: use PHPStan\BetterReflection\NodeCompiler\CompileNodeToValue;
19: use PHPStan\BetterReflection\NodeCompiler\CompilerContext;
20: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter;
21: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
22: use PHPStan\BetterReflection\Reflection\Deprecated\DeprecatedHelper;
23: use PHPStan\BetterReflection\Reflection\Exception\ClassDoesNotExist;
24: use PHPStan\BetterReflection\Reflection\Exception\CodeLocationMissing;
25: use PHPStan\BetterReflection\Reflection\Exception\NoObjectProvided;
26: use PHPStan\BetterReflection\Reflection\Exception\NotAnObject;
27: use PHPStan\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass;
28: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionPropertyStringCast;
29: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
30: use PHPStan\BetterReflection\Reflector\Reflector;
31: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
32: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
33: use PHPStan\BetterReflection\Util\ClassExistenceChecker;
34: use PHPStan\BetterReflection\Util\Exception\NoNodePosition;
35: use PHPStan\BetterReflection\Util\GetLastDocComment;
36:
37: use function array_map;
38: use function assert;
39: use function func_num_args;
40: use function is_object;
41: use function sprintf;
42: use function str_contains;
43:
44: /** @psalm-immutable */
45: class ReflectionProperty
46: {
47: private Reflector $reflector;
48: private bool $isPromoted;
49: private bool $declaredAtCompileTime;
50: /** @var non-empty-string */
51: private string $name;
52:
53: /** @var int-mask-of<ReflectionPropertyAdapter::IS_*> */
54: private int $modifiers;
55:
56: /**
57: * @var \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
58: */
59: private $type;
60:
61: /**
62: * @var \PhpParser\Node\Expr|null
63: */
64: private $default;
65:
66: /** @var non-empty-string|null */
67: private $docComment;
68:
69: /** @var list<ReflectionAttribute> */
70: private array $attributes;
71:
72: /** @var positive-int|null */
73: private $startLine;
74:
75: /** @var positive-int|null */
76: private $endLine;
77:
78: /** @var positive-int|null */
79: private $startColumn;
80:
81: /** @var positive-int|null */
82: private $endColumn;
83:
84: private ?ReflectionClass $declaringClass;
85:
86: private ?ReflectionClass $implementingClass;
87:
88: /** @var non-empty-string */
89: private string $declaringClassName;
90:
91: /** @var non-empty-string */
92: private string $implementingClassName;
93:
94: private bool $immediateVirtual;
95:
96: /** @var array{get?: ReflectionMethod, set?: ReflectionMethod} */
97: private array $immediateHooks;
98:
99: /**
100: * @var array{get?: ReflectionMethod, set?: ReflectionMethod}|null
101: * @psalm-allow-private-mutation
102: */
103: private $cachedHooks = null;
104:
105: /** @psalm-allow-private-mutation
106: * @var bool|null */
107: private $cachedVirtual = null;
108:
109: /** @psalm-allow-private-mutation
110: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null */
111: private $compiledDefaultValue = null;
112:
113: private function __construct(
114: Reflector $reflector,
115: PropertyNode $node,
116: Node\PropertyItem $propertyNode,
117: ReflectionClass $declaringClass,
118: ReflectionClass $implementingClass,
119: bool $isPromoted,
120: bool $declaredAtCompileTime
121: ) {
122: $this->reflector = $reflector;
123: $this->isPromoted = $isPromoted;
124: $this->declaredAtCompileTime = $declaredAtCompileTime;
125: $this->declaringClass = $declaringClass;
126: $this->implementingClass = $implementingClass;
127: $this->name = $propertyNode->name->name;
128: $this->modifiers = $this->computeModifiers($node);
129: $this->type = $this->createType($node);
130: $this->default = $propertyNode->default;
131: $this->docComment = GetLastDocComment::forNode($node);
132: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
133: $this->immediateVirtual = $this->computeImmediateVirtual($node);
134: $this->immediateHooks = $this->createImmediateHooks($node);
135:
136: $startLine = $node->getStartLine();
137: if ($startLine === -1) {
138: $startLine = null;
139: }
140:
141: $endLine = $node->getEndLine();
142: if ($endLine === -1) {
143: $endLine = null;
144: }
145:
146: /** @psalm-suppress InvalidPropertyAssignmentValue */
147: $this->startLine = $startLine;
148: /** @psalm-suppress InvalidPropertyAssignmentValue */
149: $this->endLine = $endLine;
150:
151: try {
152: $this->startColumn = CalculateReflectionColumn::getStartColumn($declaringClass->getLocatedSource()->getSource(), $node);
153: } catch (NoNodePosition $exception) {
154: $this->startColumn = null;
155: }
156:
157: try {
158: $this->endColumn = CalculateReflectionColumn::getEndColumn($declaringClass->getLocatedSource()->getSource(), $node);
159: } catch (NoNodePosition $exception) {
160: $this->endColumn = null;
161: }
162:
163: $this->declaringClassName = $this->declaringClass->getName();
164: $this->implementingClassName = $this->implementingClass->getName();
165: }
166:
167: /**
168: * @return array<string, mixed>
169: */
170: public function exportToCache(): array
171: {
172: return [
173: 'declaringClassName' => $this->declaringClassName,
174: 'implementingClassName' => $this->implementingClassName,
175: 'name' => $this->name,
176: 'modifiers' => $this->modifiers,
177: 'type' => $this->type !== null ? ['class' => get_class($this->type), 'data' => $this->type->exportToCache()] : null,
178: 'default' => $this->default !== null ? ExprCacheHelper::export($this->default) : null,
179: 'docComment' => $this->docComment,
180: 'attributes' => array_map(
181: static fn (ReflectionAttribute $attr) => $attr->exportToCache(),
182: $this->attributes,
183: ),
184: 'startLine' => $this->startLine,
185: 'endLine' => $this->endLine,
186: 'startColumn' => $this->startColumn,
187: 'endColumn' => $this->endColumn,
188: 'isPromoted' => $this->isPromoted,
189: 'declaredAtCompileTime' => $this->declaredAtCompileTime,
190: 'immediateVirtual' => $this->immediateVirtual,
191: 'immediateHooks' => array_map(
192: static fn (ReflectionMethod $method) => $method->exportToCache(),
193: $this->immediateHooks,
194: ),
195: ];
196: }
197:
198: /**
199: * @param array<string, mixed> $data
200: */
201: public static function importFromCache(Reflector $reflector, array $data, LocatedSource $locatedSource): self
202: {
203: $reflection = new CoreReflectionClass(self::class);
204: /** @var self $ref */
205: $ref = $reflection->newInstanceWithoutConstructor();
206: $ref->reflector = $reflector;
207: $ref->declaringClassName = $data['declaringClassName'];
208: $ref->implementingClassName = $data['implementingClassName'];
209: $ref->name = $data['name'];
210: $ref->modifiers = $data['modifiers'];
211:
212: if ($data['type'] !== null) {
213: $typeClass = $data['type']['class'];
214: $ref->type = $typeClass::importFromCache($reflector, $data['type']['data'], $ref);
215: } else {
216: $ref->type = null;
217: }
218:
219: if ($data['default'] !== null) {
220: $ref->default = ExprCacheHelper::import($data['default']);
221: } else {
222: $ref->default = null;
223: }
224:
225: $ref->docComment = $data['docComment'];
226: $ref->attributes = array_map(
227: static fn ($attrData) => ReflectionAttribute::importFromCache($reflector, $attrData, $ref),
228: $data['attributes'],
229: );
230: $ref->startLine = $data['startLine'];
231: $ref->endLine = $data['endLine'];
232: $ref->startColumn = $data['startColumn'];
233: $ref->endColumn = $data['endColumn'];
234: $ref->isPromoted = $data['isPromoted'];
235: $ref->declaredAtCompileTime = $data['declaredAtCompileTime'];
236: $ref->immediateVirtual = $data['immediateVirtual'];
237: $ref->immediateHooks = array_map(
238: static fn ($hookData) => ReflectionMethod::importFromCache($reflector, $hookData, $locatedSource, $ref),
239: $data['immediateHooks'],
240: );
241:
242: return $ref;
243: }
244:
245: /**
246: * Create a reflection of an instance's property by its name
247: *
248: * @param non-empty-string $propertyName
249: *
250: * @throws ReflectionException
251: * @throws IdentifierNotFound
252: * @throws OutOfBoundsException
253: */
254: public static function createFromInstance(object $instance, string $propertyName): self
255: {
256: $property = ReflectionClass::createFromInstance($instance)->getProperty($propertyName);
257:
258: if ($property === null) {
259: throw new OutOfBoundsException(sprintf('Could not find property: %s', $propertyName));
260: }
261:
262: return $property;
263: }
264:
265: /** @internal */
266: public function withImplementingClass(ReflectionClass $implementingClass): self
267: {
268: $clone = clone $this;
269: $clone->implementingClass = $implementingClass;
270:
271: if ($clone->type !== null) {
272: $clone->type = $clone->type->withOwner($clone);
273: }
274:
275: $clone->attributes = array_map(static fn (ReflectionAttribute $attribute): ReflectionAttribute => $attribute->withOwner($clone), $this->attributes);
276:
277: $this->compiledDefaultValue = null;
278:
279: return $clone;
280: }
281:
282: /** @return non-empty-string */
283: public function __toString(): string
284: {
285: return ReflectionPropertyStringCast::toString($this);
286: }
287:
288: /**
289: * @internal
290: *
291: * @param PropertyNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
292: */
293: public static function createFromNode(
294: Reflector $reflector,
295: PropertyNode $node,
296: Node\PropertyItem $propertyProperty,
297: ReflectionClass $declaringClass,
298: ReflectionClass $implementingClass,
299: bool $isPromoted = false,
300: bool $declaredAtCompileTime = true
301: ): self {
302: return new self(
303: $reflector,
304: $node,
305: $propertyProperty,
306: $declaringClass,
307: $implementingClass,
308: $isPromoted,
309: $declaredAtCompileTime,
310: );
311: }
312:
313: /**
314: * Has the property been declared at compile-time?
315: *
316: * Note that unless the property is static, this is hard coded to return
317: * true, because we are unable to reflect instances of classes, therefore
318: * we can be sure that all properties are always declared at compile-time.
319: */
320: public function isDefault(): bool
321: {
322: return $this->declaredAtCompileTime;
323: }
324:
325: public function isDynamic(): bool
326: {
327: return ! $this->isDefault();
328: }
329:
330: /**
331: * Get the core-reflection-compatible modifier values.
332: *
333: * @return int-mask-of<ReflectionPropertyAdapter::IS_*>
334: */
335: public function getModifiers(): int
336: {
337: /** @var int-mask-of<ReflectionPropertyAdapter::IS_*> $modifiers */
338: $modifiers = $this->modifiers
339: + ($this->isVirtual() ? ReflectionPropertyAdapter::IS_VIRTUAL_COMPATIBILITY : 0);
340:
341: return $modifiers;
342: }
343:
344: /**
345: * Get the name of the property.
346: *
347: * @return non-empty-string
348: */
349: public function getName(): string
350: {
351: return $this->name;
352: }
353:
354: /**
355: * Is the property private?
356: */
357: public function isPrivate(): bool
358: {
359: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PRIVATE);
360: }
361:
362: public function isPrivateSet(): bool
363: {
364: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY);
365: }
366:
367: /**
368: * Is the property protected?
369: */
370: public function isProtected(): bool
371: {
372: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PROTECTED);
373: }
374:
375: public function isProtectedSet(): bool
376: {
377: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY);
378: }
379:
380: /**
381: * Is the property public?
382: */
383: public function isPublic(): bool
384: {
385: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PUBLIC);
386: }
387:
388: /**
389: * Is the property static?
390: */
391: public function isStatic(): bool
392: {
393: return (bool) ($this->modifiers & CoreReflectionProperty::IS_STATIC);
394: }
395:
396: public function isFinal(): bool
397: {
398: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY);
399: }
400:
401: public function isAbstract(): bool
402: {
403: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY)
404: || $this->getDeclaringClass()->isInterface();
405: }
406:
407: public function isPromoted(): bool
408: {
409: return $this->isPromoted;
410: }
411:
412: public function isInitialized(?object $object = null): bool
413: {
414: if ($object === null && $this->isStatic()) {
415: return ! $this->hasType() || $this->hasDefaultValue();
416: }
417:
418: try {
419: $this->getValue($object);
420:
421: return true;
422:
423: /** @phpstan-ignore catch.neverThrown */
424: } catch (Error $e) {
425: if (strpos($e->getMessage(), 'must not be accessed before initialization') !== false) {
426: return false;
427: }
428:
429: throw $e;
430: }
431: }
432:
433: public function isReadOnly(): bool
434: {
435: return ($this->modifiers & ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY)
436: || $this->getDeclaringClass()->isReadOnly();
437: }
438:
439: public function getDeclaringClass(): ReflectionClass
440: {
441: return $this->declaringClass ??= $this->reflector->reflectClass($this->declaringClassName);
442: }
443:
444: public function getImplementingClass(): ReflectionClass
445: {
446: return $this->implementingClass ??= $this->reflector->reflectClass($this->implementingClassName);
447: }
448:
449: /** @return non-empty-string|null */
450: public function getDocComment(): ?string
451: {
452: return $this->docComment;
453: }
454:
455: public function hasDefaultValue(): bool
456: {
457: return ! $this->hasType() || $this->default !== null;
458: }
459:
460: public function getDefaultValueExpression(): ?\PhpParser\Node\Expr
461: {
462: return $this->default;
463: }
464:
465: /**
466: * Get the default value of the property (as defined before constructor is
467: * called, when the property is defined)
468: * @return mixed
469: */
470: public function getDefaultValue()
471: {
472: if ($this->default === null) {
473: return null;
474: }
475:
476: if ($this->compiledDefaultValue === null) {
477: $this->compiledDefaultValue = (new CompileNodeToValue())->__invoke(
478: $this->default,
479: new CompilerContext(
480: $this->reflector,
481: $this,
482: ),
483: );
484: }
485:
486: /** @psalm-var scalar|array<scalar>|null $value */
487: $value = $this->compiledDefaultValue->value;
488:
489: return $value;
490: }
491:
492: public function isDeprecated(): bool
493: {
494: return DeprecatedHelper::isDeprecated($this);
495: }
496:
497: /**
498: * Get the line number that this property starts on.
499: *
500: * @return positive-int
501: *
502: * @throws CodeLocationMissing
503: */
504: public function getStartLine(): int
505: {
506: if ($this->startLine === null) {
507: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
508: }
509:
510: return $this->startLine;
511: }
512:
513: /**
514: * Get the line number that this property ends on.
515: *
516: * @return positive-int
517: *
518: * @throws CodeLocationMissing
519: */
520: public function getEndLine(): int
521: {
522: if ($this->endLine === null) {
523: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
524: }
525:
526: return $this->endLine;
527: }
528:
529: /**
530: * @return positive-int
531: *
532: * @throws CodeLocationMissing
533: */
534: public function getStartColumn(): int
535: {
536: if ($this->startColumn === null) {
537: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
538: }
539:
540: return $this->startColumn;
541: }
542:
543: /**
544: * @return positive-int
545: *
546: * @throws CodeLocationMissing
547: */
548: public function getEndColumn(): int
549: {
550: if ($this->endColumn === null) {
551: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
552: }
553:
554: return $this->endColumn;
555: }
556:
557: /** @return list<ReflectionAttribute> */
558: public function getAttributes(): array
559: {
560: return $this->attributes;
561: }
562:
563: /** @return list<ReflectionAttribute> */
564: public function getAttributesByName(string $name): array
565: {
566: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
567: }
568:
569: /**
570: * @param class-string $className
571: *
572: * @return list<ReflectionAttribute>
573: */
574: public function getAttributesByInstance(string $className): array
575: {
576: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
577: }
578:
579: /**
580: * @throws ClassDoesNotExist
581: * @throws NoObjectProvided
582: * @throws ObjectNotInstanceOfClass
583: * @return mixed
584: */
585: public function getValue(?object $object = null)
586: {
587: $implementingClassName = $this->getImplementingClass()->getName();
588:
589: if ($this->isStatic()) {
590: $this->assertClassExist($implementingClassName);
591:
592: $closure = Closure::bind(fn (string $implementingClassName, string $propertyName) => $implementingClassName::${$propertyName}, null, $implementingClassName);
593:
594: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
595: assert($closure instanceof Closure);
596:
597: return $closure->__invoke($implementingClassName, $this->getName());
598: }
599:
600: $instance = $this->assertObject($object);
601:
602: $closure = Closure::bind(fn (object $instance, string $propertyName) => $instance->{$propertyName}, $instance, $implementingClassName);
603:
604: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
605: assert($closure instanceof Closure);
606:
607: return $closure->__invoke($instance, $this->getName());
608: }
609:
610: /**
611: * @throws ClassDoesNotExist
612: * @throws NoObjectProvided
613: * @throws NotAnObject
614: * @throws ObjectNotInstanceOfClass
615: * @param mixed $object
616: * @param mixed $value
617: */
618: public function setValue($object, $value = null): void
619: {
620: $implementingClassName = $this->getImplementingClass()->getName();
621:
622: if ($this->isStatic()) {
623: $this->assertClassExist($implementingClassName);
624:
625: $closure = Closure::bind(function (string $_implementingClassName, string $_propertyName, $value): void {
626: /** @psalm-suppress MixedAssignment */
627: $_implementingClassName::${$_propertyName} = $value;
628: }, null, $implementingClassName);
629:
630: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
631: assert($closure instanceof Closure);
632:
633: $closure->__invoke($implementingClassName, $this->getName(), func_num_args() === 2 ? $value : $object);
634:
635: return;
636: }
637:
638: $instance = $this->assertObject($object);
639:
640: $closure = Closure::bind(function (object $instance, string $propertyName, $value): void {
641: $instance->{$propertyName} = $value;
642: }, $instance, $implementingClassName);
643:
644: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
645: assert($closure instanceof Closure);
646:
647: $closure->__invoke($instance, $this->getName(), $value);
648: }
649:
650: /**
651: * Does this property allow null?
652: */
653: public function allowsNull(): bool
654: {
655: return $this->type === null || $this->type->allowsNull();
656: }
657:
658: /**
659: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
660: */
661: private function createType(PropertyNode $node)
662: {
663: $type = $node->type;
664:
665: if ($type === null) {
666: return null;
667: }
668:
669: assert($type instanceof Node\Identifier || $type instanceof Node\Name || $type instanceof Node\NullableType || $type instanceof Node\UnionType || $type instanceof Node\IntersectionType);
670:
671: return ReflectionType::createFromNode($this->reflector, $this, $type);
672: }
673:
674: /**
675: * Get the ReflectionType instance representing the type declaration for
676: * this property
677: *
678: * (note: this has nothing to do with DocBlocks).
679: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
680: */
681: public function getType()
682: {
683: return $this->type;
684: }
685:
686: /**
687: * Does this property have a type declaration?
688: *
689: * (note: this has nothing to do with DocBlocks).
690: */
691: public function hasType(): bool
692: {
693: return $this->type !== null;
694: }
695:
696: public function isVirtual(): bool
697: {
698: $this->cachedVirtual ??= $this->createCachedVirtual();
699:
700: return $this->cachedVirtual;
701: }
702:
703: public function hasHooks(): bool
704: {
705: return $this->getHooks() !== [];
706: }
707:
708: /**
709: * @param ReflectionPropertyHookType::* $hookType
710: */
711: public function hasHook(string $hookType): bool
712: {
713: return isset($this->getHooks()[$hookType]);
714: }
715:
716: /**
717: * @param ReflectionPropertyHookType::* $hookType
718: */
719: public function getHook(string $hookType): ?\PHPStan\BetterReflection\Reflection\ReflectionMethod
720: {
721: return $this->getHooks()[$hookType] ?? null;
722: }
723:
724: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
725: public function getHooks(): array
726: {
727: $this->cachedHooks ??= $this->createCachedHooks();
728:
729: return $this->cachedHooks;
730: }
731:
732: /**
733: * @param class-string $className
734: *
735: * @throws ClassDoesNotExist
736: */
737: private function assertClassExist(string $className): void
738: {
739: if (! ClassExistenceChecker::classExists($className, true) && ! ClassExistenceChecker::traitExists($className, true)) {
740: throw new ClassDoesNotExist('Property cannot be retrieved as the class does not exist');
741: }
742: }
743:
744: /**
745: * @throws NoObjectProvided
746: * @throws NotAnObject
747: * @throws ObjectNotInstanceOfClass
748: *
749: * @psalm-assert object $object
750: * @param mixed $object
751: */
752: private function assertObject($object): object
753: {
754: if ($object === null) {
755: throw NoObjectProvided::create();
756: }
757:
758: if (! is_object($object)) {
759: throw NotAnObject::fromNonObject($object);
760: }
761:
762: $implementingClassName = $this->getImplementingClass()->getName();
763:
764: if (get_class($object) !== $implementingClassName) {
765: throw ObjectNotInstanceOfClass::fromClassName($implementingClassName);
766: }
767:
768: return $object;
769: }
770:
771: /** @return int-mask-of<ReflectionPropertyAdapter::IS_*> */
772: private function computeModifiers(PropertyNode $node): int
773: {
774: $modifiers = $node->isReadonly() ? ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY : 0;
775: $modifiers += $node->isStatic() ? CoreReflectionProperty::IS_STATIC : 0;
776: $modifiers += $node->isPrivate() ? CoreReflectionProperty::IS_PRIVATE : 0;
777: $modifiers += ! $node->isPrivate() && $node->isPrivateSet() ? ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY : 0;
778: $modifiers += $node->isProtected() ? ReflectionPropertyAdapter::IS_PROTECTED : 0;
779: $modifiers += ! $node->isProtected() && $node->isProtectedSet() ? ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY : 0;
780: $modifiers += $node->isPublic() ? ReflectionPropertyAdapter::IS_PUBLIC : 0;
781: $modifiers += $node->isFinal() ? ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY : 0;
782: $modifiers += $node->isAbstract() ? ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY : 0;
783:
784: if (
785: ! ($modifiers & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY)
786: && ($modifiers & ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY)
787: ) {
788: $modifiers += ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY;
789: }
790:
791: if (
792: ! ($modifiers & (ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY))
793: && ! $node->isPublicSet()
794: && $node->isPublic()
795: && (
796: ($modifiers & ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY)
797: || $this->getDeclaringClass()->isReadOnly()
798: )
799: ) {
800: $modifiers += ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY;
801: }
802:
803: /** @phpstan-ignore return.type */
804: return $modifiers;
805: }
806:
807: private function computeImmediateVirtual(PropertyNode $node): bool
808: {
809: if ($node->hooks === []) {
810: return false;
811: }
812:
813: $setHook = null;
814: $getHook = null;
815:
816: foreach ($node->hooks as $hook) {
817: if ($hook->name->name === 'set') {
818: $setHook = $hook;
819: } elseif ($hook->name->name === 'get') {
820: $getHook = $hook;
821: }
822: }
823:
824: if ($setHook !== null && ! $this->computeImmediateVirtualBasedOnHook($setHook)) {
825: return false;
826: }
827:
828: if ($getHook === null) {
829: return true;
830: }
831:
832: return $this->computeImmediateVirtualBasedOnHook($getHook);
833: }
834:
835: private function computeImmediateVirtualBasedOnHook(Node\PropertyHook $hook): bool
836: {
837: $hookBody = $hook->getStmts();
838:
839: // Abstract property or property in interface
840: if ($hookBody === null) {
841: return true;
842: }
843:
844: $visitor = new FindingVisitor(static fn (Node $node): bool => $node instanceof Node\Expr\PropertyFetch);
845: $traverser = new NodeTraverser($visitor);
846: $traverser->traverse($hookBody);
847:
848: foreach ($visitor->getFoundNodes() as $propertyFetchNode) {
849: assert($propertyFetchNode instanceof Node\Expr\PropertyFetch);
850:
851: if (
852: $propertyFetchNode->var instanceof Node\Expr\Variable
853: && $propertyFetchNode->var->name === 'this'
854: && $propertyFetchNode->name instanceof Node\Identifier
855: && $propertyFetchNode->name->name === $this->name
856: ) {
857: return false;
858: }
859: }
860:
861: return true;
862: }
863:
864: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
865: private function createImmediateHooks(PropertyNode $node): array
866: {
867: $hooks = [];
868:
869: foreach ($node->hooks as $hook) {
870: $hookName = $hook->name->name;
871: assert($hookName === 'get' || $hookName === 'set');
872:
873: $hookType = $node->type;
874: assert($hookType === null || $hookType instanceof Node\Identifier || $hookType instanceof Node\Name || $hookType instanceof Node\NullableType || $hookType instanceof Node\UnionType || $hookType instanceof Node\IntersectionType);
875:
876: $hooks[$hookName] = ReflectionMethod::createFromPropertyHook(
877: $this->reflector,
878: $hook,
879: $this->getDeclaringClass()->getLocatedSource(),
880: sprintf('$%s::%s', $this->name, $hookName),
881: $hookType,
882: $this->getDeclaringClass(),
883: $this->getImplementingClass(),
884: $this->getDeclaringClass(),
885: $this,
886: );
887: }
888:
889: return $hooks;
890: }
891:
892: private function createCachedVirtual(): bool
893: {
894: if (! $this->immediateVirtual) {
895: return false;
896: }
897:
898: return (($nullsafeVariable1 = $this->getParentProperty()) ? $nullsafeVariable1->isVirtual() : null) ?? true;
899: }
900:
901: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
902: private function createCachedHooks(): array
903: {
904: $hooks = $this->immediateHooks;
905:
906: // Just optimization - we don't need to check parent property when both hooks are defined in this class
907: if (isset($hooks['get'], $hooks['set'])) {
908: return $hooks;
909: }
910:
911: $parentHooks = (($nullsafeVariable2 = $this->getParentProperty()) ? $nullsafeVariable2->getHooks() : null) ?? [];
912:
913: foreach ($parentHooks as $hookName => $parentHook) {
914: if (isset($hooks[$hookName])) {
915: continue;
916: }
917:
918: $hooks[$hookName] = $parentHook;
919: }
920:
921: return $hooks;
922: }
923:
924: private function getParentProperty(): ?\PHPStan\BetterReflection\Reflection\ReflectionProperty
925: {
926: return ($nullsafeVariable3 = $this->getDeclaringClass()->getParentClass()) ? $nullsafeVariable3->getProperty($this->name) : null;
927: }
928: }
929: