diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/AliasClassNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/AliasClassNameImportSkipVoter.php deleted file mode 100644 index 0812f6da40a..00000000000 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/AliasClassNameImportSkipVoter.php +++ /dev/null @@ -1,50 +0,0 @@ -classNameImportContext->resolveAliasedUses($file); - $longNameLowered = strtolower($fullyQualifiedObjectType->getClassName()); - $shortNameLowered = $fullyQualifiedObjectType->getShortNameLowered(); - - foreach ($aliasedUses as $aliasedUse) { - $aliasedUseLowered = strtolower($aliasedUse); - - // its aliased, we cannot just rename it - if (\str_ends_with($aliasedUseLowered, '\\' . $shortNameLowered)) { - return true; - } - - if ($aliasedUseLowered === $shortNameLowered && $longNameLowered === $shortNameLowered) { - return true; - } - } - - return false; - } -} diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php deleted file mode 100644 index 9fa262a7b31..00000000000 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php +++ /dev/null @@ -1,70 +0,0 @@ -classNameImportContext->resolveShortClassLikeNames($file); - if ($classLikeNames === []) { - return false; - } - - /** - * Note: Don't use ScopeFetcher::fetch() on Name instance, - * Scope can be null on Name - * This is part of ScopeAnalyzer::NON_REFRESHABLE_NODES - * @see https://github.com/rectorphp/rector-src/blob/9929af7c0179929b4fde6915cb7a06c3141dde6c/src/NodeAnalyzer/ScopeAnalyzer.php#L17 - */ - $scope = $node->getAttribute(AttributeKey::SCOPE); - $namespace = $scope instanceof Scope ? $scope->getNamespace() : null; - $namespace = strtolower((string) $namespace); - - $shortNameLowered = $fullyQualifiedObjectType->getShortNameLowered(); - $subClassName = $this->namespaceBeforeClassNameResolver->resolve($fullyQualifiedObjectType); - $fullyQualifiedObjectTypeNamespace = strtolower($subClassName); - - foreach ($classLikeNames as $classLikeName) { - if (strtolower($classLikeName) !== $shortNameLowered) { - continue; - } - - if ($namespace === '') { - return true; - } - - if ($namespace !== $fullyQualifiedObjectTypeNamespace) { - return true; - } - } - - return false; - } -} diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/FullyQualifiedNameClassNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/FullyQualifiedNameClassNameImportSkipVoter.php deleted file mode 100644 index c41a004265c..00000000000 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/FullyQualifiedNameClassNameImportSkipVoter.php +++ /dev/null @@ -1,59 +0,0 @@ -classNameImportContext->resolveShortNamesToFullyQualifiedNames($file); - $fullyQualifiedObjectTypeShortName = $fullyQualifiedObjectType->getShortName(); - $className = $fullyQualifiedObjectType->getClassName(); - - foreach ($shortNamesToFullyQualifiedNames as $shortName => $fullyQualifiedName) { - if ($fullyQualifiedObjectTypeShortName !== $shortName) { - $shortName = $this->cleanShortName($shortName); - } - - if ($fullyQualifiedObjectTypeShortName !== $shortName) { - continue; - } - - $fullyQualifiedName = ltrim($fullyQualifiedName, '\\'); - return $className !== $fullyQualifiedName; - } - - return false; - } - - private function cleanShortName(string $shortName): string - { - return str_starts_with($shortName, '\\') - ? ltrim((string) Strings::after($shortName, '\\', -1)) - : $shortName; - } -} diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/OriginalNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/OriginalNameImportSkipVoter.php deleted file mode 100644 index e8569ab0572..00000000000 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/OriginalNameImportSkipVoter.php +++ /dev/null @@ -1,33 +0,0 @@ -toCodeString(), '\\') === 1) { - return false; - } - - // verify long name, as short name verify may conflict - // see test PR: https://github.com/rectorphp/rector-src/pull/6208 - // ref https://3v4l.org/21H5j vs https://3v4l.org/GIHSB - $originalName = $node->getAttribute(AttributeKey::ORIGINAL_NAME); - return $originalName instanceof Name && $originalName->getLast() === $originalName->toString(); - } -} diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ReservedClassNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ReservedClassNameImportSkipVoter.php deleted file mode 100644 index 7e618f832fe..00000000000 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ReservedClassNameImportSkipVoter.php +++ /dev/null @@ -1,43 +0,0 @@ -getShortNameLowered(); - - return in_array($shortName, self::RESERVED_CLASS_NAMES, true); - } -} diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ShortClassImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ShortClassImportSkipVoter.php deleted file mode 100644 index 26f17091fbe..00000000000 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ShortClassImportSkipVoter.php +++ /dev/null @@ -1,26 +0,0 @@ -getClassName(), '\\'); - - if (substr_count($className, '\\') === 0) { - return ! SimpleParameterProvider::provideBoolParameter(Option::IMPORT_SHORT_CLASSES); - } - - return false; - } -} diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/UsesClassNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/UsesClassNameImportSkipVoter.php deleted file mode 100644 index eed696d7a1e..00000000000 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/UsesClassNameImportSkipVoter.php +++ /dev/null @@ -1,45 +0,0 @@ -classNameImportContext->resolveUsedImportTypes($file); - - foreach ($useImportTypes as $useImportType) { - if (! $useImportType->equals($fullyQualifiedObjectType) && $useImportType->areShortNamesEqual( - $fullyQualifiedObjectType - )) { - return true; - } - - if ($useImportType->equals($fullyQualifiedObjectType)) { - return false; - } - } - - return false; - } -} diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipper.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipper.php index 0e3d4fb01ba..58105ee094b 100644 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipper.php +++ b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipper.php @@ -4,13 +4,17 @@ namespace Rector\CodingStyle\ClassNameImport; +use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Identifier; +use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\Use_; use PhpParser\Node\UseItem; -use Rector\CodingStyle\Contract\ClassNameImport\ClassNameImportSkipVoterInterface; +use PHPStan\Analyser\Scope; +use Rector\Configuration\Option; +use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Naming\Naming\UseImportsResolver; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; @@ -19,17 +23,45 @@ final readonly class ClassNameImportSkipper { /** - * @param ClassNameImportSkipVoterInterface[] $classNameImportSkipVoters + * @var string[] */ + private const array RESERVED_CLASS_NAMES = [ + 'bool', + 'false', + 'float', + 'int', + 'null', + 'parent', + 'self', + 'static', + 'string', + 'true', + 'void', + 'never', + 'iterable', + 'object', + 'mixed', + 'array', + 'callable', + ]; + public function __construct( - private array $classNameImportSkipVoters, + private ClassNameImportContext $classNameImportContext, + private NamespaceBeforeClassNameResolver $namespaceBeforeClassNameResolver, private UseImportsResolver $useImportsResolver, ) { } public function shouldSkipNameForFullyQualifiedObjectType(File $file, Node $node, FullyQualifiedObjectType $fullyQualifiedObjectType): bool { - return array_any($this->classNameImportSkipVoters, fn (ClassNameImportSkipVoterInterface $classNameImportSkipVoter): bool => $classNameImportSkipVoter->shouldSkip($file, $fullyQualifiedObjectType, $node)); + // cheap, node-local checks first so the file-global lookups are skipped when possible + return $this->isReservedClassName($fullyQualifiedObjectType) + || $this->isNonImportableShortClass($fullyQualifiedObjectType) + || $this->shouldStayFullyQualifiedByOriginalName($node) + || $this->isConflictingUseImport($file, $fullyQualifiedObjectType) + || $this->isConflictingAlias($file, $fullyQualifiedObjectType) + || $this->isConflictingClassLikeName($file, $node, $fullyQualifiedObjectType) + || $this->isConflictingShortNameUsage($file, $fullyQualifiedObjectType); } /** @@ -88,4 +120,158 @@ private function isConflictedShortNameInUse( return $useItem->alias instanceof Identifier && $useItem->alias->toString() === $stringName; } + + private function isReservedClassName(FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + return in_array($fullyQualifiedObjectType->getShortNameLowered(), self::RESERVED_CLASS_NAMES, true); + } + + /** + * Short/global class (no namespace) is only importable when explicitly enabled + */ + private function isNonImportableShortClass(FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $className = ltrim($fullyQualifiedObjectType->getClassName(), '\\'); + + if (substr_count($className, '\\') === 0) { + return ! SimpleParameterProvider::provideBoolParameter(Option::IMPORT_SHORT_CLASSES); + } + + return false; + } + + private function shouldStayFullyQualifiedByOriginalName(Node $node): bool + { + if (! $node instanceof FullyQualified) { + return false; + } + + if (substr_count($node->toCodeString(), '\\') === 1) { + return false; + } + + // verify long name, as short name verify may conflict + // see test PR: https://github.com/rectorphp/rector-src/pull/6208 + // ref https://3v4l.org/21H5j vs https://3v4l.org/GIHSB + $originalName = $node->getAttribute(AttributeKey::ORIGINAL_NAME); + return $originalName instanceof Name && $originalName->getLast() === $originalName->toString(); + } + + /** + * Prevents importing App\Some\Product when a different Product is already imported + */ + private function isConflictingUseImport(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + foreach ($this->classNameImportContext->resolveUsedImportTypes($file) as $useImportType) { + if (! $useImportType->equals($fullyQualifiedObjectType) && $useImportType->areShortNamesEqual( + $fullyQualifiedObjectType + )) { + return true; + } + + if ($useImportType->equals($fullyQualifiedObjectType)) { + return false; + } + } + + return false; + } + + /** + * Prevents importing App\SomeClass when there is already "use App\Something as SomeClass" + */ + private function isConflictingAlias(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $longNameLowered = strtolower($fullyQualifiedObjectType->getClassName()); + $shortNameLowered = $fullyQualifiedObjectType->getShortNameLowered(); + + foreach ($this->classNameImportContext->resolveAliasedUses($file) as $aliasedUse) { + $aliasedUseLowered = strtolower($aliasedUse); + + // its aliased, we cannot just rename it + if (str_ends_with($aliasedUseLowered, '\\' . $shortNameLowered)) { + return true; + } + + if ($aliasedUseLowered === $shortNameLowered && $longNameLowered === $shortNameLowered) { + return true; + } + } + + return false; + } + + /** + * Prevents importing App\SomeClass when a class-like "SomeClass" is declared in the file + */ + private function isConflictingClassLikeName(File $file, Node $node, FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $classLikeNames = $this->classNameImportContext->resolveShortClassLikeNames($file); + if ($classLikeNames === []) { + return false; + } + + /** + * Note: Don't use ScopeFetcher::fetch() on Name instance, + * Scope can be null on Name + * This is part of ScopeAnalyzer::NON_REFRESHABLE_NODES + * @see https://github.com/rectorphp/rector-src/blob/9929af7c0179929b4fde6915cb7a06c3141dde6c/src/NodeAnalyzer/ScopeAnalyzer.php#L17 + */ + $scope = $node->getAttribute(AttributeKey::SCOPE); + $namespace = $scope instanceof Scope ? $scope->getNamespace() : null; + $namespace = strtolower((string) $namespace); + + $shortNameLowered = $fullyQualifiedObjectType->getShortNameLowered(); + $subClassName = $this->namespaceBeforeClassNameResolver->resolve($fullyQualifiedObjectType); + $fullyQualifiedObjectTypeNamespace = strtolower($subClassName); + + foreach ($classLikeNames as $classLikeName) { + if (strtolower($classLikeName) !== $shortNameLowered) { + continue; + } + + if ($namespace === '') { + return true; + } + + if ($namespace !== $fullyQualifiedObjectTypeNamespace) { + return true; + } + } + + return false; + } + + /** + * Prevents importing App\SomeClass when the short name is already used by a different FQN, + * e.g. "new X" or "X::static()" + */ + private function isConflictingShortNameUsage(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $shortNamesToFullyQualifiedNames = $this->classNameImportContext->resolveShortNamesToFullyQualifiedNames($file); + $fullyQualifiedObjectTypeShortName = $fullyQualifiedObjectType->getShortName(); + $className = $fullyQualifiedObjectType->getClassName(); + + foreach ($shortNamesToFullyQualifiedNames as $shortName => $fullyQualifiedName) { + if ($fullyQualifiedObjectTypeShortName !== $shortName) { + $shortName = $this->cleanShortName($shortName); + } + + if ($fullyQualifiedObjectTypeShortName !== $shortName) { + continue; + } + + $fullyQualifiedName = ltrim($fullyQualifiedName, '\\'); + return $className !== $fullyQualifiedName; + } + + return false; + } + + private function cleanShortName(string $shortName): string + { + return str_starts_with($shortName, '\\') + ? ltrim((string) Strings::after($shortName, '\\', -1)) + : $shortName; + } } diff --git a/rules/CodingStyle/Contract/ClassNameImport/ClassNameImportSkipVoterInterface.php b/rules/CodingStyle/Contract/ClassNameImport/ClassNameImportSkipVoterInterface.php deleted file mode 100644 index 48b13e55605..00000000000 --- a/rules/CodingStyle/Contract/ClassNameImport/ClassNameImportSkipVoterInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -autodiscover(__DIR__ . '/../NodeNameResolver/NodeNameResolver'); $rectorConfig->autodiscover(__DIR__ . '/../NodeTypeResolver/NodeTypeResolver'); $rectorConfig->autodiscover(__DIR__ . '/../ChangesReporting/Output'); - $rectorConfig->autodiscover(__DIR__ . '/../../rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter'); $rectorConfig->singleton( SymfonyStyle::class,