diff --git a/.gitignore b/.gitignore index f3029d6..134a955 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ testbench.yaml vendor node_modules .php-cs-fixer.cache +_enumhancer.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 747d4a1..fe59ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to `Enumhancer` will be documented in this file +## 1.23.0 - 2023-02-03 + +- added [PHPStan](docs/phpstan.md) support +- added [IDE-helper](docs/ide-helper.md) (requires another package) +- added global class for configuring Enumhancer +- added global [Macro](docs/macros.md#global-macros) support + ## 1.22.0 - 2023-01-07 - added [asEnum](docs/formrequests.md) to laravel's FormRequests @@ -25,7 +32,7 @@ All notable changes to `Enumhancer` will be documented in this file ### Extended features - You can now set Mapper FQCN in constants starting with - `map` and `map_flip` + `map` and `map_flip` - [Mappers](docs/mappers.md) methods now are usable statically - All Laravel rules have now macro's set on `Rule` @@ -38,27 +45,27 @@ All notable changes to `Enumhancer` will be documented in this file ## 1.19.0 - 2022-12-15 - You can now use constants for [Mappers](docs/mappers.md) - and [Defaults](docs/defaults.md) + and [Defaults](docs/defaults.md) - you can now flag a unit enum as `strict`, so you don't - have to worry about casing in [Values](docs/value.md). + have to worry about casing in [Values](docs/value.md). ## 1.18.0 - 2022-12-14 - Added Magic method functionality to [State](docs/state.md) - Added `to` and `tryTo` methods to `State` - Added `is`, `isNot`, `isIn` and `isNotIn` - to [Comparison](docs/comparison.md) + to [Comparison](docs/comparison.md) ## 1.17.0 - 2022-12-13 - Added [Flip](docs/mappers.md#flip), allowing to use - a single mapper for mapping between enums + a single mapper for mapping between enums - [From](docs/from.md) - now allows `UnitEnum` objects for use with `Flip` + now allows `UnitEnum` objects for use with `Flip` - [Comparison](docs/comparison.md) now allows different enums - when used with [Mappers](docs/mappers.md) + when used with [Mappers](docs/mappers.md) - Deprecated [Makers](docs/makers.md), replaced by - [Getters](docs/getters.md) + [Getters](docs/getters.md) ## 1.16.0 - 2022-12-11 @@ -66,7 +73,7 @@ All notable changes to `Enumhancer` will be documented in this file - Added [Dropdown](docs/dropdown.md) - [Comparison](docs/configure.md) now accepts null values - Fixed bug in [Casting](docs/casting.md) where in the latest Laravel versions - the `Keep Enum Value Case` switch no longer worked. + the `Keep Enum Value Case` switch no longer worked. ## 1.15.0 - 2022-06-21 diff --git a/README.md b/README.md index 5cc5e2f..3cf1626 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,11 @@ implemented the methods of `Getters`, `Extractor` and `Reporters`. - [Name](docs/functions.md#name) - [Value](docs/functions.md#value) +### Development + +- [IDE-Helper](docs/ide-helper.md) +- [PHPstan](docs/phpstan.md) + ### Laravel specific Features - [Blade](docs/blade.md) diff --git a/baseline.neon b/baseline.neon deleted file mode 100644 index b6de139..0000000 --- a/baseline.neon +++ /dev/null @@ -1,706 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Trait Henzeb\\\\Enumhancer\\\\Concerns\\\\Constructor is used zero times and is not analysed\\.$#" - count: 1 - path: src/Concerns/Constructor.php - - - - message: "#^Trait Henzeb\\\\Enumhancer\\\\Concerns\\\\Extractor is used zero times and is not analysed\\.$#" - count: 1 - path: src/Concerns/Extractor.php - - - - message: "#^Trait Henzeb\\\\Enumhancer\\\\Concerns\\\\Getters is used zero times and is not analysed\\.$#" - count: 1 - path: src/Concerns/Getters.php - - - - message: "#^Trait Henzeb\\\\Enumhancer\\\\Concerns\\\\Makers is used zero times and is not analysed\\.$#" - count: 1 - path: src/Concerns/Makers.php - - - - message: "#^Trait Henzeb\\\\Enumhancer\\\\Concerns\\\\Reporters is used zero times and is not analysed\\.$#" - count: 1 - path: src/Concerns/Reporters.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\EnumSubset\\:\\:cases\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/EnumSubset.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\EnumSubset\\:\\:names\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/EnumSubset.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\EnumSubset\\:\\:values\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/EnumSubset.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\Mapper\\:\\:__call\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\Mapper\\:\\:__callStatic\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\Mapper\\:\\:flipMappable\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\Mapper\\:\\:getMap\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\Mapper\\:\\:getMapWithPrefix\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\Mapper\\:\\:keysMethod\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Contracts\\\\Mapper\\:\\:mappable\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Contracts\\\\Mapper\\:\\:\\$flipped type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: src/Contracts/Mapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:__call\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 3 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:__callStatic\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 3 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:allowedTransitions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:bits\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:customTransitions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:dropdown\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:extract\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:extract\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:get\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:getArray\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:getArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:getArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:getOrReport\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:getOrReportArray\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:getOrReportArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:getOrReportArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:labels\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:makeArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:makeArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:makeOrReportArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:makeOrReportArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:mapper\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:setLabels\\(\\) has parameter \\$labels with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:setLabelsOnce\\(\\) has parameter \\$labels with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:setMapper\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:setMapperOnce\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:setTransitions\\(\\) has parameter \\$transitions with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:setTransitionsOnce\\(\\) has parameter \\$transitions with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:tryArray\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:tryArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:tryArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:tryGet\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:tryMakeArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Enums\\\\LogLevel\\:\\:tryMakeArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Enums/LogLevel.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Bitmasks\\\\Bitmask\\:\\:cases\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Bitmasks/Bitmask.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Bitmasks\\\\EnumBitmasks\\:\\:getCaseBits\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Bitmasks/EnumBitmasks.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Helpers\\\\Bitmasks\\\\EnumBitmasks\\:\\:\\$isValid has no type specified\\.$#" - count: 1 - path: src/Helpers/Bitmasks/EnumBitmasks.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumCompare\\:\\:isValidCall\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumCompare.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:extract\\(\\) has parameter \\$mappers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:extract\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:get\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:getArray\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:getArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:getArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:getOrReport\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:getOrReportArray\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:getOrReportArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:getOrReportArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:makeArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:makeArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:makeOrReportArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:makeOrReportArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:mapper\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:tryArray\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:tryArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:tryArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:tryGet\\(\\) has parameter \\$mapper with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:tryMakeArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumExtractor\\:\\:tryMakeArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumExtractor.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumGetters\\:\\:cases\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumGetters.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumGetters\\:\\:findCase\\(\\) has parameter \\$constants with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumGetters.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumGetters\\:\\:getArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumGetters.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumGetters\\:\\:getArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumGetters.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumGetters\\:\\:tryArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumGetters.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumGetters\\:\\:tryArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumGetters.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumImplements\\:\\:classUsesTrait\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumImplements.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumImplements\\:\\:traitUsesTrait\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumImplements.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumLabels\\:\\:getConfiguredLabels\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumLabels.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumLabels\\:\\:getLabels\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumLabels.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumMacros\\:\\:call\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumMacros.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumMacros\\:\\:callStatic\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumMacros.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumMacros\\:\\:\\$macros type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumMacros.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumMagicCalls\\:\\:call\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumMagicCalls.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumMagicCalls\\:\\:static\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumMagicCalls.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumProperties\\:\\:\\$global type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumProperties.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumProperties\\:\\:\\$once type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumProperties.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumProperties\\:\\:\\$properties type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumProperties.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumProperties\\:\\:\\$reserved type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumProperties.php - - - - message: "#^Access to an uninitialized readonly property Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumProxy\\:\\:\\$name\\.$#" - count: 1 - path: src/Helpers/EnumProxy.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumReporter\\:\\:getOrReportArray\\(\\) has parameter \\$keys with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/EnumReporter.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumReporter\\:\\:getOrReportArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumReporter.php - - - - message: "#^Call to method to\\(\\) on an unknown class Henzeb\\\\Enumhancer\\\\Concerns\\\\State\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Call to method tryTo\\(\\) on an unknown class Henzeb\\\\Enumhancer\\\\Concerns\\\\State\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:allowedTransitions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:call\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:castTransitions\\(\\) has parameter \\$transitions with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:castTransitions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:filterAllowedTransitions\\(\\) has parameter \\$hooks with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:filterAllowedTransitions\\(\\) has parameter \\$transitions with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:filterAllowedTransitions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:getTransitions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:transitions\\(\\) has parameter \\$userTransitions with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\EnumState\\:\\:transitions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^PHPDoc tag @var for variable \\$currentState has invalid type Henzeb\\\\Enumhancer\\\\Concerns\\\\State\\.$#" - count: 1 - path: src/Helpers/EnumState.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumArrayMapper\\:\\:__construct\\(\\) has parameter \\$mappable with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumArrayMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumArrayMapper\\:\\:mappable\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumArrayMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:getConfiguredMapper\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:getConstantMappers\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:getMappers\\(\\) has parameter \\$mappers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:getMappers\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:map\\(\\) has parameter \\$mappers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:mapArray\\(\\) has parameter \\$mappers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:mapArray\\(\\) has parameter \\$values with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:mapArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:sanitizeMapperArray\\(\\) has parameter \\$mappers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:sanitizeMapperArray\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Mappers\\\\EnumMapper\\:\\:wrapInMapper\\(\\) has parameter \\$map with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Mappers/EnumMapper.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Subset\\\\EnumSubsetMethods\\:\\:cases\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Subset/EnumSubsetMethods.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Subset\\\\EnumSubsetMethods\\:\\:dropdown\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Subset/EnumSubsetMethods.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Subset\\\\EnumSubsetMethods\\:\\:names\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Subset/EnumSubsetMethods.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Helpers\\\\Subset\\\\EnumSubsetMethods\\:\\:values\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Subset/EnumSubsetMethods.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Helpers\\\\Subset\\\\EnumSubsetMethods\\:\\:\\$enums type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Helpers/Subset/EnumSubsetMethods.php - - - - message: "#^Trait Henzeb\\\\Enumhancer\\\\Laravel\\\\Concerns\\\\CastsBasicEnumerations is used zero times and is not analysed\\.$#" - count: 1 - path: src/Laravel/Concerns/CastsBasicEnumerations.php - - - - message: "#^Trait Henzeb\\\\Enumhancer\\\\Laravel\\\\Concerns\\\\CastsStatefulEnumerations is used zero times and is not analysed\\.$#" - count: 1 - path: src/Laravel/Concerns/CastsStatefulEnumerations.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Laravel\\\\Middleware\\\\SubstituteEnums\\:\\:getParameters\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Laravel/Middleware/SubstituteEnums.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Laravel\\\\Reporters\\\\LaravelLogReporter\\:\\:getChannels\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Laravel/Reporters/LaravelLogReporter.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Laravel\\\\Reporters\\\\LaravelLogReporter\\:\\:\\$channels type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Laravel/Reporters/LaravelLogReporter.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Laravel\\\\Rules\\\\EnumBitmask\\:\\:message\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Laravel/Rules/EnumBitmask.php - - - - message: "#^Call to method isTransitionAllowed\\(\\) on an unknown class Henzeb\\\\Enumhancer\\\\Concerns\\\\State\\.$#" - count: 1 - path: src/Laravel/Rules/EnumTransition.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Laravel\\\\Rules\\\\EnumTransition\\:\\:message\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Laravel/Rules/EnumTransition.php - - - - message: "#^PHPDoc tag @var for variable \\$currentState has invalid type Henzeb\\\\Enumhancer\\\\Concerns\\\\State\\.$#" - count: 1 - path: src/Laravel/Rules/EnumTransition.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Laravel\\\\Rules\\\\IsEnum\\:\\:__construct\\(\\) has parameter \\$mappers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Laravel/Rules/IsEnum.php - - - - message: "#^Method Henzeb\\\\Enumhancer\\\\Laravel\\\\Rules\\\\IsEnum\\:\\:message\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Laravel/Rules/IsEnum.php - - - - message: "#^Property Henzeb\\\\Enumhancer\\\\Laravel\\\\Rules\\\\IsEnum\\:\\:\\$mappers type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Laravel/Rules/IsEnum.php diff --git a/composer.json b/composer.json index 56fcd2b..3c7e980 100644 --- a/composer.json +++ b/composer.json @@ -53,6 +53,7 @@ "macros", "macroable", "binding", + "ide-helper", "implicit" ], "homepage": "https://github.com/henzeb/enumhancer", @@ -69,7 +70,10 @@ "php": "^8.1" }, "require-dev": { + "composer/composer": "^2.5", + "henzeb/enumhancer-ide-helper": "main-dev", "mockery/mockery": "^1.5", + "nette/php-generator": "^4.0", "nunomaduro/larastan": "^2.3", "orchestra/testbench": "v6.24.1|^v7.18", "phpstan/phpstan": "^1.9", @@ -93,7 +97,6 @@ "test-coverage-txt": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text", "test-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html coverage", "test-dox": "vendor/bin/phpunit --testdox" - }, "config": { "sort-packages": true diff --git a/docs/defaults.md b/docs/defaults.md index b8e8517..8e634ad 100644 --- a/docs/defaults.md +++ b/docs/defaults.md @@ -37,7 +37,8 @@ Suit::tryFrom('circles'); // returns Suit::Default Suit::get('default'); // returns Suit::Default Suit::get('circles'); // throws ValueError -Suit::tryGet('circles'); // returns Suit::Default +Suit::tryGet('circles', true); // returns Suit::Default +Suit::tryGet('circles'); // returns null ```` ## The default keyword diff --git a/docs/ide-helper.md b/docs/ide-helper.md new file mode 100644 index 0000000..70f48c5 --- /dev/null +++ b/docs/ide-helper.md @@ -0,0 +1,42 @@ +# IDE-helper + +Enumhancer is using a lot of magic. For the most basic functionality, +enumhancer now comes with an IDE-helper. This is a separate package. + +## Installation + +First, you need to install the IDE-helper package. + +````bash +composer require --dev henzeb/enumhancer-ide-helper +```` + +### Configure with dump-autoload + +The IDE-helper out of the box supports the usage inside composer's +`post-autoload-dump` event. + +````json + "post-autoload-dump": [ +"Henzeb\\Enumhancer\\Composer\\IdeHelper::generate" +] +```` + +Note: You can leave this line in your composer file. The `IdeHelper` class is part +of the main library and executes `enumhancer-ide-helper` only if installed. + +### Configure as composer command + +If you don't want to call dump-autoload everytime you need to generate the helper, +it's possible to add it as a composer command. + +```json +"scripts": { +"enumhancer": "Henzeb\\Enumhancer\\Composer\\IdeHelper::generate" +} +``` + +## Ignore IDE-helper generated files + +The filename that is being generated by default is `_enumhancer.php` and is created +inside the root of your project. Simply add the filename to your `.ignore` file. diff --git a/docs/macros.md b/docs/macros.md index 9624fee..b7e9d07 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -59,6 +59,14 @@ Suit::mixin(SuitMixin::class); Suit::mixin(new SuitMixin()); ```` +## hasMacro + +This method will allow you to test if a method exists before using it. + +````php +Suit::hasMacro('toJson'); // returns true if exists, false otherwise +```` + ## flushMacros You may want to flush macro's in certain situations. This will only flush @@ -67,3 +75,20 @@ macro's for the enum it's called on. ````php Suit::flushMacros(); // only flushes macro's belonging to Suit ```` + +## Global macro's + +Enumhancer also supports global macro's, which will be available to you on +every enum using the `Macros` trait. + +````php +\Henzeb\Enumhancer\Helpers\Enumhancer::macro('macro', fn()=>true); + +\Henzeb\Enumhancer\Helpers\Enumhancer::mixin(GlobalMixin::class); +\Henzeb\Enumhancer\Helpers\Enumhancer::mixin(new GlobalMxin()); + +\Henzeb\Enumhancer\Helpers\Enumhancer::flushMacros(); +```` + +Note: flushing macro's using the global method only flushes global macro's. +flushing macro's on an enum will not touch the global macro's diff --git a/docs/phpstan.md b/docs/phpstan.md new file mode 100644 index 0000000..763887a --- /dev/null +++ b/docs/phpstan.md @@ -0,0 +1,10 @@ +# Enumhancer and PHPStan + +If you are using PHPStan for static analysis, you can enable the extension. + +Add the following to your projects phpstan.neon: + +```` +includes: +- vendor/henzeb/enumhancer/extension.neon +```` diff --git a/extension.neon b/extension.neon index 93ce085..b4d10bc 100644 --- a/extension.neon +++ b/extension.neon @@ -1,7 +1,45 @@ -includes: - - baseline.neon -parameters: - ignoreErrors: - - - '#Unsafe usage of new static.*#' +rules: + - Henzeb\Enumhancer\PHPStan\Constants\Rules\DefaultConstantRule + - Henzeb\Enumhancer\PHPStan\Constants\Rules\MapperConstantRule + - Henzeb\Enumhancer\PHPStan\Constants\Rules\StrictConstantRule +services: + - + class: Henzeb\Enumhancer\PHPStan\Constants\DefaultConstantAlwaysUsed + tags: + - phpstan.constants.alwaysUsedClassConstantsExtension + + - + class: Henzeb\Enumhancer\PHPStan\Constants\BitmaskConstantAlwaysUsed + tags: + - phpstan.constants.alwaysUsedClassConstantsExtension + + - + class: Henzeb\Enumhancer\PHPStan\Constants\StrictConstantAlwaysUsed + tags: + - phpstan.constants.alwaysUsedClassConstantsExtension + + - + class: Henzeb\Enumhancer\PHPStan\Constants\MapperConstantAlwaysUsed + tags: + - phpstan.constants.alwaysUsedClassConstantsExtension + + - + class: Henzeb\Enumhancer\PHPStan\Methods\EnumMacrosMethodsClassReflection + tags: + - phpstan.broker.methodsClassReflectionExtension + + - + class: Henzeb\Enumhancer\PHPStan\Methods\EnumComparisonMethodsClassReflection + tags: + - phpstan.broker.methodsClassReflectionExtension + + - + class: Henzeb\Enumhancer\PHPStan\Methods\EnumConstructorMethodsClassReflection + tags: + - phpstan.broker.methodsClassReflectionExtension + + - + class: Henzeb\Enumhancer\PHPStan\Methods\EnumStateMethodsClassReflection + tags: + - phpstan.broker.methodsClassReflectionExtension diff --git a/phpstan.neon b/phpstan.neon index 36af5b3..5b8cc8d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ includes: - - baseline.neon + - phpstan-baseline.neon - vendor/nunomaduro/larastan/extension.neon - vendor/phpstan/phpstan/conf/bleedingEdge.neon parameters: @@ -7,4 +7,4 @@ parameters: - src paths: - src - level: 8 + level: 6 diff --git a/src/Composer/IdeHelper.php b/src/Composer/IdeHelper.php new file mode 100644 index 0000000..3f3abf1 --- /dev/null +++ b/src/Composer/IdeHelper.php @@ -0,0 +1,110 @@ +isDevMode()) { + return; + } + + $composer = $event->getComposer(); + $config = $composer->getConfig(); + $package = $composer->getPackage(); + $vendorDir = $config->get('vendor-dir'); + + self::requireAutoloader($vendorDir); + + $alreadyBootstrapped = self::requireUserDefinedBootstrap($package); + + if (!$alreadyBootstrapped) { + self::requireLaravel($package); + } + + if (self::hasIdeHelperInstalled($package)) { + EnumIdeHelper::postAutoloadDump($event); + } + } + + private static function hasIdeHelperInstalled(RootPackageInterface $package): bool + { + $filtered = array_filter( + $package->getDevRequires(), + fn($name) => $name === 'henzeb/enumhancer-ide-helper', + ARRAY_FILTER_USE_KEY + ); + + return count($filtered) > 0; + } + + private static function requireAutoloader(string $vendorDir): void + { + $file = $vendorDir . '/autoload.php'; + + if (file_exists($vendorDir . '/autoload_runtime.php')) { + $file = $vendorDir . '/autoload_runtime.php'; + } + + require_once $file; + } + + private static function requireUserDefinedBootstrap(RootPackageInterface $package): bool + { + $file = $package->getExtra()['enumhancer']['ide-helper'] ?? null; + + if ($file) { + if (!file_exists($file)) { + throw new RuntimeException( + sprintf( + 'require_once(%s): Failed to open stream: No such file or directory', + $file + ) + ); + } + require_once $file; + return true; + } + return false; + } + + private static function requireLaravel(RootPackageInterface $package): void + { + $filtered = array_filter( + $package->getRequires(), + fn(string $name) => $name === 'laravel/framework', + ARRAY_FILTER_USE_KEY + ); + $realPath = realpath('./bootstrap/app.php'); + $hasLaravel = count($filtered) > 0 + && is_string($realPath) + && file_exists($realPath); + + if ($hasLaravel) { + $app = require_once realpath('./bootstrap/app.php'); + + /** + * already bootstrapped + */ + if (is_bool($app)) { + return; + } + + /** + * @var Kernel $kernel + */ + $kernel = $app->make(Kernel::class); + + $kernel->bootstrap(); + } + } +} diff --git a/src/Concerns/Getters.php b/src/Concerns/Getters.php index 9884375..fc4a480 100644 --- a/src/Concerns/Getters.php +++ b/src/Concerns/Getters.php @@ -11,9 +11,9 @@ public static function get(int|string|null $value): self return EnumGetters::get(self::class, $value); } - public static function tryGet(int|string|null $value): ?self + public static function tryGet(int|string|null $value, bool $useDefault = true): ?self { - return EnumGetters::tryGet(self::class, $value); + return EnumGetters::tryGet(self::class, $value, false, $useDefault); } /** @@ -29,8 +29,8 @@ public static function getArray(iterable $values): array * @param iterable $values * @return self[] */ - public static function tryArray(iterable $values): array + public static function tryArray(iterable $values, bool $useDefault = true): array { - return EnumGetters::tryArray(self::class, $values); + return EnumGetters::tryArray(self::class, $values, false, $useDefault); } } diff --git a/src/Concerns/Macros.php b/src/Concerns/Macros.php index 3bb64f4..0d66446 100644 --- a/src/Concerns/Macros.php +++ b/src/Concerns/Macros.php @@ -14,6 +14,11 @@ public static function macro(string $name, callable $callable): void EnumMacros::macro(self::class, $name, $callable); } + public static function hasMacro(string $name): bool + { + return EnumMacros::hasMacro(self::class, $name); + } + public static function mixin(string|object $mixin): void { EnumMacros::mixin(self::class, $mixin); diff --git a/src/Contracts/Mapper.php b/src/Contracts/Mapper.php index a1c5b74..a345538 100644 --- a/src/Contracts/Mapper.php +++ b/src/Contracts/Mapper.php @@ -3,6 +3,7 @@ namespace Henzeb\Enumhancer\Contracts; use UnitEnum; +use function strtolower; use function trigger_error; /** @@ -18,6 +19,9 @@ abstract class Mapper private ?array $flipped = null; private ?string $flipPrefix = null; + /** + * @return array> + */ abstract protected function mappable(): array; public function makeFlipped(string $prefix = null): self @@ -57,7 +61,11 @@ private function parseValue(mixed $value): string|int|null private function getMapWithPrefix(string $prefix = null): array { - return array_change_key_case($this->mappable()[$prefix] ?? []); + /** + * @var array $mappable + */ + $mappable = $this->mappable(); + return array_change_key_case($mappable[$prefix] ?? []); } private function getMap(string $prefix = null): array @@ -74,7 +82,7 @@ private function mapMethod(string|UnitEnum $key, string $prefix = null): string| $key = $this->parseValue($key); if (is_string($key)) { - $key = \strtolower($key); + $key = strtolower($key); } return $this->parseValue( diff --git a/src/Helpers/Concerns/EnumImplements.php b/src/Helpers/Concerns/EnumImplements.php new file mode 100644 index 0000000..3ec7c22 --- /dev/null +++ b/src/Helpers/Concerns/EnumImplements.php @@ -0,0 +1,78 @@ + $class] as $class) { + $results += self::getTraitsFrom($class); + } + + return array_unique($results); + } + + private static function getTraitsFrom(string $class): array + { + $traits = class_uses($class) ?: []; + + foreach ($traits as $class) { + $traits += self::getTraitsFrom($class); + } + + return $traits; + } +} diff --git a/src/Helpers/EnumCompare.php b/src/Helpers/EnumCompare.php index 80a06c1..82f7530 100644 --- a/src/Helpers/EnumCompare.php +++ b/src/Helpers/EnumCompare.php @@ -21,20 +21,23 @@ public static function isValidCall(string $class, string $name, array $arguments EnumCheck::check($class); return EnumImplements::comparison($class) - && !count($arguments) && str_starts_with($name, 'is'); + && !count($arguments) && str_starts_with(strtolower($name), 'is') + && self::getValueFromString($class, $name); } - public static function call(UnitEnum $enum, string $name): bool + private static function getValueFromString(string $class, string $name): ?UnitEnum { - $value = EnumGetters::tryGet( - $enum::class, - substr($name, str_starts_with($name, 'isNot') ? 5 : 2), - true + return EnumGetters::tryGet( + $class, + substr($name, str_starts_with(strtolower($name), 'isnot') ? 5 : 2), + true, + false ); + } - if (!$value) { - EnumMagicCalls::throwException($enum::class, $name); - } + public static function call(UnitEnum $enum, string $name): bool + { + $value = self::getValueFromString($enum::class, $name); return str_starts_with($name, 'isNot') !== self::equals($enum, $value); } diff --git a/src/Helpers/EnumGetters.php b/src/Helpers/EnumGetters.php index ee9a0d1..26820d4 100644 --- a/src/Helpers/EnumGetters.php +++ b/src/Helpers/EnumGetters.php @@ -2,12 +2,13 @@ namespace Henzeb\Enumhancer\Helpers; -use BackedEnum; use Henzeb\Enumhancer\Concerns\Mappers; use ReflectionClass; use UnitEnum; use ValueError; +use function array_key_exists; use function Henzeb\Enumhancer\Functions\backing; +use function is_object; use function strtolower; /** @@ -39,7 +40,7 @@ public static function get( return $class::get($value); } - $value = \is_object($value) ? $value->name : $value; + $value = is_object($value) ? $value->name : $value; $match = !is_null($value) ? self::match($class, $value) : null; @@ -80,14 +81,18 @@ public static function getArray( return $return; } - public static function tryArray(string $class, iterable $values, bool $useMapper = false): array - { + public static function tryArray( + string $class, + iterable $values, + bool $useMapper = false, + bool $useDefault = true + ): array { EnumCheck::check($class); $return = []; foreach ($values as $value) { - $return[] = self::tryGet($class, $value, $useMapper); + $return[] = self::tryGet($class, $value, $useMapper, $useDefault); } return array_filter($return); @@ -123,7 +128,7 @@ private static function match(UnitEnum|string $class, int|string $value): ?UnitE return $case; } - if (\array_key_exists($value, array_keys($constants))) { + if (array_key_exists($value, array_keys($constants))) { return $constants[array_keys($constants)[$value]]; } @@ -175,6 +180,6 @@ protected static function findCase(array $constants, int|string $value): ?UnitEn private static function isCase(mixed $case, string $name, int|string $value): bool { return $value === strtolower($name) - || strtolower(backing($case)??'') === $value; + || strtolower(backing($case) ?? '') === $value; } } diff --git a/src/Helpers/EnumImplements.php b/src/Helpers/EnumImplements.php index 7b5482a..4fa9ce9 100644 --- a/src/Helpers/EnumImplements.php +++ b/src/Helpers/EnumImplements.php @@ -11,83 +11,54 @@ use Henzeb\Enumhancer\Concerns\Mappers; use Henzeb\Enumhancer\Concerns\State; use Henzeb\Enumhancer\Concerns\Value; +use Henzeb\Enumhancer\Helpers\Concerns\EnumImplements as EnumImplementsBase; -/** - * @internal - */ final class EnumImplements { - public static function traitOn(string $class, string $implements): bool - { - EnumCheck::check($class); - - return in_array($implements, self::classUsesTrait($class)); - } + use EnumImplementsBase; public static function mappers(string $class): bool { - return self::traitOn($class, Mappers::class); + return self::implements($class, Mappers::class); } public static function defaults(string $class): bool { - return self::traitOn($class, Defaults::class); + return self::implements($class, Defaults::class); } public static function value(string $class): bool { - return self::traitOn($class, Value::class); + return self::implements($class, Value::class); } public static function state(string $class): bool { - return self::traitOn($class, State::class); + return self::implements($class, State::class); } public static function labels(string $class): bool { - return self::traitOn($class, Labels::class); + return self::implements($class, Labels::class); } public static function macros(string $class): bool { - return self::traitOn($class, Macros::class); + return self::implements($class, Macros::class); } public static function constructor(string $class): bool { - return self::traitOn($class, Constructor::class); + return self::implements($class, Constructor::class); } public static function comparison(string $class): bool { - return self::traitOn($class, Comparison::class); + return self::implements($class, Comparison::class); } public static function bitmasks(string $class): bool { - return self::traitOn($class, Bitmasks::class); - } - - private static function classUsesTrait(string $class): array - { - $results = []; - - foreach (array_reverse(class_parents($class) ?: []) + [$class => $class] as $class) { - $results += self::traitUsesTrait($class); - } - - return array_unique($results); - } - - private static function traitUsesTrait(string $trait): array - { - $traits = class_uses($trait) ?: []; - - foreach ($traits as $trait) { - $traits += self::traitUsesTrait($trait); - } - - return $traits; + return self::implements($class, Bitmasks::class); } } diff --git a/src/Helpers/EnumMacros.php b/src/Helpers/EnumMacros.php index a0db99d..7165371 100644 --- a/src/Helpers/EnumMacros.php +++ b/src/Helpers/EnumMacros.php @@ -2,12 +2,14 @@ namespace Henzeb\Enumhancer\Helpers; -use Closure; use ReflectionClass; use ReflectionException; use ReflectionFunction; use ReflectionMethod; use UnitEnum; +use function array_change_key_case; +use function array_merge; +use function is_null; use function is_string; use function sprintf; use function trigger_error; @@ -18,7 +20,8 @@ */ final class EnumMacros { - private static array $macros; + private static array $macros = []; + private static array $globalMacros = []; public static function macro(string $enum, string $name, callable $callable): void { @@ -27,28 +30,49 @@ public static function macro(string $enum, string $name, callable $callable): vo self::$macros[$enum][$name] = $callable; } + public static function globalMacro(string $name, callable $callable): void + { + self::$globalMacros[$name] = $callable; + } + + private static function getMethodsFromMixin(object $mixin): array + { + return (new ReflectionClass($mixin))->getMethods( + ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED + ); + } + /** * @throws ReflectionException */ - public static function mixin(string $enum, string|object $mixin): void + public static function mixin(?string $enum, string|object $mixin): void { - EnumCheck::check($enum); + if (!is_null($enum)) { + EnumCheck::check($enum); + } if (is_string($mixin)) { $mixin = new $mixin(); } - $methods = (new ReflectionClass($mixin))->getMethods( - ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED - ); - foreach ($methods as $method) { - self::macro($enum, $method->name, $method->invoke($mixin)); + foreach (self::getMethodsFromMixin($mixin) as $method) { + if ($enum) { + self::macro($enum, $method->name, $method->invoke($mixin)); + continue; + } + + self::globalMacro($method->name, $method->invoke($mixin)); } unset($mixin); } + public static function globalMixin(string|object $mixin): void + { + self::mixin(null, $mixin); + } + public static function flush(string $enum): void { EnumCheck::check($enum); @@ -58,11 +82,31 @@ public static function flush(string $enum): void } } + public static function flushGlobal(): void + { + self::$globalMacros = []; + } + public static function hasMacro(string $enum, string $name): bool + { + return self::getMacro($enum, $name) !== null; + } + + private static function getMacro(string $enum, string $name): ?callable { EnumCheck::check($enum); - return EnumImplements::macros($enum) && isset(self::$macros[$enum][$name]); + $name = strtolower($name); + + return array_change_key_case(self::getMacros($enum))[$name] ?? null; + } + + private static function getMacros(string $enum): array + { + return array_merge( + self::$globalMacros ?? [], + self::$macros[$enum] ?? [] + ); } /** @@ -80,17 +124,13 @@ public static function call(UnitEnum $enum, string $name, array $arguments): mix { EnumCheck::check($enum); - $macro = self::$macros[$enum::class][$name]; + $macro = self::getMacro($enum::class, $name); - if (self::isStaticMacro($macro)) { + if ($macro && self::isStaticMacro($macro)) { return self::callStatic($enum::class, $name, $arguments); } - $macro = Closure::bind($macro, $enum, $enum::class); - - if (!$macro) { - self::triggerError($enum::class, $name); - } + $macro = ($macro ?? fn() => true)(...)->bindTo($enum, $enum::class); return $macro(...$arguments); } @@ -102,12 +142,14 @@ public static function callStatic(string $enum, string $name, array $arguments): { EnumCheck::check($enum); - $macro = Closure::bind(self::$macros[$enum][$name], null, $enum); + $macro = self::getMacro($enum, $name); if (!$macro || false === self::isStaticMacro($macro)) { self::triggerError($enum, $name); } + $macro = $macro(...)->bindTo(null, $enum); + return $macro(...$arguments); } diff --git a/src/Helpers/EnumMagicCalls.php b/src/Helpers/EnumMagicCalls.php index 48b00c0..1f81113 100644 --- a/src/Helpers/EnumMagicCalls.php +++ b/src/Helpers/EnumMagicCalls.php @@ -38,7 +38,7 @@ public static function static(string $enum, string $name, array $arguments): mix } if (EnumImplements::constructor($enum)) { - return EnumGetters::tryGet($enum, $name, true) ?? self::throwException($enum, $name); + return EnumGetters::tryGet($enum, $name, true, false) ?? self::throwException($enum, $name); } self::throwException($enum, $name); diff --git a/src/Helpers/EnumProperties.php b/src/Helpers/EnumProperties.php index 42d3234..a649e87 100644 --- a/src/Helpers/EnumProperties.php +++ b/src/Helpers/EnumProperties.php @@ -82,9 +82,14 @@ public static function get(string $class, string $property): mixed ?? self::$global[$property] ?? null; } - public static function global(string $key, mixed $value): void + public static function getGlobal(string $property): mixed { - self::$global[$key] = $value; + return self::$global[$property] ?? null; + } + + public static function global(string $property, mixed $value): mixed + { + return self::$global[$property] = $value; } public static function clear(string $class, string $property = null): void @@ -100,8 +105,14 @@ public static function clear(string $class, string $property = null): void } } - public static function clearGlobal(): void + public static function clearGlobal(string $property = null): void { - self::$global = []; + if (!empty($property)) { + unset(self::$global[$property]); + } + + if (empty($property)) { + self::$global = []; + } } } diff --git a/src/Helpers/EnumProxy.php b/src/Helpers/EnumProxy.php index e4651da..c437062 100644 --- a/src/Helpers/EnumProxy.php +++ b/src/Helpers/EnumProxy.php @@ -10,10 +10,10 @@ class EnumProxy implements Stringable public readonly string $name; public readonly string $value; - public function __construct(private readonly UnitEnum $enum, bool $keepValueCase) + public function __construct(UnitEnum $enum, bool $keepValueCase) { - $this->name = $this->enum->name; - $this->value = (string)EnumValue::value($this->enum, $keepValueCase); + $this->name = $enum->name; + $this->value = (string)EnumValue::value($enum, $keepValueCase); } public function __toString(): string diff --git a/src/Helpers/EnumReporter.php b/src/Helpers/EnumReporter.php index a8f9b6d..519d901 100644 --- a/src/Helpers/EnumReporter.php +++ b/src/Helpers/EnumReporter.php @@ -8,6 +8,9 @@ use Henzeb\Enumhancer\Laravel\Reporters\LaravelLogReporter; use RuntimeException; use UnitEnum; +use function is_null; +use function is_object; +use function sprintf; /** * @internal @@ -25,9 +28,9 @@ public static function set(Reporter|string|null $reporter): void { if (!is_null($reporter) && !is_subclass_of($reporter, Reporter::class)) { throw new RuntimeException( - \sprintf( + sprintf( '%s is not a %s', - \is_object($reporter) ? $reporter::class : $reporter, + is_object($reporter) ? $reporter::class : $reporter, Reporter::class ) ); @@ -59,14 +62,14 @@ public static function getOrReport( ?BackedEnum $context, ?Reporter $reporter ): mixed { - $enum = EnumGetters::tryGet($class, $key); + $enum = EnumGetters::tryGet($class, $key, useDefault: false); if (!$enum) { if ($key instanceof UnitEnum) { $key = $key->name; } - if (!\is_null($key)) { + if (!is_null($key)) { $key = (string)$key; } diff --git a/src/Helpers/EnumState.php b/src/Helpers/EnumState.php index eba9182..5bbe49c 100644 --- a/src/Helpers/EnumState.php +++ b/src/Helpers/EnumState.php @@ -96,19 +96,26 @@ public static function isValidCall(string $class, string $name): bool { EnumCheck::check($class); - return str_starts_with($name, 'to') || str_starts_with($name, 'tryTo'); + return (str_starts_with($name, 'to') || str_starts_with($name, 'tryTo')) + && self::getToState($class, $name) !== null; } - public static function call(UnitEnum $currentState, string $name, array $arguments): mixed + private static function getToState(string $class, string $name): ?UnitEnum { - $toState = EnumGetters::tryGet( - $currentState::class, + return EnumGetters::tryGet( + $class, substr($name, str_starts_with($name, 'tryTo') ? 5 : 2), - true + true, + false ); + } + + public static function call(UnitEnum $currentState, string $name, array $arguments): mixed + { + $toState = self::getToState($currentState::class, $name); /** - * @var State $currentState + * @var State|UnitEnum $currentState */ if (str_starts_with($name, 'tryTo')) { return $currentState->tryTo($toState, ...$arguments); diff --git a/src/Helpers/Enumhancer.php b/src/Helpers/Enumhancer.php new file mode 100644 index 0000000..9ea2be7 --- /dev/null +++ b/src/Helpers/Enumhancer.php @@ -0,0 +1,47 @@ +getParameters($request->route()) as $key => $parameter) { if ($request->route($key) === null) { @@ -68,11 +69,7 @@ private function getParameters(Route|null $route): array function (ReflectionParameter $parameter) { $backedEnumClass = ltrim((string)$parameter->getType(), '?'); - if (enum_exists($backedEnumClass)) { - return [$parameter->getName() => new ReflectionEnum($backedEnumClass)]; - } - - return []; + return [$parameter->getName() => new ReflectionEnum($backedEnumClass)]; } )->filter()->toArray(); } diff --git a/src/Laravel/Rules/IsEnum.php b/src/Laravel/Rules/IsEnum.php index 694175f..3c11077 100644 --- a/src/Laravel/Rules/IsEnum.php +++ b/src/Laravel/Rules/IsEnum.php @@ -11,8 +11,17 @@ class IsEnum implements Rule { private mixed $value = null; + + + /** + * @var array|array[]|Mapper[]|null[]|string[] + */ private array $mappers; + /** + * @param string $type + * @param Mapper|string|array|null ...$mappers + */ public function __construct(private readonly string $type, Mapper|string|array|null ...$mappers) { EnumCheck::check($type); @@ -29,6 +38,9 @@ public function passes($attribute, $value): bool return (bool)EnumGetters::tryGet($this->type, $this->value, useDefault: false); } + /** + * @return string|array + */ public function message(): string|array { $message = trans( diff --git a/src/PHPStan/Constants/BitmaskConstantAlwaysUsed.php b/src/PHPStan/Constants/BitmaskConstantAlwaysUsed.php new file mode 100644 index 0000000..fc6312c --- /dev/null +++ b/src/PHPStan/Constants/BitmaskConstantAlwaysUsed.php @@ -0,0 +1,26 @@ +getName() !== \strtolower('bit_values')) { + return false; + } + + $class = $constant->getDeclaringClass(); + + if (!$class->isEnum()) { + return false; + } + + return EnumImplements::bitmasks($class->getName()); + } +} diff --git a/src/PHPStan/Constants/DefaultConstantAlwaysUsed.php b/src/PHPStan/Constants/DefaultConstantAlwaysUsed.php new file mode 100644 index 0000000..cbef1d7 --- /dev/null +++ b/src/PHPStan/Constants/DefaultConstantAlwaysUsed.php @@ -0,0 +1,22 @@ +getName(); + + if (\strtolower($constantName) === 'default') { + $class = $constant->getDeclaringClass(); + return $class->isEnum() && EnumImplements::defaults($class->getName()); + } + + return false; + } +} diff --git a/src/PHPStan/Constants/MapperConstantAlwaysUsed.php b/src/PHPStan/Constants/MapperConstantAlwaysUsed.php new file mode 100644 index 0000000..94efdb3 --- /dev/null +++ b/src/PHPStan/Constants/MapperConstantAlwaysUsed.php @@ -0,0 +1,30 @@ +getDeclaringClass(); + + if (!$class->isEnum()) { + return false; + } + + $className = $class->getName(); + $constantName = $constant->getName(); + + if (str_starts_with(strtolower($constantName), 'map')) { + return EnumImplements::mappers($className); + } + + return false; + } +} diff --git a/src/PHPStan/Constants/Rules/DefaultConstantRule.php b/src/PHPStan/Constants/Rules/DefaultConstantRule.php new file mode 100644 index 0000000..f20f7a7 --- /dev/null +++ b/src/PHPStan/Constants/Rules/DefaultConstantRule.php @@ -0,0 +1,84 @@ + + */ +class DefaultConstantRule implements Rule +{ + public function getNodeType(): string + { + return ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $constantName = $node->consts[0]->name->name; + + if ($this->isDefault($constantName)) { + return []; + } + + $reflectedClass = $scope->getClassReflection(); + + if (!$reflectedClass->isEnum()) { + return []; + } + + return $this->validate($reflectedClass, $constantName); + } + + /** + * @param ClassReflection|null $reflectedClass + * @param string $constantName + * @return string[] + * @throws ReflectionException + */ + protected function validate(?ClassReflection $reflectedClass, string $constantName): array + { + $implementsDefault = EnumImplements::defaults($reflectedClass->getName()); + + $value = $reflectedClass->getConstant($constantName)->getValueType(); + + $valueFromEnum = $value instanceof EnumCaseObjectType && $value->getClassName() === $reflectedClass->getName(); + + $return = []; + + if ($implementsDefault && !$valueFromEnum) { + $return = [ + sprintf( + 'Enumhancer: enum is implementing `Defaults`, ' + . 'but constant `%s` is not referencing to one of its own cases.', + $constantName + ) + ]; + } + + if (!$implementsDefault && $valueFromEnum) { + $return = [ + sprintf( + 'Enumhancer: Constant `%s` is not going to be used, ' + . 'because enum is not implementing `Defaults`', + $constantName + ) + ]; + } + + return $return; + } + + protected function isDefault(string $constantName): bool + { + return strtolower($constantName) !== 'default'; + } +} diff --git a/src/PHPStan/Constants/Rules/MapperConstantRule.php b/src/PHPStan/Constants/Rules/MapperConstantRule.php new file mode 100644 index 0000000..fe31a54 --- /dev/null +++ b/src/PHPStan/Constants/Rules/MapperConstantRule.php @@ -0,0 +1,108 @@ + + */ +class MapperConstantRule implements Rule +{ + public function __construct( + private ReflectionProvider $reflectionProvider + ) { + } + + public function getNodeType(): string + { + return ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $constantName = $node->consts[0]->name->name; + + if (!$this->isMapperConstant($constantName)) { + return []; + } + + $classReflection = $scope->getClassReflection(); + + if (!$classReflection->isEnum()) { + return []; + } + + return $this->validate($classReflection, $constantName); + } + + private function isMapperConstant(string $name): bool + { + return str_starts_with(strtolower($name), 'map'); + } + + /** + * @param ClassReflection|null $class + * @param string $constantName + * @return array + * @throws MissingConstantFromReflectionException + */ + protected function validate(?ClassReflection $class, string $constantName): array + { + $implementsMappers = EnumImplements::mappers($class->getName()); + $return = []; + + $isValid = $this->isValidValue($class, $constantName); + + if (!$implementsMappers && $isValid) { + $return = [ + sprintf( + 'Enumhancer: `%s` is not going to be used because enum is not implementing `Mappers`', + $constantName, + ) + ]; + } + + if ($implementsMappers && !$isValid) { + $return = [ + sprintf( + 'Enumhancer: The specified `%s` map is invalid', + $constantName, + ) + ]; + } + + return $return; + } + + /** + * @throws MissingConstantFromReflectionException + */ + protected function isValidValue( + ?ClassReflection $class, + string $constantName + ): bool { + $valueType = $class->getConstant($constantName)->getValueType(); + + $isValid = $valueType instanceof ConstantArrayType; + + if ($valueType instanceof ConstantStringType) { + $class = $valueType->getValue(); + $isValid = $valueType->isClassStringType()->yes() + && $this->reflectionProvider->getClass($class)->isSubclassOf(Mapper::class); + } + + return $isValid; + } +} diff --git a/src/PHPStan/Constants/Rules/StrictConstantRule.php b/src/PHPStan/Constants/Rules/StrictConstantRule.php new file mode 100644 index 0000000..d3792a5 --- /dev/null +++ b/src/PHPStan/Constants/Rules/StrictConstantRule.php @@ -0,0 +1,53 @@ + + */ +class StrictConstantRule implements Rule +{ + public function getNodeType(): string + { + return ClassConstantsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $constantName = $node->getConstants()[0]->consts[0]->name->name; + + if (strtolower($constantName) !== 'strict') { + return []; + } + + $class = $scope->getClassReflection(); + + if ($this->shouldProcessEnum($class)) { + return []; + } + + if ($class->getConstant($constantName)->getValueType()->isBoolean()->no()) { + return [ + sprintf('Enumhancer: constant `%s` should be a boolean.', $constantName) + ]; + } + + return []; + } + + /** + * @param ClassReflection|null $class + * @return bool + */ + protected function shouldProcessEnum(?ClassReflection $class): bool + { + return !$class->isEnum() || !EnumImplements::enumhancer($class->getName()); + } +} diff --git a/src/PHPStan/Constants/StrictConstantAlwaysUsed.php b/src/PHPStan/Constants/StrictConstantAlwaysUsed.php new file mode 100644 index 0000000..d1e8b47 --- /dev/null +++ b/src/PHPStan/Constants/StrictConstantAlwaysUsed.php @@ -0,0 +1,26 @@ +getName()) !== 'strict') { + return false; + } + + $class = $constant->getDeclaringClass(); + + if (!$class->isEnum()) { + return false; + } + + return EnumImplements::enumhancer($class->getName()); + } +} diff --git a/src/PHPStan/Methods/EnumComparisonMethodsClassReflection.php b/src/PHPStan/Methods/EnumComparisonMethodsClassReflection.php new file mode 100644 index 0000000..fbeff3a --- /dev/null +++ b/src/PHPStan/Methods/EnumComparisonMethodsClassReflection.php @@ -0,0 +1,38 @@ +isEnum()) { + return false; + } + + return EnumCompare::isValidCall( + $classReflection->getName(), + $methodName, + [] + ); + } + + public function getMethod( + ClassReflection $classReflection, + string $methodName + ): MethodReflection { + return new ClosureMethodReflection( + $classReflection, + $methodName, + new ClosureType([], new BooleanType(), false) + ); + } +} diff --git a/src/PHPStan/Methods/EnumConstructorMethodsClassReflection.php b/src/PHPStan/Methods/EnumConstructorMethodsClassReflection.php new file mode 100644 index 0000000..073e1a8 --- /dev/null +++ b/src/PHPStan/Methods/EnumConstructorMethodsClassReflection.php @@ -0,0 +1,47 @@ +isEnum()) { + return false; + } + + $className = $classReflection->getName(); + + if (!EnumImplements::constructor($className)) { + return false; + } + + return EnumGetters::tryGet($className, $methodName, useDefault: false) !== null; + } + + public function getMethod( + ClassReflection $classReflection, + string $methodName + ): MethodReflection { + + return new ClosureMethodReflection( + $classReflection, + $methodName, + new ClosureType( + [], + new ObjectType($classReflection->getName()), + false + ), + true + ); + } +} diff --git a/src/PHPStan/Methods/EnumMacrosMethodsClassReflection.php b/src/PHPStan/Methods/EnumMacrosMethodsClassReflection.php new file mode 100644 index 0000000..abd0b5b --- /dev/null +++ b/src/PHPStan/Methods/EnumMacrosMethodsClassReflection.php @@ -0,0 +1,86 @@ +isEnum()) { + return false; + } + + $className = $classReflection->getName(); + + if (EnumImplements::macros($className)) { + return $this->getMacro($className, $methodName) !== null; + } + + return false; + } + + public function getMethod( + ClassReflection $classReflection, + string $methodName + ): MethodReflection { + $className = $classReflection->getName(); + $macro = $this->getMacro($className, $methodName); + /** + * PHPStan does not support isStatic on closureType + * @phpstan-ignore-next-line + */ + $nativeReflection = new ReflectionFunction($macro); + + try { + return (new ClosureMethodReflection( + $classReflection, + $methodName, + $this->closureTypeFactory->fromClosureObject( + $macro + ), + $nativeReflection->isStatic(), + ))->setDocDocument( + $nativeReflection->getDocComment() + ); + } catch (LogicException) { + /** + * Transforming Illogic Exception into an explanatory Logically clear exception. + */ + throw new LogicException( + sprintf( + 'PHPStan Could not properly parse closure `%s` for `%s`, ' + . 'Check if a default value\'s code is not trying to execute this macro.', + $methodName, + $className + ) + ); + } + } + + public function getMacro(string $class, string $macro): ?Closure + { + return Closure::bind( + function (string $class, string $macro) { + return EnumMacros::getMacro($class, $macro); + }, + null, + EnumMacros::class + )($class, $macro); + } +} diff --git a/src/PHPStan/Methods/EnumStateMethodsClassReflection.php b/src/PHPStan/Methods/EnumStateMethodsClassReflection.php new file mode 100644 index 0000000..b8b7aeb --- /dev/null +++ b/src/PHPStan/Methods/EnumStateMethodsClassReflection.php @@ -0,0 +1,42 @@ +isEnum()) { + return false; + } + + return EnumState::isValidCall( + $classReflection->getName(), + $methodName + ); + } + + public function getMethod( + ClassReflection $classReflection, + string $methodName + ): MethodReflection { + + return new ClosureMethodReflection( + $classReflection, + $methodName, + new ClosureType( + [], + new ObjectType($classReflection->getName()), + false + ) + ); + } +} diff --git a/src/PHPStan/Reflections/ClosureMethodReflection.php b/src/PHPStan/Reflections/ClosureMethodReflection.php new file mode 100644 index 0000000..4942b72 --- /dev/null +++ b/src/PHPStan/Reflections/ClosureMethodReflection.php @@ -0,0 +1,115 @@ +docComment = $docComment; + return $this; + } + + + public function getDeclaringClass(): ClassReflection + { + return $this->classReflection; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isStatic(): bool + { + return $this->isStatic; + } + + public function getDocComment(): ?string + { + if (!$this->docComment || is_bool($this->docComment)) { + return null; + } + + return $this->docComment; + } + + public function getName(): string + { + return $this->methodName; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getPrototype(): ClassMemberReflection + { + return $this; + } + + public function getVariants(): array + { + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + null, + $this->closureType->getParameters(), + $this->closureType->isVariadic(), + $this->closureType->getReturnType() + ), + ]; + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function getThrowType(): ?Type + { + return null; + } + + public function hasSideEffects(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } +} diff --git a/tests/Fixtures/SimpleEnum.php b/tests/Fixtures/SimpleEnum.php index e1c23a1..7e5939e 100644 --- a/tests/Fixtures/SimpleEnum.php +++ b/tests/Fixtures/SimpleEnum.php @@ -6,4 +6,6 @@ enum SimpleEnum { case Open; case Closed; + + const NoDefault = 'test'; } diff --git a/tests/Unit/Composer/Fixtures/Runtime/autoload_runtime.php b/tests/Unit/Composer/Fixtures/Runtime/autoload_runtime.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/tests/Unit/Composer/Fixtures/Runtime/autoload_runtime.php @@ -0,0 +1 @@ +expects('isDevMode')->andReturn(false); + + IdeHelper::generate($event); + } + + public function testShouldNotRunIfIdeHelperPackageIsNotInstalled() + { + $event = Mockery::mock(Event::class); + $event->expects('isDevMode')->andReturn(true); + $composer = Mockery::mock(Composer::class); + $event->expects('getComposer')->andReturn($composer); + $config = Mockery::mock(Config::class); + $config->expects('get')->with('vendor-dir')->andReturn(realpath('./vendor')); + $composer->expects('getConfig')->andReturn($config); + $package = Mockery::mock(RootPackageInterface::class); + $composer->expects('getPackage')->andReturn($package); + + $package->expects('getExtra')->andReturn([]); + $package->expects('getRequires')->andReturn([]); + $package->expects('getDevRequires')->andReturn([]); + + IdeHelper::generate($event); + } + + /*public function testShouldNotCrashWithErrorsIn() + { + $event = Mockery::mock(Event::class); + $event->expects('isDevMode')->andReturn(true); + $composer = Mockery::mock(Composer::class); + $event->expects('getComposer')->andReturn($composer); + $config = Mockery::mock(Config::class); + $config->expects('get')->with('vendor-dir')->andReturn(\realpath('./vendor')); + $composer->expects('getConfig')->andReturn($config); + $package = Mockery::mock(RootPackageInterface::class); + $composer->expects('getPackage')->andReturn($package); + + $package->expects('getExtra')->andReturn([]); + $package->expects('getRequires')->andReturn([]); + $package->expects('getDevRequires')->andReturn([]); + + IdeHelper::postAutoloadDump($event); + }*/ + + public function testShouldRunWithDefaultVendor() + { + $event = Mockery::mock(Event::class); + $event->expects('isDevMode')->andReturn(true); + $composer = Mockery::mock(Composer::class); + $event->expects('getComposer')->andReturn($composer); + $config = Mockery::mock(Config::class); + $config->expects('get')->with('vendor-dir')->andReturn(realpath('./vendor')); + $composer->expects('getConfig')->andReturn($config); + $package = Mockery::mock(RootPackageInterface::class); + $composer->expects('getPackage')->andReturn($package); + + $package->expects('getExtra')->andReturn([]); + $package->expects('getRequires')->andReturn([]); + $package->expects('getDevRequires')->andReturn([ + 'henzeb/enumhancer-ide-helper' => 'require' + ]); + $event->expects('isDevMode')->andReturn(false); + + IdeHelper::generate($event); + } + + public function testShouldFailWithUserSpecifiedBootstrap() + { + $event = Mockery::mock(Event::class); + $event->expects('isDevMode')->andReturn(true); + $composer = Mockery::mock(Composer::class); + $event->expects('getComposer')->andReturn($composer); + $config = Mockery::mock(Config::class); + $config->expects('get')->with('vendor-dir')->andReturn(realpath('./vendor')); + $composer->expects('getConfig')->andReturn($config); + $package = Mockery::mock(RootPackageInterface::class); + $composer->expects('getPackage')->andReturn($package); + + $package->expects('getExtra')->andReturn([ + 'enumhancer' => ['ide-helper' => './bootstrap.php'] + ]); + + $this->expectException(RuntimeException::class); + + IdeHelper::generate($event); + } + + public function testShouldRunWithUserSpecifiedBootstrap() + { + $event = Mockery::mock(Event::class); + $event->expects('isDevMode')->andReturn(true); + $composer = Mockery::mock(Composer::class); + $event->expects('getComposer')->andReturn($composer); + $config = Mockery::mock(Config::class); + $config->expects('get')->with('vendor-dir')->andReturn(realpath('./vendor')); + $composer->expects('getConfig')->andReturn($config); + $package = Mockery::mock(RootPackageInterface::class); + $composer->expects('getPackage')->andReturn($package); + + $package->expects('getDevRequires')->andReturn([]); + + $package->expects('getExtra')->andReturn([ + 'enumhancer' => ['ide-helper' => __DIR__ . '/Fixtures/bootstrap.php'] + ]); + + IdeHelper::generate($event); + } + + public function testShouldRunWithDifferentVendor() + { + $event = Mockery::mock(Event::class); + $event->expects('isDevMode')->andReturn(true); + $composer = Mockery::mock(Composer::class); + $event->expects('getComposer')->andReturn($composer); + $config = Mockery::mock(Config::class); + $config->expects('get')->with('vendor-dir')->andReturn(__DIR__ . '/Fixtures/'); + $composer->expects('getConfig')->andReturn($config); + $package = Mockery::mock(RootPackageInterface::class); + $composer->expects('getPackage')->andReturn($package); + + $package->expects('getExtra')->andReturn([]); + $package->expects('getRequires')->andReturn([]); + $package->expects('getDevRequires')->andReturn([ + 'henzeb/enumhancer-ide-helper' => 'require' + ]); + + $event->expects('isDevMode')->andReturn(false); + + IdeHelper::generate($event); + } + + public function testShouldRunWithDifferentVendorAndRuntimeAutoload() + { + $event = Mockery::mock(Event::class); + $event->expects('isDevMode')->andReturn(true); + $composer = Mockery::mock(Composer::class); + $event->expects('getComposer')->andReturn($composer); + $config = Mockery::mock(Config::class); + $config->expects('get')->with('vendor-dir')->andReturn(__DIR__ . '/Fixtures/Runtime/'); + $composer->expects('getConfig')->andReturn($config); + $package = Mockery::mock(RootPackageInterface::class); + $composer->expects('getPackage')->andReturn($package); + + $package->expects('getExtra')->andReturn([]); + $package->expects('getRequires')->andReturn([]); + $package->expects('getDevRequires')->andReturn([ + 'henzeb/enumhancer-ide-helper' => 'require' + ]); + $event->expects('isDevMode')->andReturn(false); + + IdeHelper::generate($event); + } + + public function testShouldRunWithLaravel() + { + $event = Mockery::mock(Event::class); + $event->expects('isDevMode')->times(3)->andReturn(true); + $composer = Mockery::mock(Composer::class); + $event->expects('getComposer')->times(3)->andReturn($composer); + $config = Mockery::mock(Config::class); + $config->expects('get')->with('vendor-dir')->times(3)->andReturn(realpath('./vendor')); + $composer->expects('getConfig')->times(3)->andReturn($config); + $package = Mockery::mock(RootPackageInterface::class); + $composer->expects('getPackage')->times(2)->andReturn($package); + + $package->expects('getExtra')->twice()->andReturn([]); + $package->expects('getRequires')->twice()->andReturn([ + 'laravel/framework' => 'require' + ]); + $package->expects('getDevRequires')->twice()->andReturn([ + 'henzeb/enumhancer-ide-helper' => 'require' + ]); + $event->expects('isDevMode')->andReturn(false); + mkdir('./bootstrap'); + + copy('./vendor/orchestra/testbench-core/laravel/bootstrap/app.php', './bootstrap/app.php'); + + IdeHelper::generate($event); + + $this->assertTrue(app()->hasBeenBootstrapped()); + + /** + * when require_once is done twice, it returns a bool, which should fail as $app is thus 'true' + * and not an app. + * + * This last step makes sure that it does not, if for example, run from within laravel or in tandem + * in a user function where laravel is already loaded. + */ + IdeHelper::generate($event); + } + + public function tearDown(): void + { + if (file_exists('./bootstrap/app.php')) { + unlink('./bootstrap/app.php'); + rmdir('./bootstrap'); + } + } +} diff --git a/tests/Unit/Composer/bootstrap/.gitignore b/tests/Unit/Composer/bootstrap/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/tests/Unit/Concerns/ComparisonTest.php b/tests/Unit/Concerns/ComparisonTest.php index 82696c1..1554d72 100644 --- a/tests/Unit/Concerns/ComparisonTest.php +++ b/tests/Unit/Concerns/ComparisonTest.php @@ -129,6 +129,14 @@ public function testShouldReturnTrueUsingMagicFunction() ); } + public function testShouldFailUsingMagicFunctionThatDoesNotExist() + { + $this->expectException(BadMethodCallException::class); + $this->assertFalse( + IntBackedEnum::TEST->isClosed() + ); + } + public function testShouldReturnTrueUsingMagicFunctionIsNot() { $this->assertTrue( @@ -221,15 +229,15 @@ public function testPassingEnums(): void public function testIs(): void { - //$this->assertTrue(EnhancedBackedEnum::ANOTHER_ENUM->is('another_enum')); + $this->assertTrue(EnhancedBackedEnum::ANOTHER_ENUM->is('another_enum')); $this->assertTrue(EnhancedBackedEnum::ANOTHER_ENUM->is(1)); - //$this->assertTrue(EnhancedBackedEnum::ANOTHER_ENUM->is(EnhancedUnitEnum::ANOTHER_ENUM)); + $this->assertTrue(EnhancedBackedEnum::ANOTHER_ENUM->is(EnhancedUnitEnum::ANOTHER_ENUM)); - // $this->assertTrue(EnhancedBackedEnum::ANOTHER_ENUM->is('mapped')); + $this->assertTrue(EnhancedBackedEnum::ANOTHER_ENUM->is('mapped')); - //$this->assertFalse(EnhancedBackedEnum::ANOTHER_ENUM->is('something else')); + $this->assertFalse(EnhancedBackedEnum::ANOTHER_ENUM->is('something else')); } public function testIsNot(): void diff --git a/tests/Unit/Concerns/GettersTest.php b/tests/Unit/Concerns/GettersTest.php index ad27c59..13a8e6b 100644 --- a/tests/Unit/Concerns/GettersTest.php +++ b/tests/Unit/Concerns/GettersTest.php @@ -6,6 +6,7 @@ use Generator; use Henzeb\Enumhancer\Tests\Fixtures\IntBackedEnum; use Henzeb\Enumhancer\Tests\Fixtures\StringBackedGetEnum; +use Henzeb\Enumhancer\Tests\Fixtures\UnitEnums\Defaults\DefaultsEnum; use Henzeb\Enumhancer\Tests\Fixtures\UnitEnums\Getters\GetUnitEnum; use PHPUnit\Framework\TestCase; @@ -163,15 +164,42 @@ public function testTryGetArrayWithGenerator() ); } - public function testGetStringBackedEnumWithInteger() { + public function testGetStringBackedEnumWithInteger() + { $this->assertEquals( StringBackedGetEnum::TEST1, StringBackedGetEnum::get(1) ); } - public function testGetUnitEnumWithInteger() { + public function testGetUnitEnumWithInteger() + { $this->assertEquals( GetUnitEnum::Zero, GetUnitEnum::get(0) ); } + + public function testTryGetShouldReturnDefault(): void + { + $this->assertEquals( + DefaultsEnum::default(), + DefaultsEnum::tryGet('caseMissing') + ); + + $this->assertEquals( + [DefaultsEnum::default()], + DefaultsEnum::tryArray(['caseMissing']) + ); + } + + public function testTryGetShouldNotReturnDefault(): void + { + $this->assertEquals( + null, + DefaultsEnum::tryGet('caseMissing', false) + ); + $this->assertEquals( + [], + DefaultsEnum::tryArray(['caseMissing'], false) + ); + } } diff --git a/tests/Unit/Concerns/MacrosTest.php b/tests/Unit/Concerns/MacrosTest.php index dcbcc42..7b8ef67 100644 --- a/tests/Unit/Concerns/MacrosTest.php +++ b/tests/Unit/Concerns/MacrosTest.php @@ -3,7 +3,7 @@ namespace Henzeb\Enumhancer\Tests\Unit\Concerns; use BadMethodCallException; -use Closure; +use Henzeb\Enumhancer\Helpers\Enumhancer; use Henzeb\Enumhancer\Tests\Fixtures\UnitEnums\Macros\MacrosAnotherUnitEnum; use Henzeb\Enumhancer\Tests\Fixtures\UnitEnums\Macros\MacrosUnitEnum; use PHPUnit\Framework\TestCase; @@ -56,6 +56,15 @@ public function testNonStaticMacroShouldBeBoundToEnum(): void $this->assertEquals(MacrosUnitEnum::Hearts, MacrosUnitEnum::Hearts->test()); } + public function testShouldOverrideMethodCaseInsensitive() + { + MacrosUnitEnum::macro('test', static fn() => false); + MacrosUnitEnum::macro('TEST', static fn() => true); + + $this->assertTrue(MacrosUnitEnum::test()); + + } + public function testAllowPassingParameters() { MacrosUnitEnum::macro('test', static fn(string $string, bool $bool) => [$string, $bool]); @@ -128,6 +137,35 @@ protected function test() } + public function testHasMacros() + { + $this->assertFalse(MacrosUnitEnum::hasMacro('test')); + MacrosUnitEnum::macro('test', fn() => true); + $this->assertTrue(MacrosUnitEnum::hasMacro('test')); + + } + + public function testHasMacrosGlobal() + { + $this->assertFalse(MacrosUnitEnum::hasMacro('test')); + Enumhancer::macro('test', fn() => true); + $this->assertTrue(MacrosUnitEnum::hasMacro('test')); + } + + public function testGlobalMacroMixin() + { + $mixin = new class { + protected function test() + { + return static fn() => true; + } + }; + + Enumhancer::mixin($mixin::class); + + $this->assertTrue(MacrosUnitEnum::test()); + } + protected function tearDown(): void { MacrosUnitEnum::flushMacros(); diff --git a/tests/Unit/Concerns/MappersTest.php b/tests/Unit/Concerns/MappersTest.php index 80e9d71..e7c753c 100644 --- a/tests/Unit/Concerns/MappersTest.php +++ b/tests/Unit/Concerns/MappersTest.php @@ -3,6 +3,7 @@ namespace Henzeb\Enumhancer\Tests\Unit\Concerns; use Henzeb\Enumhancer\Contracts\Mapper; +use Henzeb\Enumhancer\Helpers\Mappers\EnumMapper; use Henzeb\Enumhancer\Tests\Fixtures\EnhancedBackedEnum; use Henzeb\Enumhancer\Tests\Fixtures\EnhancedUnitEnum; use Henzeb\Enumhancer\Tests\Fixtures\UnitEnums\Mappers\ConstantInvalidMapperEnum; @@ -373,9 +374,37 @@ public function testShouldMapWithFCQN() $this->assertEquals(ConstantMapperClassFlippedEnum::Alpha, ConstantMapperClassFlippedEnum::get('beta')); } - public function testShouldBeInvalidWhenStringIsClass() { + public function testShouldBeInvalidWhenStringIsClass() + { $this->expectException(ValueError::class); ConstantInvalidMapperEnum::get('Alpha'); } + + public function testIsValidMapper() + { + $this->assertTrue(EnumMapper::isValidMapper(ConstantMapperClassEnum::class, ConstantMapperClassEnum::Beta)); + $this->assertTrue(EnumMapper::isValidMapper( + ConstantMapperClassEnum::class, + ['test' => ConstantMapperClassEnum::Alpha]) + ); + + $this->assertFalse(EnumMapper::isValidMapper(ConstantMapperClassEnum::class, $this)); + $this->assertFalse(EnumMapper::isValidMapper(ConstantMapperClassEnum::class, $this::class)); + + $mapper = new class extends Mapper { + protected function mappable(): array + { + return []; + } + }; + + $this->assertTrue( + EnumMapper::isValidMapper(ConstantMapperClassEnum::class, $mapper) + ); + + $this->assertTrue( + EnumMapper::isValidMapper(ConstantMapperClassEnum::class, $mapper::class) + ); + } } diff --git a/tests/Unit/Helpers/EnumReporterTest.php b/tests/Unit/Helpers/EnumReporterTest.php index d0856ed..bb60a27 100644 --- a/tests/Unit/Helpers/EnumReporterTest.php +++ b/tests/Unit/Helpers/EnumReporterTest.php @@ -2,18 +2,20 @@ namespace Henzeb\Enumhancer\Tests\Unit\Helpers; -use Mockery; -use stdClass; use BackedEnum; -use RuntimeException; -use Psr\Log\LoggerInterface; -use Orchestra\Testbench\TestCase; -use Illuminate\Support\Facades\Log; -use Henzeb\Enumhancer\Enums\LogLevel; use Henzeb\Enumhancer\Contracts\Reporter; +use Henzeb\Enumhancer\Enums\LogLevel; use Henzeb\Enumhancer\Helpers\EnumReporter; -use Henzeb\Enumhancer\Laravel\Reporters\LaravelLogReporter; use Henzeb\Enumhancer\Laravel\Providers\EnumhancerServiceProvider; +use Henzeb\Enumhancer\Laravel\Reporters\LaravelLogReporter; +use Henzeb\Enumhancer\Tests\Fixtures\EnhancedBackedEnum; +use Henzeb\Enumhancer\Tests\Fixtures\EnhancedUnitEnum; +use Illuminate\Support\Facades\Log; +use Mockery; +use Orchestra\Testbench\TestCase; +use Psr\Log\LoggerInterface; +use RuntimeException; +use stdClass; class EnumReporterTest extends TestCase @@ -136,6 +138,15 @@ public function testSetLaravelReporterWithDifferenChannels() ); } + public function testMakeOrReportWithUnitEnum() + { + $mock = Mockery::mock(Reporter::class); + $mock->expects('report') + ->with(EnhancedBackedEnum::class, 'Unique', null); + + EnumReporter::getOrReport(EnhancedBackedEnum::class, EnhancedUnitEnum::Unique, null, $mock); + } + public function testMakeOrReportShouldErrorWithNonEnum() { $this->expectError(); diff --git a/tests/Unit/Helpers/EnumhancerTest.php b/tests/Unit/Helpers/EnumhancerTest.php new file mode 100644 index 0000000..2868519 --- /dev/null +++ b/tests/Unit/Helpers/EnumhancerTest.php @@ -0,0 +1,81 @@ + true); + MacrosUnitEnum::macro('localMacro', fn() => true); + Enumhancer::flushMacros(); + + $this->assertFalse(MacrosUnitEnum::hasMacro('globalMacro')); + $this->assertTrue(MacrosUnitEnum::hasMacro('localMacro')); + } + + public function testSetReporter(): void + { + $reporter = new class implements Reporter { + + public function report(string $enum, ?string $key, ?BackedEnum $context): void + { + // TODO: Implement report() method. + } + }; + + Enumhancer::setReporter( + $reporter + ); + + $this->assertTrue(EnumReporter::get() === $reporter); + } + + public function testSetProperty(): void + { + Enumhancer::property('test', fn() => true); + + $this->assertTrue(Enumhancer::property('test')()); + + $this->assertTrue(EnhancedBackedEnum::property('test')()); + } + + public function testSetPropertyGlobalAndLocal(): void + { + EnhancedBackedEnum::property('test', fn() => true); + Enumhancer::property('test', fn() => false); + + $this->assertTrue(EnhancedBackedEnum::property('test')()); + + } + + public function testUnsetProperty(): void + { + Enumhancer::property('test', fn() => true); + EnhancedBackedEnum::property('test', fn() => false); + + Enumhancer::unsetProperty('test'); + + $this->assertFalse(EnhancedBackedEnum::property('test')()); + + } + + public function testFlushGlobal(): void + { + Enumhancer::property('test', fn() => true); + EnhancedBackedEnum::property('test', fn() => false); + + Enumhancer::clearProperties(); + + $this->assertFalse(EnhancedBackedEnum::property('test')()); + + } +} diff --git a/tests/Unit/Laravel/Middleware/SubstituteEnumsTest.php b/tests/Unit/Laravel/Middleware/SubstituteEnumsTest.php index ba13f0b..4abfbcb 100644 --- a/tests/Unit/Laravel/Middleware/SubstituteEnumsTest.php +++ b/tests/Unit/Laravel/Middleware/SubstituteEnumsTest.php @@ -3,13 +3,11 @@ namespace Henzeb\Enumhancer\Tests\Unit\Laravel\Middleware; -use Henzeb\Enumhancer\Concerns\Defaults; use Henzeb\Enumhancer\Laravel\Providers\EnumhancerServiceProvider; use Henzeb\Enumhancer\Tests\Fixtures\EnhancedBackedEnum; use Henzeb\Enumhancer\Tests\Fixtures\EnhancedIntBackedEnum; use Henzeb\Enumhancer\Tests\Fixtures\SimpleEnum; use Henzeb\Enumhancer\Tests\Fixtures\UnitEnums\Defaults\DefaultsEnum; -use Illuminate\Routing\Route; use Illuminate\Support\Facades\Config; use Orchestra\Testbench\TestCase; @@ -41,6 +39,12 @@ function (SimpleEnum $status) { protected function defineWebRoutes($router) { + $router->get('/noparams', + function () { + return null; + } + ); + $router->get('/backed/{status}', function (EnhancedBackedEnum $status) { return $status->name; @@ -70,6 +74,11 @@ function (DefaultsEnum $status) { ); } + public function testApiRouteNoParameters() + { + $this->get('/noparams')->assertOk(); + } + public function testApiRouteBindsBasicEnum() { $this->get('/simpleapi/open')->assertOk(); diff --git a/tests/Unit/PHPStan/Constants/BitmaskConstantAlwaysUsedTest.php b/tests/Unit/PHPStan/Constants/BitmaskConstantAlwaysUsedTest.php new file mode 100644 index 0000000..d15a699 --- /dev/null +++ b/tests/Unit/PHPStan/Constants/BitmaskConstantAlwaysUsedTest.php @@ -0,0 +1,69 @@ +expects('getName')->andReturn('not_bit_values'); + + $constant = new BitmaskConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testShouldOnlyWorkWithEnums(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnFalse(); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + $constantReflection->expects('getName')->andReturn('bit_values'); + + $constant = new BitmaskConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testShouldOnlyWorkWithEnumsImplementingBitmask(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturn(SimpleEnum::class); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + $constantReflection->expects('getName')->andReturn('bit_values'); + + $constant = new BitmaskConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testShouldReturnTrueWhenImplementingBitmaskAndHasValue(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturn(BitmasksIntEnum::class); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + $constantReflection->expects('getName')->andReturn('bit_values'); + + $constant = new BitmaskConstantAlwaysUsed(); + + $this->assertTrue($constant->isAlwaysUsed($constantReflection)); + } +} diff --git a/tests/Unit/PHPStan/Constants/DefaultConstantAlwaysUsedTest.php b/tests/Unit/PHPStan/Constants/DefaultConstantAlwaysUsedTest.php new file mode 100644 index 0000000..8dea095 --- /dev/null +++ b/tests/Unit/PHPStan/Constants/DefaultConstantAlwaysUsedTest.php @@ -0,0 +1,85 @@ +expects('getName')->andReturns('notDefault'); + + $constant = new DefaultConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testClassIsNotEnum() + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnFalse(); + + $constantReflection = Mockery::mock(ConstantReflection::class); + $constantReflection->expects('getName')->andReturns('Default'); + $constantReflection->expects('getDeclaringClass')->andReturns($classReflection); + + $constant = new DefaultConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testConstantCorrectDefaultInEnum() + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('getName')->andReturns(DefaultsConstantEnum::class); + $classReflection->expects('isEnum')->andReturnTrue(); + + $constantReflection = Mockery::mock(ConstantReflection::class); + $constantReflection->expects('getName')->andReturns('Default'); + $constantReflection->expects('getDeclaringClass')->andReturns($classReflection); + + $constant = new DefaultConstantAlwaysUsed(); + + $this->assertTrue($constant->isAlwaysUsed($constantReflection)); + } + + public function testConstantCorrectDefaultCapitalizedInEnum() + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('getName')->andReturns(EnumWithCapitalizedDefault::class); + $classReflection->expects('isEnum')->andReturnTrue(); + + $constantReflection = Mockery::mock(ConstantReflection::class); + $constantReflection->expects('getName')->andReturns('DEFAULT'); + $constantReflection->expects('getDeclaringClass')->andReturns($classReflection); + + $constant = new DefaultConstantAlwaysUsed(); + + $this->assertTrue($constant->isAlwaysUsed($constantReflection)); + } + + public function testConstantNamedDefaultNotImplementingDefaults() + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('getName')->andReturns(EnumWithDefaultNotImplementing::class); + $classReflection->expects('isEnum')->andReturnTrue(); + + $constantReflection = Mockery::mock(ConstantReflection::class); + $constantReflection->expects('getName')->andReturns('Default'); + $constantReflection->expects('getDeclaringClass')->andReturns($classReflection); + + $constant = new DefaultConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } +} diff --git a/tests/Unit/PHPStan/Constants/MapperConstantAlwaysUsedTest.php b/tests/Unit/PHPStan/Constants/MapperConstantAlwaysUsedTest.php new file mode 100644 index 0000000..a38cb00 --- /dev/null +++ b/tests/Unit/PHPStan/Constants/MapperConstantAlwaysUsedTest.php @@ -0,0 +1,72 @@ +expects('isEnum')->andReturnFalse(); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + + $constant = new MapperConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testShouldReturnFalseWhenNotHavingMapConstant(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturn(MappersEnum::class); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + $constantReflection->expects('getName')->andReturn('NotValid'); + + $constant = new MapperConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testImplementsMappersMapperConstantWithEnumInstance(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturn(MappersEnum::class); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + $constantReflection->expects('getName')->andReturn('MAP'); + + $constant = new MapperConstantAlwaysUsed(); + + $this->assertTrue($constant->isAlwaysUsed($constantReflection)); + } + + public function testImplementsMappersMapperConstantWithValidMapper(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturn(MappersEnum::class); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + $constantReflection->expects('getName')->andReturn('MAP_ARRAY'); + + $constant = new MapperConstantAlwaysUsed(); + + $this->assertTrue($constant->isAlwaysUsed($constantReflection)); + } +} diff --git a/tests/Unit/PHPStan/Constants/Rule/DefaultConstantRuleTest.php b/tests/Unit/PHPStan/Constants/Rule/DefaultConstantRuleTest.php new file mode 100644 index 0000000..b166efb --- /dev/null +++ b/tests/Unit/PHPStan/Constants/Rule/DefaultConstantRuleTest.php @@ -0,0 +1,79 @@ +analyse( + [__DIR__ . '/../../Fixtures/Defaults/NotEnum.php'], + [ + ] + ); + } + + public function testNoErrorsIfConstantNotDefault(): void + { + $this->analyse( + [__DIR__ . '/../../../../Fixtures/SimpleEnum.php'], + [ + ] + ); + } + + public function testNoErrorsIfNotImplementingAndNotFromEnum(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Defaults/EnumWithIncorrectDefaultNotImplementing.php'], + [ + ] + ); + } + + public function testErrorsIfImplementingAndNotFromEnum(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Defaults/EnumWithIncorrectDefault.php'], + [ + [ + 'Enumhancer: enum is implementing `Defaults`, ' + . 'but constant `DEFAULT` is not referencing to one of its own cases.', + 13, + ] + ] + ); + } + + public function testErrorsIfNotImplementingAndCorrectReference(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Defaults/EnumWithDefaultNotImplementing.php'], + [ + [ + 'Enumhancer: Constant `Default` is not going to be used, because enum is not implementing `Defaults`', + 9, + ] + ] + ); + } + + public function testNoErrorsIfImplementingAndCorrectReference(): void + { + $this->analyse( + [__DIR__ . '/../../../../Fixtures/UnitEnums/Defaults/DefaultsConstantEnum.php'], + [ + ] + ); + } +} diff --git a/tests/Unit/PHPStan/Constants/Rule/MapperConstantRuleTest.php b/tests/Unit/PHPStan/Constants/Rule/MapperConstantRuleTest.php new file mode 100644 index 0000000..547b509 --- /dev/null +++ b/tests/Unit/PHPStan/Constants/Rule/MapperConstantRuleTest.php @@ -0,0 +1,73 @@ +getContainer()->getByType(ReflectionProvider::class) + ); + } + + public function testNoErrorsIfNotEnum(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Mappers/NotEnum.php'], + [ + ] + ); + } + + public function testEnumWithConstantThatIsNotMapperName(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Mappers/EnumWithNonMapperConstant.php'], + [ + ] + ); + } + + public function testEnumImplementingMappersWithInvalidMap(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Mappers/MappersEnum.php'], + [ + [ + 'Enumhancer: The specified `MapInvalid` map is invalid', + 14, + ] + ] + ); + } + + public function testEnumNotImplementingMappersWithValidMap(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Mappers/SimpleEnumWithMapperConstant.php'], + [ + [ + 'Enumhancer: `MAP_SELF` is not going to be used because enum is not implementing `Mappers`', + 07, + ] + ] + ); + } + + public function testMappersConstantContainsMapperClass(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Mappers/EnumWithMapperClass.php'], + [ + ] + ); + } +} diff --git a/tests/Unit/PHPStan/Constants/Rule/StrictConstantRuleTest.php b/tests/Unit/PHPStan/Constants/Rule/StrictConstantRuleTest.php new file mode 100644 index 0000000..7d3ff70 --- /dev/null +++ b/tests/Unit/PHPStan/Constants/Rule/StrictConstantRuleTest.php @@ -0,0 +1,69 @@ +analyse( + [__DIR__ . '/../../Fixtures/Strict/NotEnum.php'], + [ + ] + ); + } + + public function testStrictNotAvailable(): void + { + $this->analyse( + [__DIR__ . '/../../../../Fixtures/SimpleEnum.php'], + [ + ] + ); + } + + public function testStrictCheckIgnoresWhenNotImplementingEnumhancer(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Strict/SimpleStrictEnum.php'], + [ + ] + ); + } + + public function testStrictErrorWhenImplementingEnumhancer(): void + { + $this->analyse( + [__DIR__ . '/../../Fixtures/Strict/IncorrectEnum.php'], + [ + [ + 'Enumhancer: constant `STRICT` should be a boolean.', + 07 + ] + ] + ); + } + + public function testStrictCheckNoErrorsWhenEntirelyCorrect(): void + { + $this->analyse( + [ + __DIR__ . '/../../Fixtures/Strict/CorrectStrictTrueEnum.php', + __DIR__ . '/../../Fixtures/Strict/CorrectStrictFalseEnum.php' + ], + [ + ] + ); + } + +} diff --git a/tests/Unit/PHPStan/Constants/StrictConstantAlwaysUsedTest.php b/tests/Unit/PHPStan/Constants/StrictConstantAlwaysUsedTest.php new file mode 100644 index 0000000..84773eb --- /dev/null +++ b/tests/Unit/PHPStan/Constants/StrictConstantAlwaysUsedTest.php @@ -0,0 +1,69 @@ +expects('getName')->andReturn('notStrict'); + + $constant = new StrictConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testShouldOnlyWorkWithEnums(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->twice()->andReturnFalse(); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->twice()->andReturn($classReflection); + $constantReflection->expects('getName')->twice()->andReturn('strict', 'STRICT'); + + $constant = new StrictConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } + + public function testShouldReturnTrueWithEnumsImplementingValue(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturn(ValueStrictEnum::class); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + $constantReflection->expects('getName')->andReturn('STRICT'); + + $constant = new StrictConstantAlwaysUsed(); + + $this->assertTrue($constant->isAlwaysUsed($constantReflection)); + } + + public function testShouldReturnFalseWithEnumsNotImplementingValue(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturn(SimpleEnum::class); + + $constantReflection = Mockery::mock(ClassConstantReflection::class); + $constantReflection->expects('getDeclaringClass')->andReturn($classReflection); + $constantReflection->expects('getName')->andReturn('STRICT'); + + $constant = new StrictConstantAlwaysUsed(); + + $this->assertFalse($constant->isAlwaysUsed($constantReflection)); + } +} diff --git a/tests/Unit/PHPStan/Fixtures/Defaults/EnumWithCapitalizedDefault.php b/tests/Unit/PHPStan/Fixtures/Defaults/EnumWithCapitalizedDefault.php new file mode 100644 index 0000000..92849b8 --- /dev/null +++ b/tests/Unit/PHPStan/Fixtures/Defaults/EnumWithCapitalizedDefault.php @@ -0,0 +1,14 @@ + self::Hearts]; + + case Hearts; + +} diff --git a/tests/Unit/PHPStan/Fixtures/Strict/CorrectStrictFalseEnum.php b/tests/Unit/PHPStan/Fixtures/Strict/CorrectStrictFalseEnum.php new file mode 100644 index 0000000..c354b27 --- /dev/null +++ b/tests/Unit/PHPStan/Fixtures/Strict/CorrectStrictFalseEnum.php @@ -0,0 +1,12 @@ +expects('isEnum')->andReturnFalse(); + + $this->assertFalse( + (new EnumComparisonMethodsClassReflection())->hasMethod( + $reflection, + 'isHearts' + ) + ); + } + + public function testReturnsFalseIfNotImplementingComparison(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->twice()->andReturnTrue(); + $reflection->expects('getName')->twice()->andReturns(SimpleEnum::class); + + $this->assertFalse( + (new EnumComparisonMethodsClassReflection())->hasMethod( + $reflection, + 'isOpen' + ) + ); + + $this->assertFalse( + (new EnumComparisonMethodsClassReflection())->hasMethod( + $reflection, + 'isNotOpen' + ) + ); + } + + public function testReturnsFalseWithIncorrectMethod(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->twice()->andReturnTrue(); + $reflection->expects('getName')->twice()->andReturns(EnhancedUnitEnum::class); + + $this->assertFalse( + (new EnumComparisonMethodsClassReflection())->hasMethod( + $reflection, + 'isHearts' + ) + ); + + $this->assertFalse( + (new EnumComparisonMethodsClassReflection())->hasMethod( + $reflection, + 'isNotHearts' + ) + ); + } + + public function testReturnsTrueWithCorrectMethod(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->twice()->andReturnTrue(); + $reflection->expects('getName')->twice()->andReturns(EnhancedUnitEnum::class); + + $this->assertTrue( + (new EnumComparisonMethodsClassReflection())->hasMethod( + $reflection, + 'isUnique' + ) + ); + + $this->assertTrue( + (new EnumComparisonMethodsClassReflection())->hasMethod( + $reflection, + 'isNotUnique' + ) + ); + } + + public function testGetMethod(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $methodReflection = (new EnumComparisonMethodsClassReflection())->getMethod( + $reflection, + 'isHearts' + ); + + $this->assertTrue($reflection === $methodReflection->getDeclaringClass()); + + $this->assertFalse($methodReflection->isStatic()); + + $this->assertEquals([], $methodReflection->getVariants()[0]->getParameters()); + $this->assertEquals( + new BooleanType(), + $methodReflection->getVariants()[0]->getReturnType() + ); + } +} diff --git a/tests/Unit/PHPStan/Methods/EnumConstructorMethodsClassReflectionTest.php b/tests/Unit/PHPStan/Methods/EnumConstructorMethodsClassReflectionTest.php new file mode 100644 index 0000000..ede1217 --- /dev/null +++ b/tests/Unit/PHPStan/Methods/EnumConstructorMethodsClassReflectionTest.php @@ -0,0 +1,95 @@ +expects('isEnum')->andReturnFalse(); + + $this->assertFalse( + (new EnumConstructorMethodsClassReflection())->hasMethod( + $reflection, + 'Hearts' + ) + ); + } + + public function testReturnsFalseIfNotImplementingConstructor(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->andReturnTrue(); + $reflection->expects('getName')->andReturns(SimpleEnum::class); + + $this->assertFalse( + (new EnumConstructorMethodsClassReflection())->hasMethod( + $reflection, + 'Hearts' + ) + ); + } + + public function testReturnsFalseIfCaseDoesNotExists(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->andReturnTrue(); + $reflection->expects('getName')->andReturns(ConstructableUnitEnum::class); + + $this->assertFalse( + (new EnumConstructorMethodsClassReflection())->hasMethod( + $reflection, + 'Hearts' + ) + ); + } + + public function testReturnsTrueIfCaseDoesExists(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->andReturnTrue(); + $reflection->expects('getName')->andReturns(ConstructableUnitEnum::class); + + $this->assertFalse( + (new EnumConstructorMethodsClassReflection())->hasMethod( + $reflection, + 'isCallable' + ) + ); + } + + public function testGetMethod(): void + { + $reflection = Mockery::mock(ClassReflection::class); + $reflection->expects('getName')->andReturns(ConstructableUnitEnum::class); + + $methodReflection = (new EnumConstructorMethodsClassReflection())->getMethod( + $reflection, + 'isCallable' + ); + + $this->assertTrue($reflection === $methodReflection->getDeclaringClass()); + + $this->assertTrue($methodReflection->isStatic()); + + $this->assertEquals([], $methodReflection->getVariants()[0]->getParameters()); + $this->assertEquals( + new ObjectType(ConstructableUnitEnum::class), + $methodReflection->getVariants()[0]->getReturnType() + ); + + } +} diff --git a/tests/Unit/PHPStan/Methods/EnumMacrosMethodsClassReflectionTest.php b/tests/Unit/PHPStan/Methods/EnumMacrosMethodsClassReflectionTest.php new file mode 100644 index 0000000..739a836 --- /dev/null +++ b/tests/Unit/PHPStan/Methods/EnumMacrosMethodsClassReflectionTest.php @@ -0,0 +1,168 @@ +aMacroCall = function (MacrosUnitEnum $enum): self { + return $enum; + }; + + $this->aStaticMacroCall = static function (MacrosUnitEnum $enum): self { + return $enum; + }; + + $this->erroneousStaticMacroCall = static function ( + MacrosLogicExceptionEnum $enum = MacrosLogicExceptionEnum::Test + ): MacrosLogicExceptionEnum { + return $enum; + }; + + MacrosUnitEnum::macro( + 'aMacroCall', $this->aMacroCall + ); + + MacrosUnitEnum::macro( + 'aStaticMacroCall', $this->aStaticMacroCall + ); + + $this->closureFactory = self::getContainer()->getByType(ClosureTypeFactory::class); + $this->reflection = (new EnumMacrosMethodsClassReflection($this->closureFactory)); + } + + public function testShouldReturnFalseWhenNotEnum(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnFalse(); + + $this->assertFalse($this->reflection->hasMethod($classReflection, 'method')); + + } + + public function testShouldReturnFalseWhenNotImplementingMacros(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturns(SimpleEnum::class); + + $this->assertFalse($this->reflection->hasMethod($classReflection, 'method')); + + } + + public function testShouldReturnFalseWhenMacroNotSet(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->andReturnTrue(); + $classReflection->expects('getName')->andReturns(MacrosUnitEnum::class); + + $this->assertFalse($this->reflection->hasMethod($classReflection, 'notAMacro')); + } + + public function testShouldReturnTrueWhenMacroSet(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('isEnum')->twice()->andReturnTrue(); + $classReflection->expects('getName')->twice()->andReturns(MacrosUnitEnum::class); + + $this->assertTrue($this->reflection->hasMethod($classReflection, 'aMacroCall')); + $this->assertTrue($this->reflection->hasMethod($classReflection, 'aStaticMacroCall')); + } + + public function testGetMethod(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('getName')->andReturns(MacrosUnitEnum::class); + $methodReflection = $this->reflection->getMethod($classReflection, 'aMacroCall'); + + $this->assertInstanceOf(ClosureMethodReflection::class, $methodReflection); + $closureType = $this->closureFactory->fromClosureObject($this->aMacroCall); + $this->assertEquals( + [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + null, + $closureType->getParameters(), + $closureType->isVariadic(), + $closureType->getReturnType() + ), + ], + $methodReflection->getVariants() + ); + $this->assertFalse($methodReflection->isStatic()); + } + + public function testGetStaticMethod(): void + { + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('getName')->andReturns(MacrosUnitEnum::class); + $methodReflection = $this->reflection->getMethod($classReflection, 'aStaticMacroCall'); + + $this->assertInstanceOf(ClosureMethodReflection::class, $methodReflection); + $closureType = $this->closureFactory->fromClosureObject($this->aStaticMacroCall); + $this->assertEquals( + [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + null, + $closureType->getParameters(), + $closureType->isVariadic(), + $closureType->getReturnType() + ), + ], + $methodReflection->getVariants() + ); + $this->assertTrue($methodReflection->isStatic()); + } + + public function testThrowsException(): void + { + MacrosLogicExceptionEnum::macro('aStaticMacroCall', $this->erroneousStaticMacroCall); + $classReflection = Mockery::mock(ClassReflection::class); + $classReflection->expects('getName')->andReturns(MacrosLogicExceptionEnum::class); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'PHPStan Could not properly parse closure `aStaticMacroCall` for ' + . '`Henzeb\Enumhancer\Tests\Unit\PHPStan\Fixtures\Macros\MacrosLogicExceptionEnum`, ' + . 'Check if a default value\'s code is not trying to execute this macro.' + ); + $this->reflection->getMethod($classReflection, 'aStaticMacroCall'); + + } + + protected function tearDown(): void + { + parent::tearDown(); + MacrosUnitEnum::flushMacros(); + MacrosLogicExceptionEnum::flushMacros(); + } +} diff --git a/tests/Unit/PHPStan/Methods/EnumStateMethodsClassReflectionTest.php b/tests/Unit/PHPStan/Methods/EnumStateMethodsClassReflectionTest.php new file mode 100644 index 0000000..b8457c5 --- /dev/null +++ b/tests/Unit/PHPStan/Methods/EnumStateMethodsClassReflectionTest.php @@ -0,0 +1,109 @@ +expects('isEnum')->andReturnFalse(); + + $this->assertFalse( + (new EnumStateMethodsClassReflection())->hasMethod( + $reflection, + 'Hearts' + ) + ); + } + + public function testReturnsFalseIfNotImplementingState(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->andReturnTrue(); + $reflection->expects('getName')->andReturns(SimpleEnum::class); + + $this->assertFalse( + (new EnumStateMethodsClassReflection())->hasMethod( + $reflection, + 'Hearts' + ) + ); + } + + public function testReturnsFalseWithIncorrectMethod(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->twice()->andReturnTrue(); + $reflection->expects('getName')->twice()->andReturns(StateElevatorEnum::class); + + $this->assertFalse( + (new EnumStateMethodsClassReflection())->hasMethod( + $reflection, + 'toHearts' + ) + ); + + $this->assertFalse( + (new EnumStateMethodsClassReflection())->hasMethod( + $reflection, + 'tryToHearts' + ) + ); + } + + public function testReturnsTrueWithCorrectMethod(): void + { + $reflection = Mockery::mock(ClassReflection::class); + + $reflection->expects('isEnum')->twice()->andReturnTrue(); + $reflection->expects('getName')->twice()->andReturns(StateElevatorEnum::class); + + $this->assertTrue( + (new EnumStateMethodsClassReflection())->hasMethod( + $reflection, + 'toOpen' + ) + ); + + $this->assertTrue( + (new EnumStateMethodsClassReflection())->hasMethod( + $reflection, + 'tryToOpen' + ) + ); + } + + public function testGetMethod(): void + { + $reflection = Mockery::mock(ClassReflection::class); + $reflection->expects('getName')->andReturns(StateElevatorEnum::class); + + $methodReflection = (new EnumStateMethodsClassReflection())->getMethod( + $reflection, + 'toOpen' + ); + + $this->assertTrue($reflection === $methodReflection->getDeclaringClass()); + + $this->assertFalse($methodReflection->isStatic()); + + $this->assertEquals([], $methodReflection->getVariants()[0]->getParameters()); + $this->assertEquals( + new ObjectType(StateElevatorEnum::class), + $methodReflection->getVariants()[0]->getReturnType() + ); + + } +} diff --git a/tests/Unit/PHPStan/Reflections/ClosureMethodReflectionTest.php b/tests/Unit/PHPStan/Reflections/ClosureMethodReflectionTest.php new file mode 100644 index 0000000..9f000f8 --- /dev/null +++ b/tests/Unit/PHPStan/Reflections/ClosureMethodReflectionTest.php @@ -0,0 +1,121 @@ +closureFactory = self::getContainer()->getByType(ClosureTypeFactory::class); + } + + public function providesTestcases(): array + { + return [ + [ + 'method' => 'aMethod', + 'callable' => static fn() => true, + 'isStatic' => true, + 'docComment' => false, + ], + [ + 'method' => 'anotherMethod', + 'callable' => function (SimpleEnum $enum = null): ?SimpleEnum { + return $enum; + }, + 'isStatic' => false, + 'docComment' => null, + ], + [ + 'method' => 'yetAnotherMethod', + 'callable' => function (SimpleEnum $enum = null): ?SimpleEnum { + return $enum; + }, + 'isStatic' => false, + 'docComment' => true, + ], + [ + 'method' => 'callMeMaybe', + 'callable' => function (SimpleEnum $enum = null): ?SimpleEnum { + return $enum; + }, + 'isStatic' => false, + 'docComment' => '/** docblock */', + ] + ]; + } + + /** + * @param string $method + * @param callable $callable + * @param bool $isStatic + * @param bool|string|null $docComment + * @return void + * @throws ShouldNotHappenException + * + * @dataProvider providesTestcases + */ + public function testVariableMethods( + string $method, + callable $callable, + bool $isStatic, + bool|string|null $docComment + ) { + $classReflection = $this->createReflectionProvider()->getClass(self::class); + $closureType = $this->closureFactory->fromClosureObject($callable); + + $closureReflection = new ClosureMethodReflection( + $classReflection, + $method, + $closureType, + $isStatic, + ); + $closureReflection->setDocDocument($docComment); + $this->assertEquals($classReflection, $closureReflection->getDeclaringClass()); + $this->assertFalse($closureReflection->isPrivate()); + $this->assertTrue($closureReflection->isPublic()); + $this->assertTrue($closureReflection->isFinal()->no()); + $this->assertTrue($closureReflection->isInternal()->no()); + + $this->assertEquals($isStatic, $closureReflection->isStatic()); + if (is_bool($docComment)) { + $docComment = null; + } + + $this->assertEquals($docComment, $closureReflection->getDocComment()); + $this->assertEquals($method, $closureReflection->getName()); + $this->assertTrue($closureReflection->isDeprecated()->no()); + $this->assertEquals($closureReflection, $closureReflection->getPrototype()); + + $this->assertEquals( + [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + null, + $closureType->getParameters(), + $closureType->isVariadic(), + $closureType->getReturnType() + ), + ], + $closureReflection->getVariants() + ); + + $this->assertNull($closureReflection->getDeprecatedDescription()); + $this->assertNull($closureReflection->getThrowType()); + $this->assertTrue($closureReflection->hasSideEffects()->maybe()); + } +}