-
Notifications
You must be signed in to change notification settings - Fork 88
Add a @use annotation to HasFactory trait usages #374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
c3ccbba
b3d6752
36d8028
83f512d
8754876
883bfde
5e7dc1b
59ca54a
9c89d28
b21be64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace RectorLaravel\Rector\Class_; | ||
|
|
||
| use PhpParser\Node; | ||
| use PhpParser\Node\Stmt\Class_; | ||
| use PhpParser\Node\Stmt\TraitUse; | ||
| use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; | ||
| use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; | ||
| use PHPStan\Reflection\ReflectionProvider; | ||
| use PHPStan\Type\ObjectType; | ||
| use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; | ||
| use Rector\Comments\NodeDocBlock\DocBlockUpdater; | ||
| use RectorLaravel\AbstractRector; | ||
| use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; | ||
| use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | ||
|
|
||
| /** | ||
| * @see \RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\AddUseAnnotationToHasFactoryTraitRectorTest | ||
| */ | ||
| final class AddUseAnnotationToHasFactoryTraitRector extends AbstractRector | ||
| { | ||
| private const string USE_TAG_NAME = '@use'; | ||
|
|
||
| private const string HAS_FACTORY_TRAIT = 'Illuminate\Database\Eloquent\Factories\HasFactory'; | ||
|
|
||
| public function __construct( | ||
| private readonly DocBlockUpdater $docBlockUpdater, | ||
| private readonly PhpDocInfoFactory $phpDocInfoFactory, | ||
| private readonly ReflectionProvider $reflectionProvider, | ||
| ) {} | ||
|
|
||
| public function getRuleDefinition(): RuleDefinition | ||
| { | ||
| return new RuleDefinition( | ||
| 'Adds @use annotation to HasFactory trait usage to provide better IDE support.', | ||
| [new CodeSample( | ||
| <<<'CODE_SAMPLE' | ||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class User extends Model | ||
| { | ||
| use HasFactory; | ||
| } | ||
| CODE_SAMPLE, | ||
| <<<'CODE_SAMPLE' | ||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class User extends Model | ||
| { | ||
| /** @use HasFactory<\Database\Factories\UserFactory> */ | ||
| use HasFactory; | ||
| } | ||
| CODE_SAMPLE | ||
| )] | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @return array<class-string<Node>> | ||
| */ | ||
| public function getNodeTypes(): array | ||
| { | ||
| return [Class_::class]; | ||
| } | ||
|
|
||
| /** | ||
| * @param Class_ $node | ||
| */ | ||
| public function refactor(Node $node): ?Node | ||
| { | ||
| if (! $this->isObjectType($node, new ObjectType('Illuminate\Database\Eloquent\Model'))) { | ||
| return null; | ||
| } | ||
|
|
||
| $hasChanged = false; | ||
|
|
||
| foreach ($node->stmts as $stmt) { | ||
| if (! $stmt instanceof TraitUse) { | ||
| continue; | ||
| } | ||
|
|
||
| if (! $this->hasHasFactoryTrait($stmt)) { | ||
| continue; | ||
| } | ||
|
|
||
| if ($this->addUsePhpDocTag($stmt, $node)) { | ||
| $hasChanged = true; | ||
| } | ||
| } | ||
|
|
||
| if ($hasChanged) { | ||
| return $node; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| private function hasHasFactoryTrait(TraitUse $traitUse): bool | ||
| { | ||
| foreach ($traitUse->traits as $trait) { | ||
| $traitName = $this->getName($trait); | ||
| if ($traitName === self::HAS_FACTORY_TRAIT || $traitName === 'HasFactory') { | ||
imliam marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private function addUsePhpDocTag(TraitUse $traitUse, Class_ $class): bool | ||
| { | ||
| $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($traitUse); | ||
|
|
||
| if ($phpDocInfo->hasByName(self::USE_TAG_NAME)) { | ||
| return false; | ||
| } | ||
|
|
||
| $factoryClassName = $this->resolveFactoryClassName($class); | ||
| if ($factoryClassName === null) { | ||
| return false; | ||
| } | ||
|
|
||
| $useAnnotationValue = 'HasFactory<' . $factoryClassName . '>'; | ||
|
|
||
| $phpDocTagNode = new PhpDocTagNode( | ||
| self::USE_TAG_NAME, | ||
| new GenericTagValueNode($useAnnotationValue) | ||
| ); | ||
imliam marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| $phpDocInfo->addPhpDocTagNode($phpDocTagNode); | ||
|
|
||
| $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($traitUse); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private function resolveFactoryClassName(Class_ $class): ?string | ||
| { | ||
| $className = $this->getName($class); | ||
| if ($className === null) { | ||
| return null; | ||
| } | ||
|
|
||
| $modelName = $this->nodeNameResolver->getShortName($className); | ||
|
|
||
| $factoryName = $modelName . 'Factory'; | ||
|
|
||
| $currentNamespace = $class->namespacedName?->toString() ?? $className; | ||
|
|
||
| $factoryClassNames = $this->getPotentialFactoryClassNames($currentNamespace, $factoryName); | ||
|
|
||
| foreach ($factoryClassNames as $factoryClassName) { | ||
| if ($this->reflectionProvider->hasClass($factoryClassName)) { | ||
| return $factoryClassName; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
|
Comment on lines
+187
to
+201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can simplify this a lot, and even remove the subsequent There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kind of see what you mean but feel like this code is still needed to handle some edge cases after quickly playing with it. Happy to give it another shot if it's a deal breaker? |
||
| } | ||
|
|
||
| /** | ||
| * @return string[] | ||
| */ | ||
| private function getPotentialFactoryClassNames(string $modelNamespace, string $factoryName): array | ||
| { | ||
| $factoryClassNames = []; | ||
|
|
||
| if (str_contains($modelNamespace, '\\Models\\')) { | ||
| $afterModels = substr($modelNamespace, strpos($modelNamespace, '\\Models\\') + 8); | ||
|
|
||
| if (str_contains($afterModels, '\\')) { | ||
| $namespaceParts = explode('\\', $afterModels); | ||
| array_pop($namespaceParts); | ||
| $deepNamespace = implode('\\', $namespaceParts); | ||
|
|
||
| $factoryClassNames[] = '\\Database\\Factories\\' . $deepNamespace . '\\' . $factoryName; | ||
| } | ||
| } elseif (str_contains($modelNamespace, 'App\\')) { | ||
| $afterApp = substr($modelNamespace, strpos($modelNamespace, 'App\\') + 4); | ||
|
|
||
| if (str_contains($afterApp, '\\')) { | ||
| $namespaceParts = explode('\\', $afterApp); | ||
| array_pop($namespaceParts); | ||
| $deepNamespace = implode('\\', $namespaceParts); | ||
|
|
||
| $factoryClassNames[] = '\\Database\\Factories\\' . $deepNamespace . '\\' . $factoryName; | ||
| } | ||
| } | ||
| $factoryClassNames[] = '\\Database\\Factories\\' . $factoryName; | ||
|
|
||
| return array_unique($factoryClassNames); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector; | ||
|
|
||
| use Iterator; | ||
| use PHPUnit\Framework\Attributes\DataProvider; | ||
| use Rector\Testing\PHPUnit\AbstractRectorTestCase; | ||
|
|
||
| final class AddUseAnnotationToHasFactoryTraitRectorTest extends AbstractRectorTestCase | ||
| { | ||
| protected function setUp(): void | ||
| { | ||
| parent::setUp(); | ||
|
|
||
| require_once __DIR__ . '/Factories/ProductFactory.php'; | ||
| require_once __DIR__ . '/Factories/UserFactory.php'; | ||
| require_once __DIR__ . '/Factories/Tenant/UserFactory.php'; | ||
imliam marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| public static function provideData(): Iterator | ||
| { | ||
| return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); | ||
| } | ||
|
|
||
| /** | ||
| * @test | ||
| */ | ||
| #[DataProvider('provideData')] | ||
| public function test(string $filePath): void | ||
| { | ||
| $this->doTestFile($filePath); | ||
| } | ||
|
|
||
| public function provideConfigFilePath(): string | ||
| { | ||
| return __DIR__ . '/config/configured_rule.php'; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| namespace Database\Factories; | ||
|
|
||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||
|
|
||
| class ProductFactory extends Factory | ||
| { | ||
| public function definition() | ||
| { | ||
| return []; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| namespace Database\Factories\Tenant; | ||
imliam marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||
|
|
||
| class UserFactory extends Factory | ||
| { | ||
| public function definition() | ||
| { | ||
| return []; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| namespace Database\Factories; | ||
|
|
||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||
|
|
||
| class UserFactory extends Factory | ||
| { | ||
| public function definition() | ||
| { | ||
| return []; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?php | ||
|
|
||
| namespace App\Models\Tenant; | ||
imliam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class User extends Model | ||
| { | ||
| use HasFactory; | ||
| } | ||
|
|
||
| ?> | ||
| ----- | ||
| <?php | ||
|
|
||
| namespace App\Models\Tenant; | ||
|
|
||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class User extends Model | ||
| { | ||
| /** | ||
| * @use HasFactory<\Database\Factories\Tenant\UserFactory> | ||
| */ | ||
| use HasFactory; | ||
| } | ||
|
|
||
| ?> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?php | ||
|
|
||
| namespace RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\Fixture; | ||
|
|
||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class User extends Model | ||
| { | ||
| use HasFactory; | ||
| } | ||
|
|
||
| ?> | ||
| ----- | ||
| <?php | ||
|
|
||
| namespace RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\Fixture; | ||
|
|
||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class User extends Model | ||
| { | ||
| /** | ||
| * @use HasFactory<\Database\Factories\UserFactory> | ||
| */ | ||
| use HasFactory; | ||
| } | ||
|
|
||
| ?> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?php | ||
|
|
||
| namespace App\Models; | ||
|
|
||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class Product extends Model | ||
| { | ||
| use HasFactory; | ||
| } | ||
|
|
||
| ?> | ||
| ----- | ||
| <?php | ||
|
|
||
| namespace App\Models; | ||
|
|
||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class Product extends Model | ||
| { | ||
| /** | ||
| * @use HasFactory<\Database\Factories\ProductFactory> | ||
| */ | ||
| use HasFactory; | ||
| } | ||
|
|
||
| ?> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php | ||
|
|
||
| namespace RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\Fixture; | ||
|
|
||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
|
|
||
| class User extends Model | ||
| { | ||
| /** @use HasFactory<\Database\Factories\UserFactory> */ | ||
| use HasFactory; | ||
| } | ||
|
|
||
| ?> |
Uh oh!
There was an error while loading. Please reload this page.