diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ef49f69ba0..af05e642bf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1251,6 +1251,12 @@ parameters: count: 2 path: src/Type/Generic/TemplateMixedType.php + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateNullType.php + - message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' identifier: phpstanApi.instanceofType @@ -1335,6 +1341,12 @@ parameters: count: 1 path: src/Type/Generic/TemplateTypeFactory.php + - + message: '#^Doing instanceof PHPStan\\Type\\NullType is error\-prone and deprecated\. Use Type\:\:isNull\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + - message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' identifier: phpstanApi.instanceofType diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 3b7e924258..be58cefefe 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -28,6 +28,7 @@ use PHPStan\Type\IterableType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; @@ -132,6 +133,7 @@ public function check( && $boundTypeClass !== GenericObjectType::class && $boundTypeClass !== KeyOfType::class && $boundTypeClass !== IterableType::class + && $boundTypeClass !== NullType::class && !$boundType instanceof UnionType && !$boundType instanceof IntersectionType && !$boundType instanceof TemplateType diff --git a/src/Type/Generic/TemplateNullType.php b/src/Type/Generic/TemplateNullType.php new file mode 100644 index 0000000000..7a5b95e598 --- /dev/null +++ b/src/Type/Generic/TemplateNullType.php @@ -0,0 +1,37 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + NullType $bound, + ?Type $default, + ) + { + parent::__construct(); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + +} diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index 0471bc249c..fb3b2149bd 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -15,6 +15,7 @@ use PHPStan\Type\IterableType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; @@ -112,6 +113,10 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateIterableType($scope, $strategy, $variance, $name, $bound, $default); } + if ($bound instanceof NullType && ($boundClass === NullType::class || $bound instanceof TemplateType)) { + return new TemplateNullType($scope, $strategy, $variance, $name, $bound, $default); + } + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12894.php b/tests/PHPStan/Analyser/nsrt/bug-12894.php new file mode 100644 index 0000000000..67efdf1947 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12894.php @@ -0,0 +1,53 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug12894; + +use Closure; + +/** + * @template TValue of object|null + */ +interface Dependency +{ + + /** + * @return TValue + */ + public function __invoke(): object|null; + +} + +interface DependencyResolver +{ + + /** + * @template V of object|null + * @template D of Dependency + * + * @param D $dependency + * + * @return V + */ + public function resolve(Dependency $dependency): object|null; + +} + +class Resolver implements DependencyResolver +{ + /** + * @var Closure(object|null): void + */ + protected Closure $run; + + public function resolve(Dependency $dependency): object|null { + $resolved = $dependency(); + \PHPStan\Testing\assertType('V of object|null (method Bug12894\DependencyResolver::resolve(), argument)', $resolved); + $result = is_object($resolved) ? 1 : 2; + \PHPStan\Testing\assertType('V of object (method Bug12894\DependencyResolver::resolve(), argument)|V of null (method Bug12894\DependencyResolver::resolve(), argument)', $resolved); + ($this->run)($resolved); + return $resolved; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12989.php b/tests/PHPStan/Analyser/nsrt/bug-12989.php new file mode 100644 index 0000000000..0cbe192304 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12989.php @@ -0,0 +1,17 @@ += 8.1')] + public function testBug13048(): void + { + $this->analyse([__DIR__ . '/data/bug-13048.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13048.php b/tests/PHPStan/Rules/Comparison/data/bug-13048.php new file mode 100644 index 0000000000..c2b2248123 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13048.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug13048; + +enum IndexBy { + case A; + case B; +} + +/** + * @template T of IndexBy|null + * @param T $indexBy + */ +function run(?IndexBy $indexBy = null): ?string +{ + return match ($indexBy) { + IndexBy::A => 'by A', + IndexBy::B => 'by B', + null => null, + }; +} diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index f7a818012a..ef504c54da 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -56,10 +56,6 @@ public function testRule(): void 'PHPDoc tag @template T for function FunctionTemplateType\resourceBound() with bound type resource is not supported.', 50, ], - [ - 'PHPDoc tag @template T for function FunctionTemplateType\nullNotSupported() with bound type null is not supported.', - 68, - ], [ 'Call-site variance of covariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template U is redundant, template type T of class FunctionTemplateType\GenericCovariant has the same variance.', 94, diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index 8a1ff456f9..c938c5dff4 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -65,7 +65,7 @@ function nakano() } /** @template T of null */ -function nullNotSupported() +function nullSupported() { } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index dae341a690..5eae89a24e 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -320,10 +320,6 @@ public function testGenericCallables(): void 'PHPDoc tag @param for parameter $invalidBoundType template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', 25, ], - [ - 'PHPDoc tag @param for parameter $notSupported template T of Closure(T): T with bound type null is not supported.', - 32, - ], [ 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\testShadowFunction.', 40, @@ -360,10 +356,6 @@ public function testGenericCallables(): void 'PHPDoc tag @return template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', 90, ], - [ - 'PHPDoc tag @return template T of Closure(T): T with bound type null is not supported.', - 97, - ], [ 'PHPDoc tag @return template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\testShadowFunctionReturn.', 105, @@ -380,10 +372,6 @@ public function testGenericCallables(): void 'PHPDoc tag @param for parameter $invalidBoundType template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', 131, ], - [ - 'PHPDoc tag @param for parameter $notSupported template T of Closure(T): T with bound type null is not supported.', - 138, - ], [ 'PHPDoc tag @return template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', 145, @@ -396,10 +384,6 @@ public function testGenericCallables(): void 'PHPDoc tag @return template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', 159, ], - [ - 'PHPDoc tag @return template T of Closure(T): T with bound type null is not supported.', - 166, - ], [ 'PHPDoc tag @param-out for parameter $existingClass template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsParamOut.', 175, diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php index c252fa002b..0e6c44f8ff 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -186,10 +186,6 @@ public function testGenericCallables(): void 'PHPDoc tag @var template of callable(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', 26, ], - [ - 'PHPDoc tag @var template TNull of callable(TNull): TNull with bound type null is not supported.', - 31, - ], [ 'PHPDoc tag @var template TInvalid of callable(TInvalid): TInvalid has invalid bound type GenericCallableProperties\Invalid.', 36, diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php index 238d822894..165296e750 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php @@ -27,9 +27,9 @@ function invalidBoundType(Closure $invalidBoundType): void } /** - * @param Closure(T $val): T $notSupported + * @param Closure(T $val): T $closure */ -function notSupported(Closure $notSupported): void +function testNull(Closure $closure): void { } @@ -94,7 +94,7 @@ function invalidBoundTypeReturn(): Closure /** * @return Closure(T $val): T */ -function notSupportedReturn(): Closure +function nullReturn(): Closure { } @@ -133,9 +133,9 @@ public function invalidBoundType(Closure $invalidBoundType): void } /** - * @param Closure(T $val): T $notSupported + * @param Closure(T $val): T $closure */ - public function notSupported(Closure $notSupported): void + public function nullType(Closure $closure): void { } @@ -163,7 +163,7 @@ public function invalidBoundTypeReturn(): Closure /** * @return Closure(T $val): T */ - public function notSupportedReturn(): Closure + public function nullReturn(): Closure { } }