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\Expr\PropertyInitializationExpr;
15: use PHPStan\Node\Method\MethodCall;
16: use PHPStan\Node\Property\PropertyRead;
17: use PHPStan\Node\Property\PropertyWrite;
18: use PHPStan\Reflection\ClassReflection;
19: use PHPStan\Reflection\MethodReflection;
20: use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
21: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
22: use PHPStan\TrinaryLogic;
23: use PHPStan\Type\NeverType;
24: use PHPStan\Type\TypeUtils;
25: use function array_diff_key;
26: use function array_key_exists;
27: use function array_keys;
28: use function in_array;
29: use function strtolower;
30:
31: /** @api */
32: class ClassPropertiesNode extends NodeAbstract implements VirtualNode
33: {
34:
35: /**
36: * @param ClassPropertyNode[] $properties
37: * @param array<int, PropertyRead|PropertyWrite> $propertyUsages
38: * @param array<int, MethodCall> $methodCalls
39: * @param array<string, MethodReturnStatementsNode> $returnStatementNodes
40: */
41: public function __construct(
42: private ClassLike $class,
43: private ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
44: private array $properties,
45: private array $propertyUsages,
46: private array $methodCalls,
47: private array $returnStatementNodes,
48: private ClassReflection $classReflection,
49: )
50: {
51: parent::__construct($class->getAttributes());
52: }
53:
54: public function getClass(): ClassLike
55: {
56: return $this->class;
57: }
58:
59: /**
60: * @return ClassPropertyNode[]
61: */
62: public function getProperties(): array
63: {
64: return $this->properties;
65: }
66:
67: /**
68: * @return array<int, PropertyRead|PropertyWrite>
69: */
70: public function getPropertyUsages(): array
71: {
72: return $this->propertyUsages;
73: }
74:
75: public function getType(): string
76: {
77: return 'PHPStan_Node_ClassPropertiesNode';
78: }
79:
80: /**
81: * @return string[]
82: */
83: public function getSubNodeNames(): array
84: {
85: return [];
86: }
87:
88: public function getClassReflection(): ClassReflection
89: {
90: return $this->classReflection;
91: }
92:
93: /**
94: * @param string[] $constructors
95: * @param ReadWritePropertiesExtension[]|null $extensions
96: * @return array{array<string, ClassPropertyNode>, array<array{string, int, ClassPropertyNode, string, string}>, array<array{string, int, ClassPropertyNode}>}
97: */
98: public function getUninitializedProperties(
99: Scope $scope,
100: array $constructors,
101: ?array $extensions = null,
102: ): array
103: {
104: if (!$this->getClass() instanceof Class_) {
105: return [[], [], []];
106: }
107: $classReflection = $this->getClassReflection();
108:
109: $uninitializedProperties = [];
110: $originalProperties = [];
111: $initialInitializedProperties = [];
112: $initializedProperties = [];
113: if ($extensions === null) {
114: $extensions = $this->readWritePropertiesExtensionProvider->getExtensions();
115: }
116: $initializedViaExtension = [];
117: foreach ($this->getProperties() as $property) {
118: if ($property->isStatic()) {
119: continue;
120: }
121: if ($property->getNativeType() === null) {
122: continue;
123: }
124: if ($property->getDefault() !== null) {
125: continue;
126: }
127: $originalProperties[$property->getName()] = $property;
128: $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait());
129: if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) {
130: $propertyReflection = $classReflection->getNativeProperty($property->getName());
131:
132: foreach ($extensions as $extension) {
133: if (!$extension->isInitialized($propertyReflection, $property->getName())) {
134: continue;
135: }
136: $is = TrinaryLogic::createYes();
137: $initializedViaExtension[$property->getName()] = true;
138: break;
139: }
140: }
141: $initialInitializedProperties[$property->getName()] = $is;
142: foreach ($constructors as $constructor) {
143: $initializedProperties[$constructor][$property->getName()] = $is;
144: }
145: if ($is->yes()) {
146: continue;
147: }
148: $uninitializedProperties[$property->getName()] = $property;
149: }
150:
151: if ($constructors === []) {
152: return [$uninitializedProperties, [], []];
153: }
154:
155: $initializedInConstructor = [];
156: if ($classReflection->hasConstructor()) {
157: $initializedInConstructor = array_diff_key($uninitializedProperties, $this->collectUninitializedProperties([$classReflection->getConstructor()->getName()], $uninitializedProperties));
158: }
159:
160: $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $constructors, $initializedInConstructor);
161: $prematureAccess = [];
162: $additionalAssigns = [];
163:
164: foreach ($this->getPropertyUsages() as $usage) {
165: $fetch = $usage->getFetch();
166: if (!$fetch instanceof PropertyFetch) {
167: continue;
168: }
169: $usageScope = $usage->getScope();
170: if ($usageScope->getFunction() === null) {
171: continue;
172: }
173: $function = $usageScope->getFunction();
174: if (!$function instanceof MethodReflection) {
175: continue;
176: }
177: if ($function->getDeclaringClass()->getName() !== $classReflection->getName()) {
178: continue;
179: }
180: if (!array_key_exists($function->getName(), $methodsCalledFromConstructor)) {
181: continue;
182: }
183:
184: $initializedPropertiesMap = $methodsCalledFromConstructor[$function->getName()];
185:
186: if (!$fetch->name instanceof Identifier) {
187: continue;
188: }
189: $propertyName = $fetch->name->toString();
190: $fetchedOnType = $usageScope->getType($fetch->var);
191: if (TypeUtils::findThisType($fetchedOnType) === null) {
192: continue;
193: }
194:
195: $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName);
196: if ($propertyReflection === null) {
197: continue;
198: }
199: if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) {
200: continue;
201: }
202:
203: if ($usage instanceof PropertyWrite) {
204: if (array_key_exists($propertyName, $initializedPropertiesMap)) {
205: $hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
206: if (
207: !$hasInitialization->no()
208: && !$usage->isPromotedPropertyWrite()
209: && !array_key_exists($propertyName, $initializedViaExtension)
210: ) {
211: $additionalAssigns[] = [
212: $propertyName,
213: $fetch->getStartLine(),
214: $originalProperties[$propertyName],
215: ];
216: }
217: }
218: } elseif (array_key_exists($propertyName, $initializedPropertiesMap)) {
219: if (
220: strtolower($function->getName()) !== '__construct'
221: && array_key_exists($propertyName, $initializedInConstructor)
222: && in_array($function->getName(), $constructors, true)
223: ) {
224: continue;
225: }
226: $hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
227: if (!$hasInitialization->yes() && $usageScope->isInAnonymousFunction() && $usageScope->getParentScope() !== null) {
228: $hasInitialization = $hasInitialization->or($usageScope->getParentScope()->hasExpressionType(new PropertyInitializationExpr($propertyName)));
229: }
230: if (!$hasInitialization->yes()) {
231: $prematureAccess[] = [
232: $propertyName,
233: $fetch->getStartLine(),
234: $originalProperties[$propertyName],
235: $usageScope->getFile(),
236: $usageScope->getFileDescription(),
237: ];
238: }
239: }
240: }
241:
242: return [
243: $this->collectUninitializedProperties(array_keys($methodsCalledFromConstructor), $uninitializedProperties),
244: $prematureAccess,
245: $additionalAssigns,
246: ];
247: }
248:
249: /**
250: * @param list<string> $constructors
251: * @param array<string, ClassPropertyNode> $uninitializedProperties
252: * @return array<string, ClassPropertyNode>
253: */
254: private function collectUninitializedProperties(array $constructors, array $uninitializedProperties): array
255: {
256: foreach ($constructors as $constructor) {
257: $lowerConstructorName = strtolower($constructor);
258: if (!array_key_exists($lowerConstructorName, $this->returnStatementNodes)) {
259: continue;
260: }
261:
262: $returnStatementsNode = $this->returnStatementNodes[$lowerConstructorName];
263: $methodScope = null;
264: foreach ($returnStatementsNode->getExecutionEnds() as $executionEnd) {
265: $statementResult = $executionEnd->getStatementResult();
266: $endNode = $executionEnd->getNode();
267: if ($statementResult->isAlwaysTerminating()) {
268: if ($endNode instanceof Node\Stmt\Throw_) {
269: continue;
270: }
271: if ($endNode instanceof Node\Stmt\Expression) {
272: $exprType = $statementResult->getScope()->getType($endNode->expr);
273: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
274: continue;
275: }
276: }
277: }
278: if ($methodScope === null) {
279: $methodScope = $statementResult->getScope();
280: continue;
281: }
282:
283: $methodScope = $methodScope->mergeWith($statementResult->getScope());
284: }
285:
286: foreach ($returnStatementsNode->getReturnStatements() as $returnStatement) {
287: if ($methodScope === null) {
288: $methodScope = $returnStatement->getScope();
289: continue;
290: }
291: $methodScope = $methodScope->mergeWith($returnStatement->getScope());
292: }
293:
294: if ($methodScope === null) {
295: continue;
296: }
297:
298: foreach (array_keys($uninitializedProperties) as $propertyName) {
299: if (!$methodScope->hasExpressionType(new PropertyInitializationExpr($propertyName))->yes()) {
300: continue;
301: }
302:
303: unset($uninitializedProperties[$propertyName]);
304: }
305: }
306:
307: return $uninitializedProperties;
308: }
309:
310: /**
311: * @param string[] $methods
312: * @param array<string, TrinaryLogic> $initialInitializedProperties
313: * @param array<string, array<string, TrinaryLogic>> $initializedProperties
314: * @param array<string, ClassPropertyNode> $initializedInConstructorProperties
315: *
316: * @return array<string, array<string, TrinaryLogic>>
317: */
318: private function getMethodsCalledFromConstructor(
319: ClassReflection $classReflection,
320: array $initialInitializedProperties,
321: array $initializedProperties,
322: array $methods,
323: array $initializedInConstructorProperties,
324: ): array
325: {
326: $originalMap = $initializedProperties;
327: $originalMethods = $methods;
328:
329: foreach ($this->methodCalls as $methodCall) {
330: $methodCallNode = $methodCall->getNode();
331: if ($methodCallNode instanceof Array_) {
332: continue;
333: }
334: if (!$methodCallNode->name instanceof Identifier) {
335: continue;
336: }
337: $callScope = $methodCall->getScope();
338: if ($methodCallNode instanceof Node\Expr\MethodCall) {
339: $calledOnType = $callScope->getType($methodCallNode->var);
340: } else {
341: if (!$methodCallNode->class instanceof Name) {
342: continue;
343: }
344:
345: $calledOnType = $callScope->resolveTypeByName($methodCallNode->class);
346: }
347:
348: if (TypeUtils::findThisType($calledOnType) === null) {
349: continue;
350: }
351:
352: $inMethod = $callScope->getFunction();
353: if (!$inMethod instanceof MethodReflection) {
354: continue;
355: }
356: if (!in_array($inMethod->getName(), $methods, true)) {
357: continue;
358: }
359:
360: if ($inMethod->getName() !== '__construct') {
361: foreach ($initializedInConstructorProperties as $propertyName => $propertyNode) {
362: $initializedProperties[$inMethod->getName()][$propertyName] = TrinaryLogic::createYes();
363: }
364: }
365:
366: $methodName = $methodCallNode->name->toString();
367: if (array_key_exists($methodName, $initializedProperties)) {
368: foreach ($this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties) as $propertyName => $isInitialized) {
369: $initializedProperties[$methodName][$propertyName] = $initializedProperties[$methodName][$propertyName]->and($isInitialized);
370: }
371: continue;
372: }
373: $methodReflection = $callScope->getMethodReflection($calledOnType, $methodName);
374: if ($methodReflection === null) {
375: continue;
376: }
377: if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) {
378: continue;
379: }
380: $initializedProperties[$methodName] = $this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties);
381: $methods[] = $methodName;
382: }
383:
384: if ($originalMap === $initializedProperties && $originalMethods === $methods) {
385: return $initializedProperties;
386: }
387:
388: return $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $methods, $initializedInConstructorProperties);
389: }
390:
391: /**
392: * @param array<string, TrinaryLogic> $initialInitializedProperties
393: * @return array<string, TrinaryLogic>
394: */
395: private function getInitializedProperties(Scope $scope, array $initialInitializedProperties): array
396: {
397: foreach ($initialInitializedProperties as $propertyName => $isInitialized) {
398: $initialInitializedProperties[$propertyName] = $isInitialized->or($scope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
399: }
400:
401: return $initialInitializedProperties;
402: }
403:
404: }
405: