Skip to content

Instantly share code, notes, and snippets.

@janedbal
Created June 7, 2024 12:40
Show Gist options
  • Save janedbal/40cceb8987a8c9a28bbca7565192efb4 to your computer and use it in GitHub Desktop.
Save janedbal/40cceb8987a8c9a28bbca7565192efb4 to your computer and use it in GitHub Desktop.
<?php declare(strict_types = 1);
namespace ShipMonk\Rules\Rector;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\ClassLike;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\Scope;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\CallableType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IterableType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use function is_string;
/**
* Rector to add typehints to class/trait/enum/interface constants (new in PHP 8.3)
* - the native one cannot do that, see https://github.com/rectorphp/rector/issues/8612
*
* @author Jan Nedbal (https://github.com/janedbal)
*/
class TypedClassConstRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('', [new CodeSample('', '')]);
}
/**
* @return list<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [ClassLike::class];
}
public function refactor(Node $node): ?Node
{
if (!$node instanceof ClassLike) {
return null;
}
$className = $this->getName($node);
if (!is_string($className)) {
return null;
}
$classConsts = $node->getConstants();
if ($classConsts === []) {
return null;
}
/** @var MutatingScope $scope */
$scope = $node->getAttribute('scope');
$hasChanged = false;
foreach ($classConsts as $classConst) {
$typehint = null;
if ($classConst->type !== null) {
continue;
}
foreach ($classConst->consts as $constNode) {
$typehint = $this->getTypehintByType($scope->getType($constNode->value), $scope);
break;
}
if ($typehint === null) {
continue;
}
$classConst->type = new Identifier($typehint);
$hasChanged = true;
}
if (!$hasChanged) {
return null;
}
return $node;
}
private function getTypehintByType(
Type $type,
Scope $scope,
): ?string
{
$typeWithoutNull = TypeCombinator::removeNull($type);
$typeHint = null;
if ((new BooleanType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) {
$typeHint = 'bool';
} elseif ((new IntegerType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) {
$typeHint = 'int';
} elseif ((new FloatType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) {
$typeHint = 'float';
} elseif ((new ArrayType(new MixedType(), new MixedType()))->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) {
$typeHint = 'array';
} elseif ((new StringType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) {
$typeHint = 'string';
} elseif ((new CallableType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) {
$typeHint = 'callable';
} elseif ((new IterableType(new MixedType(), new MixedType()))->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) {
$typeHint = 'iterable';
} elseif ((new ObjectWithoutClassType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) {
$typeHint = 'object';
}
if ($typeHint !== null && TypeCombinator::containsNull($type)) {
$typeHint = '?' . $typeHint;
}
return $typeHint;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment