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: | |
29: | class ClassPropertiesNode extends NodeAbstract implements VirtualNode |
30: | { |
31: | |
32: | |
33: | |
34: | |
35: | |
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: | |
55: | |
56: | public function getProperties(): array |
57: | { |
58: | return $this->properties; |
59: | } |
60: | |
61: | |
62: | |
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: | |
76: | |
77: | public function getSubNodeNames(): array |
78: | { |
79: | return []; |
80: | } |
81: | |
82: | |
83: | |
84: | |
85: | |
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: | |
201: | |
202: | |
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: | |