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