diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ef7b55..7128c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## 6.9.1 + +### Fixed + +- Check if value is `int` or `string` in conversion of `Enum::hasValue()` to native enum + ## 6.9.0 ### Added diff --git a/src/Rector/ToNativeImplementationRector.php b/src/Rector/ToNativeImplementationRector.php index feb98dc..102c787 100644 --- a/src/Rector/ToNativeImplementationRector.php +++ b/src/Rector/ToNativeImplementationRector.php @@ -8,12 +8,12 @@ use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\Enum_; use PhpParser\Node\Stmt\EnumCase; use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\Type\ObjectType; -use PHPStan\Type\Type; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\BetterPhpDocParser\Printer\PhpDocInfoPrinter; use Rector\NodeTypeResolver\Node\AttributeKey; @@ -28,7 +28,9 @@ public function __construct( protected PhpDocInfoPrinter $phpDocInfoPrinter, protected PhpDocInfoFactory $phpDocInfoFactory, protected ValueResolver $valueResolver, - ) {} + ) { + parent::__construct($valueResolver); + } public function getRuleDefinition(): RuleDefinition { @@ -106,28 +108,32 @@ public function refactor(Node $class): ?Node $enum->stmts = $class->getTraitUses(); $constants = $class->getConstants(); - if ($constants !== []) { - // Assume the first constant value has the correct type - $value = $this->valueResolver->getValue($constants[0]->consts[0]->value); - $enum->scalarType = is_string($value) - ? new Identifier('string') - : new Identifier('int'); - - foreach ($constants as $constant) { - $constConst = $constant->consts[0]; - $enumCase = new EnumCase( - $constConst->name, - $constConst->value, - [], - ['startLine' => $constConst->getStartLine(), 'endLine' => $constConst->getEndLine()], - ); - - // mirror comments - $enumCase->setAttribute(AttributeKey::PHP_DOC_INFO, $constant->getAttribute(AttributeKey::PHP_DOC_INFO)); - $enumCase->setAttribute(AttributeKey::COMMENTS, $constant->getAttribute(AttributeKey::COMMENTS)); - - $enum->stmts[] = $enumCase; - } + + $constantValues = array_map( + fn (ClassConst $classConst): mixed => $this->valueResolver->getValue( + $classConst->consts[0]->value + ), + $constants + ); + $enumScalarType = $this->enumScalarType($constantValues); + if ($enumScalarType) { + $enum->scalarType = new Identifier($enumScalarType); + } + + foreach ($constants as $constant) { + $constConst = $constant->consts[0]; + $enumCase = new EnumCase( + $constConst->name, + $constConst->value, + [], + ['startLine' => $constConst->getStartLine(), 'endLine' => $constConst->getEndLine()], + ); + + // mirror comments + $enumCase->setAttribute(AttributeKey::PHP_DOC_INFO, $constant->getAttribute(AttributeKey::PHP_DOC_INFO)); + $enumCase->setAttribute(AttributeKey::COMMENTS, $constant->getAttribute(AttributeKey::COMMENTS)); + + $enum->stmts[] = $enumCase; } $enum->stmts = [...$enum->stmts, ...$class->getMethods()]; diff --git a/src/Rector/ToNativeRector.php b/src/Rector/ToNativeRector.php index 6c0e0f8..6c19524 100644 --- a/src/Rector/ToNativeRector.php +++ b/src/Rector/ToNativeRector.php @@ -2,9 +2,11 @@ namespace BenSampo\Enum\Rector; +use Illuminate\Support\Arr; use PhpParser\Node; use PHPStan\Type\ObjectType; use Rector\Contract\Rector\ConfigurableRectorInterface; +use Rector\PhpParser\Node\Value\ValueResolver; use Rector\Rector\AbstractRector; /** @@ -19,6 +21,10 @@ abstract class ToNativeRector extends AbstractRector implements ConfigurableRect /** @var array */ protected array $classes; + public function __construct( + protected ValueResolver $valueResolver + ) {} + /** @param array $configuration */ public function configure(array $configuration): void { @@ -38,4 +44,24 @@ protected function inConfiguredClasses(Node $node): bool return false; } + + /** @param array $constantValues */ + protected function enumScalarType(array $constantValues): ?string + { + if ($constantValues === []) { + return null; + } + + // Assume the first constant value has the correct type + $value = Arr::first($constantValues); + if (is_string($value)) { + return 'string'; + } + + if (is_int($value)) { + return 'int'; + } + + return null; + } } diff --git a/src/Rector/ToNativeUsagesRector.php b/src/Rector/ToNativeUsagesRector.php index 91a709a..69a59f8 100644 --- a/src/Rector/ToNativeUsagesRector.php +++ b/src/Rector/ToNativeUsagesRector.php @@ -16,6 +16,7 @@ use PhpParser\Node\Expr\AssignOp; use PhpParser\Node\Expr\AssignRef; use PhpParser\Node\Expr\BinaryOp; +use PhpParser\Node\Expr\BinaryOp\BooleanAnd; use PhpParser\Node\Expr\BinaryOp\Coalesce; use PhpParser\Node\Expr\BinaryOp\Equal; use PhpParser\Node\Expr\BinaryOp\Identical; @@ -427,12 +428,30 @@ protected function refactorHasValue(StaticCall $call): ?Node if ($call->isFirstClassCallable()) { $valueVariable = new Variable('value'); + $valueVariableArg = new Arg($valueVariable); + + $tryFromNotNull = $makeTryFromNotNull($valueVariableArg); + + $enumScalarType = $this->enumScalarTypeFromClassName($class); + if ($enumScalarType === 'int') { + $expr = new BooleanAnd( + new FuncCall(new Name('is_int'), [$valueVariableArg]), + $tryFromNotNull + ); + } elseif ($enumScalarType === 'string') { + $expr = new BooleanAnd( + new FuncCall(new Name('is_string'), [$valueVariableArg]), + $tryFromNotNull + ); + } else { + $expr = $tryFromNotNull; + } return new ArrowFunction([ 'static' => true, 'params' => [new Param($valueVariable, null, 'mixed')], 'returnType' => 'bool', - 'expr' => $makeTryFromNotNull(new Arg($valueVariable)), + 'expr' => $expr, ]); } @@ -440,6 +459,7 @@ protected function refactorHasValue(StaticCall $call): ?Node $firstArg = $args[0] ?? null; if ($firstArg instanceof Arg) { $firstArgValue = $firstArg->value; + if ( $firstArgValue instanceof ClassConstFetch && $firstArgValue->class->toString() === $class->toString() @@ -447,6 +467,40 @@ protected function refactorHasValue(StaticCall $call): ?Node return new ConstFetch(new Name('true')); } + $firstArgType = $this->getType($firstArgValue); + + $enumScalarType = $this->enumScalarTypeFromClassName($class); + if ($enumScalarType === 'int') { + $firstArgTypeIsInt = $firstArgType->isInteger(); + if ($firstArgTypeIsInt->yes()) { + return $makeTryFromNotNull($firstArg); + } + + if ($firstArgTypeIsInt->no()) { + return new ConstFetch(new Name('false')); + } + + return new BooleanAnd( + new FuncCall(new Name('is_int'), [$firstArg]), + $makeTryFromNotNull($firstArg) + ); + } + if ($enumScalarType === 'string') { + $firstArgTypeIsString = $firstArgType->isString(); + if ($firstArgTypeIsString->yes()) { + return $makeTryFromNotNull($firstArg); + } + + if ($firstArgTypeIsString->no()) { + return new ConstFetch(new Name('false')); + } + + return new BooleanAnd( + new FuncCall(new Name('is_string'), [$firstArg]), + $makeTryFromNotNull($firstArg) + ); + } + return $makeTryFromNotNull($firstArg); } } @@ -454,6 +508,26 @@ protected function refactorHasValue(StaticCall $call): ?Node return null; } + protected function enumScalarTypeFromClassName(Name $class): ?string + { + $type = $this->getType($class); + if (! $type instanceof FullyQualifiedObjectType) { + return null; + } + + $classReflection = $type->getClassReflection(); + if (! $classReflection) { + return null; + } + + $nativeReflection = $classReflection->getNativeReflection(); + if (! $nativeReflection instanceof \ReflectionClass) { + return null; + } + + return $this->enumScalarType($nativeReflection->getConstants()); + } + /** * @see Enum::__callStatic() * @see Enum::__call() diff --git a/tests/Rector/Usages/hasValue.php.inc b/tests/Rector/Usages/hasValue.php.inc index 357a344..fbd68cc 100644 --- a/tests/Rector/Usages/hasValue.php.inc +++ b/tests/Rector/Usages/hasValue.php.inc @@ -1,17 +1,49 @@ UserType::tryFrom($value) !== null; + +false; +StringValues::tryFrom($string) !== null; +false; +is_string($mixed) && StringValues::tryFrom($mixed) !== null; +true; + +static fn(mixed $value): bool => is_int($value) && UserType::tryFrom($value) !== null; +static fn(mixed $value): bool => is_string($value) && StringValues::tryFrom($value) !== null;