From 35e63e67be013bf64c557adb5d819b734e32ef7d Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Thu, 23 May 2024 15:44:44 +0200 Subject: [PATCH] Implement coding standards and static analysis --- .github/workflows/ci.yml | 38 +++++++++++++++++-- Makefile | 27 +++++++++++++ README.md | 1 + composer.json | 9 ++++- ecs.php | 33 ++++++++++++++++ phpstan.neon | 10 +++++ src/CustomTypes/CustomTypeNamer.php | 10 +++-- .../RegisterDoctrineTypesCompilerPass.php | 31 +++++++-------- src/HeadsnetDoctrineToolsBundle.php | 19 ++++++---- tests/CustomTypes/CustomTypeNamerTest.php | 1 - .../CustomTypes/Fixtures/NotADoctrineType.php | 4 +- .../RegisterDoctrineTypesCompilerPassTest.php | 24 ++++++++---- tests/HeadsnetDoctrineToolsBundleTest.php | 3 ++ 13 files changed, 168 insertions(+), 42 deletions(-) create mode 100644 Makefile create mode 100644 ecs.php create mode 100644 phpstan.neon diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27c575e..0d5b277 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ -name: Headsnet Doctrine Tools Test +name: CI Pipeline on: push: ~ @@ -11,7 +11,7 @@ jobs: name: PHP ${{ matrix.php }} and Symfony ${{ matrix.symfony }} strategy: matrix: - operating-system: [ 'ubuntu-22.04', 'windows-2022' ] + operating-system: [ 'ubuntu-22.04' ] php: [ '8.1', '8.2', '8.3' ] symfony: ['6.4.*', '7.0.*'] exclude: @@ -26,6 +26,7 @@ jobs: with: php-version: ${{ matrix.php }} tools: flex + coverage: pcov - name: Download dependencies env: @@ -33,4 +34,35 @@ jobs: uses: ramsey/composer-install@v3 - name: Run test suite on PHP ${{ matrix.php }} and Symfony ${{ matrix.symfony }} - run: ./vendor/bin/phpunit + run: ./vendor/bin/phpunit --coverage-clover clover.xml + + - name: Make code coverage badge + uses: timkrase/phpunit-coverage-badge@v1.2.1 + with: + coverage_badge_path: output/coverage.svg + push_badge: false + + - name: Git push to image-data branch + uses: peaceiris/actions-gh-pages@v4 + with: + publish_dir: ./output + publish_branch: image-data + github_token: ${{ secrets.GITHUB_TOKEN }} + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + + ecs: + name: Easy Coding Standard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ramsey/composer-install@v3 + - run: vendor/bin/ecs + + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ramsey/composer-install@v3 + - run: vendor/bin/phpstan diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ec893ca --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: help +.DEFAULT_GOAL := help + +PHP = php +PHPUNIT = ${PHP} vendor/bin/phpunit + +cs: ## Run ECS Coding Standards analysis + @${PHP} vendor/bin/ecs check + +cs-fix: ## Fix ECS Coding Standards + @${PHP} vendor/bin/ecs check --fix + +static: ## Run PHPStan static analysis + @${PHP} vendor/bin/phpstan analyse + +test: ## Run PHPUnit tests + @${PHPUNIT} + +test-coverage: ## Run PHPUnit tests with code coverage + @${PHPUNIT} --coverage-html=var/coverage + +######################################################################################################################## +# https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +help: + @echo "\nMakefile is used to run various utilities related to this project\n" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' +######################################################################################################################## diff --git a/README.md b/README.md index ad4a002..aea0e41 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Doctrine Tools ==== ![Build Status](https://github.com/headsnet/doctrine-tools-bundle/actions/workflows/ci.yml/badge.svg) +![Coverage](https://raw.githubusercontent.com/headsnet/doctrine-tools-bundle/image-data/coverage.svg) [![Latest Stable Version](https://poser.pugx.org/headsnet/doctrine-tools-bundle/v)](//packagist.org/packages/headsnet/doctrine-tools-bundle) [![Total Downloads](https://poser.pugx.org/headsnet/doctrine-tools-bundle/downloads)](//packagist.org/packages/headsnet/doctrine-tools-bundle) [![License](https://poser.pugx.org/headsnet/doctrine-tools-bundle/license)](//packagist.org/packages/headsnet/doctrine-tools-bundle) diff --git a/composer.json b/composer.json index f6ad525..8ac33a5 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,9 @@ }, "require-dev": { "phpunit/phpunit": "^10.0", - "nyholm/symfony-bundle-test": "^3.0" + "nyholm/symfony-bundle-test": "^3.0", + "phpstan/phpstan": "^1.11", + "symplify/easy-coding-standard": "^12.2" }, "autoload": { "psr-4": { @@ -30,5 +32,10 @@ "psr-4": { "Headsnet\\DoctrineToolsBundle\\Tests\\": "tests/" } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..ac76e58 --- /dev/null +++ b/ecs.php @@ -0,0 +1,33 @@ +paths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]); + + $ecsConfig->skip([ + BlankLineAfterOpeningTagFixer::class, + NotOperatorWithSuccessorSpaceFixer::class, + ]); + + $ecsConfig->rules([ + NoUnusedImportsFixer::class, + ]); + + $ecsConfig->sets([ + SetList::SPACES, + SetList::ARRAY, + SetList::DOCBLOCK, + SetList::NAMESPACES, + SetList::COMMENTS, + SetList::PSR_12, + ]); +}; diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..b915243 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + + level: 9 + + paths: + - src/ + - tests/ + + ignoreErrors: + - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::children\(\).#' diff --git a/src/CustomTypes/CustomTypeNamer.php b/src/CustomTypes/CustomTypeNamer.php index 24c7574..8baf843 100644 --- a/src/CustomTypes/CustomTypeNamer.php +++ b/src/CustomTypes/CustomTypeNamer.php @@ -3,6 +3,7 @@ namespace Headsnet\DoctrineToolsBundle\CustomTypes; +use Doctrine\DBAL\Types\Type; use ReflectionClass; /** @@ -12,20 +13,21 @@ */ final class CustomTypeNamer { + /** + * @param ReflectionClass $reflection + */ public static function getTypeName(ReflectionClass $reflection): string { $attribute = $reflection->getAttributes(CustomType::class)[0]; - $attributeArgs = $attribute->getArguments(); - if (isset($attributeArgs['name'])) - { + if (isset($attributeArgs['name'])) { return $attributeArgs['name']; } $typeName = str_replace('Type', '', $reflection->getShortName()); - $typeName = preg_replace("/(?<=.)([A-Z])/", "_$1", $typeName); + $typeName = (string) preg_replace("/(?<=.)([A-Z])/", "_$1", $typeName); return strtolower($typeName); } diff --git a/src/CustomTypes/RegisterDoctrineTypesCompilerPass.php b/src/CustomTypes/RegisterDoctrineTypesCompilerPass.php index 6113ec6..e564b99 100644 --- a/src/CustomTypes/RegisterDoctrineTypesCompilerPass.php +++ b/src/CustomTypes/RegisterDoctrineTypesCompilerPass.php @@ -21,60 +21,58 @@ final class RegisterDoctrineTypesCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { - if (!$container->hasParameter(self::TYPE_DEFINITION_PARAMETER)) - { + if (!$container->hasParameter(self::TYPE_DEFINITION_PARAMETER)) { return; } /** @var array $typeDefinitions */ $typeDefinitions = $container->getParameter(self::TYPE_DEFINITION_PARAMETER); + /** @var array $scanDirs */ $scanDirs = $container->getParameter('headsnet_doctrine_tools.custom_types.scan_dirs'); $types = $this->findTypesInApplication($scanDirs); - foreach ($types as $type) - { + foreach ($types as $type) { $name = $type['name']; $class = $type['class']; // Do not add the type if it's been manually defined already - if (array_key_exists($name, $typeDefinitions)) - { + if (array_key_exists($name, $typeDefinitions)) { continue; } - $typeDefinitions[$name] = ['class' => $class]; + $typeDefinitions[$name] = [ + 'class' => $class, + ]; } $container->setParameter(self::TYPE_DEFINITION_PARAMETER, $typeDefinitions); } /** + * @param array $scanDirs + * * @return Generator */ - private function findTypesInApplication($scanDirs): iterable + private function findTypesInApplication(array $scanDirs): iterable { $classNames = ConstructFinder::locatedIn(...$scanDirs)->findClassNames(); - foreach ($classNames as $className) - { + foreach ($classNames as $className) { $reflection = new ReflectionClass($className); // If the class is not a Doctrine Type - if (!$reflection->isSubclassOf(Type::class)) - { + if (!$reflection->isSubclassOf(Type::class)) { continue; } // Skip any abstract parent types - if ($reflection->isAbstract()) - { + if ($reflection->isAbstract()) { continue; } // Only register types that have the #[CustomType] attribute - if ($reflection->getAttributes(CustomType::class)) - { + if ($reflection->getAttributes(CustomType::class)) { yield [ 'name' => CustomTypeNamer::getTypeName($reflection), 'class' => $className, @@ -82,5 +80,4 @@ private function findTypesInApplication($scanDirs): iterable } } } - } diff --git a/src/HeadsnetDoctrineToolsBundle.php b/src/HeadsnetDoctrineToolsBundle.php index 9f6de57..ba7187c 100644 --- a/src/HeadsnetDoctrineToolsBundle.php +++ b/src/HeadsnetDoctrineToolsBundle.php @@ -1,4 +1,5 @@ rootNode() ->children() - ->arrayNode('custom_types') - ->children() - ->arrayNode('scan_dirs') - ->scalarPrototype()->end() - ->end() - ->end() - ->end() + ->arrayNode('custom_types') + ->children() + ->arrayNode('scan_dirs') + ->scalarPrototype()->end() + ->end() + ->end() + ->end() ->end() ; } + /** + * @param array{custom_types: array{scan_dirs: array}} $config + */ public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { $container->parameters() ->set('headsnet_doctrine_tools.custom_types.scan_dirs', $config['custom_types']['scan_dirs']) ; - } public function build(ContainerBuilder $container): void diff --git a/tests/CustomTypes/CustomTypeNamerTest.php b/tests/CustomTypes/CustomTypeNamerTest.php index 21c455e..bb11d53 100644 --- a/tests/CustomTypes/CustomTypeNamerTest.php +++ b/tests/CustomTypes/CustomTypeNamerTest.php @@ -3,7 +3,6 @@ namespace Headsnet\DoctrineToolsBundle\Tests\CustomTypes; -use Headsnet\DoctrineToolsBundle\CustomTypes\CustomType; use Headsnet\DoctrineToolsBundle\CustomTypes\CustomTypeNamer; use Headsnet\DoctrineToolsBundle\Tests\CustomTypes\Fixtures\DummyCustomType; use Headsnet\DoctrineToolsBundle\Tests\CustomTypes\Fixtures\DummyCustomTypeWithName; diff --git a/tests/CustomTypes/Fixtures/NotADoctrineType.php b/tests/CustomTypes/Fixtures/NotADoctrineType.php index 8116793..02b8a18 100644 --- a/tests/CustomTypes/Fixtures/NotADoctrineType.php +++ b/tests/CustomTypes/Fixtures/NotADoctrineType.php @@ -4,12 +4,14 @@ namespace Headsnet\DoctrineToolsBundle\Tests\CustomTypes\Fixtures; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Types\Type; use Headsnet\DoctrineToolsBundle\CustomTypes\CustomType; #[CustomType] class NotADoctrineType { + /** + * @param array{length?: int, fixed?: bool} $column + */ public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { return ''; diff --git a/tests/CustomTypes/RegisterDoctrineTypesCompilerPassTest.php b/tests/CustomTypes/RegisterDoctrineTypesCompilerPassTest.php index c0593dd..9b34bce 100644 --- a/tests/CustomTypes/RegisterDoctrineTypesCompilerPassTest.php +++ b/tests/CustomTypes/RegisterDoctrineTypesCompilerPassTest.php @@ -22,7 +22,7 @@ public function can_find_and_register_types(): void $container = new ContainerBuilder(); $container->setParameter('doctrine.dbal.connection_factory.types', []); $container->setParameter('headsnet_doctrine_tools.custom_types.scan_dirs', [ - __DIR__ . '/Fixtures' + __DIR__ . '/Fixtures', ]); $sut = new RegisterDoctrineTypesCompilerPass(); @@ -30,8 +30,12 @@ public function can_find_and_register_types(): void $result = $container->getParameter('doctrine.dbal.connection_factory.types'); $expected = [ - 'dummy_custom' => ['class' => DummyCustomType::class], - 'my_custom_name' => ['class' => DummyCustomTypeWithName::class] + 'dummy_custom' => [ + 'class' => DummyCustomType::class, + ], + 'my_custom_name' => [ + 'class' => DummyCustomTypeWithName::class, + ], ]; $this->assertEquals($expected, $result); } @@ -41,10 +45,12 @@ public function ignores_types_that_are_manually_registered(): void { $container = new ContainerBuilder(); $container->setParameter('doctrine.dbal.connection_factory.types', [ - 'dummy_custom' => ['class' => DummyCustomType::class] + 'dummy_custom' => [ + 'class' => DummyCustomType::class, + ], ]); $container->setParameter('headsnet_doctrine_tools.custom_types.scan_dirs', [ - __DIR__ . '/Fixtures' + __DIR__ . '/Fixtures', ]); $sut = new RegisterDoctrineTypesCompilerPass(); @@ -52,8 +58,12 @@ public function ignores_types_that_are_manually_registered(): void $result = $container->getParameter('doctrine.dbal.connection_factory.types'); $expected = [ - 'dummy_custom' => ['class' => DummyCustomType::class], - 'my_custom_name' => ['class' => DummyCustomTypeWithName::class] + 'dummy_custom' => [ + 'class' => DummyCustomType::class, + ], + 'my_custom_name' => [ + 'class' => DummyCustomTypeWithName::class, + ], ]; $this->assertEquals($expected, $result); } diff --git a/tests/HeadsnetDoctrineToolsBundleTest.php b/tests/HeadsnetDoctrineToolsBundleTest.php index 75c08e0..1eddace 100644 --- a/tests/HeadsnetDoctrineToolsBundleTest.php +++ b/tests/HeadsnetDoctrineToolsBundleTest.php @@ -20,6 +20,9 @@ protected static function getKernelClass(): string return TestKernel::class; } + /** + * @param array{debug?: bool, environment?: string} $options + */ protected static function createKernel(array $options = []): KernelInterface { /** @var TestKernel $kernel $kernel */