Skip to content

Commit

Permalink
feat: introduce rector rules
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil committed Jan 7, 2024
1 parent 9d2ae0e commit 2f25ba3
Show file tree
Hide file tree
Showing 69 changed files with 3,065 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
/phpunit-dama-doctrine.xml.dist export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore
/utils/rector/tests export-ignore
/utils/rector/composer.json export-ignore
/utils/rector/phpunit.xml.dist export-ignore
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,30 @@ jobs:
- name: Run Psalm on factories generated with maker
run: bin/tools/psalm/vendor/vimeo/psalm/psalm

test-rector-rules:
name: Test rector rules
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: none

- name: Install dependencies
uses: ramsey/composer-install@v2
with:
composer-options: --prefer-dist
working-directory: "./utils/rector"

- name: Test
run: vendor/bin/phpunit
shell: bash
working-directory: "./utils/rector"

fixcs:
name: Run php-cs-fixer
needs: sync-with-template
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
/.env.local
/docker-compose.override.yaml
/tests/Fixtures/Migrations/
/utils/rector/vendor
/utils/rector/.phpunit.result.cache
/utils/rector/composer.lock
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
"doctrine/mongodb-odm": "2.5.0"
},
"autoload": {
"psr-4": {"Zenstruck\\Foundry\\": "src/"},
"psr-4": {
"Zenstruck\\Foundry\\": "src/",
"Zenstruck\\Foundry\\Utils\\Rector\\": "utils/rector/src/"
},
"files": [
"src/functions.php",
"src/Persistence/functions.php"
Expand Down
31 changes: 31 additions & 0 deletions utils/rector/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "zenstruck/foundry-rector",
"autoload": {
"psr-4": {
"Zenstruck\\Foundry\\Utils\\Rector\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Zenstruck\\Foundry\\Utils\\Rector\\Tests\\": "tests/"
}
},
"authors": [
{
"name": "Nicolas PHILIPPE",
"email": "[email protected]"
}
],
"require-dev": {
"rector/rector": "^0.18.13",
"phpunit/phpunit": "^10.5",
"phpstan/phpstan-doctrine": "^1.3",
"zenstruck/foundry": "dev-1.x-bc",
"symfony/var-dumper": "^7.0",
"doctrine/orm": "^2.17",
"symfony/framework-bundle": "^7.0"
},
"require": {
"symfony/cache": "^7.0"
}
}
71 changes: 71 additions & 0 deletions utils/rector/config/foundry-set.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\FuncCall\RenameFunctionRector;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
use Rector\Renaming\ValueObject\MethodCallRename;
use Zenstruck\Foundry\Persistence\Proxy;
use Zenstruck\Foundry\Proxy as LegacyProxy;
use Zenstruck\Foundry\Utils\Rector\AddProxyToFactoryCollectionTypeInPhpDoc;
use Zenstruck\Foundry\Utils\Rector\ChangeDisableEnablePersist;
use Zenstruck\Foundry\Utils\Rector\ChangeFactoryBaseClass;
use Zenstruck\Foundry\Utils\Rector\ChangeFactoryMethodCalls;
use Zenstruck\Foundry\Utils\Rector\ChangeFunctionsCalls;
use Zenstruck\Foundry\Utils\Rector\ChangeInstantiatorMethodCalls;
use Zenstruck\Foundry\Utils\Rector\ChangeLegacyClassImports;
use Zenstruck\Foundry\Utils\Rector\PersistenceResolver;
use Zenstruck\Foundry\Utils\Rector\RemoveProxyRealObjectMethodCallsForNotProxifiedObjects;
use Zenstruck\Foundry\Utils\Rector\RuleRequirementsChecker;

return static function (RectorConfig $rectorConfig): void {
RuleRequirementsChecker::checkRequirements();

/**
* This must be overridden in user's `rector.php` to provide a path to the object manager
* @see https://github.com/phpstan/phpstan-doctrine#configuration
*/
$rectorConfig->singleton(PersistenceResolver::class);

$renameMethodConfigurationFactory = static function (string $proxyClass): array {
$proxyMethodsReplacement = [
'object' => '_real',
'save' => '_save',
'remove' => '_delete',
'refresh' => '_refresh',
'forceSet' => '_set',
'get' => '_get',
'repository' => '_repository',
'enableAutoRefresh' => '_enableAutoRefresh',
'disableAutoRefresh' => '_disableAutoRefresh',
'withoutAutoRefresh' => '_withoutAutoRefresh',
];

$methodCallRenames = [];
foreach ($proxyMethodsReplacement as $oldMethod => $newMethod) {
$methodCallRenames[] = new MethodCallRename($proxyClass, $oldMethod, $newMethod);
}

return $methodCallRenames;
};

$rectorConfig->ruleWithConfiguration(
RenameMethodRector::class,
[
...$renameMethodConfigurationFactory(Proxy::class),
...$renameMethodConfigurationFactory(LegacyProxy::class),
]
);

$rectorConfig->rules([
ChangeFactoryBaseClass::class,
ChangeLegacyClassImports::class,
RemoveProxyRealObjectMethodCallsForNotProxifiedObjects::class,
ChangeInstantiatorMethodCalls::class,
ChangeDisableEnablePersist::class,
AddProxyToFactoryCollectionTypeInPhpDoc::class,
ChangeFactoryMethodCalls::class,
ChangeFunctionsCalls::class,
]);
};
14 changes: 14 additions & 0 deletions utils/rector/phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/9.5/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Project Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
137 changes: 137 additions & 0 deletions utils/rector/src/AddProxyToFactoryCollectionTypeInPhpDoc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\Utils\Rector;

use PhpParser\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
use Rector\Core\Rector\AbstractRector;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Rector\StaticTypeMapper\ValueObject\Type\ShortenedObjectType;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Zenstruck\Foundry\FactoryCollection;
use Zenstruck\Foundry\Persistence\Proxy;

final class AddProxyToFactoryCollectionTypeInPhpDoc extends AbstractRector
{
private PersistenceResolver $persistenceResolver;

public function __construct(
private PhpDocInfoFactory $phpDocInfoFactory,
private StaticTypeMapper $staticTypeMapper,
private DocBlockUpdater $docBlockUpdater,
PersistenceResolver|null $persistenceResolver,
) {
$this->persistenceResolver = $persistenceResolver ?? new PersistenceResolver();
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Add "Proxy" generic type to FactoryCollection in docblock if missing. This will only affect persistent objects using proxy.',
[
new CodeSample(
<<<'CODE_SAMPLE'
/**
* @param FactoryCollection<SomeObject> $factoryCollection
*/
public function foo(FactoryCollection $factoryCollection);
CODE_SAMPLE,
<<<'CODE_SAMPLE'
/**
* @param FactoryCollection<\Zenstruck\Foundry\Persistence\Proxy<SomeObject>> $factoryCollection
*/
public function foo(FactoryCollection $factoryCollection);
CODE_SAMPLE
),
]
);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Node\Stmt\ClassMethod::class];
}

