From 4b059d01fb68ee3e06cdd7da94498042f5146207 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Sat, 30 Mar 2024 14:44:55 +0100 Subject: [PATCH 01/13] Update BackedEnum stub --- stubs/Php81.phpstub | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stubs/Php81.phpstub b/stubs/Php81.phpstub index 4c81bf3e209..4aa96cc3197 100644 --- a/stubs/Php81.phpstub +++ b/stubs/Php81.phpstub @@ -11,19 +11,25 @@ namespace { public static function cases(): array; } + /** @template T of int|string */ interface BackedEnum extends UnitEnum { /** @var non-empty-string $name */ public readonly string $name; + /** @var T $value */ public readonly int|string $value; /** + * @template T of int|string * @psalm-pure + * @param T $value */ public static function from(string|int $value): static; /** + * @template T of int|string * @psalm-pure + * @param T $value */ public static function tryFrom(string|int $value): ?static; From 75b7046ae6e995f43b4fe9871d80b5f56d99467e Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Mon, 1 Apr 2024 16:55:30 +0200 Subject: [PATCH 02/13] Fix typo --- src/Psalm/Storage/ClassLikeStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 6aec220047d..e3697db3882 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -354,7 +354,7 @@ final class ClassLikeStorage implements HasAttributesInterface /** * A map of which generic classlikes are extended or implemented by this class or interface. * - * This is only used in the populator, which poulates the $template_extended_params property below. + * This is only used in the populator, which populates the $template_extended_params property below. * * @internal * @var array>|null From c4fabdb15d3d5905dffd51259d3abf43a15c6325 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Mon, 1 Apr 2024 17:50:17 +0200 Subject: [PATCH 03/13] Implemented possibility to check more specific types for enum cases --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 134 +++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 8e850570012..fd3f692dbc8 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -74,6 +74,7 @@ use Psalm\Storage\FunctionLikeParameter; use Psalm\Storage\MethodStorage; use Psalm\Type; +use Psalm\Type\Atomic; use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; @@ -84,6 +85,7 @@ use UnexpectedValueException; use function array_filter; +use function array_key_first; use function array_keys; use function array_map; use function array_merge; @@ -99,9 +101,11 @@ use function preg_match; use function preg_replace; use function reset; +use function sprintf; use function str_replace; use function strtolower; use function substr; +use function trim; /** * @internal @@ -2193,6 +2197,18 @@ private function checkImplementedInterfaces( ); } + if ($fq_interface_name_lc === 'backedenum' && + $codebase->analysis_php_version_id >= 8_01_00 + ) { + $this->checkTemplateParams( + $codebase, + $storage, + $interface_storage, + $code_location, + $storage->template_type_implements_count[$fq_interface_name_lc] ?? 0, + ); + } + if (($fq_interface_name_lc === 'unitenum' || $fq_interface_name_lc === 'backedenum') && !$storage->is_enum @@ -2521,6 +2537,25 @@ private function checkEnum(): void { $storage = $this->storage; + /** @var Atomic|null $enum_implemented_type */ + $enum_implemented_type = null; + + foreach ($storage->template_extended_params ?? [] as $template_extended_params_map) { + foreach ($template_extended_params_map as $template) { + if (count($template_extended_params_map) > 1) { + throw new LogicException('BackedEnum should only have 1 template parameter in its stub'); + } + + $enum_implemented_types = $template->getAtomicTypes(); + + if (count($enum_implemented_types) === 1) { + $enum_implemented_type = $enum_implemented_types[array_key_first($enum_implemented_types)] ?? null; + + break 2; + } + } + } + $seen_values = []; foreach ($storage->enum_cases as $case_storage) { $case_value = $case_storage->getValue($this->getCodebase()->classlikes); @@ -2541,8 +2576,11 @@ private function checkEnum(): void ), ); } elseif ($case_value !== null) { - if ((is_int($case_value) && $storage->enum_type === 'string') - || (is_string($case_value) && $storage->enum_type === 'int') + $is_int_case_value = is_int($case_value); + $is_string_case_value = is_string($case_value); + + if (($is_int_case_value && $storage->enum_type === 'string') || + ($is_string_case_value && $storage->enum_type === 'int') ) { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( @@ -2552,6 +2590,98 @@ private function checkEnum(): void ), ); } + + if ($is_string_case_value && $storage->enum_type === 'string') { + if ($enum_implemented_type instanceof Type\Atomic\TNonEmptyString) { + if (trim($case_value) === '') { + IssueBuffer::maybeAdd( + new InvalidEnumCaseValue( + sprintf( + 'Enum case value type should be %s, got `%s`', + $enum_implemented_type->getId(), + $case_value, + ), + $case_storage->stmt_location, + $storage->name, + ), + ); + } + } + } + + if ($is_int_case_value && $storage->enum_type === 'int') { + if ($enum_implemented_type instanceof Type\Atomic\TIntRange) { + if ($enum_implemented_type->isPositive() && $case_value < 1) { + IssueBuffer::maybeAdd( + new InvalidEnumCaseValue( + sprintf( + 'Enum case value type should be %s, got `%d`', + $enum_implemented_type->getId(), + $case_value, + ), + $case_storage->stmt_location, + $storage->name, + ), + ); + } + + if ($enum_implemented_type->isPositiveOrZero() && $case_value < 0) { + IssueBuffer::maybeAdd( + new InvalidEnumCaseValue( + sprintf( + 'Enum case value type should be %s, got `%d`', + $enum_implemented_type->getId(), + $case_value, + ), + $case_storage->stmt_location, + $storage->name, + ), + ); + } + + if ($enum_implemented_type->isNegative() && $case_value >= 0) { + IssueBuffer::maybeAdd( + new InvalidEnumCaseValue( + sprintf( + 'Enum case value type should be %s, got `%d`', + $enum_implemented_type->getId(), + $case_value, + ), + $case_storage->stmt_location, + $storage->name, + ), + ); + } + + if ($enum_implemented_type->isNegativeOrZero() && $case_value > 0) { + IssueBuffer::maybeAdd( + new InvalidEnumCaseValue( + sprintf( + 'Enum case value type should be %s, got `%d`', + $enum_implemented_type->getId(), + $case_value, + ), + $case_storage->stmt_location, + $storage->name, + ), + ); + } + + if ($enum_implemented_type->contains($case_value) === false) { + IssueBuffer::maybeAdd( + new InvalidEnumCaseValue( + sprintf( + 'Enum case value type should be %s, got `%d`', + $enum_implemented_type->getId(), + $case_value, + ), + $case_storage->stmt_location, + $storage->name, + ), + ); + } + } + } } if ($case_value !== null) { From f71395e6a1b775b3ab0d7f6141d97bae3a7260d6 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Mon, 1 Apr 2024 18:12:02 +0200 Subject: [PATCH 04/13] Implemented possibility for BackedEnum to not implement template by default as the backed type is already inferred by the code structure --- src/Psalm/Internal/Codebase/Populator.php | 45 ++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 864a27e7c91..2e5299e171f 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -451,7 +451,7 @@ private function populateDataFromTrait( $storage->pseudo_property_set_types += $trait_storage->pseudo_property_set_types; $storage->pseudo_static_methods += $trait_storage->pseudo_static_methods; - + $storage->pseudo_methods += $trait_storage->pseudo_methods; $storage->declaring_pseudo_method_ids += $trait_storage->declaring_pseudo_method_ids; } @@ -645,11 +645,23 @@ private static function extendTemplateParams( } } } + + $backedEnumDefaultTemplateType = self::getBackedEnumDefaultTemplateTypeIfNotImplemented($storage); + + if ($backedEnumDefaultTemplateType !== null) { + $storage->template_extended_offsets["BackedEnum"] = $backedEnumDefaultTemplateType; + } } else { - foreach ($parent_storage->template_types as $template_name => $template_type_map) { - foreach ($template_type_map as $template_type) { - $default_param = $template_type->setProperties(['from_docblock' => false]); - $storage->template_extended_params[$parent_storage->name][$template_name] = $default_param; + $backedEnumDefaultTemplateType = self::getBackedEnumDefaultTemplateTypeIfNotImplemented($storage); + + if ($backedEnumDefaultTemplateType !== null) { + $storage->template_extended_params['BackedEnum'] = $backedEnumDefaultTemplateType; + } else { + foreach ($parent_storage->template_types as $template_name => $template_type_map) { + foreach ($template_type_map as $template_type) { + $default_param = $template_type->setProperties(['from_docblock' => false]); + $storage->template_extended_params[$parent_storage->name][$template_name] = $default_param; + } } } @@ -670,6 +682,29 @@ private static function extendTemplateParams( } } + /** + * This allows a BackedEnum to not implement any template via docblock as the default type is inferred + * by the backed type, unless the user wants to define a more specific type for the backed enum. + * + * @return array{T: Union}|null + */ + private static function getBackedEnumDefaultTemplateTypeIfNotImplemented(ClassLikeStorage $storage): ?array + { + $enum_type = $storage->enum_type; + + if ($enum_type === null || $storage->template_type_implements_count !== null) { + return null; + } + + if ($enum_type === 'string') { + return ['T' => new Union(['string' => new TString()])]; + } elseif ($enum_type === 'int') { + return ['T' => new Union(['int' => new TInt()])]; + } + + return null; + } + private function populateInterfaceDataFromParentInterface( ClassLikeStorage $storage, ClassLikeStorageProvider $storage_provider, From 40757eb869c01c0ca8fc332cc411d8dea4492e49 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Mon, 1 Apr 2024 18:30:39 +0200 Subject: [PATCH 05/13] Added Issues when the implemented type from template is not a subtype of the backed type --- .../Internal/Analyzer/ClassLikeAnalyzer.php | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index bc20c08f8ae..cf5917ee3a4 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -32,6 +32,7 @@ use Psalm\Type\Union; use UnexpectedValueException; +use function array_filter; use function array_keys; use function array_pop; use function array_search; @@ -636,10 +637,39 @@ protected function checkTemplateParams( CodeLocation $code_location, int $given_param_count ): void { + $enum_type = $storage->enum_type; + $parent_storage_class_template_types = $parent_storage->getClassTemplateTypes(); + + $is_backed_enum_like = $storage->is_enum && + in_array( + $enum_type, + array_keys( + count($parent_storage_class_template_types) ? + $parent_storage_class_template_types[0]->getAtomicTypes() ?? [] + : [], + true, + ), + ); + $expected_param_count = $parent_storage->template_types === null ? 0 : count($parent_storage->template_types); + /** + * 1) BackedEnum do not always need a template to infer the type as it must be specified by default when + * it is declared, the template is only needed for more specific types such as non-empty-string + * 2) BackedEnum can only be extended by interfaces and cannot be implemented by classes + */ + if (($is_backed_enum_like && $given_param_count === 0) || + ( + $storage->is_interface === false && + $is_backed_enum_like === false && + $parent_storage->name === 'BackedEnum' + ) + ) { + $expected_param_count = 0; + } + if ($expected_param_count > $given_param_count) { IssueBuffer::maybeAdd( new MissingTemplateParam( @@ -702,7 +732,13 @@ protected function checkTemplateParams( if (isset($parent_storage->template_covariants[$i]) && !$parent_storage->template_covariants[$i] ) { - foreach ($extended_type->getAtomicTypes() as $t) { + $extended_type_atomic_types = $extended_type->getAtomicTypes(); + $extended_type_atomic_types_int_string_diff = array_filter( + $extended_type_atomic_types, + fn($at) => !$at instanceof Type\Atomic\TInt && !$at instanceof Type\Atomic\TString, + ); + + foreach ($extended_type_atomic_types as $t) { if ($t instanceof TTemplateParam && $storage->template_types && $storage->template_covariants @@ -720,6 +756,30 @@ protected function checkTemplateParams( $storage->suppressed_issues + $this->getSuppressedIssues(), ); } + + if ($is_backed_enum_like && $extended_type_atomic_types_int_string_diff === []) { + if ($t instanceof Type\Atomic\TInt && $enum_type === 'string') { + IssueBuffer::maybeAdd( + new InvalidTemplateParam( + 'Extended template param ' . $template_name + . ' expects type ' . $enum_type + . ', type ' . $extended_type->getId() . ' given', + $code_location, + ), + ); + } + + if ($t instanceof Type\Atomic\TString && $enum_type === 'int') { + IssueBuffer::maybeAdd( + new InvalidTemplateParam( + 'Extended template param ' . $template_name + . ' expects type ' . $enum_type + . ', type ' . $extended_type->getId() . ' given', + $code_location, + ), + ); + } + } } } From 140cd3053417fad35d35e09d696d982b34bed207 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Mon, 1 Apr 2024 18:31:55 +0200 Subject: [PATCH 06/13] Fix old tests --- tests/EnumTest.php | 12 ++++++++++++ tests/ReturnTypeProvider/GetObjectVarsTest.php | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/tests/EnumTest.php b/tests/EnumTest.php index f66cdae3889..ff5ce90c1db 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -436,6 +436,10 @@ enum Status: int { interface ExtendedUnitEnum extends \UnitEnum {} static fn (ExtendedUnitEnum $tag): string => $tag->name; + /** + * @template T of int|string + * @extends BackedEnum + */ interface ExtendedBackedEnum extends \BackedEnum {} static fn (ExtendedBackedEnum $tag): string|int => $tag->value; ', @@ -513,6 +517,10 @@ enum Test: string ], 'methodInheritanceByInterfaces' => [ 'code' => ' + */ interface I extends BackedEnum {} /** @var I $i */ $a = $i::cases(); @@ -636,6 +644,10 @@ enum BarEnum: int { interface I {} interface UE extends UnitEnum {} + /** + * @template T of int|string + * @extends BackedEnum + */ interface BE extends BackedEnum {} function f(I $i): void { diff --git a/tests/ReturnTypeProvider/GetObjectVarsTest.php b/tests/ReturnTypeProvider/GetObjectVarsTest.php index 814b9decb91..9e793c9148b 100644 --- a/tests/ReturnTypeProvider/GetObjectVarsTest.php +++ b/tests/ReturnTypeProvider/GetObjectVarsTest.php @@ -291,6 +291,10 @@ function getA(): A { return B::One; } yield 'Interface extending BackedEnum' => [ 'code' => <<<'PHP' + */ interface A extends BackedEnum {} enum B: int implements A { case One = 1; } function getA(): A { return B::One; } From 72b35bb0c70ff9ba44784e33220cd5da16035527 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Mon, 1 Apr 2024 19:06:56 +0200 Subject: [PATCH 07/13] Fix psalm errors --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 4 ++++ .../Internal/Analyzer/ClassLikeAnalyzer.php | 2 +- src/Psalm/Internal/Codebase/Populator.php | 23 +++++++++++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index fd3f692dbc8..a2e46ce4d61 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -2592,6 +2592,8 @@ private function checkEnum(): void } if ($is_string_case_value && $storage->enum_type === 'string') { + $case_value = (string) $case_value; + if ($enum_implemented_type instanceof Type\Atomic\TNonEmptyString) { if (trim($case_value) === '') { IssueBuffer::maybeAdd( @@ -2610,6 +2612,8 @@ private function checkEnum(): void } if ($is_int_case_value && $storage->enum_type === 'int') { + $case_value = (int) $case_value; + if ($enum_implemented_type instanceof Type\Atomic\TIntRange) { if ($enum_implemented_type->isPositive() && $case_value < 1) { IssueBuffer::maybeAdd( diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index cf5917ee3a4..95eb5dbd1f8 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -645,7 +645,7 @@ protected function checkTemplateParams( $enum_type, array_keys( count($parent_storage_class_template_types) ? - $parent_storage_class_template_types[0]->getAtomicTypes() ?? [] + $parent_storage_class_template_types[0]->getAtomicTypes() : [], true, ), diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 2e5299e171f..ed093b536cb 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -649,12 +649,26 @@ private static function extendTemplateParams( $backedEnumDefaultTemplateType = self::getBackedEnumDefaultTemplateTypeIfNotImplemented($storage); if ($backedEnumDefaultTemplateType !== null) { - $storage->template_extended_offsets["BackedEnum"] = $backedEnumDefaultTemplateType; + /** + * @psalm-suppress TypeDoesNotContainNull It contains it according to the code + */ + if ($storage->template_extended_offsets === null) { + $storage->template_extended_offsets = []; + } + + $storage->template_extended_offsets['BackedEnum'] = $backedEnumDefaultTemplateType; } } else { $backedEnumDefaultTemplateType = self::getBackedEnumDefaultTemplateTypeIfNotImplemented($storage); if ($backedEnumDefaultTemplateType !== null) { + /** + * @psalm-suppress DocblockTypeContradiction It contains it according to the code + */ + if ($storage->template_extended_params === null) { + $storage->template_extended_params = []; + } + $storage->template_extended_params['BackedEnum'] = $backedEnumDefaultTemplateType; } else { foreach ($parent_storage->template_types as $template_name => $template_type_map) { @@ -696,13 +710,14 @@ private static function getBackedEnumDefaultTemplateTypeIfNotImplemented(ClassLi return null; } + /** + * The T comes from the BackedEnum stub + */ if ($enum_type === 'string') { return ['T' => new Union(['string' => new TString()])]; - } elseif ($enum_type === 'int') { - return ['T' => new Union(['int' => new TInt()])]; } - return null; + return ['T' => new Union(['int' => new TInt()])]; } private function populateInterfaceDataFromParentInterface( From 17dba3b6a8e2868dbdad0337314019b566bb5cd9 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Mon, 1 Apr 2024 19:08:30 +0200 Subject: [PATCH 08/13] Fix psalm errors --- src/Psalm/Storage/ClassLikeStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index e3697db3882..3af6a9572e3 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -357,7 +357,7 @@ final class ClassLikeStorage implements HasAttributesInterface * This is only used in the populator, which populates the $template_extended_params property below. * * @internal - * @var array>|null + * @var array>|null */ public $template_extended_offsets; From c215437265bd5ccdd1d5d4a88c36b96b660495d7 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Sat, 13 Apr 2024 16:27:21 +0200 Subject: [PATCH 09/13] Allow interfaces implementing BackedEnum to have default BackedEnum templates --- .../Internal/Analyzer/ClassLikeAnalyzer.php | 1 - src/Psalm/Internal/Codebase/Populator.php | 54 ++++++++++++++----- .../ReturnTypeProvider/GetObjectVarsTest.php | 4 -- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 95eb5dbd1f8..d13f06bae02 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -662,7 +662,6 @@ protected function checkTemplateParams( */ if (($is_backed_enum_like && $given_param_count === 0) || ( - $storage->is_interface === false && $is_backed_enum_like === false && $parent_storage->name === 'BackedEnum' ) diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index ed093b536cb..70a9812c89b 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -646,9 +646,10 @@ private static function extendTemplateParams( } } - $backedEnumDefaultTemplateType = self::getBackedEnumDefaultTemplateTypeIfNotImplemented($storage); + $defaultTemplate = self::getDefaultTemplateForInterfaceImplementingBackedEnum($storage, $parent_storage) + ?? self::getDefaultTemplateForBackedEnum($storage); - if ($backedEnumDefaultTemplateType !== null) { + if ($defaultTemplate !== null) { /** * @psalm-suppress TypeDoesNotContainNull It contains it according to the code */ @@ -656,12 +657,13 @@ private static function extendTemplateParams( $storage->template_extended_offsets = []; } - $storage->template_extended_offsets['BackedEnum'] = $backedEnumDefaultTemplateType; + $storage->template_extended_offsets['BackedEnum'] = $defaultTemplate; } } else { - $backedEnumDefaultTemplateType = self::getBackedEnumDefaultTemplateTypeIfNotImplemented($storage); + $defaultTemplate = self::getDefaultTemplateForInterfaceImplementingBackedEnum($storage, $parent_storage) + ?? self::getDefaultTemplateForBackedEnum($storage); - if ($backedEnumDefaultTemplateType !== null) { + if ($defaultTemplate !== null) { /** * @psalm-suppress DocblockTypeContradiction It contains it according to the code */ @@ -669,7 +671,7 @@ private static function extendTemplateParams( $storage->template_extended_params = []; } - $storage->template_extended_params['BackedEnum'] = $backedEnumDefaultTemplateType; + $storage->template_extended_params['BackedEnum'] = $defaultTemplate; } else { foreach ($parent_storage->template_types as $template_name => $template_type_map) { foreach ($template_type_map as $template_type) { @@ -696,28 +698,52 @@ private static function extendTemplateParams( } } + /** + * @param non-empty-string $mapped_name from the BackedEnum stub + * @return array{Union}|null + */ + private static function getDefaultTemplateForInterfaceImplementingBackedEnum( + ClassLikeStorage $storage, + ClassLikeStorage $parent_storage, + string $mapped_name = 'T' + ): ?array { + $is_interface = $storage->is_interface; + + if ($is_interface === null || $parent_storage->name !== "BackedEnum") { + return null; + } + + $t_template_param = new TTemplateParam( + $mapped_name, + new Union(['int' => new TInt(), 'string' => new TString()]), + $storage->name, + ); + + return [$mapped_name => new Union(['types' => $t_template_param])]; + } + /** * This allows a BackedEnum to not implement any template via docblock as the default type is inferred * by the backed type, unless the user wants to define a more specific type for the backed enum. * - * @return array{T: Union}|null + * @param non-empty-string $mapped_name from the BackedEnum stub + * @return array{Union}|null */ - private static function getBackedEnumDefaultTemplateTypeIfNotImplemented(ClassLikeStorage $storage): ?array - { + private static function getDefaultTemplateForBackedEnum( + ClassLikeStorage $storage, + string $mapped_name = 'T' + ): ?array { $enum_type = $storage->enum_type; if ($enum_type === null || $storage->template_type_implements_count !== null) { return null; } - /** - * The T comes from the BackedEnum stub - */ if ($enum_type === 'string') { - return ['T' => new Union(['string' => new TString()])]; + return [$mapped_name => new Union(['string' => new TString()])]; } - return ['T' => new Union(['int' => new TInt()])]; + return [$mapped_name => new Union(['int' => new TInt()])]; } private function populateInterfaceDataFromParentInterface( diff --git a/tests/ReturnTypeProvider/GetObjectVarsTest.php b/tests/ReturnTypeProvider/GetObjectVarsTest.php index 9e793c9148b..814b9decb91 100644 --- a/tests/ReturnTypeProvider/GetObjectVarsTest.php +++ b/tests/ReturnTypeProvider/GetObjectVarsTest.php @@ -291,10 +291,6 @@ function getA(): A { return B::One; } yield 'Interface extending BackedEnum' => [ 'code' => <<<'PHP' - */ interface A extends BackedEnum {} enum B: int implements A { case One = 1; } function getA(): A { return B::One; } From cd2f769e56563747feddfb9e312b320e7db49119 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Sat, 13 Apr 2024 16:34:00 +0200 Subject: [PATCH 10/13] Fix tests --- tests/EnumTest.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/EnumTest.php b/tests/EnumTest.php index ff5ce90c1db..f66cdae3889 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -436,10 +436,6 @@ enum Status: int { interface ExtendedUnitEnum extends \UnitEnum {} static fn (ExtendedUnitEnum $tag): string => $tag->name; - /** - * @template T of int|string - * @extends BackedEnum - */ interface ExtendedBackedEnum extends \BackedEnum {} static fn (ExtendedBackedEnum $tag): string|int => $tag->value; ', @@ -517,10 +513,6 @@ enum Test: string ], 'methodInheritanceByInterfaces' => [ 'code' => ' - */ interface I extends BackedEnum {} /** @var I $i */ $a = $i::cases(); @@ -644,10 +636,6 @@ enum BarEnum: int { interface I {} interface UE extends UnitEnum {} - /** - * @template T of int|string - * @extends BackedEnum - */ interface BE extends BackedEnum {} function f(I $i): void { From ab0d21cb5a6a495a479d9c5712f85d2b1788c108 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Sat, 13 Apr 2024 16:49:45 +0200 Subject: [PATCH 11/13] Fix psalm errors --- src/Psalm/Internal/Codebase/Populator.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 70a9812c89b..37f91fe0c45 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -699,20 +699,21 @@ private static function extendTemplateParams( } /** - * @param non-empty-string $mapped_name from the BackedEnum stub - * @return array{Union}|null + * @return array{T: Union}|null */ private static function getDefaultTemplateForInterfaceImplementingBackedEnum( ClassLikeStorage $storage, ClassLikeStorage $parent_storage, - string $mapped_name = 'T' ): ?array { $is_interface = $storage->is_interface; - if ($is_interface === null || $parent_storage->name !== "BackedEnum") { + if ($is_interface === false || $parent_storage->name !== "BackedEnum") { return null; } + // it comes from the BackedEnum stub + $mapped_name = 'T'; + $t_template_param = new TTemplateParam( $mapped_name, new Union(['int' => new TInt(), 'string' => new TString()]), @@ -726,19 +727,18 @@ private static function getDefaultTemplateForInterfaceImplementingBackedEnum( * This allows a BackedEnum to not implement any template via docblock as the default type is inferred * by the backed type, unless the user wants to define a more specific type for the backed enum. * - * @param non-empty-string $mapped_name from the BackedEnum stub - * @return array{Union}|null + * @return array{T: Union}|null */ - private static function getDefaultTemplateForBackedEnum( - ClassLikeStorage $storage, - string $mapped_name = 'T' - ): ?array { + private static function getDefaultTemplateForBackedEnum(ClassLikeStorage $storage): ?array { $enum_type = $storage->enum_type; if ($enum_type === null || $storage->template_type_implements_count !== null) { return null; } + // it comes from the BackedEnum stub + $mapped_name = 'T'; + if ($enum_type === 'string') { return [$mapped_name => new Union(['string' => new TString()])]; } From 7471d20a127943b71b71bd87797bfd3a195141a9 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Sat, 13 Apr 2024 17:30:18 +0200 Subject: [PATCH 12/13] Fix code style --- src/Psalm/Internal/Codebase/Populator.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 37f91fe0c45..950947a64d8 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -703,7 +703,7 @@ private static function extendTemplateParams( */ private static function getDefaultTemplateForInterfaceImplementingBackedEnum( ClassLikeStorage $storage, - ClassLikeStorage $parent_storage, + ClassLikeStorage $parent_storage ): ?array { $is_interface = $storage->is_interface; @@ -729,7 +729,8 @@ private static function getDefaultTemplateForInterfaceImplementingBackedEnum( * * @return array{T: Union}|null */ - private static function getDefaultTemplateForBackedEnum(ClassLikeStorage $storage): ?array { + private static function getDefaultTemplateForBackedEnum(ClassLikeStorage $storage): ?array + { $enum_type = $storage->enum_type; if ($enum_type === null || $storage->template_type_implements_count !== null) { From 905dcee5e3dc37d6b697372a76b715cef25ddb17 Mon Sep 17 00:00:00 2001 From: "M.Cozzolino" Date: Sat, 13 Apr 2024 21:26:24 +0200 Subject: [PATCH 13/13] Add tests --- tests/EnumTest.php | 243 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/tests/EnumTest.php b/tests/EnumTest.php index f66cdae3889..d2149924a0f 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -713,6 +713,98 @@ enum Foo: string { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'positiveIntTemplateType' => [ + 'code' => ' + */ + enum PositiveNumber: int { + case One = 1; + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'nonNegativeIntTemplateType' => [ + 'code' => ' + */ + enum NonNegativeNumber: int { + case Zero = 0; + case One = 1; + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'negativeIntTemplateType' => [ + 'code' => ' + */ + enum NegativeNumber: int { + case MinusOne = -1; + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intRangeTemplateType' => [ + 'code' => '> + */ + enum IntRangeNumber: int { + case Zero = 0; + case One = 1; + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'nonEmptyStringTemplateType' => [ + 'code' => ' + */ + enum StringEnum: string { + case Zero = "0"; + case One = "1"; + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'validTemplateParameterTypePassedToFromMethod' => [ + 'code' => ' + */ + enum NumberEnum: int { + case Zero = 0; + case One = 1; + }; + + $nonNegativeNumber = NumberEnum::from(0);', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'validTemplateParameterTypePassedToTryFromMethod' => [ + 'code' => ' + */ + enum NumberEnum: int { + case One = 1; + }; + + $negativeNumber = NumberEnum::tryFrom(1);', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -1207,6 +1299,157 @@ enum Bar: int 'ignored_issues' => [], 'php_version' => '8.1', ], + 'positiveIntTemplateTypeDoesNotMatchEnumValue' => [ + 'code' => ' + */ + enum PositiveNumber: int { + case Zero = 0; + case One = 1; + }', + 'error_message' => 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'nonNegativeIntTemplateTypeDoesNotMatchEnumValue' => [ + 'code' => ' + */ + enum NonNegativeNumber: int { + case MinusOne = -1; + case Zero = 0; + case One = 1; + }', + 'error_message' => 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'negativeIntTemplateTypeDoesNotMatchEnumValue' => [ + 'code' => ' + */ + enum NegativeNumber: int { + case MinusOne = -1; + case Zero = 0; + case One = 1; + }', + 'error_message' => 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intRangeTemplateTypeDoesNotMatchEnumValue' => [ + 'code' => '> + */ + enum IntRangeNumber: int { + case TwoHundred = 200; + case Zero = 0; + case One = 1; + case OneHundred = 100; + }', + 'error_message' => 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'nonEmptyStringTemplateTypeDoesNotMatchEnumValue' => [ + 'code' => ' + */ + enum StringEnum: string { + case Zero = "0"; + case One = "1"; + case Empty = ""; + }', + 'error_message' => 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'stringTemplateTypeDoesNotMatchBackingType' => [ + 'code' => ' + */ + enum Number: int { + case Zero = 0; + case One = 1; + }', + 'error_message' => 'InvalidTemplateParam', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intTemplateTypeDoesNotMatchBackingType' => [ + 'code' => ' + */ + enum Number: string { + case Zero = "0"; + case One = "1"; + }', + 'error_message' => 'InvalidTemplateParam', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'nonAllowedTemplateType' => [ + 'code' => ' + */ + enum NumberEnum: int { + case Zero = 0; + case One = 1; + }', + 'error_message' => 'InvalidTemplateParam', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'nonAllowedTemplateUnionType' => [ + 'code' => ' + */ + enum NumberEnum: int { + case Zero = 0; + case One = 1; + }', + 'error_message' => 'InvalidTemplateParam', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidTemplateParameterTypePassedToFromMethod' => [ + 'code' => ' + */ + enum NumberEnum: int { + case Zero = 0; + case One = 1; + }; + + $negativeNumber = NumberEnum::from(-10);', + 'error_message' => 'InvalidArgument', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidTemplateParameterTypePassedToTryFromMethod' => [ + 'code' => ' + */ + enum NumberEnum: int { + case One = 1; + }; + + $nonPositiveNumber = NumberEnum::tryFrom(0);', + 'error_message' => 'InvalidArgument', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } }