Skip to content

Commit 00ab1ed

Browse files
committed
Check if value is int or string in conversion of Enum::hasValue() to native enum
1 parent 3fda3ce commit 00ab1ed

File tree

5 files changed

+174
-30
lines changed

5 files changed

+174
-30
lines changed

Diff for: CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
## 6.9.1
11+
12+
### Fixed
13+
14+
- Check if value is `int` or `string` in conversion of `Enum::hasValue()` to native enum
15+
1016
## 6.9.0
1117

1218
### Added

Diff for: src/Rector/ToNativeImplementationRector.php

+30-24
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
use PhpParser\Node;
99
use PhpParser\Node\Identifier;
1010
use PhpParser\Node\Stmt\Class_;
11+
use PhpParser\Node\Stmt\ClassConst;
1112
use PhpParser\Node\Stmt\Enum_;
1213
use PhpParser\Node\Stmt\EnumCase;
1314
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
1415
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
1516
use PHPStan\Type\ObjectType;
16-
use PHPStan\Type\Type;
1717
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
1818
use Rector\BetterPhpDocParser\Printer\PhpDocInfoPrinter;
1919
use Rector\NodeTypeResolver\Node\AttributeKey;
@@ -28,7 +28,9 @@ public function __construct(
2828
protected PhpDocInfoPrinter $phpDocInfoPrinter,
2929
protected PhpDocInfoFactory $phpDocInfoFactory,
3030
protected ValueResolver $valueResolver,
31-
) {}
31+
) {
32+
parent::__construct($valueResolver);
33+
}
3234