public function refactor(Node $node): ?Node
{
return match ($node::class) {
Node\Stmt\ClassMethod::class => $this->changeParamFactoryCollection($node) ? $node : null,
default => null
};
}

private function changeParamFactoryCollection(Node\Stmt\ClassMethod $node): bool
{
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($node);

if (!$phpDocInfo) {
return false;
}

$hasChanged = false;

$phpDocNode = $phpDocInfo->getPhpDocNode();
foreach ($phpDocNode->children as $phpDocChildNode) {
// assert we have a param with format `@param FactoryCollection<Something>`
if (!$phpDocChildNode instanceof PhpDocTagNode
|| $phpDocChildNode->name !== '@param'
|| !$phpDocChildNode->value instanceof ParamTagValueNode
|| !($genericTypeNode = $phpDocChildNode->value->type) instanceof GenericTypeNode
|| !str_contains((string)$phpDocChildNode, 'FactoryCollection<')
|| count($genericTypeNode->genericTypes) !== 1
|| $genericTypeNode->genericTypes[0] instanceof GenericTypeNode
) {
continue;
}

// assert we really have a `FactoryCollection` from foundry
if ($this->getFullyQualifiedClassName($genericTypeNode->type, $node) !== FactoryCollection::class) {
continue;
}

// assert generic type will effectively come from PersistentProxyObjectFactory
$targetClassName = $this->getFullyQualifiedClassName($genericTypeNode->genericTypes[0], $node);
if (!$targetClassName || !$this->persistenceResolver->shouldUseProxyFactory($targetClassName)) {
continue;
}

$hasChanged = true;
$phpDocChildNode->value->type->genericTypes = [new GenericTypeNode(new IdentifierTypeNode('\\'.Proxy::class), $genericTypeNode->genericTypes)];
}

if ($hasChanged) {
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
}

return $hasChanged;
}

/**
* @return class-string
*/
private function getFullyQualifiedClassName(TypeNode $typeNode, Node $node): string|null
{
$type = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($typeNode, $node);

return match ($type::class) {
FullyQualifiedObjectType::class => $type->getClassName(),
ShortenedObjectType::class => $type->getFullyQualifiedName(),
default => null,
};
}
}
Loading

0 comments on commit 2f25ba3

Please sign in to comment.