diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dc3cd9c..af88d14 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -69,3 +69,6 @@ jobs: - name: Run phpstan run: bin/phpstan -V && bin/phpstan --error-format=github + + - name: Run phpmd + run: bin/phpmd --version && composer phpmd diff --git a/composer.json b/composer.json index 4f2524b..b097c41 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "symfony/console": "^7.1", "symfony/config": "^7.1", "symfony/yaml": "^7.1", - "symfony/dependency-injection": "^7.1" + "symfony/dependency-injection": "^7.1", + "symfony/messenger": "^7.1" }, "autoload": { "psr-4": { @@ -69,7 +70,7 @@ "phpstan analyse src/" ], "phpmd": [ - "bin/phpmd ./src text cleancode,codesize,controversial,design" + "bin/phpmd ./src/ text phpmd.xml" ], "benchmark": [ "bin/phpbench run tests/Benchmark/ --report=aggregate" @@ -77,6 +78,7 @@ "all": [ "@cscheck", "@analyze", + "@phpmd", "@test" ], "build-phar": [ diff --git a/composer.lock b/composer.lock index 08d3626..8719206 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "66b078c5c8d9a33a83c5ad72ce006f9b", + "content-hash": "b911dc6cd3703e4851a12882d818361c", "packages": [ { "name": "nikic/php-parser", @@ -64,6 +64,54 @@ }, "time": "2024-09-15T16:40:33+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -117,6 +165,130 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, { "name": "symfony/config", "version": "v7.1.1", @@ -498,6 +670,92 @@ ], "time": "2024-09-17T09:16:35+00:00" }, + { + "name": "symfony/messenger", + "version": "v7.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/messenger.git", + "reference": "e1c0ced845e3dac12ab428c5ed42dbe7a58ca576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/messenger/zipball/e1c0ced845e3dac12ab428c5ed42dbe7a58ca576", + "reference": "e1c0ced845e3dac12ab428c5ed42dbe7a58ca576", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/clock": "^6.4|^7.0" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/event-dispatcher-contracts": "<2.5", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v7.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-08T12:32:26+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.31.0", @@ -816,6 +1074,82 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.5.0", @@ -2738,56 +3072,6 @@ ], "time": "2024-09-19T10:54:28+00:00" }, - { - "name": "psr/log", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" - }, - "time": "2024-09-11T13:17:53+00:00" - }, { "name": "roave/security-advisories", "version": "dev-latest", diff --git a/config.yml b/config.yml index 3e0731b..dd874fd 100644 --- a/config.yml +++ b/config.yml @@ -2,7 +2,7 @@ cognitive: excludeFilePatterns: excludePatterns: scoreThreshold: 0.5 - showOnlyMethodsExceedingThreshold: false + showOnlyMethodsExceedingThreshold: true metrics: lineCount: threshold: 60 @@ -17,11 +17,11 @@ cognitive: scale: 5.0 enabled: true variableCount: - threshold: 2 + threshold: 4 scale: 5.0 enabled: true propertyCallCount: - threshold: 2 + threshold: 4 scale: 15.0 enabled: true ifCount: diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..5497422 --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,21 @@ + + + + Phauthentic PHPMD rule set + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index aa1d602..ef59533 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,32 @@ - + + + + + + tests - tests/Store/AbstractStoreTest.php + src + diff --git a/src/Application.php b/src/Application.php index 1dc8f9f..90e410a 100644 --- a/src/Application.php +++ b/src/Application.php @@ -6,11 +6,15 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\BaselineService; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\FileProcessed; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\SourceFilesFound; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Parser; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\ScoreCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner; -use Phauthentic\CognitiveCodeAnalysis\Command\Cognitive\CognitiveCollectorShellOutputPlugin; use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsCommand; use Phauthentic\CognitiveCodeAnalysis\Business\MetricsFacade; +use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\ProgressBarHandler; +use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\VerboseHandler; use Phauthentic\CognitiveCodeAnalysis\Command\Presentation\CognitiveMetricTextRenderer; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigLoader; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; @@ -22,9 +26,14 @@ use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Messenger\Handler\HandlersLocator; +use Symfony\Component\Messenger\MessageBus; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; /** * @@ -77,16 +86,17 @@ private function registerServices(): void $this->containerBuilder->register(NodeTraverserInterface::class, NodeTraverser::class) ->setPublic(true); - $this->containerBuilder->register(OutputInterface::class, ConsoleOutput::class) + $outputClass = getenv('APP_ENV') === 'test' ? NullOutput::class : ConsoleOutput::class; + $this->containerBuilder->register(OutputInterface::class, $outputClass) ->setPublic(true); $this->containerBuilder->register(InputInterface::class, ArgvInput::class) ->setPublic(true); - $this->containerBuilder->register(CognitiveCollectorShellOutputPlugin::class, CognitiveCollectorShellOutputPlugin::class) + $this->containerBuilder->register(Parser::class, Parser::class) ->setArguments([ - new Reference(InputInterface::class), - new Reference(OutputInterface::class) + new Reference(ParserFactory::class), + new Reference(NodeTraverserInterface::class), ]) ->setPublic(true); } @@ -94,6 +104,7 @@ private function registerServices(): void private function bootstrap(): void { $this->registerServices(); + $this->configureEventBus(); $this->bootstrapMetricsCollectors(); $this->configureConfigService(); $this->registerMetricsFacade(); @@ -105,17 +116,44 @@ private function bootstrapMetricsCollectors(): void { $this->containerBuilder->register(CognitiveMetricsCollector::class, CognitiveMetricsCollector::class) ->setArguments([ - new Reference(ParserFactory::class), - new Reference(NodeTraverserInterface::class), + new Reference(Parser::class), new Reference(DirectoryScanner::class), new Reference(ConfigService::class), - [ - $this->containerBuilder->get(CognitiveCollectorShellOutputPlugin::class), - ] + new Reference(MessageBusInterface::class) ]) ->setPublic(true); } + private function configureEventBus(): void + { + $progressbar = new ProgressBarHandler( + $this->get(OutputInterface::class) + ); + + $verbose = new VerboseHandler( + $this->get(InputInterface::class), + $this->get(OutputInterface::class) + ); + + // Set up event handlers locator + $handlersLocator = new HandlersLocator([ + SourceFilesFound::class => [ + $progressbar, + $verbose + ], + FileProcessed::class => [ + $progressbar, + $verbose + ], + ]); + + $messageBus = new MessageBus([ + new HandleMessageMiddleware($handlersLocator), + ]); + + $this->containerBuilder->set(MessageBusInterface::class, $messageBus); + } + private function configureConfigService(): void { $this->containerBuilder->register(ConfigService::class, ConfigService::class) @@ -165,8 +203,13 @@ public function run(): void ); } - public function get(string $id): mixed + public function get(string $identifier): mixed + { + return $this->containerBuilder->get($identifier); + } + + public function getContainer(): ContainerBuilder { - return $this->containerBuilder->get($id); + return $this->containerBuilder; } } diff --git a/src/Business/Cognitive/BaselineService.php b/src/Business/Cognitive/BaselineService.php index ee0d442..e2facc9 100644 --- a/src/Business/Cognitive/BaselineService.php +++ b/src/Business/Cognitive/BaselineService.php @@ -25,7 +25,7 @@ public function calculateDeltas(CognitiveMetricsCollection $metricsCollection, a continue; } - $previousMetrics = CognitiveMetrics::fromArray($methodData); + $previousMetrics = new CognitiveMetrics($methodData); $metrics->calculateDeltas($previousMetrics); } } diff --git a/src/Business/Cognitive/CognitiveMetrics.php b/src/Business/Cognitive/CognitiveMetrics.php index 6c30c43..e828515 100644 --- a/src/Business/Cognitive/CognitiveMetrics.php +++ b/src/Business/Cognitive/CognitiveMetrics.php @@ -8,7 +8,7 @@ use JsonSerializable; /** - * + * @SuppressWarnings(PHPMD) */ class CognitiveMetrics implements JsonSerializable { diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index f974839..c2e73f6 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -4,35 +4,27 @@ namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\FileProcessed; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\SourceFilesFound; use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; -use Phauthentic\CognitiveCodeAnalysis\PhpParser\CognitiveMetricsVisitor; -use PhpParser\Error; -use PhpParser\NodeTraverserInterface; -use PhpParser\Parser; -use PhpParser\ParserFactory; use SplFileInfo; +use Symfony\Component\Messenger\Exception\ExceptionInterface; +use Symfony\Component\Messenger\MessageBusInterface; /** * CognitiveMetricsCollector class that collects cognitive metrics from source files */ class CognitiveMetricsCollector { - protected Parser $parser; - - /** - * @param array $findMetricsPlugins - */ public function __construct( - protected readonly ParserFactory $parserFactory, - protected readonly NodeTraverserInterface $traverser, + protected readonly Parser $parser, protected readonly DirectoryScanner $directoryScanner, protected readonly ConfigService $configService, - protected readonly array $findMetricsPlugins = [] + protected readonly MessageBusInterface $messageBus, ) { - $this->parser = $parserFactory->createForHostVersion(); } /** @@ -41,13 +33,21 @@ public function __construct( * @param string $path * @param CognitiveConfig $config * @return CognitiveMetricsCollection - * @throws CognitiveAnalysisException + * @throws CognitiveAnalysisException|ExceptionInterface */ public function collect(string $path, CognitiveConfig $config): CognitiveMetricsCollection { $files = $this->findSourceFiles($path, $config->excludeFilePatterns); - return $this->findMetrics($files); + /** @var SplFileInfo[] $clonedFiles */ + $clonedFiles = []; + foreach ($files as $file) { + $clonedFiles[] = clone $file; + } + + $this->messageBus->dispatch(new SourceFilesFound($clonedFiles)); + + return $this->findMetrics($clonedFiles); } private function getCodeFromFile(SplFileInfo $file): string @@ -66,39 +66,25 @@ private function getCodeFromFile(SplFileInfo $file): string * * @param iterable $files * @return CognitiveMetricsCollection - * @throws CognitiveAnalysisException + * @throws CognitiveAnalysisException|ExceptionInterface */ private function findMetrics(iterable $files): CognitiveMetricsCollection { $metricsCollection = new CognitiveMetricsCollection(); - $visitor = new CognitiveMetricsVisitor(); - - foreach ($this->findMetricsPlugins as $plugin) { - $plugin->beforeIteration($files); - } foreach ($files as $file) { - foreach ($this->findMetricsPlugins as $plugin) { - $plugin->beforeFindMetrics($file); - } - - $code = $this->getCodeFromFile($file); - - $this->traverser->addVisitor($visitor); - $this->traverseAbstractSyntaxTree($code); - - $methodMetrics = $visitor->getMethodMetrics(); - $this->traverser->removeVisitor($visitor); - - $this->processMethodMetrics($methodMetrics, $metricsCollection); - - foreach ($this->findMetricsPlugins as $plugin) { - $plugin->afterFindMetrics($file); - } - } - - foreach ($this->findMetricsPlugins as $plugin) { - $plugin->afterIteration($metricsCollection); + $metrics = $this->parser->parse( + $this->getCodeFromFile($file) + ); + + $this->processMethodMetrics( + $metrics, + $metricsCollection + ); + + $this->messageBus->dispatch(new FileProcessed( + $file, + )); } return $metricsCollection; @@ -139,7 +125,7 @@ private function isExcluded(string $classAndMethod): bool $regexes = $this->configService->getConfig()->excludePatterns; foreach ($regexes as $regex) { - if (preg_match('/' . $regex . '/', $classAndMethod, $matches)) { + if (preg_match('/' . $regex . '/', $classAndMethod)) { return true; } } @@ -158,22 +144,4 @@ private function findSourceFiles(string $path, array $exclude = []): iterable { return $this->directoryScanner->scan([$path], ['^(?!.*\.php$).+'] + $exclude); // Exclude non-PHP files } - - /** - * @throws CognitiveAnalysisException - */ - private function traverseAbstractSyntaxTree(string $code): void - { - try { - $ast = $this->parser->parse($code); - } catch (Error $e) { - throw new CognitiveAnalysisException("Parse error: {$e->getMessage()}", 0, $e); - } - - if ($ast === null) { - throw new CognitiveAnalysisException("Could not parse the code."); - } - - $this->traverser->traverse($ast); - } } diff --git a/src/Business/Cognitive/Events/FileProcessed.php b/src/Business/Cognitive/Events/FileProcessed.php new file mode 100644 index 0000000..1769dad --- /dev/null +++ b/src/Business/Cognitive/Events/FileProcessed.php @@ -0,0 +1,16 @@ + $files + */ + public function __construct( + public readonly array $files + ) { + } +} diff --git a/src/Business/Cognitive/Parser.php b/src/Business/Cognitive/Parser.php new file mode 100644 index 0000000..8dd8da5 --- /dev/null +++ b/src/Business/Cognitive/Parser.php @@ -0,0 +1,62 @@ +parser = $parserFactory->createForHostVersion(); + $this->visitor = new CognitiveMetricsVisitor(); + $this->traverser->addVisitor($this->visitor); + } + + /** + * @return array> + * @throws CognitiveAnalysisException + */ + public function parse(string $code): array + { + $this->traverseAbstractSyntaxTree($code); + + $methodMetrics = $this->visitor->getMethodMetrics(); + $this->visitor->resetValues(); + + return $methodMetrics; + } + + /** + * @throws CognitiveAnalysisException + */ + private function traverseAbstractSyntaxTree(string $code): void + { + try { + $ast = $this->parser->parse($code); + } catch (Error $e) { + throw new CognitiveAnalysisException("Parse error: {$e->getMessage()}", 0, $e); + } + + if ($ast === null) { + throw new CognitiveAnalysisException("Could not parse the code."); + } + + $this->traverser->traverse($ast); + } +} diff --git a/src/Command/Cognitive/CognitiveCollectorShellOutputPlugin.php b/src/Command/Cognitive/CognitiveCollectorShellOutputPlugin.php deleted file mode 100644 index a516c3d..0000000 --- a/src/Command/Cognitive/CognitiveCollectorShellOutputPlugin.php +++ /dev/null @@ -1,76 +0,0 @@ -startTime = microtime(true); - } - - public function afterFindMetrics(SplFileInfo $fileInfo): void - { - if ( - $this->input->hasOption(CognitiveMetricsCommand::OPTION_DEBUG) - && $this->input->getOption(CognitiveMetricsCommand::OPTION_DEBUG) === false - ) { - return; - } - - $runtime = microtime(true) - $this->startTime; - - $this->output->writeln('Processed ' . $fileInfo->getRealPath()); - $this->output->writeln('Number: ' . $this->count . ' Memory: ' . $this->formatBytes(memory_get_usage(true)) . ' -- Runtime: ' . round($runtime, 4) . 's'); - - $this->count++; - } - - public function beforeIteration(iterable $files): void - { - } - - public function afterIteration(CognitiveMetricsCollection $metricsCollection): void - { - } - - /** - * Converts memory size to a human-readable format (bytes, KB, MB, GB, TB). - * - * @param int $size Memory size in bytes. - * @return string Human-readable memory size. - */ - private function formatBytes(int $size): string - { - $units = ['B', 'KB', 'MB', 'GB', 'TB']; - $i = 0; - - while ($size >= 1024 && $i < count($units) - 1) { - $size /= 1024; - $i++; - } - - return round($size, 2) . ' ' . $units[$i]; - } -} diff --git a/src/Command/CognitiveMetricsCommand.php b/src/Command/CognitiveMetricsCommand.php index 77a0518..3c34356 100644 --- a/src/Command/CognitiveMetricsCommand.php +++ b/src/Command/CognitiveMetricsCommand.php @@ -13,7 +13,6 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** diff --git a/src/Command/EventHandler/ProgressBarHandler.php b/src/Command/EventHandler/ProgressBarHandler.php new file mode 100644 index 0000000..79b1e33 --- /dev/null +++ b/src/Command/EventHandler/ProgressBarHandler.php @@ -0,0 +1,45 @@ +totalFiles = count($event->files); + $this->output->writeln('Found ' . $this->totalFiles . ' files. Starting analysis.'); + $this->progressBar = new ProgressBar($this->output, $this->totalFiles); + } + + if ($event instanceof FileProcessed) { + $this->progressBar->advance(1); + $this->processedFiles++; + } + + if ($this->processedFiles === $this->totalFiles) { + $this->progressBar->finish(); + $this->output->writeln(''); + $this->totalFiles = 0; + } + } +} diff --git a/src/Command/EventHandler/VerboseHandler.php b/src/Command/EventHandler/VerboseHandler.php new file mode 100644 index 0000000..8d6a073 --- /dev/null +++ b/src/Command/EventHandler/VerboseHandler.php @@ -0,0 +1,65 @@ +input->hasOption(CognitiveMetricsCommand::OPTION_DEBUG) + && $this->input->getOption(CognitiveMetricsCommand::OPTION_DEBUG) === false + ) { + return; + } + + if ($event instanceof SourceFilesFound) { + $this->startTime = microtime(true); + } + + if ($event instanceof FileProcessed) { + $runtime = (microtime(true) - $this->startTime); + + $this->output->writeln('Processed ' . $event->file->getRealPath()); + $this->output->writeln(' Memory: ' . $this->formatBytes(memory_get_usage(true)) . ' || Total Time: ' . round($runtime, 4) . 's'); + } + } + + /** + * Converts memory size to a human-readable format (bytes, KB, MB, GB, TB). + * + * @param int $size Memory size in bytes. + * @return string Human-readable memory size. + */ + private function formatBytes(int $size): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $index = 0; + + while ($size >= 1024 && $index < count($units) - 1) { + $size /= 1024; + $index++; + } + + return round($size, 2) . ' ' . $units[$index]; + } +} diff --git a/src/Command/Presentation/CognitiveMetricTextRenderer.php b/src/Command/Presentation/CognitiveMetricTextRenderer.php index ca169f5..1e9535a 100644 --- a/src/Command/Presentation/CognitiveMetricTextRenderer.php +++ b/src/Command/Presentation/CognitiveMetricTextRenderer.php @@ -21,6 +21,13 @@ public function __construct( ) { } + public function metricExceedsThreshold(CognitiveMetrics $metric, CognitiveConfig $config): bool + { + return + $config->showOnlyMethodsExceedingThreshold && + $metric->getScore() <= $config->scoreThreshold; + } + /** * @param CognitiveMetricsCollection $metricsCollection */ @@ -31,10 +38,7 @@ public function render(CognitiveMetricsCollection $metricsCollection, CognitiveC foreach ($groupedByClass as $className => $metrics) { $rows = []; foreach ($metrics as $metric) { - if ( - $config->showOnlyMethodsExceedingThreshold && - $metric->getScore() <= $config->scoreThreshold - ) { + if ($this->metricExceedsThreshold($metric, $config)) { continue; } @@ -60,6 +64,7 @@ private function renderTable(string $className, array $rows): void $table->setStyle('box'); $table->setHeaders($this->getTableHeaders()); $this->output->writeln("Class: $className"); + $table->setRows($rows); $table->render(); $this->output->writeln(""); @@ -68,7 +73,7 @@ private function renderTable(string $className, array $rows): void /** * @return string[] */ - protected function getTableHeaders(): array + private function getTableHeaders(): array { return [ "Method Name", @@ -88,7 +93,7 @@ protected function getTableHeaders(): array * @param CognitiveMetrics $metrics * @return array */ - protected function prepareTableRow(CognitiveMetrics $metrics): array + private function prepareTableRow(CognitiveMetrics $metrics): array { $row = [ 'methodName' => $metrics->getMethod(), diff --git a/src/Config/ConfigService.php b/src/Config/ConfigService.php index c9f409d..1d4472f 100644 --- a/src/Config/ConfigService.php +++ b/src/Config/ConfigService.php @@ -17,6 +17,9 @@ class ConfigService */ private array $config; + /** + * @SuppressWarnings(PHPMD.StaticAccess) + */ public function __construct( private readonly Processor $processor, private readonly ConfigLoader $configuration @@ -26,6 +29,9 @@ public function __construct( ]); } + /** + * @SuppressWarnings(PHPMD.StaticAccess) + */ public function loadConfig(string $configFilePath): void { $this->config = $this->processor->processConfiguration($this->configuration, [ diff --git a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php index dff86bd..918752d 100644 --- a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php +++ b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php @@ -4,17 +4,25 @@ namespace Phauthentic\CognitiveCodeAnalysis\Tests\Unit\Business\Cognitive; -use Phauthentic\CognitiveCodeAnalysis\Business\AbstractMetricCollector; +use Phauthentic\CognitiveCodeAnalysis\Application; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Parser; use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigLoader; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; +use PHPMD\Console\OutputInterface; use PhpParser\NodeTraverser; use PhpParser\ParserFactory; use PHPUnit\Framework\TestCase; use RuntimeException; use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Handler\HandlersLocator; +use Symfony\Component\Messenger\MessageBus; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; /** * @@ -23,18 +31,33 @@ class CognitiveMetricsCollectorTest extends TestCase { private CognitiveMetricsCollector $metricsCollector; private ConfigService $configService; + private MessageBusInterface $messageBus; protected function setUp(): void { parent::setUp(); + + $bus = $this->getMockBuilder(MessageBusInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $bus->expects($this->any()) + ->method('dispatch') + ->willReturn(new Envelope(new \stdClass())); + + $this->messageBus = $bus; + $this->metricsCollector = new CognitiveMetricsCollector( - new ParserFactory(), - new NodeTraverser(), + new Parser( + new ParserFactory(), + new NodeTraverser(), + ), new DirectoryScanner(), new ConfigService( new Processor(), new ConfigLoader(), - ) + ), + $bus ); $this->configService = new ConfigService( @@ -64,10 +87,13 @@ public function testCollectWithExcludedClasses(): void $configService->loadConfig(__DIR__ . '/../../../Fixtures/config-with-exclude-patterns.yml'); $metricsCollector = new CognitiveMetricsCollector( - new ParserFactory(), - new NodeTraverser(), + new Parser( + new ParserFactory(), + new NodeTraverser(), + ), new DirectoryScanner(), $configService, + $this->messageBus ); $path = './tests/TestCode'; diff --git a/tests/Unit/Business/DirectoryScannerTest.php b/tests/Unit/Business/DirectoryScannerTest.php index 649d734..fd058ef 100644 --- a/tests/Unit/Business/DirectoryScannerTest.php +++ b/tests/Unit/Business/DirectoryScannerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Phauthentic\CognitiveCodeAnalysis\Tests\Unit\Business\Halstead; +namespace Phauthentic\CognitiveCodeAnalysis\Tests\Unit\Business; use FilesystemIterator; use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner;