Skip to content

Commit 20c7c73

Browse files
authored
ForbidIdenticalClassComparisonRule: fix false positive with TemplateMixedType (#115)
1 parent 9ffe7b7 commit 20c7c73

File tree

2 files changed

+34
-17
lines changed

2 files changed

+34
-17
lines changed

src/Rule/ForbidIdenticalClassComparisonRule.php

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111
use PHPStan\Analyser\Scope;
1212
use PHPStan\Reflection\ReflectionProvider;
1313
use PHPStan\Rules\Rule;
14-
use PHPStan\Type\CallableType;
15-
use PHPStan\Type\MixedType;
1614
use PHPStan\Type\ObjectType;
17-
use PHPStan\Type\ObjectWithoutClassType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeUtils;
1817
use PHPStan\Type\VerbosityLevel;
1918
use function count;
2019

@@ -25,11 +24,6 @@ class ForbidIdenticalClassComparisonRule implements Rule
2524
{
2625

2726
private const DEFAULT_BLACKLIST = [DateTimeInterface::class];
28-
private const IGNORED_TYPES = [
29-
MixedType::class, // mixed is "maybe" accepted by any (denied) class
30-
ObjectWithoutClassType::class, // object is "maybe" accepted by any (denied) class
31-
CallableType::class, // any non-final class descendant can have __invoke method causing it to be "maybe" accepted by any (denied) class
32-
];
3327

3428
/**
3529
* @var array<int, class-string<object>>
@@ -80,20 +74,14 @@ public function processNode(Node $node, Scope $scope): array
8074
return []; // always-true or always-false, already reported by native PHPStan (like $a === $a)
8175
}
8276

83-
foreach (self::IGNORED_TYPES as $ignoredType) {
84-
if ($leftType instanceof $ignoredType || $rightType instanceof $ignoredType) {
85-
return [];
86-
}
87-
}
88-
8977
$errors = [];
9078

9179
foreach ($this->blacklist as $className) {
9280
$forbiddenObjectType = new ObjectType($className);
9381

9482
if (
95-
!$forbiddenObjectType->accepts($leftType, $scope->isDeclareStrictTypes())->no()
96-
&& !$forbiddenObjectType->accepts($rightType, $scope->isDeclareStrictTypes())->no()
83+
$this->containsClass($leftType, $className)
84+
&& $this->containsClass($rightType, $className)
9785
) {
9886
$errors[] = "Using {$node->getOperatorSigil()} with {$forbiddenObjectType->describe(VerbosityLevel::typeOnly())} is denied";
9987
}
@@ -102,4 +90,19 @@ public function processNode(Node $node, Scope $scope): array
10290
return $errors;
10391
}
10492

93+
private function containsClass(Type $type, string $className): bool
94+
{
95+
$benevolentType = TypeUtils::toBenevolentUnion($type);
96+
97+
foreach ($benevolentType->getObjectClassNames() as $classNameInType) {
98+
$classInType = new ObjectType($classNameInType);
99+
100+
if ($classInType->isInstanceOf($className)->yes()) {
101+
return true;
102+
}
103+
}
104+
105+
return false;
106+
}
107+
105108
}

tests/Rule/data/ForbidIdenticalClassComparisonRule/code.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,30 @@ public function testNonObject(?DateTimeImmutable $a, string $b): void
2323
}
2424
}
2525

26+
/**
27+
* @param TItem|null $mixedTemplate1
28+
* @param TItem|null $mixedTemplate2
29+
* @param callable(DateTimeImmutable): void $callable1
30+
* @param callable(DateTimeImmutable): void $callable2
31+
*
32+
* @template TItem
33+
*/
2634
public function testProblematicTypes(
2735
DateTimeImmutable $a,
2836
mixed $b,
2937
object $c,
30-
callable $d
38+
callable $d,
39+
mixed $mixedTemplate1,
40+
mixed $mixedTemplate2,
41+
callable $callable1,
42+
callable $callable2
3143
): void
3244
{
3345
$a === $b;
3446
$a === $c;
3547
$a === $d;
48+
$mixedTemplate1 === $mixedTemplate2;
49+
$callable1 === $callable2;
3650
}
3751

3852
public function testRegular(DateTimeImmutable $a, DateTimeImmutable $b): void

0 commit comments

Comments
 (0)