Skip to content

Commit 089d4c6

Browse files
committed
Infer explicit mixed when instantiating generic class with unknown template types only in bleeding edge
1 parent b49df58 commit 089d4c6

9 files changed

+104
-2
lines changed

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ parameters:
22
featureToggles:
33
bleedingEdge: true
44
skipCheckGenericClasses: []
5+
explicitMixedInUnknownGenericNew: true
56
stubFiles:
67
- ../stubs/bleedingEdge/Countable.stub

conf/config.neon

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ parameters:
2727
- CallbackFilterIterator
2828
- FilterIterator
2929
- RecursiveCallbackFilterIterator
30+
explicitMixedInUnknownGenericNew: false
3031
fileExtensions:
3132
- php
3233
checkAdvancedIsset: false
@@ -200,6 +201,7 @@ parametersSchema:
200201
bleedingEdge: bool(),
201202
disableRuntimeReflectionProvider: bool(),
202203
skipCheckGenericClasses: listOf(string()),
204+
explicitMixedInUnknownGenericNew: bool(),
203205
])
204206
fileExtensions: listOf(string())
205207
checkAdvancedIsset: bool()

src/Analyser/DirectScopeFactory.php

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public function __construct(
3939
private bool $treatPhpDocTypesAsCertain,
4040
Container $container,
4141
private PhpVersion $phpVersion,
42+
private bool $explicitMixedInUnknownGenericNew,
4243
)
4344
{
4445
$this->dynamicConstantNames = $container->getParameter('dynamicConstantNames');
@@ -107,6 +108,7 @@ public function create(
107108
$this->treatPhpDocTypesAsCertain,
108109
$afterExtractCall,
109110
$parentScope,
111+
$this->explicitMixedInUnknownGenericNew,
110112
);
111113
}
112114

src/Analyser/LazyScopeFactory.php

+4
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ class LazyScopeFactory implements ScopeFactory
2424

2525
private bool $treatPhpDocTypesAsCertain;
2626

27+
private bool $explicitMixedInUnknownGenericNew;
28+
2729
public function __construct(
2830
private string $scopeClass,
2931
private Container $container,
3032
)
3133
{
3234
$this->dynamicConstantNames = $container->getParameter('dynamicConstantNames');
3335
$this->treatPhpDocTypesAsCertain = $container->getParameter('treatPhpDocTypesAsCertain');
36+
$this->explicitMixedInUnknownGenericNew = $this->container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'];
3437
}
3538

3639
/**
@@ -96,6 +99,7 @@ public function create(
9699
$this->treatPhpDocTypesAsCertain,
97100
$afterExtractCall,
98101
$parentScope,
102+
$this->explicitMixedInUnknownGenericNew,
99103
);
100104
}
101105

src/Analyser/MutatingScope.php

+30-1
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ public function __construct(
200200
private bool $treatPhpDocTypesAsCertain = true,
201201
private bool $afterExtractCall = false,
202202
private ?Scope $parentScope = null,
203+
private bool $explicitMixedInUnknownGenericNew = false,
203204
)
204205
{
205206
if ($namespace === '') {
@@ -2894,6 +2895,7 @@ public function doNotTreatPhpDocTypesAsCertain(): Scope
28942895
false,
28952896
$this->afterExtractCall,
28962897
$this->parentScope,
2898+
$this->explicitMixedInUnknownGenericNew,
28972899
);
28982900
}
28992901

@@ -5649,9 +5651,36 @@ private function exactInstantiation(New_ $node, string $className): ?Type
56495651
$constructorMethod->getVariants(),
56505652
);
56515653

5654+
if ($this->explicitMixedInUnknownGenericNew) {
5655+
return new GenericObjectType(
5656+
$resolvedClassName,
5657+
$classReflection->typeMapToList($parametersAcceptor->getResolvedTemplateTypeMap()),
5658+
);
5659+
}
5660+
5661+
$resolvedPhpDoc = $classReflection->getResolvedPhpDoc();
5662+
if ($resolvedPhpDoc === null) {
5663+
return $objectType;
5664+
}
5665+
5666+
$list = [];
5667+
$typeMap = $parametersAcceptor->getResolvedTemplateTypeMap();
5668+
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
5669+
$templateType = $typeMap->getType($tag->getName());
5670+
if ($templateType !== null) {
5671+
$list[] = $templateType;
5672+
continue;
5673+
}
5674+
$bound = $tag->getBound();
5675+
if ($bound instanceof MixedType && $bound->isExplicitMixed()) {
5676+
$bound = new MixedType(false);
5677+
}
5678+
$list[] = $bound;
5679+
}
5680+
56525681
return new GenericObjectType(
56535682
$resolvedClassName,
5654-
$classReflection->typeMapToList($parametersAcceptor->getResolvedTemplateTypeMap()),
5683+
$list,
56555684
);
56565685
}
56575686

src/Testing/PHPStanTestCase.php

+1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeS
162162
$this->shouldTreatPhpDocTypesAsCertain(),
163163
$container,
164164
$container->getByType(PhpVersion::class),
165+
$container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'],
165166
);
166167
}
167168

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Rules\RuleLevelHelper;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<TypesAssignedToPropertiesRule>
11+
*/
12+
class TypesAssignedToPropertiesRuleNoBleedingEdgeTest extends RuleTestCase
13+
{
14+
15+
private bool $checkExplicitMixed = false;
16+
17+
protected function getRule(): Rule
18+
{
19+
return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed), new PropertyDescriptor(), new PropertyReflectionFinder());
20+
}
21+
22+
public function testGenericObjectWithUnspecifiedTemplateTypes(): void
23+
{
24+
$this->checkExplicitMixed = true;
25+
$this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], []);
26+
}
27+
28+
public static function getAdditionalConfigFiles(): array
29+
{
30+
// no bleeding edge
31+
return [];
32+
}
33+
34+
}

tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php

+14-1
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
class TypesAssignedToPropertiesRuleTest extends RuleTestCase
1414
{
1515

16+
private bool $checkExplicitMixed = false;
17+
1618
protected function getRule(): Rule
1719
{
18-
return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), new PropertyDescriptor(), new PropertyReflectionFinder());
20+
return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed), new PropertyDescriptor(), new PropertyReflectionFinder());
1921
}
2022

2123
public function testTypesAssignedToProperties(): void
@@ -387,4 +389,15 @@ public function testBug6117(): void
387389
$this->analyse([__DIR__ . '/data/bug-6117.php'], []);
388390
}
389391

392+
public function testGenericObjectWithUnspecifiedTemplateTypes(): void
393+
{
394+
$this->checkExplicitMixed = true;
395+
$this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [
396+
[
397+
'Property GenericObjectUnspecifiedTemplateTypes\Foo::$obj (ArrayObject<int, string>) does not accept ArrayObject<(int|string), mixed>.',
398+
13,
399+
],
400+
]);
401+
}
402+
390403
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace GenericObjectUnspecifiedTemplateTypes;
4+
5+
class Foo
6+
{
7+
8+
/** @var \ArrayObject<int, string> */
9+
private $obj;
10+
11+
public function __construct()
12+
{
13+
$this->obj = new \ArrayObject();
14+
}
15+
16+
}

0 commit comments

Comments
 (0)