From 7590e610a632662609164493f0624dfe199470c7 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 18 Jun 2026 23:39:50 +0200 Subject: [PATCH] [code-quality] Add FixClassCaseSensitivityVarDocblockRector --- ...ssCaseSensitivityVarDocblockRectorTest.php | 28 +++ .../Fixture/inline_var.php.inc | 37 ++++ .../Fixture/property_var.php.inc | 31 +++ .../Fixture/skip_correct_and_scalar.php.inc | 26 +++ .../Fixture/skip_unknown_class.php.inc | 11 ++ .../Source/AutoMailingService.php | 9 + .../config/configured_rule.php | 9 + ...xClassCaseSensitivityVarDocblockRector.php | 180 ++++++++++++++++++ src/Config/Level/CodeQualityLevel.php | 2 + 9 files changed, 333 insertions(+) create mode 100644 rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/FixClassCaseSensitivityVarDocblockRectorTest.php create mode 100644 rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/inline_var.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/property_var.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/skip_correct_and_scalar.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/skip_unknown_class.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Source/AutoMailingService.php create mode 100644 rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/config/configured_rule.php create mode 100644 rules/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector.php diff --git a/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/FixClassCaseSensitivityVarDocblockRectorTest.php b/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/FixClassCaseSensitivityVarDocblockRectorTest.php new file mode 100644 index 00000000000..497ad68a34f --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/FixClassCaseSensitivityVarDocblockRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/inline_var.php.inc b/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/inline_var.php.inc new file mode 100644 index 00000000000..1fcca45a4ab --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/inline_var.php.inc @@ -0,0 +1,37 @@ +getService(); + + return $service; + } +} + +?> +----- +getService(); + + return $service; + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/property_var.php.inc b/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/property_var.php.inc new file mode 100644 index 00000000000..c93e4946b98 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/property_var.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/skip_correct_and_scalar.php.inc b/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/skip_correct_and_scalar.php.inc new file mode 100644 index 00000000000..9ede4cb1356 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector/Fixture/skip_correct_and_scalar.php.inc @@ -0,0 +1,26 @@ +withRules([FixClassCaseSensitivityVarDocblockRector::class]); diff --git a/rules/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector.php b/rules/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector.php new file mode 100644 index 00000000000..913b2c76b49 --- /dev/null +++ b/rules/CodeQuality/Rector/Property/FixClassCaseSensitivityVarDocblockRector.php @@ -0,0 +1,180 @@ +getService(); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +/** @var AutoMailingService */ +$service = $this->getService(); +CODE_SAMPLE + ), + ] + ); + } + + public function getNodeTypes(): array + { + return [Property::class, Expression::class]; + } + + /** + * @param Property|Expression $node + */ + public function refactor(Node $node): ?Node + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (! $phpDocInfo instanceof PhpDocInfo) { + return null; + } + + $hasChanged = false; + foreach ($phpDocInfo->getPhpDocNode()->getVarTagValues() as $varTagValueNode) { + $correctedTypeNode = $this->correctClassNameCasing($varTagValueNode->type, $node); + if ($correctedTypeNode instanceof IdentifierTypeNode) { + $varTagValueNode->type = $correctedTypeNode; + $hasChanged = true; + } + } + + if (! $hasChanged) { + return null; + } + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + private function correctClassNameCasing(TypeNode $typeNode, Node $node): ?IdentifierTypeNode + { + if (! $typeNode instanceof IdentifierTypeNode) { + return null; + } + + $writtenName = $typeNode->name; + $existingClassName = $this->resolveExistingClassName($writtenName, $node); + if ($existingClassName === null) { + return null; + } + + $realClassName = $this->reflectionProvider->getClass($existingClassName) + ->getName(); + + $hasLeadingSlash = str_starts_with($writtenName, '\\'); + + $writtenParts = explode('\\', ltrim($writtenName, '\\')); + $realParts = array_slice(explode('\\', $realClassName), -count($writtenParts)); + + // not a pure casing difference, e.g. an alias or partial name + if (strtolower(implode('\\', $writtenParts)) !== strtolower(implode('\\', $realParts))) { + return null; + } + + $correctedName = ($hasLeadingSlash ? '\\' : '') . implode('\\', $realParts); + if ($correctedName === $writtenName) { + return null; + } + + return new IdentifierTypeNode($correctedName); + } + + /** + * Resolve the existing class name for a written, possibly miss-cased, identifier. + * Lookups in PHPStan reflection are case-insensitive, so the namespace must match, + * while the class name casing may differ. + */ + private function resolveExistingClassName(string $writtenName, Node $node): ?string + { + $bareName = ltrim($writtenName, '\\'); + + // already fully qualified + if (str_starts_with($writtenName, '\\')) { + return $this->reflectionProvider->hasClass($bareName) ? $bareName : null; + } + + $parts = explode('\\', $bareName); + $firstPart = $parts[0]; + + // resolve via use imports, matching the imported short name case-insensitively + foreach ($this->useImportsResolver->resolve() as $use) { + $prefix = $this->useImportsResolver->resolvePrefix($use); + + foreach ($use->uses as $useItem) { + if ($useItem->alias instanceof Identifier) { + continue; + } + + if (strtolower($useItem->name->getLast()) !== strtolower($firstPart)) { + continue; + } + + $candidate = $prefix . $useItem->name->toString(); + if (count($parts) > 1) { + $candidate .= '\\' . implode('\\', array_slice($parts, 1)); + } + + if ($this->reflectionProvider->hasClass($candidate)) { + return $candidate; + } + } + } + + // same namespace + $scope = $node->getAttribute(AttributeKey::SCOPE); + if ($scope instanceof Scope) { + $namespace = $scope->getNamespace(); + if ($namespace !== null && $this->reflectionProvider->hasClass($namespace . '\\' . $bareName)) { + return $namespace . '\\' . $bareName; + } + } + + // global namespace + if ($this->reflectionProvider->hasClass($bareName)) { + return $bareName; + } + + return null; + } +} diff --git a/src/Config/Level/CodeQualityLevel.php b/src/Config/Level/CodeQualityLevel.php index a813ecd9e8a..372a0858cad 100644 --- a/src/Config/Level/CodeQualityLevel.php +++ b/src/Config/Level/CodeQualityLevel.php @@ -71,6 +71,7 @@ use Rector\CodeQuality\Rector\New_\NewStaticToNewSelfRector; use Rector\CodeQuality\Rector\NotEqual\CommonNotEqualRector; use Rector\CodeQuality\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector; +use Rector\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector; use Rector\CodeQuality\Rector\StmtsAwareInterface\MoveInnerFunctionToTopLevelRector; use Rector\CodeQuality\Rector\Switch_\SingularSwitchToIfRector; use Rector\CodeQuality\Rector\Switch_\SwitchTrueToIfRector; @@ -108,6 +109,7 @@ final class CodeQualityLevel * @var array> */ public const array RULES = [ + FixClassCaseSensitivityVarDocblockRector::class, CombinedAssignRector::class, SimplifyEmptyArrayCheckRector::class, ReplaceMultipleBooleanNotRector::class,