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