Skip to content

Commit

Permalink
Bleeding edge - check mixed in binary operator
Browse files Browse the repository at this point in the history
  • Loading branch information
schlndh authored Jul 15, 2024
1 parent d64b39f commit ef2a246
Show file tree
Hide file tree
Showing 10 changed files with 1,172 additions and 76 deletions.
7 changes: 6 additions & 1 deletion conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ rules:
- PHPStan\Rules\Generics\UsedTraitsRule
- PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule
- PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule
- PHPStan\Rules\Operators\InvalidBinaryOperationRule
- PHPStan\Rules\Operators\InvalidUnaryOperationRule
- PHPStan\Rules\Operators\InvalidComparisonOperationRule
- PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule
Expand Down Expand Up @@ -138,3 +137,9 @@ services:

-
class: PHPStan\Rules\Pure\PureMethodRule
-
class: PHPStan\Rules\Operators\InvalidBinaryOperationRule
arguments:
bleedingEdge: %featureToggles.bleedingEdge%
tags:
- phpstan.rules.rule
4 changes: 4 additions & 0 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,10 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback):
return $this->getNeverType($leftType, $rightType);
}

if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) {
return new ErrorType();
}

$leftTypes = $leftType->getConstantScalarTypes();
$rightTypes = $rightType->getConstantScalarTypes();
$leftTypesCount = count($leftTypes);
Expand Down
149 changes: 76 additions & 73 deletions src/Rules/Operators/InvalidBinaryOperationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class InvalidBinaryOperationRule implements Rule
public function __construct(
private ExprPrinter $exprPrinter,
private RuleLevelHelper $ruleLevelHelper,
private bool $bleedingEdge,
)
{
}
Expand All @@ -44,81 +45,83 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

if ($scope->getType($node) instanceof ErrorType) {
$leftName = '__PHPSTAN__LEFT__';
$rightName = '__PHPSTAN__RIGHT__';
$leftVariable = new Node\Expr\Variable($leftName);
$rightVariable = new Node\Expr\Variable($rightName);
if ($node instanceof Node\Expr\AssignOp) {
$identifier = 'assignOp';
$newNode = clone $node;
$newNode->setAttribute('phpstan_cache_printer', null);
$left = $node->var;
$right = $node->expr;
$newNode->var = $leftVariable;
$newNode->expr = $rightVariable;
} else {
$identifier = 'binaryOp';
$newNode = clone $node;
$newNode->setAttribute('phpstan_cache_printer', null);
$left = $node->left;
$right = $node->right;
$newNode->left = $leftVariable;
$newNode->right = $rightVariable;
}

if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) {
$callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType;
} else {
$callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType;
}

$leftType = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$left,
'',
$callback,
)->getType();
if ($leftType instanceof ErrorType) {
return [];
}

$rightType = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$right,
'',
$callback,
)->getType();
if ($rightType instanceof ErrorType) {
return [];
}

if (!$scope instanceof MutatingScope) {
throw new ShouldNotHappenException();
}

$scope = $scope
->assignVariable($leftName, $leftType, $leftType)
->assignVariable($rightName, $rightType, $rightType);

if (!$scope->getType($newNode) instanceof ErrorType) {
return [];
}

return [
RuleErrorBuilder::message(sprintf(
'Binary operation "%s" between %s and %s results in an error.',
substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)),
$scope->getType($left)->describe(VerbosityLevel::value()),
$scope->getType($right)->describe(VerbosityLevel::value()),
))
->line($left->getStartLine())
->identifier(sprintf('%s.invalid', $identifier))
->build(),
];
if (!$scope->getType($node) instanceof ErrorType && !$this->bleedingEdge) {
return [];
}

$leftName = '__PHPSTAN__LEFT__';
$rightName = '__PHPSTAN__RIGHT__';
$leftVariable = new Node\Expr\Variable($leftName);
$rightVariable = new Node\Expr\Variable($rightName);
if ($node instanceof Node\Expr\AssignOp) {
$identifier = 'assignOp';
$newNode = clone $node;
$newNode->setAttribute('phpstan_cache_printer', null);
$left = $node->var;
$right = $node->expr;
$newNode->var = $leftVariable;
$newNode->expr = $rightVariable;
} else {
$identifier = 'binaryOp';
$newNode = clone $node;
$newNode->setAttribute('phpstan_cache_printer', null);
$left = $node->left;
$right = $node->right;
$newNode->left = $leftVariable;
$newNode->right = $rightVariable;
}

if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) {
$callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType;
} elseif ($node instanceof Node\Expr\AssignOp\Plus || $node instanceof Node\Expr\BinaryOp\Plus) {
$callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType || $type->isArray()->yes();
} else {
$callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType;
}

$leftType = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$left,
'',
$callback,
)->getType();
if ($leftType instanceof ErrorType) {
return [];
}

$rightType = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$right,
'',
$callback,
)->getType();
if ($rightType instanceof ErrorType) {
return [];
}

if (!$scope instanceof MutatingScope) {
throw new ShouldNotHappenException();
}

$scope = $scope
->assignVariable($leftName, $leftType, $leftType)
->assignVariable($rightName, $rightType, $rightType);

if (!$scope->getType($newNode) instanceof ErrorType) {
return [];
}

return [];
return [
RuleErrorBuilder::message(sprintf(
'Binary operation "%s" between %s and %s results in an error.',
substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)),
$scope->getType($left)->describe(VerbosityLevel::value()),
$scope->getType($right)->describe(VerbosityLevel::value()),
))
->line($left->getStartLine())
->identifier(sprintf('%s.invalid', $identifier))
->build(),
];
}

}
10 changes: 9 additions & 1 deletion src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,15 @@ private function findTypeToCheckImplementation(
}

if (count($newTypes) > 0) {
return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null);
$newUnion = TypeCombinator::union(...$newTypes);
if (
!$this->checkBenevolentUnionTypes
&& $type instanceof BenevolentUnionType
) {
$newUnion = TypeUtils::toBenevolentUnion($newUnion);
}

return new FoundTypeResult($newUnion, $directClassNames, [], null);
}
}

Expand Down
Loading

0 comments on commit ef2a246

Please sign in to comment.