From e77be145f1fdd9d00aeb51357a3c19b17e272622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 26 Sep 2024 22:52:43 +0200 Subject: [PATCH 1/5] Added debug output, optimized performance --- src/Application.php | 26 ++- src/Business/AbstractMetricCollector.php | 7 +- src/Business/Cognitive/CognitiveMetrics.php | 56 +++-- .../Cognitive/CognitiveMetricsCollection.php | 16 +- .../Cognitive/CognitiveMetricsCollector.php | 18 +- .../Cognitive/FindMetricsPluginInterface.php | 24 ++ .../CognitiveCollectorShellOutputPlugin.php | 76 +++++++ src/Command/CognitiveMetricsCommand.php | 13 +- .../CognitiveMetricsCollectionTest.php | 215 ++++++++++-------- 9 files changed, 318 insertions(+), 133 deletions(-) create mode 100644 src/Business/Cognitive/FindMetricsPluginInterface.php create mode 100644 src/Command/Cognitive/CognitiveCollectorShellOutputPlugin.php diff --git a/src/Application.php b/src/Application.php index 5c45059..c49a49c 100644 --- a/src/Application.php +++ b/src/Application.php @@ -9,6 +9,7 @@ use Phauthentic\CodeQualityMetrics\Business\Cognitive\ScoreCalculator; use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; use Phauthentic\CodeQualityMetrics\Business\Halstead\HalsteadMetricsCollector; +use Phauthentic\CodeQualityMetrics\Command\Cognitive\CognitiveCollectorShellOutputPlugin; use Phauthentic\CodeQualityMetrics\Command\CognitiveMetricsCommand; use Phauthentic\CodeQualityMetrics\Command\HalsteadMetricsCommand; use Phauthentic\CodeQualityMetrics\Business\MetricsFacade; @@ -21,6 +22,10 @@ use PhpParser\ParserFactory; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Console\Application as SymfonyApplication; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -74,6 +79,19 @@ private function registerServices(): void $this->containerBuilder->register(NodeTraverserInterface::class, NodeTraverser::class) ->setPublic(true); + + $this->containerBuilder->register(OutputInterface::class, ConsoleOutput::class) + ->setPublic(true); + + $this->containerBuilder->register(InputInterface::class, ArgvInput::class) + ->setPublic(true); + + $this->containerBuilder->register(CognitiveCollectorShellOutputPlugin::class, CognitiveCollectorShellOutputPlugin::class) + ->setArguments([ + new Reference(InputInterface::class), + new Reference(OutputInterface::class) + ]) + ->setPublic(true); } private function bootstrap(): void @@ -93,6 +111,9 @@ private function bootstrapMetricsCollectors(): void new Reference(ParserFactory::class), new Reference(NodeTraverserInterface::class), new Reference(DirectoryScanner::class), + [ + $this->containerBuilder->get(CognitiveCollectorShellOutputPlugin::class) + ] ]) ->setPublic(true); @@ -157,7 +178,10 @@ public function run(): void { $application = $this->containerBuilder->get(SymfonyApplication::class); // @phpstan-ignore-next-line - $application->run(); + $application->run( + $this->containerBuilder->get(InputInterface::class), + $this->containerBuilder->get(OutputInterface::class) + ); } public function get(string $id): mixed diff --git a/src/Business/AbstractMetricCollector.php b/src/Business/AbstractMetricCollector.php index c5ed599..c974e9f 100644 --- a/src/Business/AbstractMetricCollector.php +++ b/src/Business/AbstractMetricCollector.php @@ -5,6 +5,7 @@ namespace Phauthentic\CodeQualityMetrics\Business; use Generator; +use Phauthentic\CodeQualityMetrics\Business\Cognitive\FindMetricsPluginInterface; use PhpParser\Error; use PhpParser\NodeTraverserInterface; use PhpParser\Parser; @@ -32,10 +33,14 @@ protected function getExcludePatternsFromConfig(array $config): array return []; } + /** + * @param array $findMetricsPlugins + */ public function __construct( protected readonly ParserFactory $parserFactory, protected readonly NodeTraverserInterface $traverser, - protected readonly DirectoryScanner $directoryScanner + protected readonly DirectoryScanner $directoryScanner, + protected readonly array $findMetricsPlugins = [] ) { $this->parser = $this->parserFactory->createForHostVersion(); } diff --git a/src/Business/Cognitive/CognitiveMetrics.php b/src/Business/Cognitive/CognitiveMetrics.php index ccdc545..b4762a2 100644 --- a/src/Business/Cognitive/CognitiveMetrics.php +++ b/src/Business/Cognitive/CognitiveMetrics.php @@ -13,17 +13,17 @@ class CognitiveMetrics implements JsonSerializable { /** - * @var array + * @var array */ private array $metrics = [ - 'lineCount', - 'argCount', - 'returnCount', - 'variableCount', - 'propertyCallCount', - 'ifCount', - 'ifNestingLevel', - 'elseCount' + 'lineCount' => 'lineCount', + 'argCount' => 'argCount', + 'returnCount' => 'returnCount', + 'variableCount' => 'variableCount', + 'propertyCallCount' => 'propertyCallCount', + 'ifCount' => 'ifCount', + 'ifNestingLevel' => 'ifNestingLevel', + 'elseCount' => 'elseCount' ]; private string $class; @@ -64,6 +64,7 @@ public function __construct(array $metrics) { $this->assertArrayKeyIsPresent($metrics, 'class'); $this->assertArrayKeyIsPresent($metrics, 'method'); + $this->method = $metrics['method']; $this->class = $metrics['class']; @@ -77,10 +78,20 @@ public function __construct(array $metrics) */ private function setRequiredMetricProperties(array $metrics): void { - foreach ($this->metrics as $metricName) { - $this->assertArrayKeyIsPresent($metrics, $metricName); - $this->$metricName = $metrics[$metricName]; + $missingKeys = array_diff_key($this->metrics, $metrics); + if (!empty($missingKeys)) { + throw new InvalidArgumentException('Missing required keys'); } + + // Not pretty to set each but more efficient than using a loop and $this->metrics + $this->lineCount = $metrics['lineCount']; + $this->argCount = $metrics['argCount']; + $this->returnCount = $metrics['returnCount']; + $this->variableCount = $metrics['variableCount']; + $this->propertyCallCount = $metrics['propertyCallCount']; + $this->ifCount = $metrics['ifCount']; + $this->ifNestingLevel = $metrics['ifNestingLevel']; + $this->elseCount = $metrics['elseCount']; } /** @@ -89,12 +100,15 @@ private function setRequiredMetricProperties(array $metrics): void */ private function setOptionalMetricProperties(array $metrics): void { - foreach ($this->metrics as $metricName) { - $property = $metricName . 'Weight'; - if (array_key_exists($property, $metrics)) { - $this->$property = $metrics[$property]; - } - } + // Not pretty to set each but more efficient than using a loop and $this->metrics + $this->lineCountWeight = $metrics['lineCountWeight'] ?? 0.0; + $this->argCountWeight = $metrics['argCountWeight'] ?? 0.0; + $this->returnCountWeight = $metrics['returnCountWeight'] ?? 0.0; + $this->variableCountWeight = $metrics['variableCountWeight'] ?? 0.0; + $this->propertyCallCountWeight = $metrics['propertyCallCountWeight'] ?? 0.0; + $this->ifCountWeight = $metrics['ifCountWeight'] ?? 0.0; + $this->ifNestingLevelWeight = $metrics['ifNestingLevelWeight'] ?? 0.0; + $this->elseCountWeight = $metrics['elseCountWeight'] ?? 0.0; } private function assertSame(self $other): void @@ -108,8 +122,7 @@ private function assertSame(self $other): void $this->getClass(), $this->getMethod(), $other->getClass(), - $other->getMethod( - ) + $other->getMethod() )); } @@ -146,12 +159,11 @@ public static function fromArray(array $metrics): self */ private function assertArrayKeyIsPresent(array $array, string $key): void { - if (!array_key_exists($key, $array)) { + if (!isset($array[$key])) { throw new InvalidArgumentException("Missing required key: $key"); } } - // Getters for read-only attributes public function getClass(): string { return $this->class; diff --git a/src/Business/Cognitive/CognitiveMetricsCollection.php b/src/Business/Cognitive/CognitiveMetricsCollection.php index 4f26ce5..89d4562 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollection.php +++ b/src/Business/Cognitive/CognitiveMetricsCollection.php @@ -28,7 +28,7 @@ class CognitiveMetricsCollection implements IteratorAggregate, Countable, JsonSe */ public function add(CognitiveMetrics $metric): void { - $this->metrics[] = $metric; + $this->metrics[$metric->getClass() . '::' . $metric->getMethod()] = $metric; } /** @@ -75,21 +75,13 @@ public function filterWithScoreGreaterThan(float $score): CognitiveMetricsCollec public function contains(CognitiveMetrics $otherMetric): bool { - foreach ($this->metrics as $metric) { - if ($otherMetric->equals($metric)) { - return true; - } - } - - return false; + return isset($this->metrics[$otherMetric->getClass() . '::' . $otherMetric->getMethod()]); } public function getClassWithMethod(string $class, string $method): ?CognitiveMetrics { - foreach ($this->metrics as $metric) { - if ($metric->getClass() === $class && $metric->getMethod() === $method) { - return $metric; - } + if (isset($this->metrics[$class . '::' . $method])) { + return $this->metrics[$class . '::' . $method]; } return null; diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index 41c266c..a4f2559 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -39,7 +39,15 @@ protected 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 = file_get_contents($file->getRealPath()); if ($code === false) { @@ -53,6 +61,14 @@ protected function findMetrics(iterable $files): CognitiveMetricsCollection $this->traverser->removeVisitor($visitor); $this->processMethodMetrics($methodMetrics, $metricsCollection); + + foreach ($this->findMetricsPlugins as $plugin) { + $plugin->afterFindMetrics($file); + } + } + + foreach ($this->findMetricsPlugins as $plugin) { + $plugin->afterIteration($metricsCollection); } return $metricsCollection; @@ -76,7 +92,7 @@ private function processMethodMetrics( 'method' => $method ]); - $metric = CognitiveMetrics::fromArray($metricsArray); + $metric = new CognitiveMetrics($metricsArray); if (!$metricsCollection->contains($metric)) { $metricsCollection->add($metric); diff --git a/src/Business/Cognitive/FindMetricsPluginInterface.php b/src/Business/Cognitive/FindMetricsPluginInterface.php new file mode 100644 index 0000000..e562061 --- /dev/null +++ b/src/Business/Cognitive/FindMetricsPluginInterface.php @@ -0,0 +1,24 @@ + $files + */ + public function beforeIteration(iterable $files): void; + + public function afterIteration(CognitiveMetricsCollection $metricsCollection): void; +} diff --git a/src/Command/Cognitive/CognitiveCollectorShellOutputPlugin.php b/src/Command/Cognitive/CognitiveCollectorShellOutputPlugin.php new file mode 100644 index 0000000..a9c0e59 --- /dev/null +++ b/src/Command/Cognitive/CognitiveCollectorShellOutputPlugin.php @@ -0,0 +1,76 @@ +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 a6a0cae..cda875a 100644 --- a/src/Command/CognitiveMetricsCommand.php +++ b/src/Command/CognitiveMetricsCommand.php @@ -13,6 +13,7 @@ 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; /** @@ -25,10 +26,11 @@ class CognitiveMetricsCommand extends Command { // Option names for exporting metrics in different formats and loading a configuration file. - private const OPTION_CONFIG_FILE = 'config'; - private const OPTION_BASELINE = 'baseline'; - private const OPTION_REPORT_TYPE = 'report-type'; - private const OPTION_REPORT_FILE = 'report-file'; + public const OPTION_CONFIG_FILE = 'config'; + public const OPTION_BASELINE = 'baseline'; + public const OPTION_REPORT_TYPE = 'report-type'; + public const OPTION_REPORT_FILE = 'report-file'; + public const OPTION_DEBUG = 'debug'; // Argument name for the path to the PHP files or directories. private const ARGUMENT_PATH = 'path'; @@ -54,7 +56,8 @@ protected function configure(): void ->addOption(self::OPTION_CONFIG_FILE, 'c', InputArgument::OPTIONAL, 'Path to a configuration file', null) ->addOption(self::OPTION_BASELINE, 'b', InputArgument::OPTIONAL, 'Baseline file to get the delta.', null) ->addOption(self::OPTION_REPORT_TYPE, 'r', InputArgument::OPTIONAL, 'Type of report to generate (json, csv, html).', null, ['json', 'csv', 'html']) - ->addOption(self::OPTION_REPORT_FILE, 'f', InputArgument::OPTIONAL, 'File to write the report to.'); + ->addOption(self::OPTION_REPORT_FILE, 'f', InputArgument::OPTIONAL, 'File to write the report to.') + ->addOption(self::OPTION_DEBUG, null, InputArgument::OPTIONAL, 'Enables debug output', false); } /** diff --git a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectionTest.php b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectionTest.php index 3859dab..d521dce 100644 --- a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectionTest.php +++ b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectionTest.php @@ -11,16 +11,44 @@ use PHPUnit\Framework\TestCase; /** - * + * Unit tests for CognitiveMetricsCollection */ class CognitiveMetricsCollectionTest extends TestCase { + private function createCognitiveMetrics(array $data): CognitiveMetrics + { + return CognitiveMetrics::fromArray($data); + } + public function testAddAndCount(): void { $metricsCollection = new CognitiveMetricsCollection(); - $metrics1 = $this->createMock(CognitiveMetrics::class); - $metrics2 = $this->createMock(CognitiveMetrics::class); + $metrics1 = $this->createCognitiveMetrics([ + 'class' => 'ClassA', + 'method' => 'methodA', + 'lineCount' => 10, + 'argCount' => 2, + 'returnCount' => 1, + 'variableCount' => 3, + 'propertyCallCount' => 4, + 'ifCount' => 2, + 'ifNestingLevel' => 1, + 'elseCount' => 0 + ]); + + $metrics2 = $this->createCognitiveMetrics([ + 'class' => 'ClassB', + 'method' => 'methodB', + 'lineCount' => 20, + 'argCount' => 4, + 'returnCount' => 2, + 'variableCount' => 5, + 'propertyCallCount' => 3, + 'ifCount' => 3, + 'ifNestingLevel' => 2, + 'elseCount' => 1 + ]); $this->assertSame(0, $metricsCollection->count()); @@ -34,8 +62,31 @@ public function testGetIterator(): void { $metricsCollection = new CognitiveMetricsCollection(); - $metrics1 = $this->createMock(CognitiveMetrics::class); - $metrics2 = $this->createMock(CognitiveMetrics::class); + $metrics1 = $this->createCognitiveMetrics([ + 'class' => 'ClassA', + 'method' => 'methodA', + 'lineCount' => 10, + 'argCount' => 2, + 'returnCount' => 1, + 'variableCount' => 3, + 'propertyCallCount' => 4, + 'ifCount' => 2, + 'ifNestingLevel' => 1, + 'elseCount' => 0 + ]); + + $metrics2 = $this->createCognitiveMetrics([ + 'class' => 'ClassB', + 'method' => 'methodB', + 'lineCount' => 20, + 'argCount' => 4, + 'returnCount' => 2, + 'variableCount' => 5, + 'propertyCallCount' => 3, + 'ifCount' => 3, + 'ifNestingLevel' => 2, + 'elseCount' => 1 + ]); $metricsCollection->add($metrics1); $metricsCollection->add($metrics2); @@ -50,11 +101,35 @@ public function testFilter(): void { $metricsCollection = new CognitiveMetricsCollection(); - $metrics1 = $this->createMock(CognitiveMetrics::class); - $metrics2 = $this->createMock(CognitiveMetrics::class); - - $metrics1->method('getScore')->willReturn(5.0); - $metrics2->method('getScore')->willReturn(10.0); + $metrics1 = $this->createCognitiveMetrics([ + 'class' => 'ClassA', + 'method' => 'methodA', + 'lineCount' => 10, + 'argCount' => 2, + 'returnCount' => 1, + 'variableCount' => 3, + 'propertyCallCount' => 4, + 'ifCount' => 2, + 'ifNestingLevel' => 1, + 'elseCount' => 0, + 'score' => 5.0 + ]); + $metrics1->setScore(5.0); + + $metrics2 = $this->createCognitiveMetrics([ + 'class' => 'ClassB', + 'method' => 'methodB', + 'lineCount' => 20, + 'argCount' => 4, + 'returnCount' => 2, + 'variableCount' => 5, + 'propertyCallCount' => 3, + 'ifCount' => 3, + 'ifNestingLevel' => 2, + 'elseCount' => 1, + 'score' => 10.0 + ]); + $metrics2->setScore(10.0); $metricsCollection->add($metrics1); $metricsCollection->add($metrics2); @@ -64,38 +139,51 @@ public function testFilter(): void }); $this->assertCount(1, $filtered); - $this->assertSame(10.0, $filtered->getIterator()[0]->getScore()); - } - - public function testFilterWithScoreGreaterThan(): void - { - $metricsCollection = new CognitiveMetricsCollection(); - - $metrics1 = $this->createMock(CognitiveMetrics::class); - $metrics2 = $this->createMock(CognitiveMetrics::class); - - $metrics1->method('getScore')->willReturn(5.0); - $metrics2->method('getScore')->willReturn(10.0); - - $metricsCollection->add($metrics1); - $metricsCollection->add($metrics2); - - $filtered = $metricsCollection->filterWithScoreGreaterThan(7.0); - - $this->assertCount(1, $filtered); - $this->assertSame(10.0, $filtered->getIterator()[0]->getScore()); + $this->assertSame(10.0, $filtered->getIterator()['ClassB::methodB']->getScore()); } public function testContains(): void { $metricsCollection = new CognitiveMetricsCollection(); - $metrics1 = $this->createMock(CognitiveMetrics::class); - $metrics2 = $this->createMock(\Phauthentic\CodeQualityMetrics\Business\Cognitive\CognitiveMetrics::class); - $metrics3 = $this->createMock(CognitiveMetrics::class); - - $metrics1->method('equals')->willReturn(false); - $metrics2->method('equals')->willReturn(true); + $metrics1 = $this->createCognitiveMetrics([ + 'class' => 'ClassA', + 'method' => 'methodA', + 'lineCount' => 10, + 'argCount' => 2, + 'returnCount' => 1, + 'variableCount' => 3, + 'propertyCallCount' => 4, + 'ifCount' => 2, + 'ifNestingLevel' => 1, + 'elseCount' => 0 + ]); + + $metrics2 = $this->createCognitiveMetrics([ + 'class' => 'ClassB', + 'method' => 'methodB', + 'lineCount' => 20, + 'argCount' => 4, + 'returnCount' => 2, + 'variableCount' => 5, + 'propertyCallCount' => 3, + 'ifCount' => 3, + 'ifNestingLevel' => 2, + 'elseCount' => 1 + ]); + + $metrics3 = $this->createCognitiveMetrics([ + 'class' => 'ClassC', + 'method' => 'methodC', + 'lineCount' => 30, + 'argCount' => 6, + 'returnCount' => 3, + 'variableCount' => 8, + 'propertyCallCount' => 7, + 'ifCount' => 4, + 'ifNestingLevel' => 3, + 'elseCount' => 2 + ]); $metricsCollection->add($metrics1); $metricsCollection->add($metrics2); @@ -103,59 +191,4 @@ public function testContains(): void $this->assertTrue($metricsCollection->contains($metrics2)); $this->assertFalse($metricsCollection->contains($metrics3)); } - - public function testFilterByClassName(): void - { - $metricsCollection = new CognitiveMetricsCollection(); - - $metrics1 = $this->createMock(CognitiveMetrics::class); - $metrics2 = $this->createMock(CognitiveMetrics::class); - - $metrics1->method('getClass')->willReturn('ClassA'); - $metrics2->method('getClass')->willReturn('ClassB'); - - $metricsCollection->add($metrics1); - $metricsCollection->add($metrics2); - - $filtered = $metricsCollection->filterByClassName('ClassA'); - - $this->assertCount(1, $filtered); - $this->assertSame('ClassA', $filtered->getIterator()[0]->getClass()); - } - - public function testGroupBy(): void - { - $metricsCollection = new CognitiveMetricsCollection(); - - $metrics1 = $this->createMock(CognitiveMetrics::class); - $metrics2 = $this->createMock(CognitiveMetrics::class); - $metrics3 = $this->createMock(\Phauthentic\CodeQualityMetrics\Business\Cognitive\CognitiveMetrics::class); - - $metrics1->method('getClass')->willReturn('ClassA'); - $metrics2->method('getClass')->willReturn('ClassB'); - $metrics3->method('getClass')->willReturn('ClassA'); - - $metricsCollection->add($metrics1); - $metricsCollection->add($metrics2); - $metricsCollection->add($metrics3); - - $grouped = $metricsCollection->groupBy('class'); - - $this->assertCount(2, $grouped); - $this->assertCount(2, $grouped['ClassA']); - $this->assertCount(1, $grouped['ClassB']); - } - - public function testGroupByThrowsExceptionOnInvalidProperty(): void - { - $metricsCollection = new CognitiveMetricsCollection(); - - $metrics = $this->createMock(CognitiveMetrics::class); - $metricsCollection->add($metrics); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Property 'invalidProperty' does not exist in CognitiveMetrics class"); - - $metricsCollection->groupBy('invalidProperty'); - } } From 3e41450f9c2b2d988a604d95111ca7d9afac33f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 26 Sep 2024 22:57:18 +0200 Subject: [PATCH 2/5] Removing mutation tests from Github CI --- .github/workflows/ci.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b34ff78..dc3cd9c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -69,6 +69,3 @@ jobs: - name: Run phpstan run: bin/phpstan -V && bin/phpstan --error-format=github - - - name: Run Infection - run: bin/infection From e0ddb239c0186648819f04e32ab4237b6b068025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Fri, 27 Sep 2024 01:08:47 +0200 Subject: [PATCH 3/5] Made exclusion of classes and methods possible --- config.yml | 7 +- docs/Configuration.md | 66 +++++++++------ src/Application.php | 7 +- src/Business/AbstractMetricCollector.php | 75 ----------------- .../Cognitive/CognitiveMetricsCollector.php | 81 ++++++++++++++++++- .../Halstead/HalsteadMetricsCollector.php | 58 ++++++++++++- src/Command/CognitiveMetricsCommand.php | 1 - src/Config/ConfigLoader.php | 19 ----- tests/Fixtures/config-with-one-metric.yml | 6 +- .../CognitiveMetricsCollectorTest.php | 11 ++- .../Halstead/HalsteadMetricsCollectorTest.php | 2 +- tests/Unit/Business/MetricsFacadeTest.php | 4 - tests/Unit/Config/ConfigLoaderTest.php | 2 - 13 files changed, 194 insertions(+), 145 deletions(-) delete mode 100644 src/Business/AbstractMetricCollector.php diff --git a/config.yml b/config.yml index 0bd3f65..7ef89c2 100644 --- a/config.yml +++ b/config.yml @@ -1,7 +1,5 @@ cognitive: - excludedClasses: [], - excludedMethods: [], - excludePatterns: [], + excludePatterns: metrics: lineCount: threshold: 60 @@ -29,9 +27,6 @@ cognitive: scale: 1.0 halstead: - excludedClasses: [], - excludedMethods: [], - excludePatterns: [], threshold: difficulty: 0.0 effort: 0.0 diff --git a/docs/Configuration.md b/docs/Configuration.md index 89781e9..5bfef72 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -8,38 +8,52 @@ You can specify another configuration file by passing it to the config options: php analyse.php metrics:cognitive --config= ``` +## Excluding Classes and Methods + +You can exclude classes and methods via a regex in the configuration. + +The following configuration will exclude all constructors and all methods of classes that end with `Transformer`. + +```yaml +cognitive: + excludePatterns: + - '(.*)::__construct' + - '(.*)Transformer::(.*)' +``` + ## Tuning the calculation The configuration file can contain the following settings for the calculation of cognitive complexity. Feel free to adjust the values to your match your opinion on what makes code complex. -``` -metrics: - lineCount: - threshold: 60 - scale: 2.0 - argCount: - threshold: 4 - scale: 1.0 - returnCount: - threshold: 2 - scale: 5.0 - variableCount: - threshold: 2 - scale: 5.0 - propertyCallCount: - threshold: 2 - scale: 15.0 - ifCount: - threshold: 3 - scale: 1.0 - ifNestingLevel: - threshold: 1 - scale: 1.0 - elseCount: - threshold: 1 - scale: 1.0 +```yaml +cognitive: + metrics: + lineCount: + threshold: 60 + scale: 2.0 + argCount: + threshold: 4 + scale: 1.0 + returnCount: + threshold: 2 + scale: 5.0 + variableCount: + threshold: 2 + scale: 5.0 + propertyCallCount: + threshold: 2 + scale: 15.0 + ifCount: + threshold: 3 + scale: 1.0 + ifNestingLevel: + threshold: 1 + scale: 1.0 + elseCount: + threshold: 1 + scale: 1.0 ``` It is recommended to play with the values until you get weights that you are comfortable with. The default values are a good starting point. diff --git a/src/Application.php b/src/Application.php index c49a49c..389332d 100644 --- a/src/Application.php +++ b/src/Application.php @@ -111,9 +111,10 @@ private function bootstrapMetricsCollectors(): void new Reference(ParserFactory::class), new Reference(NodeTraverserInterface::class), new Reference(DirectoryScanner::class), + new Reference(ConfigService::class), [ $this->containerBuilder->get(CognitiveCollectorShellOutputPlugin::class) - ] + ], ]) ->setPublic(true); @@ -122,6 +123,10 @@ private function bootstrapMetricsCollectors(): void new Reference(ParserFactory::class), new Reference(NodeTraverserInterface::class), new Reference(DirectoryScanner::class), + new Reference(ConfigService::class), + [ + $this->containerBuilder->get(CognitiveCollectorShellOutputPlugin::class) + ], ]) ->setPublic(true); } diff --git a/src/Business/AbstractMetricCollector.php b/src/Business/AbstractMetricCollector.php deleted file mode 100644 index c974e9f..0000000 --- a/src/Business/AbstractMetricCollector.php +++ /dev/null @@ -1,75 +0,0 @@ - $config - * @return array - */ - protected function getExcludePatternsFromConfig(array $config): array - { - if (isset($config['excludePatterns'])) { - return $config['excludePatterns']; - } - - return []; - } - - /** - * @param array $findMetricsPlugins - */ - public function __construct( - protected readonly ParserFactory $parserFactory, - protected readonly NodeTraverserInterface $traverser, - protected readonly DirectoryScanner $directoryScanner, - protected readonly array $findMetricsPlugins = [] - ) { - $this->parser = $this->parserFactory->createForHostVersion(); - } - - /** - * Find source files using DirectoryScanner - * - * @param string $path Path to the directory or file to scan - * @param array $exclude List of regx to exclude - * @return Generator An iterable of SplFileInfo objects - */ - protected function findSourceFiles(string $path, array $exclude = []): iterable - { - return $this->directoryScanner->scan([$path], ['^(?!.*\.php$).+'] + $exclude); // Exclude non-PHP files - } - - - protected function traverseAbstractSyntaxTree(string $code): void - { - try { - $ast = $this->parser->parse($code); - } catch (Error $e) { - throw new RuntimeException("Parse error: {$e->getMessage()}", 0, $e); - } - - if ($ast === null) { - throw new RuntimeException("Could not parse the code."); - } - - $this->traverser->traverse($ast); - } -} diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index a4f2559..2dd1e8f 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -4,16 +4,49 @@ namespace Phauthentic\CodeQualityMetrics\Business\Cognitive; -use Phauthentic\CodeQualityMetrics\Business\AbstractMetricCollector; +use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; +use Phauthentic\CodeQualityMetrics\Config\ConfigService; use Phauthentic\CodeQualityMetrics\PhpParser\CognitiveMetricsVisitor; +use PhpParser\Error; +use PhpParser\NodeTraverserInterface; +use PhpParser\Parser; +use PhpParser\ParserFactory; use RuntimeException; use SplFileInfo; /** * CognitiveMetricsCollector class that collects cognitive metrics from source files */ -class CognitiveMetricsCollector extends AbstractMetricCollector +class CognitiveMetricsCollector { + protected Parser $parser; + + /** + * @param array $findMetricsPlugins + */ + public function __construct( + protected readonly ParserFactory $parserFactory, + protected readonly NodeTraverserInterface $traverser, + protected readonly DirectoryScanner $directoryScanner, + protected readonly ConfigService $configService, + protected readonly array $findMetricsPlugins = [] + ) { + $this->parser = $parserFactory->createForHostVersion(); + } + + /** + * @param array $config + * @return array + */ + protected function getExcludePatternsFromConfig(array $config): array + { + if (isset($config['excludePatterns'])) { + return $config['excludePatterns']; + } + + return []; + } + /** * Collect cognitive metrics from the given path * @@ -85,6 +118,10 @@ private function processMethodMetrics( CognitiveMetricsCollection $metricsCollection ): void { foreach ($methodMetrics as $classAndMethod => $metrics) { + if ($this->isExcluded($classAndMethod)) { + continue; + } + [$class, $method] = explode('::', $classAndMethod); $metricsArray = array_merge($metrics, [ @@ -99,4 +136,44 @@ private function processMethodMetrics( } } } + + public function isExcluded(string $classAndMethod): bool + { + $regexes = $this->configService->getConfig()['cognitive']['excludePatterns']; + + foreach ($regexes as $regex) { + if (preg_match('/' . $regex . '/', $classAndMethod, $matches)) { + return true; + } + } + + return false; + } + + /** + * Find source files using DirectoryScanner + * + * @param string $path Path to the directory or file to scan + * @param array $exclude List of regx to exclude + * @return iterable An iterable of SplFileInfo objects + */ + protected function findSourceFiles(string $path, array $exclude = []): iterable + { + return $this->directoryScanner->scan([$path], ['^(?!.*\.php$).+'] + $exclude); // Exclude non-PHP files + } + + protected function traverseAbstractSyntaxTree(string $code): void + { + try { + $ast = $this->parser->parse($code); + } catch (Error $e) { + throw new RuntimeException("Parse error: {$e->getMessage()}", 0, $e); + } + + if ($ast === null) { + throw new RuntimeException("Could not parse the code."); + } + + $this->traverser->traverse($ast); + } } diff --git a/src/Business/Halstead/HalsteadMetricsCollector.php b/src/Business/Halstead/HalsteadMetricsCollector.php index 4cd27e5..345648d 100644 --- a/src/Business/Halstead/HalsteadMetricsCollector.php +++ b/src/Business/Halstead/HalsteadMetricsCollector.php @@ -4,16 +4,70 @@ namespace Phauthentic\CodeQualityMetrics\Business\Halstead; -use Phauthentic\CodeQualityMetrics\Business\AbstractMetricCollector; +use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; use Phauthentic\CodeQualityMetrics\PhpParser\HalsteadMetricsVisitor; +use PhpParser\Error; +use PhpParser\NodeTraverserInterface; +use PhpParser\Parser; +use PhpParser\ParserFactory; use RuntimeException; use SplFileInfo; /** * HalsteadMetricsCollector class that collects Halstead metrics from source files. */ -class HalsteadMetricsCollector extends AbstractMetricCollector +class HalsteadMetricsCollector { + protected Parser $parser; + + public function __construct( + protected readonly ParserFactory $parserFactory, + protected readonly NodeTraverserInterface $traverser, + protected readonly DirectoryScanner $directoryScanner, + ) { + $this->parser = $parserFactory->createForHostVersion(); + } + + /** + * @param array $config + * @return array + */ + protected function getExcludePatternsFromConfig(array $config): array + { + if (isset($config['excludePatterns'])) { + return $config['excludePatterns']; + } + + return []; + } + + /** + * Find source files using DirectoryScanner + * + * @param string $path Path to the directory or file to scan + * @param array $exclude List of regx to exclude + * @return iterable An iterable of SplFileInfo objects + */ + protected function findSourceFiles(string $path, array $exclude = []): iterable + { + return $this->directoryScanner->scan([$path], ['^(?!.*\.php$).+'] + $exclude); // Exclude non-PHP files + } + + protected function traverseAbstractSyntaxTree(string $code): void + { + try { + $ast = $this->parser->parse($code); + } catch (Error $e) { + throw new RuntimeException("Parse error: {$e->getMessage()}", 0, $e); + } + + if ($ast === null) { + throw new RuntimeException("Could not parse the code."); + } + + $this->traverser->traverse($ast); + } + /** * Collect Halstead metrics from the given path. * diff --git a/src/Command/CognitiveMetricsCommand.php b/src/Command/CognitiveMetricsCommand.php index cda875a..f3c4dbc 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/Config/ConfigLoader.php b/src/Config/ConfigLoader.php index 89e2d2c..ea97c65 100644 --- a/src/Config/ConfigLoader.php +++ b/src/Config/ConfigLoader.php @@ -80,16 +80,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->arrayNode('cognitive') ->children() - ->arrayNode('excludedClasses') - ->scalarPrototype() - ->defaultValue([]) - ->end() - ->end() - ->arrayNode('excludedMethods') - ->scalarPrototype() - ->defaultValue([]) - ->end() - ->end() ->arrayNode('excludePatterns') ->scalarPrototype() ->defaultValue([]) @@ -120,15 +110,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->arrayNode('halstead') ->children() - ->arrayNode('excludedClasses') - ->scalarPrototype()->end() - ->end() - ->arrayNode('excludedMethods') - ->scalarPrototype()->end() - ->end() - ->arrayNode('excludePatterns') - ->scalarPrototype()->end() - ->end() ->arrayNode('threshold') ->children() ->floatNode('difficulty')->end() diff --git a/tests/Fixtures/config-with-one-metric.yml b/tests/Fixtures/config-with-one-metric.yml index 8419bef..3773495 100644 --- a/tests/Fixtures/config-with-one-metric.yml +++ b/tests/Fixtures/config-with-one-metric.yml @@ -1,8 +1,6 @@ cognitive: - excludedClasses: [] - excludedMethods: - - '*.test' - excludePatterns: [] + excludePatterns: + - '*.test' metrics: lineCount: threshold: 1000 diff --git a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php index a57e9bf..f169125 100644 --- a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php +++ b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php @@ -8,17 +8,20 @@ use Phauthentic\CodeQualityMetrics\Business\Cognitive\CognitiveMetricsCollection; use Phauthentic\CodeQualityMetrics\Business\Cognitive\CognitiveMetricsCollector; use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; +use Phauthentic\CodeQualityMetrics\Config\ConfigLoader; +use Phauthentic\CodeQualityMetrics\Config\ConfigService; use PhpParser\NodeTraverser; use PhpParser\ParserFactory; use PHPUnit\Framework\TestCase; use RuntimeException; +use Symfony\Component\Config\Definition\Processor; /** * */ class CognitiveMetricsCollectorTest extends TestCase { - private AbstractMetricCollector $metricsCollector; + private CognitiveMetricsCollector $metricsCollector; protected function setUp(): void { @@ -26,7 +29,11 @@ protected function setUp(): void $this->metricsCollector = new CognitiveMetricsCollector( new ParserFactory(), new NodeTraverser(), - new DirectoryScanner() + new DirectoryScanner(), + new ConfigService( + new Processor(), + new ConfigLoader(), + ) ); } diff --git a/tests/Unit/Business/Halstead/HalsteadMetricsCollectorTest.php b/tests/Unit/Business/Halstead/HalsteadMetricsCollectorTest.php index 0d13455..c85b7ec 100644 --- a/tests/Unit/Business/Halstead/HalsteadMetricsCollectorTest.php +++ b/tests/Unit/Business/Halstead/HalsteadMetricsCollectorTest.php @@ -20,7 +20,7 @@ public function testCount() $collector = new HalsteadMetricsCollector( new ParserFactory(), new NodeTraverser(), - new DirectoryScanner() + new DirectoryScanner(), ); $collection = $collector->collect('./tests/TestCode'); diff --git a/tests/Unit/Business/MetricsFacadeTest.php b/tests/Unit/Business/MetricsFacadeTest.php index 79e0b50..4807826 100644 --- a/tests/Unit/Business/MetricsFacadeTest.php +++ b/tests/Unit/Business/MetricsFacadeTest.php @@ -5,10 +5,6 @@ namespace Phauthentic\CodeQualityMetrics\Tests\Unit\Business; use Phauthentic\CodeQualityMetrics\Application; -use Phauthentic\CodeQualityMetrics\Business\Cognitive\ScoreCalculator; -use Phauthentic\CodeQualityMetrics\Business\Halstead\HalsteadMetricsCollector; -use Phauthentic\CodeQualityMetrics\Config\ConfigService; -use PHP_CodeSniffer\Config; use PHPUnit\Framework\TestCase; use Phauthentic\CodeQualityMetrics\Business\MetricsFacade; use Symfony\Component\Yaml\Exception\ParseException; diff --git a/tests/Unit/Config/ConfigLoaderTest.php b/tests/Unit/Config/ConfigLoaderTest.php index 2e32409..9e163d2 100644 --- a/tests/Unit/Config/ConfigLoaderTest.php +++ b/tests/Unit/Config/ConfigLoaderTest.php @@ -25,8 +25,6 @@ public function testConfigTreeBuilder(): void $config = [ 'cognitive' => [ - 'excludedClasses' => ['Class1', 'Class2'], - 'excludedMethods' => ['method1', 'method2'], 'metrics' => [ 'lineCount' => [ 'threshold' => 60.0, From e3ee74b1fe627303c992a588d7bffac1005b3d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Fri, 27 Sep 2024 01:27:46 +0200 Subject: [PATCH 4/5] Fixing tests --- src/Application.php | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index 5c45059..96b6174 100644 --- a/src/Application.php +++ b/src/Application.php @@ -6,9 +6,11 @@ use Phauthentic\CodeQualityMetrics\Business\Cognitive\BaselineService; use Phauthentic\CodeQualityMetrics\Business\Cognitive\CognitiveMetricsCollector; +use Phauthentic\CodeQualityMetrics\Business\Cognitive\FindMetricsPluginInterface; use Phauthentic\CodeQualityMetrics\Business\Cognitive\ScoreCalculator; use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; use Phauthentic\CodeQualityMetrics\Business\Halstead\HalsteadMetricsCollector; +use Phauthentic\CodeQualityMetrics\Command\Cognitive\CognitiveCollectorShellOutputPlugin; use Phauthentic\CodeQualityMetrics\Command\CognitiveMetricsCommand; use Phauthentic\CodeQualityMetrics\Command\HalsteadMetricsCommand; use Phauthentic\CodeQualityMetrics\Business\MetricsFacade; @@ -21,6 +23,10 @@ use PhpParser\ParserFactory; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Console\Application as SymfonyApplication; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -74,6 +80,22 @@ private function registerServices(): void $this->containerBuilder->register(NodeTraverserInterface::class, NodeTraverser::class) ->setPublic(true); + + $this->containerBuilder->register(NodeTraverserInterface::class, NodeTraverser::class) + ->setPublic(true); + + $this->containerBuilder->register(OutputInterface::class, ConsoleOutput::class) + ->setPublic(true); + + $this->containerBuilder->register(InputInterface::class, ArgvInput::class) + ->setPublic(true); + + $this->containerBuilder->register(CognitiveCollectorShellOutputPlugin::class, CognitiveCollectorShellOutputPlugin::class) + ->setArguments([ + new Reference(InputInterface::class), + new Reference(OutputInterface::class) + ]) + ->setPublic(true); } private function bootstrap(): void @@ -93,6 +115,10 @@ private function bootstrapMetricsCollectors(): void new Reference(ParserFactory::class), new Reference(NodeTraverserInterface::class), new Reference(DirectoryScanner::class), + new Reference(ConfigService::class), + [ + $this->containerBuilder->get(CognitiveCollectorShellOutputPlugin::class), + ] ]) ->setPublic(true); @@ -157,7 +183,10 @@ public function run(): void { $application = $this->containerBuilder->get(SymfonyApplication::class); // @phpstan-ignore-next-line - $application->run(); + $application->run( + $this->containerBuilder->get(InputInterface::class), + $this->containerBuilder->get(OutputInterface::class) + ); } public function get(string $id): mixed From db7ec26b395e669866350c36a0f3bfadfe8177b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Fri, 27 Sep 2024 01:44:00 +0200 Subject: [PATCH 5/5] Adding a test for the exclusion --- .../Cognitive/CognitiveMetricsCollector.php | 11 +++++--- src/CognitiveAnalysisException.php | 14 +++++++++++ .../Fixtures/config-with-exclude-patterns.yml | 3 +++ tests/Fixtures/config-with-one-metric.yml | 1 - .../CognitiveMetricsCollectorTest.php | 25 +++++++++++++++++++ 5 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 src/CognitiveAnalysisException.php create mode 100644 tests/Fixtures/config-with-exclude-patterns.yml diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index 2dd1e8f..a65ef24 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -5,13 +5,13 @@ namespace Phauthentic\CodeQualityMetrics\Business\Cognitive; use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; +use Phauthentic\CodeQualityMetrics\CognitiveAnalysisException; use Phauthentic\CodeQualityMetrics\Config\ConfigService; use Phauthentic\CodeQualityMetrics\PhpParser\CognitiveMetricsVisitor; use PhpParser\Error; use PhpParser\NodeTraverserInterface; use PhpParser\Parser; use PhpParser\ParserFactory; -use RuntimeException; use SplFileInfo; /** @@ -84,7 +84,7 @@ protected function findMetrics(iterable $files): CognitiveMetricsCollection $code = file_get_contents($file->getRealPath()); if ($code === false) { - throw new RuntimeException("Could not read file: {$file->getRealPath()}"); + throw new CognitiveAnalysisException("Could not read file: {$file->getRealPath()}"); } $this->traverser->addVisitor($visitor); @@ -162,16 +162,19 @@ protected function findSourceFiles(string $path, array $exclude = []): iterable return $this->directoryScanner->scan([$path], ['^(?!.*\.php$).+'] + $exclude); // Exclude non-PHP files } + /** + * @throws CognitiveAnalysisException + */ protected function traverseAbstractSyntaxTree(string $code): void { try { $ast = $this->parser->parse($code); } catch (Error $e) { - throw new RuntimeException("Parse error: {$e->getMessage()}", 0, $e); + throw new CognitiveAnalysisException("Parse error: {$e->getMessage()}", 0, $e); } if ($ast === null) { - throw new RuntimeException("Could not parse the code."); + throw new CognitiveAnalysisException("Could not parse the code."); } $this->traverser->traverse($ast); diff --git a/src/CognitiveAnalysisException.php b/src/CognitiveAnalysisException.php new file mode 100644 index 0000000..b58c532 --- /dev/null +++ b/src/CognitiveAnalysisException.php @@ -0,0 +1,14 @@ +assertCount(23, $metricsCollection); } + public function testCollectWithExcludedClasses(): void + { + $configService = new ConfigService( + new Processor(), + new ConfigLoader(), + ); + + // It will exclude just the constructor methods + $configService->loadConfig(__DIR__ . '/../../../Fixtures/config-with-exclude-patterns.yml'); + + $metricsCollector = new CognitiveMetricsCollector( + new ParserFactory(), + new NodeTraverser(), + new DirectoryScanner(), + $configService, + ); + + $path = './tests/TestCode'; + + $metricsCollection = $metricsCollector->collect($path); + + $this->assertInstanceOf(CognitiveMetricsCollection::class, $metricsCollection); + $this->assertCount(22, $metricsCollection); + } + public function testCollectWithValidFilePath(): void { $path = './tests/TestCode/Paginator.php';