1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Node;
4:
5: use PhpParser\Node;
6: use PhpParser\Node\Expr\Array_;
7: use PhpParser\Node\Expr\PropertyFetch;
8: use PhpParser\Node\Identifier;
9: use PhpParser\Node\Name;
10: use PhpParser\Node\Stmt\Class_;
11: use PhpParser\Node\Stmt\ClassLike;
12: use PhpParser\NodeAbstract;
13: use PHPStan\Analyser\Scope;
14: use PHPStan\Node\Method\MethodCall;
15: use PHPStan\Node\Property\PropertyRead;
16: use PHPStan\Node\Property\PropertyWrite;
17: use PHPStan\Reflection\MethodReflection;
18: use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
19: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
20: use PHPStan\ShouldNotHappenException;
21: use PHPStan\Type\MixedType;
22: use PHPStan\Type\ObjectType;
23: use function array_key_exists;
24: use function array_keys;
25: use function count;
26: use function in_array;
27:
28: /** @api */
29: class ClassPropertiesNode extends NodeAbstract implements VirtualNode
30: {
31:
32: /**
33: * @param ClassPropertyNode[] $properties
34: * @param array<int, PropertyRead|PropertyWrite> $propertyUsages
35: * @param array<int, MethodCall> $methodCalls
36: */
37: public function __construct(
38: private ClassLike $class,
39: private ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
40: private array $properties,
41: private array $propertyUsages,
42: private array $methodCalls,
43: )
44: {
45: parent::__construct($class->getAttributes());
46: }
47:
48: public function getClass(): ClassLike
49: {
50: return $this->class;
51: }
52:
53: /**
54: * @return ClassPropertyNode[]
55: */
56: public function getProperties(): array
57: {
58: return $this->properties;
59: }
60:
61: /**
62: * @return array<int, PropertyRead|PropertyWrite>
63: */
64: public function getPropertyUsages(): array
65: {
66: return $this->propertyUsages;
67: }
68:
69: public function getType(): string
70: {
71: return 'PHPStan_Node_ClassPropertiesNode';
72: }
73:
74: /**
75: * @return string[]
76: */
77: public function getSubNodeNames(): array
78: {
79: return [];
80: }
81:
82: /**
83: * @param string[] $constructors
84: * @param ReadWritePropertiesExtension[]|null $extensions
85: * @return array{array<string, ClassPropertyNode>, array<array{string, int, ClassPropertyNode}>, array<array{string, int, ClassPropertyNode}>}
86: */
87: public function getUninitializedProperties(
88: Scope $scope,
89: array $constructors,
90: ?array $extensions = null,
91: ): array
92: {
93: if (!$this->getClass() instanceof Class_) {
94: return [[], [], []];
95: }
96: if (!$scope->isInClass()) {
97: throw new ShouldNotHappenException();
98: }
99: $classReflection = $scope->getClassReflection();
100:
101: $properties = [];
102: foreach ($this->getProperties() as $property) {
103: if ($property->isStatic()) {
104: continue;
105: }
106: if ($property->getNativeType() === null) {
107: continue;
108: }
109: if ($property->getDefault() !== null) {
110: continue;
111: }
112: $properties[$property->getName()] = $property;
113: }
114:
115: if ($extensions === null) {
116: $extensions = $this->readWritePropertiesExtensionProvider->getExtensions();
117: }
118:
119: foreach (array_keys($properties) as $name) {
120: foreach ($extensions as $extension) {
121: if (!$classReflection->hasNativeProperty($name)) {
122: continue;
123: }
124: $propertyReflection = $classReflection->getNativeProperty($name);
125: if (!$extension->isInitialized($propertyReflection, $name)) {
126: continue;
127: }
128: unset($properties[$name]);
129: break;
130: }
131: }
132:
133: if ($constructors === []) {
134: return [$properties, [], []];
135: }
136: $classType = new ObjectType($scope->getClassReflection()->getName());
137: $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classType, $this->methodCalls, $constructors);
138: $prematureAccess = [];
139: $additionalAssigns = [];
140: $originalProperties = $properties;
141: foreach ($this->getPropertyUsages() as $usage) {
142: $fetch = $usage->getFetch();
143: if (!$fetch instanceof PropertyFetch) {
144: continue;
145: }
146: $usageScope = $usage->getScope();
147: if ($usageScope->getFunction() === null) {
148: continue;
149: }
150: $function = $usageScope->getFunction();
151: if (!$function instanceof MethodReflection) {
152: continue;
153: }
154: if ($function->getDeclaringClass()->getName() !== $classReflection->getName()) {
155: continue;
156: }
157: if (!in_array($function->getName(), $methodsCalledFromConstructor, true)) {
158: continue;
159: }
160:
161: if (!$fetch->name instanceof Identifier) {
162: continue;
163: }
164: $propertyName = $fetch->name->toString();
165: $fetchedOnType = $usageScope->getType($fetch->var);
166: if ($classType->isSuperTypeOf($fetchedOnType)->no()) {
167: continue;
168: }
169: if ($fetchedOnType instanceof MixedType) {
170: continue;
171: }
172:
173: if ($usage instanceof PropertyWrite) {
174: if (array_key_exists($propertyName, $properties)) {
175: unset($properties[$propertyName]);
176: } elseif (array_key_exists($propertyName, $originalProperties)) {
177: $additionalAssigns[] = [
178: $propertyName,
179: $fetch->getLine(),
180: $originalProperties[$propertyName],
181: ];
182: }
183: } elseif (array_key_exists($propertyName, $properties)) {
184: $prematureAccess[] = [
185: $propertyName,
186: $fetch->getLine(),
187: $properties[$propertyName],
188: ];
189: }
190: }
191:
192: return [
193: $properties,
194: $prematureAccess,
195: $additionalAssigns,
196: ];
197: }
198:
199: /**
200: * @param MethodCall[] $methodCalls
201: * @param string[] $methods
202: * @return string[]
203: */
204: private function getMethodsCalledFromConstructor(
205: ObjectType $classType,
206: array $methodCalls,
207: array $methods,
208: ): array
209: {
210: $originalCount = count($methods);
211: foreach ($methodCalls as $methodCall) {
212: $methodCallNode = $methodCall->getNode();
213: if ($methodCallNode instanceof Array_) {
214: continue;
215: }
216: if (!$methodCallNode->name instanceof Identifier) {
217: continue;
218: }
219: $callScope = $methodCall->getScope();
220: if ($methodCallNode instanceof Node\Expr\MethodCall) {
221: $calledOnType = $callScope->getType($methodCallNode->var);
222: } else {
223: if (!$methodCallNode->class instanceof Name) {
224: continue;
225: }
226:
227: $calledOnType = $callScope->resolveTypeByName($methodCallNode->class);
228: }
229: if ($classType->isSuperTypeOf($calledOnType)->no()) {
230: continue;
231: }
232: if ($calledOnType instanceof MixedType) {
233: continue;
234: }
235: $methodName = $methodCallNode->name->toString();
236: if (in_array($methodName, $methods, true)) {
237: continue;
238: }
239: $inMethod = $callScope->getFunction();
240: if (!$inMethod instanceof MethodReflection) {
241: continue;
242: }
243: if (!in_array($inMethod->getName(), $methods, true)) {
244: continue;
245: }
246: $methods[] = $methodName;
247: }
248:
249: if ($originalCount === count($methods)) {
250: return $methods;
251: }
252:
253: return $this->getMethodsCalledFromConstructor($classType, $methodCalls, $methods);
254: }
255:
256: }
257: