Skip to content

Commit 943fd2a

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix ObjectType::equals (phpstan#5399)
1 parent 8cf2aa3 commit 943fd2a

File tree

11 files changed

+392
-12
lines changed

11 files changed

+392
-12
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,12 +1473,6 @@ parameters:
14731473
count: 1
14741474
path: src/Type/ObjectShapeType.php
14751475

1476-
-
1477-
rawMessage: 'Doing instanceof PHPStan\Type\Enum\EnumCaseObjectType is error-prone and deprecated. Use Type::getEnumCases() instead.'
1478-
identifier: phpstanApi.instanceofType
1479-
count: 1
1480-
path: src/Type/ObjectType.php
1481-
14821476
-
14831477
rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.
14841478
identifier: phpstanApi.instanceofType
@@ -1488,7 +1482,7 @@ parameters:
14881482
-
14891483
rawMessage: 'Doing instanceof PHPStan\Type\ObjectType is error-prone and deprecated. Use Type::isObject() or Type::getObjectClassNames() instead.'
14901484
identifier: phpstanApi.instanceofType
1491-
count: 3
1485+
count: 2
14921486
path: src/Type/ObjectType.php
14931487

14941488
-

src/Type/ObjectType.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
use function array_map;
5757
use function array_values;
5858
use function count;
59+
use function get_class;
5960
use function implode;
6061
use function in_array;
6162
use function sprintf;
@@ -624,11 +625,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
624625

625626
public function equals(Type $type): bool
626627
{
627-
if (!$type instanceof self) {
628-
return false;
629-
}
630-
631-
if ($type instanceof EnumCaseObjectType) {
628+
if (get_class($type) !== static::class) {
632629
return false;
633630
}
634631

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,13 @@ public function testBug13566(): void
11951195
]);
11961196
}
11971197

1198+
#[RequiresPhp('>= 8.1')]
1199+
public function testBug14429(): void
1200+
{
1201+
$this->treatPhpDocTypesAsCertain = false;
1202+
$this->analyse([__DIR__ . '/data/bug-14429.php'], []);
1203+
}
1204+
11981205
public function testBug13799(): void
11991206
{
12001207
$this->treatPhpDocTypesAsCertain = true;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14429;
6+
7+
function throw_if(bool $condition, string $message): void
8+
{
9+
if ($condition) { throw new \Exception($message); }
10+
}
11+
12+
class Foo
13+
{
14+
/**
15+
* @param list<string> $tags
16+
* @param list<float> $scores
17+
* @param \ArrayObject<string, string> $stringMap
18+
* @param \ArrayObject<string, int> $intKeyMap
19+
*/
20+
public function __construct(
21+
public array $tags,
22+
public array $scores,
23+
public ?\ArrayObject $stringMap = null,
24+
public ?\ArrayObject $intKeyMap = null,
25+
) {
26+
foreach ($tags as $tagsItem) {
27+
throw_if(!is_string($tagsItem), 'tags item must be string');
28+
}
29+
foreach ($scores as $scoresItem) {
30+
throw_if(!is_int($scoresItem) && !is_float($scoresItem), 'scores item must be number');
31+
}
32+
if ($stringMap !== null) {
33+
foreach ($stringMap as $stringMapValue) {
34+
throw_if(!is_string($stringMapValue), 'stringMap value must be string');
35+
}
36+
}
37+
if ($intKeyMap !== null) {
38+
foreach ($intKeyMap as $intKeyMapValue) {
39+
throw_if(!is_int($intKeyMapValue), 'intKeyMap value must be int');
40+
}
41+
}
42+
}
43+
}

tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,19 @@ public function testBug12973(): void
404404
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-12973.php'], []);
405405
}
406406

