-
Notifications
You must be signed in to change notification settings - Fork 569
Use declared property type instead of scope-narrowed type when inferring generic new expression types
#5467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5b343a8
5417fdd
fcde102
cba17ba
64f3fe3
b642c04
8439cfe
9051e36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| <?php // lint >= 8.0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Moved
|
||
|
|
||
| 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); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 theConstructorClassTemplateTraverserfound no template types consumed by constructor parameters. This is the case for:WeakMap(empty constructor, but hasTKey/TValuetemplates)SplObjectStorage(same pattern)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 andmake phpstanreports no errors.