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