Skip to content

Commit

Permalink
Check if value is int or string in conversion of `Enum::hasValue(…
Browse files Browse the repository at this point in the history
…)` to native enum
  • Loading branch information
spawnia committed Feb 13, 2024
1 parent 3fda3ce commit 00ab1ed
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 30 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 30 additions & 24 deletions src/Rector/ToNativeImplementationRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,7 +28,9 @@ public function __construct(
protected PhpDocInfoPrinter $phpDocInfoPrinter,
protected PhpDocInfoFactory $phpDocInfoFactory,
protected ValueResolver $valueResolver,
) {}
) {
parent::__construct($valueResolver);
}

public function getRuleDefinition(): RuleDefinition
{
Expand Down Expand Up @@ -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()];
Expand Down
26 changes: 26 additions & 0 deletions src/Rector/ToNativeRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -19,6 +21,10 @@ abstract class ToNativeRector extends AbstractRector implements ConfigurableRect
/** @var array<ObjectType> */
protected array $classes;

public function __construct(
protected ValueResolver $valueResolver
) {}

/** @param array<class-string> $configuration */
public function configure(array $configuration): void
{
Expand All @@ -38,4 +44,24 @@ protected function inConfiguredClasses(Node $node): bool

return false;
}

/** @param array<mixed> $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;
}
}
76 changes: 75 additions & 1 deletion src/Rector/ToNativeUsagesRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -427,33 +428,106 @@ 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,
]);
}

$args = $call->args;
$firstArg = $args[0] ?? null;
if ($firstArg instanceof Arg) {
$firstArgValue = $firstArg->value;

if (
$firstArgValue instanceof ClassConstFetch
&& $firstArgValue->class->toString() === $class->toString()
) {
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);
}
}

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()
Expand Down
42 changes: 37 additions & 5 deletions tests/Rector/Usages/hasValue.php.inc
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
<?php

use BenSampo\Enum\Tests\Enums\UserType;
use BenSampo\Enum\Tests\Enums\StringValues;

UserType::hasValue('foo');
UserType::hasValue('foo', false);
/** @var int $int */
/** @var string $string */
/** @var bool $bool */
/** @var mixed $mixed */

UserType::hasValue($int);
UserType::hasValue($string);
UserType::hasValue($bool);
UserType::hasValue($mixed);
UserType::hasValue(UserType::Administrator);

StringValues::hasValue($int);
StringValues::hasValue($string);
StringValues::hasValue($bool);
StringValues::hasValue($mixed);
StringValues::hasValue(StringValues::Administrator);

UserType::hasValue(...);
StringValues::hasValue(...);
-----
<?php

use BenSampo\Enum\Tests\Enums\UserType;
use BenSampo\Enum\Tests\Enums\StringValues;

/** @var int $int */
/** @var string $string */
/** @var bool $bool */
/** @var mixed $mixed */

UserType::tryFrom('foo') !== null;
UserType::tryFrom('foo') !== null;
UserType::tryFrom($int) !== null;
false;
false;
is_int($mixed) && UserType::tryFrom($mixed) !== null;
true;
static fn(mixed $value): bool => 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;

0 comments on commit 00ab1ed

Please sign in to comment.