3335
public function getRuleDefinition(): RuleDefinition
3436
{
@@ -106,28 +108,32 @@ public function refactor(Node $class): ?Node
106108
$enum->stmts = $class->getTraitUses();
107109

108110
$constants = $class->getConstants();
109-
if ($constants !== []) {
110-
// Assume the first constant value has the correct type
111-
$value = $this->valueResolver->getValue($constants[0]->consts[0]->value);
112-
$enum->scalarType = is_string($value)
113-
? new Identifier('string')
114-
: new Identifier('int');
115-
116-
foreach ($constants as $constant) {
117-
$constConst = $constant->consts[0];
118-
$enumCase = new EnumCase(
119-
$constConst->name,
120-
$constConst->value,
121-
[],
122-
['startLine' => $constConst->getStartLine(), 'endLine' => $constConst->getEndLine()],
123-
);
124-
125-
// mirror comments
126-
$enumCase->setAttribute(AttributeKey::PHP_DOC_INFO, $constant->getAttribute(AttributeKey::PHP_DOC_INFO));
127-
$enumCase->setAttribute(AttributeKey::COMMENTS, $constant->getAttribute(AttributeKey::COMMENTS));
128-
129-
$enum->stmts[] = $enumCase;
130-
}
111+
112+
$constantValues = array_map(
113+
fn (ClassConst $classConst): mixed => $this->valueResolver->getValue(
114+
$classConst->consts[0]->value
115+
),
116+
$constants
117+
);
118+
$enumScalarType = $this->enumScalarType($constantValues);
119+
if ($enumScalarType) {
120+
$enum->scalarType = new Identifier($enumScalarType);
121+
}
122+
123+
foreach ($constants as $constant) {
124+
$constConst = $constant->consts[0];
125+
$enumCase = new EnumCase(
126+
$constConst->name,
127+
$constConst->value,
128+
[],
129+
['startLine' => $constConst->getStartLine(), 'endLine' => $constConst->getEndLine()],
130+
);
131+
132+
// mirror comments
133+
$enumCase->setAttribute(AttributeKey::PHP_DOC_INFO, $constant->getAttribute(AttributeKey::PHP_DOC_INFO));
134+
$enumCase->setAttribute(AttributeKey::COMMENTS, $constant->getAttribute(AttributeKey::COMMENTS));
135+
136+
$enum->stmts[] = $enumCase;
131137
}
132138

133139
$enum->stmts = [...$enum->stmts, ...$class->getMethods()];

Diff for: src/Rector/ToNativeRector.php

+26
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace BenSampo\Enum\Rector;
44

5+
use Illuminate\Support\Arr;
56
use PhpParser\Node;
67
use PHPStan\Type\ObjectType;
78
use Rector\Contract\Rector\ConfigurableRectorInterface;
9+
use Rector\PhpParser\Node\Value\ValueResolver;
810
use Rector\Rector\AbstractRector;
911

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

24+
public function __construct(
25+
protected ValueResolver $valueResolver
26+
) {}
27+
2228
/** @param array<class-string> $configuration */
2329
public function configure(array $configuration): void
2430
{
@@ -38,4 +44,24 @@ protected function inConfiguredClasses(Node $node): bool
3844

3945
return false;
4046
}
47+
48+
/** @param array<mixed> $constantValues */
49+
protected function enumScalarType(array $constantValues): ?string
50+
{
51+
if ($constantValues === []) {
52+
return null;
53+
}
54+
55+
// Assume the first constant value has the correct type
56+
$value = Arr::first($constantValues);
57+
if (is_string($value)) {
58+
return 'string';
59+
}
60+
61+
if (is_int($value)) {
62+
return 'int';
63+
}
64+
65+
return null;
66+
}
4167
}

Diff for: src/Rector/ToNativeUsagesRector.php

+75-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PhpParser\Node\Expr\AssignOp;
1717
use PhpParser\Node\Expr\AssignRef;
1818
use PhpParser\Node\Expr\BinaryOp;
19+
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
1920
use PhpParser\Node\Expr\BinaryOp\Coalesce;
2021
use PhpParser\Node\Expr\BinaryOp\Equal;
2122
use PhpParser\Node\Expr\BinaryOp\Identical;
@@ -427,33 +428,106 @@ protected function refactorHasValue(StaticCall $call): ?Node
427428

428429
if ($call->isFirstClassCallable()) {
429430
$valueVariable = new Variable('value');
431+
$valueVariableArg = new Arg($valueVariable);
432+
433+
$tryFromNotNull = $makeTryFromNotNull($valueVariableArg);
434+
435+
$enumScalarType = $this->enumScalarTypeFromClassName($class);
436+
if ($enumScalarType === 'int') {
437+
$expr = new BooleanAnd(
438+
new FuncCall(new Name('is_int'), [$valueVariableArg]),
439+
$tryFromNotNull
440+
);
441+
} elseif ($enumScalarType === 'string') {
442+
$expr = new BooleanAnd(
443+
new FuncCall(new Name('is_string'), [$valueVariableArg]),
444+
$tryFromNotNull
445+
);
446+
} else {
447+
$expr = $tryFromNotNull;
448+
}
430449

431450
return new ArrowFunction([
432451
'static' => true,
433452
'params' => [new Param($valueVariable, null, 'mixed')],
434453
'returnType' => 'bool',
435-
'expr' => $makeTryFromNotNull(new Arg($valueVariable)),
454+
'expr' => $expr,
436455
]);
437456
}
438457

439458
$args = $call->args;
440459
$firstArg = $args[0] ?? null;
441460
if ($firstArg instanceof Arg) {
442461
$firstArgValue = $firstArg->value;
462+
443463
if (
444464
$firstArgValue instanceof ClassConstFetch
445465
&& $firstArgValue->class->toString() === $class->toString()
446466
) {
447467
return new ConstFetch(new Name('true'));
448468
}
449469

470+
$firstArgType = $this->getType($firstArgValue);
471+
472+
$enumScalarType = $this->enumScalarTypeFromClassName($class);
473+
if ($enumScalarType === 'int') {
474+
$firstArgTypeIsInt = $firstArgType->isInteger();
475+
if ($firstArgTypeIsInt->yes()) {
476+
return $makeTryFromNotNull($firstArg);
477+
}
478+
479+
if ($firstArgTypeIsInt->no()) {
480+
return new ConstFetch(new Name('false'));
481+
}
482+
483+
return new BooleanAnd(
484+
new FuncCall(new Name('is_int'), [$firstArg]),
485+
$makeTryFromNotNull($firstArg)
486+
);
487+
}
488+
if ($enumScalarType === 'string') {
489+
$firstArgTypeIsString = $firstArgType->isString();
490+
if ($firstArgTypeIsString->yes()) {
491+
return $makeTryFromNotNull($firstArg);
492+
}
493+
494+
if ($firstArgTypeIsString->no()) {
495+
return new ConstFetch(new Name('false'));
496+
}
497+
498+
return new BooleanAnd(
499+
new FuncCall(new Name('is_string'), [$firstArg]),
500+
$makeTryFromNotNull($firstArg)
501+
);
502+
}
503+
450504
return $makeTryFromNotNull($firstArg);
451505
}
452506
}
453507

