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