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
155 changes: 102 additions & 53 deletions src/Analyser/ExprHandler/BinaryOpHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
final class BinaryOpHandler implements ExprHandler
{

private const COMPARISON_FORMS_ATTRIBUTE = 'phpstan_comparison_forms';

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private RicherScopeGetTypeHelper $richerScopeGetTypeHelper,
Expand Down Expand Up @@ -737,12 +739,13 @@
return null;
}

foreach ($forms as [$form, $isComplement]) {
if (!$scope->hasExpressionType($form)->yes()) {
foreach ($forms as [, $isComplement, $formKey]) {
$holder = $scope->expressionTypes[$formKey] ?? null;
if ($holder === null || !$holder->getCertainty()->yes()) {

Check warning on line 744 in src/Analyser/ExprHandler/BinaryOpHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ foreach ($forms as [, $isComplement, $formKey]) { $holder = $scope->expressionTypes[$formKey] ?? null; - if ($holder === null || !$holder->getCertainty()->yes()) { + if ($holder === null || $holder->getCertainty()->no()) { continue; }

Check warning on line 744 in src/Analyser/ExprHandler/BinaryOpHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ foreach ($forms as [, $isComplement, $formKey]) { $holder = $scope->expressionTypes[$formKey] ?? null; - if ($holder === null || !$holder->getCertainty()->yes()) { + if ($holder === null || $holder->getCertainty()->no()) { continue; }
continue;
}

$formType = $scope->getTrackedExpressionType($form)->toBoolean();
$formType = $holder->getType()->toBoolean();
if ($formType->isTrue()->yes()) {
return new ConstantBooleanType(!$isComplement);
}
Expand Down Expand Up @@ -776,9 +779,12 @@

$exprKey = $this->exprPrinter->printExpr($expr);
$sureTypes = [];
foreach ($forms as [$form, $isComplement]) {
$formKey = $this->exprPrinter->printExpr($form);
if ($formKey === $exprKey || !$scope->hasExpressionType($form)->yes()) {
foreach ($forms as [$form, $isComplement, $formKey]) {
if ($formKey === $exprKey) {
continue;
}
$holder = $scope->expressionTypes[$formKey] ?? null;
if ($holder === null || !$holder->getCertainty()->yes()) {

Check warning on line 787 in src/Analyser/ExprHandler/BinaryOpHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ continue; } $holder = $scope->expressionTypes[$formKey] ?? null; - if ($holder === null || !$holder->getCertainty()->yes()) { + if ($holder === null || $holder->getCertainty()->no()) { continue; }

Check warning on line 787 in src/Analyser/ExprHandler/BinaryOpHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ continue; } $holder = $scope->expressionTypes[$formKey] ?? null; - if ($holder === null || !$holder->getCertainty()->yes()) { + if ($holder === null || $holder->getCertainty()->no()) { continue; }
continue;
}

Expand All @@ -797,78 +803,121 @@
* paired with whether it is the complement (always the opposite value).
*
* @param callable(Expr): Type $getType
* @return list<array{BinaryOp, bool}>|null
* @return list<array{BinaryOp, bool, string}>|null
*/
private function getEquivalentComparisonForms(BinaryOp $expr, callable $getType): ?array
{
if (
!$expr instanceof BinaryOp\Identical
&& !$expr instanceof BinaryOp\NotIdentical
&& !$expr instanceof BinaryOp\Equal
&& !$expr instanceof BinaryOp\NotEqual
&& !$expr instanceof BinaryOp\Smaller
&& !$expr instanceof BinaryOp\SmallerOrEqual
&& !$expr instanceof BinaryOp\Greater
&& !$expr instanceof BinaryOp\GreaterOrEqual
) {
return null;
}

$left = $expr->left;
$right = $expr->right;

// The forms are built and printed once per comparison node and kept
// in its attributes: scopes are keyed by the printed expression, and
// fresh form nodes would be printed again on every lookup. Attributes
// travel to new nodes, which can be of another class or have other
// operands.
$cached = $expr->getAttribute(self::COMPARISON_FORMS_ATTRIBUTE);
if ($cached === null || $cached[0] !== get_class($expr) || $cached[1] !== $left || $cached[2] !== $right) {
$cached = [get_class($expr), $left, $right, ...$this->createComparisonForms($expr)];
$expr->setAttribute(self::COMPARISON_FORMS_ATTRIBUTE, $cached);
}

// `<` and `>=` only complement each other for comparable operands
// (NAN and uncomparable arrays/objects are neither smaller nor
// larger-or-equal)
if ($cached[4] === null || !$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) {

Check warning on line 840 in src/Analyser/ExprHandler/BinaryOpHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // `<` and `>=` only complement each other for comparable operands // (NAN and uncomparable arrays/objects are neither smaller nor // larger-or-equal) - if ($cached[4] === null || !$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) { + if ($cached[4] === null || !$getType($left)->isInteger()->yes() || $getType($right)->isInteger()->no()) { return $cached[3]; }

Check warning on line 840 in src/Analyser/ExprHandler/BinaryOpHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // `<` and `>=` only complement each other for comparable operands // (NAN and uncomparable arrays/objects are neither smaller nor // larger-or-equal) - if ($cached[4] === null || !$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) { + if ($cached[4] === null || $getType($left)->isInteger()->no() || !$getType($right)->isInteger()->yes()) { return $cached[3]; }

Check warning on line 840 in src/Analyser/ExprHandler/BinaryOpHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // `<` and `>=` only complement each other for comparable operands // (NAN and uncomparable arrays/objects are neither smaller nor // larger-or-equal) - if ($cached[4] === null || !$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) { + if ($cached[4] === null || !$getType($left)->isInteger()->yes() || $getType($right)->isInteger()->no()) { return $cached[3]; }

Check warning on line 840 in src/Analyser/ExprHandler/BinaryOpHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // `<` and `>=` only complement each other for comparable operands // (NAN and uncomparable arrays/objects are neither smaller nor // larger-or-equal) - if ($cached[4] === null || !$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) { + if ($cached[4] === null || $getType($left)->isInteger()->no() || !$getType($right)->isInteger()->yes()) { return $cached[3]; }
return $cached[3];
}

return $cached[4];
}

/**
* The equivalent forms, and for `<`, `<=`, `>` and `>=` also the
* equivalent forms followed by the complement forms that hold only for
* comparable operands - each form with its node key (a comparison's node
* key is its printed form).
*
* @return array{list<array{BinaryOp, bool, string}>, list<array{BinaryOp, bool, string}>|null}
*/
private function createComparisonForms(BinaryOp $expr): array
{
$left = $expr->left;
$right = $expr->right;
$complementForms = [];
if ($expr instanceof BinaryOp\Identical || $expr instanceof BinaryOp\NotIdentical) {
$isNegated = $expr instanceof BinaryOp\NotIdentical;

return [
$forms = [
[new BinaryOp\Identical($left, $right), $isNegated],
[new BinaryOp\Identical($right, $left), $isNegated],
[new BinaryOp\NotIdentical($left, $right), !$isNegated],
[new BinaryOp\NotIdentical($right, $left), !$isNegated],
];
}

if ($expr instanceof BinaryOp\Equal || $expr instanceof BinaryOp\NotEqual) {
} elseif ($expr instanceof BinaryOp\Equal || $expr instanceof BinaryOp\NotEqual) {
$isNegated = $expr instanceof BinaryOp\NotEqual;

return [
$forms = [
[new BinaryOp\Equal($left, $right), $isNegated],
[new BinaryOp\Equal($right, $left), $isNegated],
[new BinaryOp\NotEqual($left, $right), !$isNegated],
[new BinaryOp\NotEqual($right, $left), !$isNegated],
];
}

if (
!$expr instanceof BinaryOp\Smaller
&& !$expr instanceof BinaryOp\SmallerOrEqual
&& !$expr instanceof BinaryOp\Greater
&& !$expr instanceof BinaryOp\GreaterOrEqual
) {
return null;
}

// normalized to `$smaller < $larger` / `$smaller <= $larger`
if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) {
$smaller = $left;
$larger = $right;
} else {
$smaller = $right;
$larger = $left;
// normalized to `$smaller < $larger` / `$smaller <= $larger`
if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) {
$smaller = $left;
$larger = $right;
} else {
$smaller = $right;
$larger = $left;
}

if ($expr instanceof BinaryOp\SmallerOrEqual || $expr instanceof BinaryOp\GreaterOrEqual) {
$forms = [
[new BinaryOp\SmallerOrEqual($smaller, $larger), false],
[new BinaryOp\GreaterOrEqual($larger, $smaller), false],
];
$complementForms = [
[new BinaryOp\Smaller($larger, $smaller), true],
[new BinaryOp\Greater($smaller, $larger), true],
];
} else {
$forms = [
[new BinaryOp\Smaller($smaller, $larger), false],
[new BinaryOp\Greater($larger, $smaller), false],
];
$complementForms = [
[new BinaryOp\SmallerOrEqual($larger, $smaller), true],
[new BinaryOp\GreaterOrEqual($smaller, $larger), true],
];
}
}
$orEqual = $expr instanceof BinaryOp\SmallerOrEqual || $expr instanceof BinaryOp\GreaterOrEqual;
$forms = $orEqual
? [
[new BinaryOp\SmallerOrEqual($smaller, $larger), false],
[new BinaryOp\GreaterOrEqual($larger, $smaller), false],
]
: [
[new BinaryOp\Smaller($smaller, $larger), false],
[new BinaryOp\Greater($larger, $smaller), false],
];

// `<` and `>=` only complement each other for comparable operands
// (NAN and uncomparable arrays/objects are neither smaller nor
// larger-or-equal)
if (!$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) {
return $forms;
$keyedForms = [];
foreach ($forms as [$form, $isComplement]) {
$keyedForms[] = [$form, $isComplement, $this->exprPrinter->printExpr($form)];
}
if ($complementForms === []) {
return [$keyedForms, null];
}

if ($orEqual) {
$forms[] = [new BinaryOp\Smaller($larger, $smaller), true];
$forms[] = [new BinaryOp\Greater($smaller, $larger), true];
} else {
$forms[] = [new BinaryOp\SmallerOrEqual($larger, $smaller), true];
$forms[] = [new BinaryOp\GreaterOrEqual($smaller, $larger), true];
$keyedWithComplements = $keyedForms;
foreach ($complementForms as [$form, $isComplement]) {
$keyedWithComplements[] = [$form, $isComplement, $this->exprPrinter->printExpr($form)];
}

return $forms;
return [$keyedForms, $keyedWithComplements];
}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ private static function canRemoveUnionAtOnce(Type $fromType): bool
public static function doRemove(Type $fromType, Type $typeToRemove): Type
{
if ($typeToRemove instanceof UnionType) {
// Plain mixed minus a union is mixed with that union subtracted -
// peeling the members would rebuild the subtracted union once per
// member and end with the same one.
if (
get_class($fromType) === MixedType::class
&& $fromType->getSubtractedType() === null
&& get_class($typeToRemove) === UnionType::class
) {
return $fromType->subtract($typeToRemove);
}

// Removing the members one at a time is what keeps the result precise:
// removing one can narrow the type so that removing the next achieves
// more. A sealed hierarchy is the exception - there the subtraction is
Expand Down
Loading
Loading