Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector;

use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class FixClassCaseSensitivityVarDocblockRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Fixture;

use Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Source\AutoMailingService;

final class InlineVar
{
public function run()
{
/** @var AutomailingService $service */
$service = $this->getService();

return $service;
}
}

?>
-----
<?php

namespace Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Fixture;

use Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Source\AutoMailingService;

final class InlineVar
{
public function run()
{
/** @var AutoMailingService $service */
$service = $this->getService();

return $service;
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Fixture;

use Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Source\AutoMailingService;

final class PropertyVar
{
/**
* @var AUTOMAILINGSERVICE
*/
private $service;
}

?>
-----
<?php

namespace Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Fixture;

use Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Source\AutoMailingService;

final class PropertyVar
{
/**
* @var AutoMailingService
*/
private $service;
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Fixture;

use Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Source\AutoMailingService;

final class SkipCorrectAndScalar
{
/**
* @var AutoMailingService
*/
private $service;

/**
* @var string
*/
private $name;

public function run()
{
/** @var self $self */
$self = $this;

return $self;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Fixture;

final class SkipUnknownClass
{
/**
* @var SomeNonExistingService
*/
private $service;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\Source;

final class AutoMailingService
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

use Rector\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
->withRules([FixClassCaseSensitivityVarDocblockRector::class]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

declare(strict_types=1);

namespace Rector\CodeQuality\Rector\Property;

use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Property;
use PHPStan\Analyser\Scope;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Reflection\ReflectionProvider;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
use Rector\Naming\Naming\UseImportsResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Rector\Tests\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector\FixClassCaseSensitivityVarDocblockRectorTest
*/
final class FixClassCaseSensitivityVarDocblockRector extends AbstractRector
{
public function __construct(
private readonly PhpDocInfoFactory $phpDocInfoFactory,
private readonly DocBlockUpdater $docBlockUpdater,
private readonly ReflectionProvider $reflectionProvider,
private readonly UseImportsResolver $useImportsResolver,
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Fix a misspelled class name casing in a @var docblock to match the real class name',
[
new CodeSample(
<<<'CODE_SAMPLE'
/** @var AutomailingService */
$service = $this->getService();
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
/** @var AutoMailingService */
Comment thread
TomasVotruba marked this conversation as resolved.
$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;
}
}
2 changes: 2 additions & 0 deletions src/Config/Level/CodeQualityLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -108,6 +109,7 @@ final class CodeQualityLevel
* @var array<class-string<RectorInterface>>
*/
public const array RULES = [
FixClassCaseSensitivityVarDocblockRector::class,
CombinedAssignRector::class,
SimplifyEmptyArrayCheckRector::class,
ReplaceMultipleBooleanNotRector::class,
Expand Down
Loading