407+
public function testBug3028(): void
408+
{
409+
$this->checkNullables = true;
410+
$this->checkExplicitMixed = true;
411+
$this->analyse([__DIR__ . '/data/bug-3028.php'], [
412+
[
413+
'Function Bug3028\run() should return Bug3028\Format<Bug3028\Output> but returns Bug3028\Format<O of Bug3028\Output>.',
414+
50,
415+
'Template type O on class Bug3028\Format is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
416+
],
417+
]);
418+
}
419+
407420
public function testBug12397(): void
408421
{
409422
$this->checkNullables = true;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug3028;
4+
5+
interface Output { }
6+
7+
final class OutputImpl1 implements Output { }
8+
final class OutputImpl2 implements Output { }
9+
10+
/** @psalm-template O of Output */
11+
interface Format
12+
{
13+
/** @psalm-return O */
14+
public function output() : Output;
15+
16+
/** @psalm-param O $o */
17+
public function replace(Output $o) : void;
18+
}
19+
20+
/** @implements Format<OutputImpl1> */
21+
final class FormatImpl1 implements Format
22+
{
23+
public OutputImpl1 $o;
24+
25+
public function __construct() {
26+
$this->o = new OutputImpl1;
27+
}
28+
29+
public function output() : Output
30+
{
31+
return new OutputImpl1();
32+
}
33+
34+
/**
35+
* @param OutputImpl1 $o
36+
*/
37+
public function replace(Output $o) : void
38+
{
39+
$this->o = $o;
40+
}
41+
}
42+
43+
44+
/**
45+
* @psalm-template O of Output
46+
* @psalm-param Format<O> $outputFormat
47+
* @return Format<Output>
48+
*/
49+
function run(Format $outputFormat) : Format {
50+
return $outputFormat;
51+
}
52+
53+
$a = new FormatImpl1;
54+
run($a)->replace(new OutputImpl2);

tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,22 @@ public function testBug12457(): void
586586
]);
587587
}
588588