454508
return null;
455509
}
456510

511+
protected function enumScalarTypeFromClassName(Name $class): ?string
512+
{
513+
$type = $this->getType($class);
514+
if (! $type instanceof FullyQualifiedObjectType) {
515+
return null;
516+
}
517+
518+
$classReflection = $type->getClassReflection();
519+
if (! $classReflection) {
520+
return null;
521+
}
522+
523+
$nativeReflection = $classReflection->getNativeReflection();
524+
if (! $nativeReflection instanceof \ReflectionClass) {
525+
return null;
526+
}
527+
528+
return $this->enumScalarType($nativeReflection->getConstants());
529+
}
530+
457531
/**
458532
* @see Enum::__callStatic()
459533
* @see Enum::__call()

Diff for: tests/Rector/Usages/hasValue.php.inc

+37-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,49 @@
11
<?php
22

33
use BenSampo\Enum\Tests\Enums\UserType;
4+
use BenSampo\Enum\Tests\Enums\StringValues;
45

5-
UserType::hasValue('foo');
6-
UserType::hasValue('foo', false);
6+
/** @var int $int */
7+
/** @var string $string */
8+
/** @var bool $bool */
9+
/** @var mixed $mixed */
10+
11+
UserType::hasValue($int);
12+
UserType::hasValue($string);
13+
UserType::hasValue($bool);
14+
UserType::hasValue($mixed);
715
UserType::hasValue(UserType::Administrator);
16+
17+
StringValues::hasValue($int);
18+
StringValues::hasValue($string);
19+
StringValues::hasValue($bool);
20+
StringValues::hasValue($mixed);
21+
StringValues::hasValue(StringValues::Administrator);
22+
823
UserType::hasValue(...);
24+
StringValues::hasValue(...);
925
-----
1026
<?php
1127

1228
use BenSampo\Enum\Tests\Enums\UserType;
29+
use BenSampo\Enum\Tests\Enums\StringValues;
30+
31+
/** @var int $int */
32+
/** @var string $string */
33+
/** @var bool $bool */
34+
/** @var mixed $mixed */
1335

14-
UserType::tryFrom('foo') !== null;
15-
UserType::tryFrom('foo') !== null;
36+
UserType::tryFrom($int) !== null;
37+
false;
38+
false;
39+
is_int($mixed) && UserType::tryFrom($mixed) !== null;
1640
true;
17-
static fn(mixed $value): bool => UserType::tryFrom($value) !== null;
41+
42+
false;
43+
StringValues::tryFrom($string) !== null;
44+
false;
45+
is_string($mixed) && StringValues::tryFrom($mixed) !== null;
46+
true;
47+
48+
static fn(mixed $value): bool => is_int($value) && UserType::tryFrom($value) !== null;
49+
static fn(mixed $value): bool => is_string($value) && StringValues::tryFrom($value) !== null;

0 commit comments

Comments
 (0)