diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 4b5bf98ee..31d64e600 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -13,6 +13,7 @@ $finder = PhpCsFixer\Finder::create(); $finder ->in(__DIR__.'/src') + ->in(__DIR__.'/utils') ->append([__FILE__]) ->exclude(['docs/', 'tests/app']) ->notPath('tests/app') diff --git a/composer.json b/composer.json index f6d3eb27a..d3567bca0 100644 --- a/composer.json +++ b/composer.json @@ -67,6 +67,7 @@ "fakerphp/faker": "^1.13", "jangregor/phpstan-prophecy": "^1.0", "nelmio/alice": "^3.8", + "nikic/php-parser": "^4.15", "phpspec/prophecy-phpunit": "^2.0", "phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^1.9", @@ -110,7 +111,9 @@ "psr-4": { "App\\": "tests/app/src/", "Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", - "Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests" + "Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", + "Utils\\Rector\\": "utils/rector/src", + "Utils\\Rector\\Tests\\": "utils/rector/tests" } }, "config": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 56b7c2228..62dbe0468 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,6 +2,7 @@ parameters: level: 5 paths: - src/ + - utils/ tmpDir: .cache/ reportUnmatchedIgnoredErrors: false excludePaths: diff --git a/phpunit.rector.xml b/phpunit.rector.xml new file mode 100644 index 000000000..f8d9d3a11 --- /dev/null +++ b/phpunit.rector.xml @@ -0,0 +1,29 @@ + + + + + utils/rector/tests + + + + + + utils/rector/src + + + diff --git a/utils/rector/src/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector.php b/utils/rector/src/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector.php new file mode 100644 index 000000000..c41ecfa91 --- /dev/null +++ b/utils/rector/src/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector.php @@ -0,0 +1,170 @@ +classAnalyzer->hasImplements($node, FilterInterface::class)) { + return null; + } + + $buildFormStmtIndex = null; + $hasGetFormDefaultDataMethod = false; + foreach ($node->stmts as $k => $stmt) { + if (!$stmt instanceof Node\Stmt\ClassMethod) { + continue; + } + + if ('buildForm' === $stmt->name->name) { + $buildFormStmtIndex = $k; + } + + if ('getFormDefaultData' === $stmt->name->name) { + $hasGetFormDefaultDataMethod = true; + } + } + + if ($hasGetFormDefaultDataMethod || null === $buildFormStmtIndex) { + return null; + } + + $stmtBefore = array_slice($node->stmts, 0, $buildFormStmtIndex, false); + $stmtAfter = array_slice($node->stmts, $buildFormStmtIndex + 1); + + // lines to satisfay phpstan parser + if (!$node->stmts[$buildFormStmtIndex] instanceof Node\Stmt\ClassMethod) { + throw new \LogicException(); + } + + ['build_form_method' => $buildFormMethod, 'empty_to_replace' => $emptyToReplace] + = $this->filterBuildFormMethod($node->stmts[$buildFormStmtIndex]); + + $node->stmts = [ + ...$stmtBefore, + $buildFormMethod, + $this->makeGetFormDefaultData($node->stmts[$buildFormStmtIndex], $emptyToReplace), + ...$stmtAfter, + ]; + + return $node; + } + + private function makeGetFormDefaultData(Node\Stmt\ClassMethod $buildFormMethod, array $emptyToReplace): Node\Stmt\ClassMethod + { + $method = new Node\Stmt\ClassMethod('getFormDefaultData'); + $method->flags = Node\Stmt\Class_::MODIFIER_PUBLIC; + $method->returnType = new Node\Identifier('array'); + + $data = new Node\Expr\Array_([]); + + foreach ($emptyToReplace as $key => $value) { + $item = new Node\Expr\ArrayItem($value, new Node\Scalar\String_($key)); + $data->items[] = $item; + } + + $method->stmts[] = new Node\Stmt\Return_($data); + + return $method; + } + + /** + * @param Node\Stmt\ClassMethod $buildFormMethod + * @return array{"build_form_method": Node\Stmt\ClassMethod, "empty_to_replace": array} + */ + private function filterBuildFormMethod(Node\Stmt\ClassMethod $buildFormMethod): array + { + $builderName = $buildFormMethod->params[0]->var->name; + + $newStmts = []; + $emptyDataToReplace = []; + + foreach ($buildFormMethod->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\Expression + // it must be a method call + && $stmt->expr instanceof Node\Expr\MethodCall + // the method call must be "add" + && $stmt->expr->name instanceof Node\Identifier + && $stmt->expr->name->name === 'add' + // and the method call must apply on the builder (compare with builderName) + && $stmt->expr->var instanceof Node\Expr\Variable + && $stmt->expr->var->name === $builderName + // it must have a first argument, a string + // TODO what happens if a value, or a const ? + && ($stmt->expr->args[0] ?? null) instanceof Node\Arg + && $stmt->expr->args[0]->value instanceof Node\Scalar\String_ + // and a third argument, an array + && ($stmt->expr->args[2] ?? null) instanceof Node\Arg + && $stmt->expr->args[2]->value instanceof Node\Expr\Array_ + ) { + + // we parse on the 3rd argument, to find if there is an 'empty_data' key + $emptyDataIndex = null; + foreach ($stmt->expr->args[2]->value->items as $arrayItemIndex => $item) { + /* @phpstan-ignore-next-line */ + if ($item->key->value === 'data') { + $k = $stmt->expr->args[0]->value->value; + $emptyDataToReplace[$k] = $item->value; + $emptyDataIndex = $arrayItemIndex; + } + } + + if (null !== $emptyDataIndex) { + $stmt->expr->args[2]->value->items = array_values( + array_filter( + $stmt->expr->args[2]->value->items, + /* @phpstan-ignore-next-line */ + fn (Node\Expr\ArrayItem $item) => $item->key->value !== 'data' + ) + ); + } + + $newStmts[] = $stmt; + } else { + $newStmts[] = $stmt; + } + } + + $buildFormMethod->stmts = $newStmts; + + return ['build_form_method' => $buildFormMethod, "empty_to_replace" => $emptyDataToReplace]; + } +} diff --git a/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRectorTest.php b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRectorTest.php new file mode 100644 index 000000000..800d2876f --- /dev/null +++ b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRectorTest.php @@ -0,0 +1,40 @@ +doTestFile($file); + } + + public function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/config.php'; + } +} diff --git a/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-multiple-reuse-data-on-form-default-data.php.inc b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-multiple-reuse-data-on-form-default-data.php.inc new file mode 100644 index 000000000..5429d3c82 --- /dev/null +++ b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-multiple-reuse-data-on-form-default-data.php.inc @@ -0,0 +1,107 @@ +add('foo', PickRollingDateType::class, [ + 'label' => 'Test thing', + 'data' => new RollingDate(RollingDate::T_TODAY) + ]); + + $builder->add('baz', TextType::class, [ + 'label' => 'OrNiCar', + 'data' => 'Castor' + ]); + } + + public function getTitle() + { + // TODO: Implement getTitle() method. + } + + public function addRole(): ?string + { + // TODO: Implement addRole() method. + } + + public function alterQuery(QueryBuilder $qb, $data) + { + // TODO: Implement alterQuery() method. + } + + public function applyOn() + { + // TODO: Implement applyOn() method. + } +} +?> +----- +add('foo', PickRollingDateType::class, [ + 'label' => 'Test thing' + ]); + + $builder->add('baz', TextType::class, [ + 'label' => 'OrNiCar' + ]); + } + public function getFormDefaultData(): array + { + return ['foo' => new RollingDate(RollingDate::T_TODAY), 'baz' => 'Castor']; + } + + public function getTitle() + { + // TODO: Implement getTitle() method. + } + + public function addRole(): ?string + { + // TODO: Implement addRole() method. + } + + public function alterQuery(QueryBuilder $qb, $data) + { + // TODO: Implement alterQuery() method. + } + + public function applyOn() + { + // TODO: Implement applyOn() method. + } +} +?> diff --git a/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-no-data-on-builder.php.inc b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-no-data-on-builder.php.inc new file mode 100644 index 000000000..285c16b50 --- /dev/null +++ b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-no-data-on-builder.php.inc @@ -0,0 +1,105 @@ +add('foo', PickRollingDateType::class, [ + 'label' => 'Test thing', + ]); + + $builder->add('baz', TextType::class, [ + 'label' => 'OrNiCar', + ]); + } + + public function getTitle() + { + // TODO: Implement getTitle() method. + } + + public function addRole(): ?string + { + // TODO: Implement addRole() method. + } + + public function alterQuery(QueryBuilder $qb, $data) + { + // TODO: Implement alterQuery() method. + } + + public function applyOn() + { + // TODO: Implement applyOn() method. + } +} +?> +----- +add('foo', PickRollingDateType::class, [ + 'label' => 'Test thing', + ]); + + $builder->add('baz', TextType::class, [ + 'label' => 'OrNiCar', + ]); + } + public function getFormDefaultData(): array + { + return []; + } + + public function getTitle() + { + // TODO: Implement getTitle() method. + } + + public function addRole(): ?string + { + // TODO: Implement addRole() method. + } + + public function alterQuery(QueryBuilder $qb, $data) + { + // TODO: Implement alterQuery() method. + } + + public function applyOn() + { + // TODO: Implement applyOn() method. + } +} +?> diff --git a/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-reuse-data-on-form-default-data.php.inc b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-reuse-data-on-form-default-data.php.inc new file mode 100644 index 000000000..b2e78e49c --- /dev/null +++ b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-reuse-data-on-form-default-data.php.inc @@ -0,0 +1,96 @@ +add('test', PickRollingDateType::class, [ + 'label' => 'Test thing', + 'data' => new RollingDate(RollingDate::T_TODAY) + ]); + } + + public function getTitle() + { + // TODO: Implement getTitle() method. + } + + public function addRole(): ?string + { + // TODO: Implement addRole() method. + } + + public function alterQuery(QueryBuilder $qb, $data) + { + // TODO: Implement alterQuery() method. + } + + public function applyOn() + { + // TODO: Implement applyOn() method. + } +} +?> +----- +add('test', PickRollingDateType::class, [ + 'label' => 'Test thing' + ]); + } + public function getFormDefaultData(): array + { + return ['test' => new RollingDate(RollingDate::T_TODAY)]; + } + + public function getTitle() + { + // TODO: Implement getTitle() method. + } + + public function addRole(): ?string + { + // TODO: Implement addRole() method. + } + + public function alterQuery(QueryBuilder $qb, $data) + { + // TODO: Implement alterQuery() method. + } + + public function applyOn() + { + // TODO: Implement applyOn() method. + } +} +?> diff --git a/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-with-no-method-get-form-default-data.php.inc b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-with-no-method-get-form-default-data.php.inc new file mode 100644 index 000000000..687bd9d0c --- /dev/null +++ b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/filter-with-no-method-get-form-default-data.php.inc @@ -0,0 +1,87 @@ + +----- + diff --git a/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/skip-filter-existing-get-form-default-data-method.php.inc b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/skip-filter-existing-get-form-default-data-method.php.inc new file mode 100644 index 000000000..2fefc908d --- /dev/null +++ b/utils/rector/tests/Rector/ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector/Fixture/skip-filter-existing-get-form-default-data-method.php.inc @@ -0,0 +1,46 @@ +rule(\Utils\Rector\Rector\ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector::class); +};