constructorAssignDetector = $constructorAssignDetector; $this->propertyAssignMatcher = $propertyAssignMatcher; $this->propertyDefaultAssignDetector = $propertyDefaultAssignDetector; $this->nullTypeAssignDetector = $nullTypeAssignDetector; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->typeFactory = $typeFactory; $this->nodeTypeResolver = $nodeTypeResolver; $this->exprAnalyzer = $exprAnalyzer; $this->valueResolver = $valueResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; } public function inferPropertyInClassLike(Property $property, string $propertyName, ClassLike $classLike) : ?Type { if ($this->hasAssignDynamicPropertyValue($classLike, $propertyName)) { return null; } $assignedExprTypes = $this->getAssignedExprTypes($classLike, $propertyName); if ($this->shouldAddNullType($classLike, $propertyName, $assignedExprTypes)) { $assignedExprTypes[] = new NullType(); } return $this->resolveTypeWithVerifyDefaultValue($property, $assignedExprTypes); } /** * @param Type[] $assignedExprTypes */ private function resolveTypeWithVerifyDefaultValue(Property $property, array $assignedExprTypes) : ?Type { $defaultPropertyValue = $property->props[0]->default; if ($assignedExprTypes === []) { // not typed, never assigned, but has default value, then pull type from default value if (!$property->type instanceof Node && $defaultPropertyValue instanceof Expr) { return $this->nodeTypeResolver->getType($defaultPropertyValue); } return null; } $inferredType = $this->typeFactory->createMixedPassedOrUnionType($assignedExprTypes); if ($this->shouldSkipWithDifferentDefaultValueType($defaultPropertyValue, $inferredType)) { return null; } return $inferredType; } private function shouldSkipWithDifferentDefaultValueType(?Expr $expr, Type $inferredType) : bool { if (!$expr instanceof Expr) { return \false; } if ($this->valueResolver->isNull($expr)) { return \false; } $defaultType = $this->nodeTypeResolver->getNativeType($expr); return $inferredType->isSuperTypeOf($defaultType)->no(); } private function resolveExprStaticTypeIncludingDimFetch(Assign $assign) : Type { $exprStaticType = $this->nodeTypeResolver->getNativeType($assign->expr); if ($assign->var instanceof ArrayDimFetch) { return new ArrayType(new MixedType(), $exprStaticType); } return $exprStaticType; } /** * @param Type[] $assignedExprTypes */ private function shouldAddNullType(ClassLike $classLike, string $propertyName, array $assignedExprTypes) : bool { $hasPropertyDefaultValue = $this->propertyDefaultAssignDetector->detect($classLike, $propertyName); $isAssignedInConstructor = $this->constructorAssignDetector->isPropertyAssigned($classLike, $propertyName); if ($assignedExprTypes === [] && ($isAssignedInConstructor || $hasPropertyDefaultValue)) { return \false; } $shouldAddNullType = $this->nullTypeAssignDetector->detect($classLike, $propertyName); if ($shouldAddNullType) { if ($isAssignedInConstructor) { return \false; } return !$hasPropertyDefaultValue; } if ($assignedExprTypes === []) { return \false; } if ($isAssignedInConstructor) { return \false; } return !$hasPropertyDefaultValue; } private function hasAssignDynamicPropertyValue(ClassLike $classLike, string $propertyName) : bool { $hasAssignDynamicPropertyValue = \false; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use($propertyName, &$hasAssignDynamicPropertyValue) : ?int { if (!$node instanceof Assign) { return null; } $expr = $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName); if (!$expr instanceof Expr) { if (!$this->propertyFetchAnalyzer->isLocalPropertyFetch($node->var)) { return null; } /** @var PropertyFetch|StaticPropertyFetch $assignVar */ $assignVar = $node->var; if (!$assignVar->name instanceof Identifier) { $hasAssignDynamicPropertyValue = \true; return NodeTraverser::STOP_TRAVERSAL; } return null; } return null; }); return $hasAssignDynamicPropertyValue; } /** * @return array */ private function getAssignedExprTypes(ClassLike $classLike, string $propertyName) : array { $assignedExprTypes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use($propertyName, &$assignedExprTypes) : ?int { if (!$node instanceof Assign) { return null; } $expr = $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName); if (!$expr instanceof Expr) { return null; } if ($this->exprAnalyzer->isNonTypedFromParam($node->expr)) { $assignedExprTypes = []; return NodeTraverser::STOP_TRAVERSAL; } $assignedExprTypes[] = $this->resolveExprStaticTypeIncludingDimFetch($node); return null; }); return $assignedExprTypes; } }