2020-05-02 18:52:16 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
2020-05-02 21:25:24 +00:00
|
|
|
namespace Rector\Naming\Naming;
|
2020-05-02 18:52:16 +00:00
|
|
|
|
|
|
|
use Nette\Utils\Strings;
|
2020-05-03 11:26:26 +00:00
|
|
|
use PHPStan\Type\NullType;
|
2020-05-02 18:52:16 +00:00
|
|
|
use PHPStan\Type\StaticType;
|
|
|
|
use PHPStan\Type\Type;
|
|
|
|
use PHPStan\Type\TypeWithClassName;
|
2020-05-03 11:26:26 +00:00
|
|
|
use PHPStan\Type\UnionType;
|
2020-05-02 18:52:16 +00:00
|
|
|
use Rector\PHPStan\Type\SelfObjectType;
|
|
|
|
use Rector\PHPStan\Type\ShortenedObjectType;
|
|
|
|
|
|
|
|
final class PropertyNaming
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var string[]
|
|
|
|
*/
|
|
|
|
private const EXCLUDED_CLASSES = [
|
|
|
|
'#Closure#',
|
|
|
|
'#^Spl#',
|
|
|
|
'#FileInfo#',
|
|
|
|
'#^std#',
|
|
|
|
'#Iterator#',
|
|
|
|
'#SimpleXML#',
|
|
|
|
// anything that ends with underscore
|
|
|
|
'#_$#',
|
|
|
|
];
|
|
|
|
|
|
|
|
public function getExpectedNameFromType(Type $type): ?string
|
|
|
|
{
|
2020-05-03 11:26:26 +00:00
|
|
|
if ($type instanceof UnionType) {
|
|
|
|
$type = $this->unwrapNullableType($type);
|
|
|
|
}
|
|
|
|
|
2020-05-02 18:52:16 +00:00
|
|
|
if (! $type instanceof TypeWithClassName) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($type instanceof SelfObjectType) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($type instanceof StaticType) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$className = $this->getClassName($type);
|
|
|
|
|
|
|
|
foreach (self::EXCLUDED_CLASSES as $excludedClass) {
|
|
|
|
if (Strings::match($className, $excludedClass)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$shortClassName = $this->resolveShortClassName($className);
|
|
|
|
$shortClassName = $this->removePrefixesAndSuffixes($shortClassName);
|
|
|
|
|
|
|
|
// if all is upper-cased, it should be lower-cased
|
|
|
|
if ($shortClassName === strtoupper($shortClassName)) {
|
|
|
|
$shortClassName = strtolower($shortClassName);
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove "_"
|
|
|
|
$shortClassName = Strings::replace($shortClassName, '#_#', '');
|
|
|
|
$shortClassName = $this->normalizeUpperCase($shortClassName);
|
|
|
|
|
|
|
|
return lcfirst($shortClassName);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function removePrefixesAndSuffixes(string $shortClassName): string
|
|
|
|
{
|
|
|
|
// is SomeInterface
|
|
|
|
if (Strings::endsWith($shortClassName, 'Interface')) {
|
|
|
|
$shortClassName = Strings::substring($shortClassName, 0, -strlen('Interface'));
|
|
|
|
}
|
|
|
|
|
|
|
|
// is ISomeClass
|
|
|
|
if ($this->isPrefixedInterface($shortClassName)) {
|
|
|
|
$shortClassName = Strings::substring($shortClassName, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// is AbstractClass
|
|
|
|
if (Strings::startsWith($shortClassName, 'Abstract')) {
|
|
|
|
$shortClassName = Strings::substring($shortClassName, strlen('Abstract'));
|
|
|
|
}
|
|
|
|
|
|
|
|
return $shortClassName;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function isPrefixedInterface(string $shortClassName): bool
|
|
|
|
{
|
|
|
|
if (strlen($shortClassName) <= 3) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! Strings::startsWith($shortClassName, 'I')) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctype_upper($shortClassName[1]) && ctype_lower($shortClassName[2]);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getClassName(TypeWithClassName $typeWithClassName): string
|
|
|
|
{
|
|
|
|
if ($typeWithClassName instanceof ShortenedObjectType) {
|
|
|
|
return $typeWithClassName->getFullyQualifiedName();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $typeWithClassName->getClassName();
|
|
|
|
}
|
|
|
|
|
|
|
|
private function resolveShortClassName(string $className): string
|
|
|
|
{
|
|
|
|
if (Strings::contains($className, '\\')) {
|
|
|
|
return Strings::after($className, '\\', -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $className;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function normalizeUpperCase(string $shortClassName): string
|
|
|
|
{
|
|
|
|
// turns $SOMEUppercase => $someUppercase
|
|
|
|
for ($i = 0; $i <= strlen($shortClassName); ++$i) {
|
2020-05-13 20:43:48 +00:00
|
|
|
if (ctype_upper($shortClassName[$i]) && $this->isNumberOrUpper($shortClassName[$i + 1])) {
|
2020-05-02 18:52:16 +00:00
|
|
|
$shortClassName[$i] = strtolower($shortClassName[$i]);
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-05-13 20:43:48 +00:00
|
|
|
|
2020-05-02 18:52:16 +00:00
|
|
|
return $shortClassName;
|
|
|
|
}
|
2020-05-03 11:26:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* E.g. null|ClassType → ClassType
|
|
|
|
*/
|
|
|
|
private function unwrapNullableType(UnionType $unionType): ?Type
|
|
|
|
{
|
|
|
|
if (count($unionType->getTypes()) !== 2) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! $unionType->isSuperTypeOf(new NullType())->yes()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($unionType->getTypes() as $unionedType) {
|
|
|
|
if ($unionedType instanceof NullType) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $unionedType;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
2020-05-13 20:43:48 +00:00
|
|
|
|
|
|
|
private function isNumberOrUpper(string $char): bool
|
|
|
|
{
|
|
|
|
if (ctype_upper($char)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctype_digit($char);
|
|
|
|
}
|
2020-05-02 18:52:16 +00:00
|
|
|
}
|