Skip to content
Merged
13 changes: 9 additions & 4 deletions src/Analyser/ExprHandler/NewHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\GenericObjectType;
Expand Down Expand Up @@ -66,6 +67,7 @@ public function __construct(
private ReflectionProvider $reflectionProvider,
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider,
private PropertyReflectionFinder $propertyReflectionFinder,
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
private bool $implicitThrows,
)
Expand Down Expand Up @@ -416,10 +418,13 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas
$classTemplateTypes = $traverser->getClassTemplateTypes();

if (count($classTemplateTypes) === count($originalClassTemplateTypes)) {
$propertyType = TypeCombinator::removeNull($scope->getType($assignedToProperty));
$nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection);
if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) {
return $propertyType;
$foundProperty = $this->propertyReflectionFinder->findPropertyReflectionFromNode($assignedToProperty, $scope);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this fix required for WeakMap but not for other generic objects?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed. To answer staabm's question:

The fix is not WeakMap-specific — it's needed for any generic class whose constructor doesn't reference its template types. The code path at line 420 is entered when count($classTemplateTypes) === count($originalClassTemplateTypes), which means the ConstructorClassTemplateTraverser found no template types consumed by constructor parameters. This is the case for:

  • WeakMap (empty constructor, but has TKey/TValue templates)
  • SplObjectStorage (same pattern)
  • Any custom generic class with a no-arg constructor

For generic classes whose constructors do reference template types (e.g. new GenericContainer($value) where $value: T), the template types are resolved from the constructor arguments instead, so this code path is never reached — and the bug doesn't apply.

I added test cases with a custom CustomGenericCache<TKey, TValue> class (with an explicit no-arg constructor) to demonstrate the fix is general. Both instance and static property variants are tested. All 11857 tests pass and make phpstan reports no errors.

if ($foundProperty !== null) {
$nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection);
$propertyType = TypeCombinator::intersect($foundProperty->getWritableType(), $nonFinalObjectType);
if (!$propertyType instanceof NeverType) {
return $propertyType;
}
}
}
}
Expand Down
211 changes: 211 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11844.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?php // lint >= 8.0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this file into tests/PHPStan/Analyser/nsrt and adjust paths to point to the moved path

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Moved tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php to tests/PHPStan/Analyser/nsrt/bug-11844.php as requested by staabm:

  • The file is now in the nsrt/ directory where it's auto-discovered by NodeScopeResolverTest, so the explicit yield was removed to avoid a duplicate key error
  • Updated the path in TooWidePropertyTypeRuleTest::testBug11844() to reference the new location
  • All 11857 tests pass and make phpstan reports no errors


declare(strict_types = 1);

namespace Bug11844;

use function PHPStan\Testing\assertType;

class StaticPropertyCase
{
/**
* @var \WeakMap<object, string>|null
*/
private static ?\WeakMap $map = null;

public static function init(): void
{
if (self::$map === null) {
self::$map = new \WeakMap();
assertType('WeakMap<object, string>', self::$map);
}
}
}

class InstancePropertyCase
{
/**
* @var \WeakMap<object, string>|null
*/
private ?\WeakMap $map = null;

public function init(): void
{
if ($this->map === null) {
$this->map = new \WeakMap();
assertType('WeakMap<object, string>', $this->map);
}
}
}

/** @template T */
class GenericContainer
{
/** @var T */
private $value;

/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
}

class NullOrFalsePropertyCase
{
/**
* @var \WeakMap<object, string>|null|false
*/
private \WeakMap|null|false $map = false;

public function init(): void
{
if ($this->map !== false) {
if ($this->map === null) {
$this->map = new \WeakMap();
assertType('WeakMap<object, string>', $this->map);
}
}
}

public function reset(): void
{
$this->map = null;
}
}

class StaticNullOrFalsePropertyCase
{
/**
* @var \WeakMap<object, string>|null|false
*/
private static \WeakMap|null|false $map = false;

public static function init(): void
{
if (self::$map !== false) {
if (self::$map === null) {
self::$map = new \WeakMap();
assertType('WeakMap<object, string>', self::$map);
}
}
}

public static function reset(): void
{
self::$map = null;
}
}

class OtherGenericCase
{
/**
* @var \SplObjectStorage<object, string>|null
*/
private static ?\SplObjectStorage $storage = null;

public static function init(): void
{
if (self::$storage === null) {
self::$storage = new \SplObjectStorage();
assertType('SplObjectStorage<object, string>', self::$storage);
}
}
}

/**
* Custom generic class whose constructor does NOT reference template types.
* This proves the fix is general, not WeakMap-specific.
*
* @template TKey of string
* @template TValue
*/
class CustomGenericCache
{
/** @var array<TKey, TValue> */
private array $data = [];

public function __construct()
{
}

/**
* @param TKey $key
* @param TValue $value
*/
public function set(string $key, mixed $value): void
{
$this->data[$key] = $value;
}
}

class CustomGenericPropertyCase
{
/**
* @var CustomGenericCache<string, int>|null
*/
private ?CustomGenericCache $cache = null;

public function init(): void
{
if ($this->cache === null) {
$this->cache = new CustomGenericCache();
assertType('Bug11844\CustomGenericCache<string, int>', $this->cache);
}
}
}

class StaticCustomGenericPropertyCase
{
/**
* @var CustomGenericCache<string, int>|null
*/
private static ?CustomGenericCache $cache = null;

public static function init(): void
{
if (self::$cache === null) {
self::$cache = new CustomGenericCache();
assertType('Bug11844\CustomGenericCache<string, int>', self::$cache);
}
}
}

/**
* @template T of object
* @template U
*/
class TemplatePropertyCase
{
/**
* @var \WeakMap<T, U>|null
*/
private ?\WeakMap $map = null;

public function init(): void
{
if ($this->map === null) {
$this->map = new \WeakMap();
assertType('WeakMap<T of object (class Bug11844\TemplatePropertyCase, argument), U (class Bug11844\TemplatePropertyCase, argument)>', $this->map);
}
}
}

/**
* @template T of object
* @template U
*/
class StaticTemplatePropertyCase
{
/**
* @var \WeakMap<T, U>|null
*/
private static ?\WeakMap $map = null;

public static function init(): void
{
if (self::$map === null) {
self::$map = new \WeakMap();
assertType('WeakMap<T of object (class Bug11844\StaticTemplatePropertyCase, argument), U (class Bug11844\StaticTemplatePropertyCase, argument)>', self::$map);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,10 @@ public function testBug13624(): void
$this->analyse([__DIR__ . '/data/bug-13624.php'], []);
}

#[RequiresPhp('>= 8.0')]
public function testBug11844(): void
{
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11844.php'], []);
}

}
Loading