Skip to content

Commit ef2a246

Browse files
authored
Bleeding edge - check mixed in binary operator
1 parent d64b39f commit ef2a246

File tree

10 files changed

+1172
-76
lines changed

10 files changed

+1172
-76
lines changed

conf/config.level2.neon

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ rules:
3030
- PHPStan\Rules\Generics\UsedTraitsRule
3131
- PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule
3232
- PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule
33-
- PHPStan\Rules\Operators\InvalidBinaryOperationRule
3433
- PHPStan\Rules\Operators\InvalidUnaryOperationRule
3534
- PHPStan\Rules\Operators\InvalidComparisonOperationRule
3635
- PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule
@@ -138,3 +137,9 @@ services:
138137

139138
-
140139
class: PHPStan\Rules\Pure\PureMethodRule
140+
-
141+
class: PHPStan\Rules\Operators\InvalidBinaryOperationRule
142+
arguments:
143+
bleedingEdge: %featureToggles.bleedingEdge%
144+
tags:
145+
- phpstan.rules.rule

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,10 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback):
856856
return $this->getNeverType($leftType, $rightType);
857857
}
858858

859+
if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) {
860+
return new ErrorType();
861+
}
862+
859863
$leftTypes = $leftType->getConstantScalarTypes();
860864
$rightTypes = $rightType->getConstantScalarTypes();
861865
$leftTypesCount = count($leftTypes);

src/Rules/Operators/InvalidBinaryOperationRule.php

Lines changed: 76 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class InvalidBinaryOperationRule implements Rule
2626
public function __construct(
2727
private ExprPrinter $exprPrinter,
2828
private RuleLevelHelper $ruleLevelHelper,
29+
private bool $bleedingEdge,
2930
)
3031
{
3132
}
@@ -44,81 +45,83 @@ public function processNode(Node $node, Scope $scope): array
4445
return [];
4546
}
4647

