Skip to content

Commit 275f816

Browse files
Improve TypeCombinator::unionWithSubtractedType
1 parent 6726c9f commit 275f816

File tree

7 files changed

+93
-43
lines changed

7 files changed

+93
-43
lines changed

src/Type/MixedType.php

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use PHPStan\Type\Generic\TemplateType;
3737
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
3838
use PHPStan\Type\Traits\NonGenericTypeTrait;
39+
use PHPStan\Type\Traits\SubstractableTypeTrait;
3940
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
4041
use function get_class;
4142
use function sprintf;
@@ -47,6 +48,7 @@ class MixedType implements CompoundType, SubtractableType
4748
use NonGenericTypeTrait;
4849
use UndecidedComparisonCompoundTypeTrait;
4950
use NonGeneralizableTypeTrait;
51+
use SubstractableTypeTrait;
5052

5153
private ?Type $subtractedType;
5254

@@ -471,23 +473,9 @@ public function describe(VerbosityLevel $level): string
471473
return $level->handle(
472474
static fn (): string => 'mixed',
473475
static fn (): string => 'mixed',
476+
fn (): string => 'mixed' . $this->describeSubtractedType($this->subtractedType, $level),
474477
function () use ($level): string {
475-
$description = 'mixed';
476-
if ($this->subtractedType !== null) {
477-
$description .= $this->subtractedType instanceof UnionType
478-
? sprintf('~(%s)', $this->subtractedType->describe($level))
479-
: sprintf('~%s', $this->subtractedType->describe($level));
480-
}
481-
482-
return $description;
483-
},
484-
function () use ($level): string {
485-
$description = 'mixed';
486-
if ($this->subtractedType !== null) {
487-
$description .= $this->subtractedType instanceof UnionType
488-
? sprintf('~(%s)', $this->subtractedType->describe($level))
489-
: sprintf('~%s', $this->subtractedType->describe($level));
490-
}
478+
$description = 'mixed' . $this->describeSubtractedType($this->subtractedType, $level);
491479

492480
if ($this->isExplicitMixed) {
493481
$description .= '=explicit';

src/Type/ObjectType.php

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
use PHPStan\Type\Traits\NonArrayTypeTrait;
4848
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
4949
use PHPStan\Type\Traits\NonGenericTypeTrait;
50+
use PHPStan\Type\Traits\SubstractableTypeTrait;
5051
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
5152
use Stringable;
5253
use Throwable;
@@ -69,6 +70,7 @@ class ObjectType implements TypeWithClassName, SubtractableType
6970
use NonGenericTypeTrait;
7071
use UndecidedComparisonTypeTrait;
7172
use NonGeneralizableTypeTrait;
73+
use SubstractableTypeTrait;
7274

7375
private const EXTRA_OFFSET_CLASSES = [
7476
'DOMNamedNodeMap', // Only read and existence
@@ -505,16 +507,7 @@ public function describe(VerbosityLevel $level): string
505507
return $reflectionProvider->getClassName($this->className);
506508
};
507509

508-
$preciseWithSubtracted = function () use ($level): string {
509-
$description = $this->className;
510-
if ($this->subtractedType !== null) {
511-
$description .= $this->subtractedType instanceof UnionType
512-
? sprintf('~(%s)', $this->subtractedType->describe($level))
513-
: sprintf('~%s', $this->subtractedType->describe($level));
514-
}
515-
516-
return $description;
517-
};
510+
$preciseWithSubtracted = fn (): string => $this->className . $this->describeSubtractedType($this->subtractedType, $level);
518511

519512
return $level->handle(
520513
$preciseNameCallback,
@@ -560,11 +553,7 @@ private function describeCache(): string
560553
$description .= '<' . implode(', ', $typeDescriptions) . '>';
561554
}
562555

563-
if ($this->subtractedType !== null) {
564-
$description .= $this->subtractedType instanceof UnionType
565-
? sprintf('~(%s)', $this->subtractedType->describe(VerbosityLevel::cache()))
566-
: sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache()));
567-
}
556+
$description .= $this->describeSubtractedType($this->subtractedType, VerbosityLevel::cache());
568557

569558
$reflection = $this->classReflection;
570559
if ($reflection !== null) {

src/Type/ObjectWithoutClassType.php

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
88
use PHPStan\Type\Traits\NonGenericTypeTrait;
99
use PHPStan\Type\Traits\ObjectTypeTrait;
10+
use PHPStan\Type\Traits\SubstractableTypeTrait;
1011
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
11-
use function sprintf;
1212

1313
/** @api */
1414
class ObjectWithoutClassType implements SubtractableType
@@ -18,6 +18,7 @@ class ObjectWithoutClassType implements SubtractableType
1818
use NonGenericTypeTrait;
1919
use UndecidedComparisonTypeTrait;
2020
use NonGeneralizableTypeTrait;
21+
use SubstractableTypeTrait;
2122

2223
private ?Type $subtractedType;
2324

@@ -120,16 +121,7 @@ public function describe(VerbosityLevel $level): string
120121
return $level->handle(
121122
static fn (): string => 'object',
122123
static fn (): string => 'object',
123-
function () use ($level): string {
124-
$description = 'object';
125-
if ($this->subtractedType !== null) {
126-
$description .= $this->subtractedType instanceof UnionType
127-
? sprintf('~(%s)', $this->subtractedType->describe($level))
128-
: sprintf('~%s', $this->subtractedType->describe($level));
129-
}
130-
131-
return $description;
132-
},
124+
fn (): string => 'object' . $this->describeSubtractedType($this->subtractedType, $level),
133125
);
134126
}
135127

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Traits;
4+
5+
use PHPStan\Type\SubtractableType;
6+
use PHPStan\Type\Type;
7+
use PHPStan\Type\UnionType;
8+
use PHPStan\Type\VerbosityLevel;
9+
use function sprintf;
10+
11+
trait SubstractableTypeTrait
12+
{
13+
14+
public function describeSubtractedType(?Type $subtractedType, VerbosityLevel $level): string
15+
{
16+
if ($subtractedType === null) {
17+
return '';
18+
}
19+
20+
if (
21+
$subtractedType instanceof UnionType
22+
|| ($subtractedType instanceof SubtractableType && $subtractedType->getSubtractedType() !== null)
23+
) {
24+
return sprintf('~(%s)', $subtractedType->describe($level));
25+
}
26+
27+
return sprintf('~%s', $subtractedType->describe($level));
28+
}
29+
30+
}

src/Type/TypeCombinator.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,18 @@ private static function unionWithSubtractedType(
540540
return $type;
541541
}
542542

543+
if ($subtractedType instanceof SubtractableType) {
544+
$withoutSubtracted = $subtractedType->getTypeWithoutSubtractedType();
545+
if ($withoutSubtracted->isSuperTypeOf($type)->yes()) {
546+
$subtractedSubtractedType = $subtractedType->getSubtractedType();
547+
if ($subtractedSubtractedType === null) {
548+
return new NeverType();
549+
}
550+
551+
return self::intersect($type, $subtractedSubtractedType);
552+
}
553+
}
554+
543555
if ($type instanceof SubtractableType) {
544556
$subtractedType = $type->getSubtractedType() === null
545557
? $subtractedType
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Substracted;
4+
5+
6+
use function PHPStan\Testing\assertType;
7+
8+
class HelloWorld
9+
{
10+
/**
11+
* @param mixed $date
12+
* @param bool $foo
13+
*/
14+
public function sayHello($date, $foo): void
15+
{
16+
if(is_object($date)){
17+
18+
} else {
19+
assertType('mixed~object', $date);
20+
21+
if ($foo) {
22+
$date = new \stdClass();
23+
}
24+
assertType('mixed~(object~stdClass)', $date);
25+
26+
if (is_object($date)) {
27+
assertType('stdClass', $date);
28+
}
29+
}
30+
}
31+
}

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1140,7 +1140,7 @@ public static function dataUnion(): iterable
11401140
new ObjectType('InvalidArgumentException'),
11411141
],
11421142
MixedType::class,
1143-
'mixed~Exception~InvalidArgumentException=implicit',
1143+
'mixed~(Exception~InvalidArgumentException)=implicit',
11441144
],
11451145
[
11461146
[
@@ -4143,6 +4143,14 @@ public static function dataIntersect(): iterable
41434143
ObjectWithoutClassType::class,
41444144
'object',
41454145
],
4146+
[
4147+
[
4148+
new MixedType(subtractedType: new ObjectWithoutClassType(new ObjectType('stdClass'))),
4149+
new ObjectWithoutClassType(),
4150+
],
4151+
ObjectType::class,
4152+
'stdClass',
4153+
],
41464154
];
41474155

41484156
if (PHP_VERSION_ID < 80100) {

0 commit comments

Comments
 (0)