589+
public function testGenericSubtype(): void
590+
{
591+
$this->checkTypeAgainstPhpDocType = true;
592+
$this->strictWideningCheck = true;
593+
$this->analyse([__DIR__ . '/data/generic-subtype.php'], [
594+
[
595+
'PHPDoc tag @var with type GenericSubtype\IRepository<E of GenericSubtype\IEntity> is not subtype of type GenericSubtype\IRepository<GenericSubtype\IEntity>.',
596+
78,
597+
],
598+
[
599+
'PHPDoc tag @var with type GenericSubtype\IRepository<GenericSubtype\Foo> is not subtype of type GenericSubtype\IRepository<GenericSubtype\IEntity>.',
600+
131,
601+
],
602+
]);
603+
}
604+
589605
public function testNewIsAlwaysFinalClass(): void
590606
{
591607
$this->checkTypeAgainstPhpDocType = true;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php // lint >= 8.0
2+
3+
namespace GenericSubtype;
4+
5+
interface IEntity {
6+
/**
7+
* @return IRepository<IEntity>
8+
*/
9+
public function getRepository(): IRepository;
10+
}
11+
12+
interface IProperty {}
13+
14+
interface IPropertyContainer extends IProperty {}
15+
16+
/**
17+
* @template E of IEntity
18+
*/
19+
interface IEntityAwareProperty extends IProperty {}
20+
21+
/**
22+
* @template E of IEntity
23+
* @extends IEntityAwareProperty<E>
24+
*/
25+
interface IRelationshipContainer extends IPropertyContainer, IEntityAwareProperty {}
26+
27+
interface IModel {
28+
/**
29+
* @template E of IEntity
30+
* @template T of IRepository<E>
31+
* @param class-string<T> $className
32+
* @return T
33+
*/
34+
public function getRepository(string $className): IRepository;
35+
}
36+
37+
/**
38+
* @template E of IEntity
39+
*/
40+
interface IRepository {
41+
public function getModel(): IModel;
42+
}
43+
44+
class PropertyRelationshipMetadata {
45+
/** @var class-string<IRepository<IEntity>> */
46+
public string $repository;
47+
}
48+
49+
/**
50+
* @template E of IEntity
51+
* @implements IRelationshipContainer<E>
52+
*/
53+
class HasOne implements IRelationshipContainer
54+
{
55+
/** @var E|null */
56+
protected ?IEntity $parent = null;
57+
58+
/** @var IRepository<E>|null */
59+
protected ?IRepository $targetRepository = null;
60+
61+
protected PropertyRelationshipMetadata $metadataRelationship;
62+
63+
/**
64+
* @return E
65+
*/
66+
protected function getParentEntity(): IEntity
67+
{
68+
return $this->parent ?? throw new \InvalidArgumentException('Relationship is not attached to a parent entity.');
69+
}
70+
71+
/**
72+
* @return IRepository<E>
73+
*/
74+
protected function getTargetRepository(): IRepository
75+
{
76+
if ($this->targetRepository === null) {
77+
/** @var IRepository<E> $targetRepository */
78+
$targetRepository = $this->getParentEntity()
79+
->getRepository()
80+
->getModel()
81+
->getRepository($this->metadataRelationship->repository);
82+
83+
$this->test($targetRepository);
84+
85+
$this->targetRepository = $targetRepository;
86+
}
87+
88+
return $this->targetRepository;
89+
}
90+
91+
/**
92+
* @param IRepository<IEntity>
93+
*/
94+
protected function test(): void {}
95+
}
96+
97+
class Foo implements IEntity {
98+
public function getRepository(): IRepository {
99+
throw new \BadMethodCallException();
100+
}
101+
}
102+
103+
/**
104+
* @implements IRelationshipContainer<Foo>
105+
*/
106+
class HasOne2 implements IRelationshipContainer
107+
{
108+
/** @var Foo|null */
109+
protected ?IEntity $parent = null;
110+
111+
/** @var IRepository<Foo>|null */
112+
protected ?IRepository $targetRepository = null;
113+
114+
protected PropertyRelationshipMetadata $metadataRelationship;
115+
116+
/**
117+
* @return Foo
118+
*/
119+
protected function getParentEntity(): IEntity
120+
{
121+
return $this->parent ?? throw new \InvalidArgumentException('Relationship is not attached to a parent entity.');
122+
}
123+
124+
/**
125+
* @return IRepository<Foo>
126+
*/
127+
protected function getTargetRepository(): IRepository
128+
{
129+
if ($this->targetRepository === null) {
130+
/** @var IRepository<Foo> $targetRepository */
131+
$targetRepository = $this->getParentEntity()
132+
->getRepository()
133+
->getModel()
134+
->getRepository($this->metadataRelationship->repository);
135+
136+
$this->test($targetRepository);
137+
138+
$this->targetRepository = $targetRepository;
139+
}
140+
141+
return $this->targetRepository;
142+
}
143+
144+
/**
145+
* @param IRepository<IEntity> $repository
146+
*/
147+
protected function test($repository): void {}
148+
}

tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,17 @@ public function testBug4525(): void
10501050
$this->analyse([__DIR__ . '/data/bug-4525.php'], []);
10511051
}
10521052

1053+
public function testBug5298(): void
1054+
{
1055+
$this->analyse([__DIR__ . '/data/bug-5298.php'], [
1056+
[
1057+
'Property Bug5298\WorldProviderManager::$providers (array<string, Bug5298\WorldProviderManagerEntry<Bug5298\WorldProvider>>) does not accept non-empty-array<string, Bug5298\WorldProviderManagerEntry<Bug5298\WorldProvider>|Bug5298\WorldProviderManagerEntry<T of Bug5298\WorldProvider>>.',
1058+
37,
1059+
'Template type TWorldProvider on class Bug5298\WorldProviderManagerEntry is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
1060+
],
1061+
]);
1062+
}
1063+
10531064
public function testBug10924(): void
10541065
{
10551066
$this->analyse([__DIR__ . '/../Methods/data/bug-10924.php'], []);

0 commit comments

Comments
 (0)