47-
if ($scope->getType($node) instanceof ErrorType) {
48-
$leftName = '__PHPSTAN__LEFT__';
49-
$rightName = '__PHPSTAN__RIGHT__';
50-
$leftVariable = new Node\Expr\Variable($leftName);
51-
$rightVariable = new Node\Expr\Variable($rightName);
52-
if ($node instanceof Node\Expr\AssignOp) {
53-
$identifier = 'assignOp';
54-
$newNode = clone $node;
55-
$newNode->setAttribute('phpstan_cache_printer', null);
56-
$left = $node->var;
57-
$right = $node->expr;
58-
$newNode->var = $leftVariable;
59-
$newNode->expr = $rightVariable;
60-
} else {
61-
$identifier = 'binaryOp';
62-
$newNode = clone $node;
63-
$newNode->setAttribute('phpstan_cache_printer', null);
64-
$left = $node->left;
65-
$right = $node->right;
66-
$newNode->left = $leftVariable;
67-
$newNode->right = $rightVariable;
68-
}
69-
70-
if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) {
71-
$callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType;
72-
} else {
73-
$callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType;
74-
}
75-
76-
$leftType = $this->ruleLevelHelper->findTypeToCheck(
77-
$scope,
78-
$left,
79-
'',
80-
$callback,
81-
)->getType();
82-
if ($leftType instanceof ErrorType) {
83-
return [];
84-
}
85-
86-
$rightType = $this->ruleLevelHelper->findTypeToCheck(
87-
$scope,
88-
$right,
89-
'',
90-
$callback,
91-
)->getType();
92-
if ($rightType instanceof ErrorType) {
93-
return [];
94-
}
95-
96-
if (!$scope instanceof MutatingScope) {
97-
throw new ShouldNotHappenException();
98-
}
99-
100-
$scope = $scope
101-
->assignVariable($leftName, $leftType, $leftType)
102-
->assignVariable($rightName, $rightType, $rightType);
103-
104-
if (!$scope->getType($newNode) instanceof ErrorType) {
105-
return [];
106-
}
107-
108-
return [
109-
RuleErrorBuilder::message(sprintf(
110-
'Binary operation "%s" between %s and %s results in an error.',
111-
substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)),
112-
$scope->getType($left)->describe(VerbosityLevel::value()),
113-
$scope->getType($right)->describe(VerbosityLevel::value()),
114-
))
115-
->line($left->getStartLine())
116-
->identifier(sprintf('%s.invalid', $identifier))
117-
->build(),
118-
];
48+
if (!$scope->getType($node) instanceof ErrorType && !$this->bleedingEdge) {
49+
return [];
50+
}
51+
52+
$leftName = '__PHPSTAN__LEFT__';
53+
$rightName = '__PHPSTAN__RIGHT__';
54+
$leftVariable = new Node\Expr\Variable($leftName);
55+
$rightVariable = new Node\Expr\Variable($rightName);
56+
if ($node instanceof Node\Expr\AssignOp) {
57+
$identifier = 'assignOp';
58+
$newNode = clone $node;
59+
$newNode->setAttribute('phpstan_cache_printer', null);
60+
$left = $node->var;
61+
$right = $node->expr;
62+
$newNode->var = $leftVariable;
63+
$newNode->expr = $rightVariable;
64+
} else {
65+
$identifier = 'binaryOp';
66+
$newNode = clone $node;
67+
$newNode->setAttribute('phpstan_cache_printer', null);
68+
$left = $node->left;
69+
$right = $node->right;
70+
$newNode->left = $leftVariable;
71+
$newNode->right = $rightVariable;
72+
}
73+
74+
if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) {
75+
$callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType;
76+
} elseif ($node instanceof Node\Expr\AssignOp\Plus || $node instanceof Node\Expr\BinaryOp\Plus) {
77+
$callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType || $type->isArray()->yes();
78+
} else {
79+
$callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType;
80+
}
81+
82+
$leftType = $this->ruleLevelHelper->findTypeToCheck(
83+
$scope,
84+
$left,
85+
'',
86+
$callback,
87+
)->getType();
88+
if ($leftType instanceof ErrorType) {
89+
return [];
90+
}
91+
92+
$rightType = $this->ruleLevelHelper->findTypeToCheck(
93+
$scope,
94+
$right,
95+
'',
96+
$callback,
97+
)->getType();
98+
if ($rightType instanceof ErrorType) {
99+
return [];
100+
}
101+
102+
if (!$scope instanceof MutatingScope) {
103+
throw new ShouldNotHappenException();
104+
}
105+
106+
$scope = $scope
107+
->assignVariable($leftName, $leftType, $leftType)
108+
->assignVariable($rightName, $rightType, $rightType);
109+
110+
if (!$scope->getType($newNode) instanceof ErrorType) {
111+
return [];
119112
}
120113

121-
return [];
114+
return [
115+
RuleErrorBuilder::message(sprintf(
116+
'Binary operation "%s" between %s and %s results in an error.',
117+
substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)),
118+
$scope->getType($left)->describe(VerbosityLevel::value()),
119+
$scope->getType($right)->describe(VerbosityLevel::value()),
120+
))
121+
->line($left->getStartLine())
122+
->identifier(sprintf('%s.invalid', $identifier))
123+
->build(),
124+
];
122125
}
123126

124127
}

src/Rules/RuleLevelHelper.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,15 @@ private function findTypeToCheckImplementation(
439439
}
440440

441441
if (count($newTypes) > 0) {
442-
return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null);
442+
$newUnion = TypeCombinator::union(...$newTypes);
443+
if (
444+
!$this->checkBenevolentUnionTypes
445+
&& $type instanceof BenevolentUnionType
446+
) {
447+
$newUnion = TypeUtils::toBenevolentUnion($newUnion);
448+
}
449+
450+
return new FoundTypeResult($newUnion, $directClassNames, [], null);
443451
}
444452
}
445453

0 commit comments

Comments
 (0)