Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 76 additions & 109 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NonexistentParentClassType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
Expand Down Expand Up @@ -163,103 +162,25 @@ public function specifyTypesInCondition(
if ($context->true()) {
return $this->create($exprNode, new ObjectWithoutClassType(), $context);
}
} elseif ($expr instanceof Node\Expr\BinaryOp\Identical) {
$expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
if ($expressions !== null) {
/** @var Expr $exprNode */
$exprNode = $expressions[0];
if ($exprNode instanceof Expr\Assign) {
$exprNode = $exprNode->var;
}
/** @var \PHPStan\Type\ConstantScalarType $constantType */
$constantType = $expressions[1];
if ($constantType->getValue() === false) {
$types = $this->create($exprNode, $constantType, $context);
return $types->unionWith($this->specifyTypesInCondition(
$scope,
$exprNode,
$context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate()
));
}

if ($constantType->getValue() === true) {
$types = $this->create($exprNode, $constantType, $context);
return $types->unionWith($this->specifyTypesInCondition(
$scope,
$exprNode,
$context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate()
));
}

if ($constantType->getValue() === null) {
return $this->create($exprNode, $constantType, $context);
}

if (
!$context->null()
&& $exprNode instanceof FuncCall
&& count($exprNode->args) === 1
&& $exprNode->name instanceof Name
&& strtolower((string) $exprNode->name) === 'count'
&& $constantType instanceof ConstantIntegerType
) {
if ($context->truthy() || $constantType->getValue() === 0) {
$newContext = $context;
if ($constantType->getValue() === 0) {
$newContext = $newContext->negate();
}
$argType = $scope->getType($exprNode->args[0]->value);
if ((new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($argType)->yes()) {
return $this->create($exprNode->args[0]->value, new NonEmptyArrayType(), $newContext);
}
}
}
}

if ($context->true()) {
$type = TypeCombinator::intersect($scope->getType($expr->right), $scope->getType($expr->left));
$leftTypes = $this->create($expr->left, $type, $context);
$rightTypes = $this->create($expr->right, $type, $context);
return $leftTypes->unionWith($rightTypes);

} elseif ($context->false()) {
$identicalType = $scope->getType($expr);
if ($identicalType instanceof ConstantBooleanType) {
$never = new NeverType();
$contextForTypes = $identicalType->getValue() ? $context->negate() : $context;
$leftTypes = $this->create($expr->left, $never, $contextForTypes);
$rightTypes = $this->create($expr->right, $never, $contextForTypes);
return $leftTypes->unionWith($rightTypes);
}
} elseif ($expr instanceof Node\Expr\BinaryOp\Identical && !$context->null()) {
$leftType = $scope->getType($expr->left);
$rightType = $scope->getType($expr->right);

if (
(
$expr->left instanceof Node\Scalar
|| $expr->left instanceof Expr\Array_
)
&& !$expr->right instanceof Node\Scalar
) {
return $this->create(
$expr->right,
$scope->getType($expr->left),
$context
);
if ($context->truthy()) {
return $this->specifyTypesInExpression($scope, $expr->left, $rightType, $context)
->unionWith($this->specifyTypesInExpression($scope, $expr->right, $leftType, $context));
} elseif ($context->falsey()) {
$specifiedTypes = new SpecifiedTypes();
if (TypeUtils::isOneDefiniteType($leftType)->yes()) {
$specifiedTypes = $this->specifyTypesInExpression($scope, $expr->right, $leftType, $context);
}
if (
(
$expr->right instanceof Node\Scalar
|| $expr->right instanceof Expr\Array_
)
&& !$expr->left instanceof Node\Scalar
) {
return $this->create(
$expr->left,
$scope->getType($expr->right),
$context
if (TypeUtils::isOneDefiniteType($rightType)->yes()) {
$specifiedTypes = $specifiedTypes->unionWith(
$this->specifyTypesInExpression($scope, $expr->left, $rightType, $context)
);
}
return $specifiedTypes;
}

} elseif ($expr instanceof Node\Expr\BinaryOp\NotIdentical) {
return $this->specifyTypesInCondition(
$scope,
Expand Down Expand Up @@ -462,7 +383,7 @@ public function specifyTypesInCondition(
}

if ($defaultHandleFunctions) {
return $this->handleDefaultTruthyOrFalseyContext($context, $expr);
return $this->handleDefaultTruthyOrFalseyContext($scope, $context, $expr);
}
} elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) {
$methodCalledOnType = $scope->getType($expr->var);
Expand All @@ -485,7 +406,7 @@ public function specifyTypesInCondition(
}

if ($defaultHandleFunctions) {
return $this->handleDefaultTruthyOrFalseyContext($context, $expr);
return $this->handleDefaultTruthyOrFalseyContext($scope, $context, $expr);
}
} elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) {
if ($expr->class instanceof Name) {
Expand Down Expand Up @@ -513,7 +434,7 @@ public function specifyTypesInCondition(
}

if ($defaultHandleFunctions) {
return $this->handleDefaultTruthyOrFalseyContext($context, $expr);
return $this->handleDefaultTruthyOrFalseyContext($scope, $context, $expr);
}
} elseif ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) {
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context);
Expand All @@ -529,11 +450,9 @@ public function specifyTypesInCondition(
if (!$scope instanceof MutatingScope) {
throw new \PHPStan\ShouldNotHappenException();
}
if ($context->null()) {
return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context);
}

return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context);
$scope = $scope->exitFirstLevelStatements();
return $this->specifyTypesInCondition($scope, $expr->var, $context)
->unionWith($this->specifyTypesInCondition($scope, $expr->expr, $context));
} elseif (
(
$expr instanceof Expr\Isset_
Expand Down Expand Up @@ -652,20 +571,24 @@ public function specifyTypesInCondition(
} elseif ($expr instanceof Expr\ErrorSuppress) {
return $this->specifyTypesInCondition($scope, $expr->expr, $context, $defaultHandleFunctions);
} elseif (!$context->null()) {
return $this->handleDefaultTruthyOrFalseyContext($context, $expr);
return $this->handleDefaultTruthyOrFalseyContext($scope, $context, $expr);
}

return new SpecifiedTypes();
}

private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr): SpecifiedTypes
private function handleDefaultTruthyOrFalseyContext(Scope $scope, TypeSpecifierContext $context, Expr $expr): SpecifiedTypes
{
if (!$context->truthy()) {
$type = StaticTypeFactory::truthy();
return $this->create($expr, $type, TypeSpecifierContext::createFalse());
} elseif (!$context->falsey()) {
$type = StaticTypeFactory::falsey();
return $this->create($expr, $type, TypeSpecifierContext::createFalse());
if ($context->truthy()) {
$falsey = StaticTypeFactory::falsey();
if (!$falsey->isSuperTypeOf($scope->getType($expr))->no()) {
return $this->create($expr, $falsey, TypeSpecifierContext::createFalsey());
}
} elseif ($context->falsey()) {
$falsey = StaticTypeFactory::falsey();
if (!$falsey->isSuperTypeOf($scope->getType($expr))->yes()) {
return $this->create($expr, $falsey, TypeSpecifierContext::createTruthy());
}
}

return new SpecifiedTypes();
Expand Down Expand Up @@ -800,4 +723,48 @@ private function getTypeSpecifyingExtensionsForType(array $extensions, string $c
return array_merge(...$extensionsForClass);
}

private function specifyTypesInExpression(Scope $scope, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes
{
if ($expr instanceof Expr\Assign) {
return $this->specifyTypesInExpression($scope, $expr->var, $type, $context)
->unionWith($this->specifyTypesInExpression($scope, $expr->expr, $type, $context));
}

$originalType = $scope->getType($expr);

if ($context->truthy()) {
$newType = TypeCombinator::intersect($originalType, $type);
} elseif ($context->falsey()) {
$newType = TypeCombinator::remove($originalType, $type);
} else {
return new SpecifiedTypes();
}

if ($originalType->equals($newType)) {
return new SpecifiedTypes();
}

$specifiedTypes = $this->create($expr, $type, $context);

if (
!StaticTypeFactory::truthy()->isSuperTypeOf($originalType)->yes()
&& StaticTypeFactory::truthy()->isSuperTypeOf($newType)->yes()
) {
$specifiedTypes = $specifiedTypes->unionWith(
$this->specifyTypesInCondition($scope, $expr, TypeSpecifierContext::createTruthy())
);
}

if (
!StaticTypeFactory::falsey()->isSuperTypeOf($originalType)->yes()
&& StaticTypeFactory::falsey()->isSuperTypeOf($newType)->yes()
) {
$specifiedTypes = $specifiedTypes->unionWith(
$this->specifyTypesInCondition($scope, $expr, TypeSpecifierContext::createFalsey()),
);
}

return $specifiedTypes;
}

}
35 changes: 35 additions & 0 deletions src/Type/TypeUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Type;

use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryType;
use PHPStan\Type\Accessory\HasPropertyType;
use PHPStan\Type\Constant\ConstantArrayType;
Expand Down Expand Up @@ -339,4 +340,38 @@ public static function containsCallable(Type $type): bool
return false;
}

public static function isOneDefiniteType(Type $type): TrinaryLogic
{
if ($type instanceof ConstantScalarType) {
return TrinaryLogic::createYes();
}

if ($type instanceof ConstantArrayType) {
if (count($type->getOptionalKeys()) !== 0) {
return TrinaryLogic::createNo();
}
foreach ($type->getValueTypes() as $valueType) {
if (!self::isOneDefiniteType($valueType)) {
return TrinaryLogic::createNo();
}
}
return TrinaryLogic::createYes();
}

if ($type instanceof UnionType) {
return TrinaryLogic::createNo();
}

if ($type instanceof IntersectionType) {
foreach ($type->getTypes() as $innerType) {
$innerTypeIsOneDefiniteType = self::isOneDefiniteType($innerType);
if (!$innerTypeIsOneDefiniteType->maybe()) {
return $innerTypeIsOneDefiniteType;
}
}
}

return TrinaryLogic::createMaybe();
}

}
4 changes: 4 additions & 0 deletions src/Type/UnionTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ public static function sortTypes(array $types): array
return 0;
}

if ($a instanceof IntegerRangeType && $b instanceof IntegerRangeType) {
return $a->getMin() <=> $b->getMin();
}

if ($a instanceof ConstantStringType && $b instanceof ConstantStringType) {
return strcasecmp($a->getValue(), $b->getValue());
}
Expand Down
16 changes: 8 additions & 8 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2343,7 +2343,7 @@ public function dataBinaryOperations(): array
'@$stringOrNull ?: 12',
],
[
'int<1, max>|int<min, -1>',
'int<min, -1>|int<1, max>',
'$integer ?: 12',
],
[
Expand Down Expand Up @@ -5244,7 +5244,7 @@ public function dataArrayFunctions(): array
'array_filter($union)',
],
[
'array<int, int<1, max>|int<min, -1>|true>',
'array<int, int<min, -1>|int<1, max>|true>',
'array_filter($withPossiblyFalsey)',
],
[
Expand Down Expand Up @@ -7591,17 +7591,17 @@ public function dataWhileLoopVariables(): array
{
return [
[
'int',
'int<min, 10>',
'$i',
"'begin'",
],
[
'int',
'int<min, 10>',
'$i',
"'end'",
],
[
'int',
'int<min, 10>',
'$i',
"'afterLoop'",
],
Expand Down Expand Up @@ -8921,7 +8921,7 @@ public function dataConstantTypeAfterDuplicateCondition(): array
"'afterFirst'",
],
[
'int',
'int<min, -1>|int<1, max>',
'$a',
"'afterSecond'",
],
Expand All @@ -8936,12 +8936,12 @@ public function dataConstantTypeAfterDuplicateCondition(): array
"'afterSecond'",
],
[
'int',
'int<min, -1>|int<1, max>',
'$a',
"'afterThird'",
],
[
'int',
'int<min, -1>|int<1, max>',
'$b',
"'afterThird'",
],
Expand Down
Loading