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