diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index d6bcacf..fca68de 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.3-cli +FROM php:8.5-cli RUN apt-get update \ && apt-get install -y libzip-dev zip git wget gpg \ diff --git a/src/Business/Cognitive/Report/CognitiveReportFactory.php b/src/Business/Cognitive/Report/CognitiveReportFactory.php index a1fccc3..c8511d7 100644 --- a/src/Business/Cognitive/Report/CognitiveReportFactory.php +++ b/src/Business/Cognitive/Report/CognitiveReportFactory.php @@ -37,7 +37,7 @@ public function create(string $type): ReportGeneratorInterface $builtIn = match ($type) { 'json' => new JsonReport(), 'csv' => new CsvReport(), - 'html' => new HtmlReport(), + 'html' => new HtmlReport($config), 'markdown' => new MarkdownReport($config), 'checkstyle' => new CheckstyleReport($config), 'junit' => new JUnitReport($config), diff --git a/src/Business/Cognitive/Report/HtmlReport.php b/src/Business/Cognitive/Report/HtmlReport.php index 199a64b..2b0fb9d 100644 --- a/src/Business/Cognitive/Report/HtmlReport.php +++ b/src/Business/Cognitive/Report/HtmlReport.php @@ -4,10 +4,12 @@ namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetrics; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Delta; use Phauthentic\CognitiveCodeAnalysis\Business\Utility\Datetime; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; +use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; /** * HtmlReport class for exporting metrics as an HTML file. @@ -27,15 +29,16 @@ class HtmlReport implements ReportGeneratorInterface 'Return Count', 'Variable Count', 'Property Call Count', - 'Combined Cognitive Complexity' + 'Combined Cognitive Complexity', ]; + public function __construct( + private readonly CognitiveConfig $config, + ) { + } + /** * Export metrics to an HTML file using Bootstrap 5. - * - * @param CognitiveMetricsCollection $metrics - * @param string $filename - * @return void */ public function export(CognitiveMetricsCollection $metrics, string $filename): void { @@ -69,17 +72,68 @@ public function generateMetricRow(int $count, float $weight, ?Delta $delta): str return $metricRow . ''; } - /** - * Generate HTML content using the metrics data. - * - * @param CognitiveMetricsCollection $metrics - * @return string - */ private function generateHtml(CognitiveMetricsCollection $metrics): string + { + return $this->config->groupByClass + ? $this->generateGroupedHtml($metrics) + : $this->generateSingleTableHtml($metrics); + } + + private function generateGroupedHtml(CognitiveMetricsCollection $metrics): string + { + $groupedByClass = $metrics->groupBy('class'); + + ob_start(); + $this->renderDocumentStart(count($groupedByClass)); + foreach ($groupedByClass as $class => $methods) { + ?> + + + + + + renderHeaderRow(false); ?> + + + + renderMethodRow($data, false); ?> + + +
escape((string)$class); ?>
+ renderDocumentEnd(); + + return $this->captureOutput(); + } + + private function generateSingleTableHtml(CognitiveMetricsCollection $metrics): string { $groupedByClass = $metrics->groupBy('class'); + $totalMethods = count($metrics); ob_start(); + $this->renderDocumentStart(count($groupedByClass)); + ?> +

All Methods ( total)

+ + + renderHeaderRow(true); ?> + + + + renderMethodRow($data, true); ?> + + +
+ renderDocumentEnd(); + + return $this->captureOutput(); + } + + private function renderDocumentStart(int $classCount): void + { ?> @@ -93,44 +147,73 @@ private function generateHtml(CognitiveMetricsCollection $metrics): string

Cognitive Metrics Report - format('Y-m-d H:i:s') ?>

- This report contains the cognitive complexity metrics for the analyzed code in classes. + This report contains the cognitive complexity metrics for the analyzed code in classes.

+ $methods) : ?> - - - - - - - header as $column) : ?> - - - - - - - - - generateMetricRow($data->getLineCount(), $data->getLineCountWeight(), $data->getLineCountWeightDelta()); ?> - generateMetricRow($data->getArgCount(), $data->getArgCountWeight(), $data->getArgCountWeightDelta()); ?> - generateMetricRow($data->getIfCount(), $data->getIfCountWeight(), $data->getIfCountWeightDelta()); ?> - generateMetricRow($data->getIfNestingLevel(), $data->getIfNestingLevelWeight(), $data->getIfNestingLevelWeightDelta()); ?> - generateMetricRow($data->getElseCount(), $data->getElseCountWeight(), $data->getElseCountWeightDelta()); ?> - generateMetricRow($data->getReturnCount(), $data->getReturnCountWeight(), $data->getReturnCountWeightDelta()); ?> - generateMetricRow($data->getVariableCount(), $data->getVariableCountWeight(), $data->getVariableCountWeightDelta()); ?> - generateMetricRow($data->getPropertyCallCount(), $data->getPropertyCallCountWeight(), $data->getPropertyCallCountWeightDelta()); ?> - - - - -
escape((string)$class); ?>
escape($column); ?>
escape($data->getMethod()); ?>formatNumber($data->getScore()); ?>
- - + private function renderDocumentEnd(): void + { + ?>
+ + + Class + + header as $column) : ?> + escape($column); ?> + + + + + + escape($data->getClass()); ?> + + escape($data->getMethod()); ?> + generateMetricRow($data->getLineCount(), $data->getLineCountWeight(), $data->getLineCountWeightDelta()); ?> + generateMetricRow($data->getArgCount(), $data->getArgCountWeight(), $data->getArgCountWeightDelta()); ?> + generateMetricRow($data->getIfCount(), $data->getIfCountWeight(), $data->getIfCountWeightDelta()); ?> + generateMetricRow($data->getIfNestingLevel(), $data->getIfNestingLevelWeight(), $data->getIfNestingLevelWeightDelta()); ?> + generateMetricRow($data->getElseCount(), $data->getElseCountWeight(), $data->getElseCountWeightDelta()); ?> + generateMetricRow($data->getReturnCount(), $data->getReturnCountWeight(), $data->getReturnCountWeightDelta()); ?> + generateMetricRow($data->getVariableCount(), $data->getVariableCountWeight(), $data->getVariableCountWeightDelta()); ?> + generateMetricRow($data->getPropertyCallCount(), $data->getPropertyCallCountWeight(), $data->getPropertyCallCountWeightDelta()); ?> + formatNumber($data->getScore()); ?> + + */ @@ -428,11 +428,12 @@ private function generateSingleTableMarkdown(CognitiveMetricsCollection $metrics */ public function startReport(string $filename): void { - $this->fileHandle = fopen($filename, 'wb'); - if ($this->fileHandle === false) { + $fileHandle = fopen($filename, 'wb'); + if ($fileHandle === false) { throw new CognitiveAnalysisException(sprintf('Could not open file %s for writing', $filename)); } + $this->fileHandle = $fileHandle; $this->isStreaming = true; $this->processedClasses = []; @@ -447,7 +448,13 @@ public function startReport(string $filename): void $header .= "---\n\n"; - fwrite($this->fileHandle, $header); + if (!$this->config->groupByClass) { + $header .= "## All Methods\n\n"; + $header .= $this->buildTableHeaderWithClass() . "\n"; + $header .= $this->buildTableSeparatorWithClass() . "\n"; + } + + fwrite($fileHandle, $header); } /** @@ -455,13 +462,24 @@ public function startReport(string $filename): void */ public function writeMetricBatch(CognitiveMetricsCollection $batch): void { - if (!$this->isStreaming || $this->fileHandle === null) { + $fileHandle = $this->fileHandle; + if (!$this->isStreaming || $fileHandle === null) { throw new CognitiveAnalysisException('Streaming not started. Call startReport() first.'); } - // Type guard: fileHandle is guaranteed to be resource at this point - assert($this->fileHandle !== false); + if (!$this->config->groupByClass) { + $this->writeSingleTableBatch($batch, $fileHandle); + return; + } + + $this->writeGroupedByClassBatch($batch, $fileHandle); + } + /** + * @param resource $fileHandle + */ + private function writeGroupedByClassBatch(CognitiveMetricsCollection $batch, $fileHandle): void + { $groupedByClass = $batch->groupBy('class'); foreach ($groupedByClass as $class => $methods) { @@ -471,39 +489,56 @@ public function writeMetricBatch(CognitiveMetricsCollection $batch): void continue; } - // Check if we've already processed this class if (in_array($class, $this->processedClasses, true)) { continue; } $this->processedClasses[] = $class; - // Get file path from first method in the collection - $firstMethod = null; - foreach ($filteredMethods as $method) { - $firstMethod = $method; - break; - } + fwrite($fileHandle, $this->buildClassSection((string) $class, $filteredMethods)); + } + } - $classSection = "* **Class:** " . $this->escape((string)$class) . "\n"; - if ($firstMethod !== null) { - $classSection .= "* **File:** " . $this->escape($firstMethod->getFileName()) . "\n"; - } - $classSection .= "\n"; + private function buildClassSection(string $class, CognitiveMetricsCollection $filteredMethods): string + { + $firstMethod = null; + foreach ($filteredMethods as $method) { + $firstMethod = $method; + break; + } - // Table header and separator - $classSection .= $this->buildTableHeader() . "\n"; - $classSection .= $this->buildTableSeparator() . "\n"; + $classSection = "* **Class:** " . $this->escape($class) . "\n"; + if ($firstMethod !== null) { + $classSection .= "* **File:** " . $this->escape($firstMethod->getFileName()) . "\n"; + } + $classSection .= "\n"; + $classSection .= $this->buildTableHeader() . "\n"; + $classSection .= $this->buildTableSeparator() . "\n"; - // Table rows - foreach ($filteredMethods as $data) { - $classSection .= $this->buildTableRow($data) . "\n"; - } + foreach ($filteredMethods as $data) { + $classSection .= $this->buildTableRow($data) . "\n"; + } - $classSection .= "\n---\n\n"; + return $classSection . "\n---\n\n"; + } - fwrite($this->fileHandle, $classSection); + /** + * @param resource $fileHandle + */ + private function writeSingleTableBatch(CognitiveMetricsCollection $batch, $fileHandle): void + { + $filteredMetrics = $this->filterMetrics($batch); + $rows = ''; + + foreach ($filteredMetrics as $data) { + $rows .= $this->buildTableRowWithClass($data) . "\n"; } + + if ($rows === '') { + return; + } + + fwrite($fileHandle, $rows); } /** @@ -511,14 +546,12 @@ public function writeMetricBatch(CognitiveMetricsCollection $batch): void */ public function finalizeReport(): void { - if (!$this->isStreaming || $this->fileHandle === null) { + $fileHandle = $this->fileHandle; + if (!$this->isStreaming || $fileHandle === null) { throw new CognitiveAnalysisException('Streaming not started or file handle not available.'); } - // Type guard: fileHandle is guaranteed to be resource at this point - assert($this->fileHandle !== false); - - fclose($this->fileHandle); + fclose($fileHandle); $this->isStreaming = false; $this->fileHandle = null; diff --git a/tests/Unit/Business/Cognitive/Report/HtmlReporterContent.html b/tests/Unit/Business/Cognitive/Report/HtmlReporterContent.html index 15abd69..9b71551 100644 --- a/tests/Unit/Business/Cognitive/Report/HtmlReporterContent.html +++ b/tests/Unit/Business/Cognitive/Report/HtmlReporterContent.html @@ -12,34 +12,32 @@

Cognitive Metrics Report - 2023-10-01 12:00:00

This report contains the cognitive complexity metrics for the analyzed code in 1 classes.

- - - - - - - - - - - - - - - - - - - - +
TestClass
MethodLine CountArgument CountIf CountIf Nesting LevelElse CountReturn CountVariable CountProperty Call CountCombined Cognitive Complexity
+ + + + + + + + + + + + + + + + + + - - - + + + -
TestClass
MethodLine CountArgument CountIf CountIf Nesting LevelElse CountReturn CountVariable CountProperty Call CountCombined Cognitive Complexity
testMethod10 (0.500) 2 (0.300) 4 (0.600) 2 (0.500) 1 (0.200) 1 (0.200) 5 (0.400) 3 (0.300) 0.000
testMethod10 (0.500) 2 (0.300) 4 (0.600) 2 (0.500) 1 (0.200) 1 (0.200) 5 (0.400) 3 (0.300) 0.000
- - + + \ No newline at end of file diff --git a/tests/Unit/Business/Cognitive/Report/HtmlReporterContent_SingleTable.html b/tests/Unit/Business/Cognitive/Report/HtmlReporterContent_SingleTable.html new file mode 100644 index 0000000..597c46d --- /dev/null +++ b/tests/Unit/Business/Cognitive/Report/HtmlReporterContent_SingleTable.html @@ -0,0 +1,53 @@ + + + + + + Cognitive Complexity Metrics Report + + + +
+

Cognitive Metrics Report - 2023-10-01 12:00:00

+

+ This report contains the cognitive complexity metrics for the analyzed code in 2 classes. +

+

All Methods (3 total)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassMethodLine CountArgument CountIf CountIf Nesting LevelElse CountReturn CountVariable CountProperty Call CountCombined Cognitive Complexity
TestClasstestMethod10 (0.500) 2 (0.300) 4 (0.600) 2 (0.500) 1 (0.200) 1 (0.200) 5 (0.400) 3 (0.300) 0.300
TestClassanotherMethod5 (0.200) 1 (0.100) 1 (0.200) 1 (0.100) 0 (0.000) 1 (0.100) 2 (0.100) 1 (0.100) 0.050
AnotherClasscomplexMethod20 (1.000) 4 (0.600) 8 (1.200) 3 (1.000) 4 (0.600) 3 (0.500) 10 (0.800) 5 (0.500) 0.800
+
+ + + \ No newline at end of file diff --git a/tests/Unit/Business/Cognitive/Report/HtmlReporterTest.php b/tests/Unit/Business/Cognitive/Report/HtmlReporterTest.php index 254ad35..908f55d 100644 --- a/tests/Unit/Business/Cognitive/Report/HtmlReporterTest.php +++ b/tests/Unit/Business/Cognitive/Report/HtmlReporterTest.php @@ -8,19 +8,21 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report\HtmlReport; use Phauthentic\CognitiveCodeAnalysis\Business\Utility\Datetime; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigLoader; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\Processor; class HtmlReporterTest extends TestCase { - private HtmlReport $csvExporter; private string $filename; protected function setUp(): void { parent::setUp(); - $this->csvExporter = new HtmlReport(); - $this->filename = sys_get_temp_dir() . '/test_metrics.csv'; + $this->filename = sys_get_temp_dir() . '/test_metrics.html'; Datetime::$fixedDate = '2023-10-01 12:00:00'; } @@ -30,14 +32,79 @@ protected function tearDown(): void if (file_exists($this->filename)) { unlink($this->filename); } - DateTime::$fixedDate = null; + Datetime::$fixedDate = null; + } + + /** + * @return array + */ + public static function configurationProvider(): array + { + return [ + 'Grouped by class' => ['all-metrics-config.yml', 'HtmlReporterContent.html', true], + 'Single table' => ['single-table-config.yml', 'HtmlReporterContent_SingleTable.html', false], + ]; } #[Test] - public function testExportCreatesFile(): void + #[DataProvider('configurationProvider')] + public function testExportWithConfiguration(string $configFile, string $expectedFile, bool $useSimpleMetrics): void + { + $config = $this->loadConfig($configFile); + $exporter = new HtmlReport($config); + $metricsCollection = $useSimpleMetrics + ? $this->createSimpleMetricsCollection() + : $this->createMultiClassMetricsCollection(); + + $exporter->export($metricsCollection, $this->filename); + + $this->assertFileEquals(__DIR__ . '/' . $expectedFile, $this->filename); + } + + private function loadConfig(string $configFile): \Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig + { + $configService = new ConfigService( + new Processor(), + new ConfigLoader() + ); + $configService->loadConfig(__DIR__ . '/../../../../Fixtures/' . $configFile); + + return $configService->getConfig(); + } + + private function createSimpleMetricsCollection(): CognitiveMetricsCollection + { + $metricsCollection = new CognitiveMetricsCollection(); + $metricsCollection->add(new CognitiveMetrics([ + 'class' => 'TestClass', + 'method' => 'testMethod', + 'file' => 'TestClass.php', + 'lineCount' => 10, + 'argCount' => 2, + 'returnCount' => 1, + 'variableCount' => 5, + 'propertyCallCount' => 3, + 'ifCount' => 4, + 'ifNestingLevel' => 2, + 'elseCount' => 1, + 'lineCountWeight' => 0.5, + 'argCountWeight' => 0.3, + 'returnCountWeight' => 0.2, + 'variableCountWeight' => 0.4, + 'propertyCallCountWeight' => 0.3, + 'ifCountWeight' => 0.6, + 'ifNestingLevelWeight' => 0.5, + 'elseCountWeight' => 0.2, + ])); + + return $metricsCollection; + } + + private function createMultiClassMetricsCollection(): CognitiveMetricsCollection { $metricsCollection = new CognitiveMetricsCollection(); - $metrics = new CognitiveMetrics([ + + $metrics1 = new CognitiveMetrics([ 'class' => 'TestClass', 'method' => 'testMethod', 'file' => 'TestClass.php', @@ -58,14 +125,57 @@ public function testExportCreatesFile(): void 'ifNestingLevelWeight' => 0.5, 'elseCountWeight' => 0.2, ]); + $metrics1->setScore(0.3); + $metricsCollection->add($metrics1); - $metricsCollection->add($metrics); + $metrics2 = new CognitiveMetrics([ + 'class' => 'TestClass', + 'method' => 'anotherMethod', + 'file' => 'TestClass.php', + 'lineCount' => 5, + 'argCount' => 1, + 'returnCount' => 1, + 'variableCount' => 2, + 'propertyCallCount' => 1, + 'ifCount' => 1, + 'ifNestingLevel' => 1, + 'elseCount' => 0, + 'lineCountWeight' => 0.2, + 'argCountWeight' => 0.1, + 'returnCountWeight' => 0.1, + 'variableCountWeight' => 0.1, + 'propertyCallCountWeight' => 0.1, + 'ifCountWeight' => 0.2, + 'ifNestingLevelWeight' => 0.1, + 'elseCountWeight' => 0.0, + ]); + $metrics2->setScore(0.05); + $metricsCollection->add($metrics2); - $this->csvExporter->export($metricsCollection, $this->filename); + $metrics3 = new CognitiveMetrics([ + 'class' => 'AnotherClass', + 'method' => 'complexMethod', + 'file' => 'AnotherClass.php', + 'lineCount' => 20, + 'argCount' => 4, + 'returnCount' => 3, + 'variableCount' => 10, + 'propertyCallCount' => 5, + 'ifCount' => 8, + 'ifNestingLevel' => 3, + 'elseCount' => 4, + 'lineCountWeight' => 1.0, + 'argCountWeight' => 0.6, + 'returnCountWeight' => 0.5, + 'variableCountWeight' => 0.8, + 'propertyCallCountWeight' => 0.5, + 'ifCountWeight' => 1.2, + 'ifNestingLevelWeight' => 1.0, + 'elseCountWeight' => 0.6, + ]); + $metrics3->setScore(0.8); + $metricsCollection->add($metrics3); - $this->assertFileEquals( - __DIR__ . '/HtmlReporterContent.html', - $this->filename - ); + return $metricsCollection; } } diff --git a/tests/Unit/Business/Cognitive/Report/MarkdownReporterContent_SingleTableStreaming.md b/tests/Unit/Business/Cognitive/Report/MarkdownReporterContent_SingleTableStreaming.md new file mode 100644 index 0000000..43e6c1c --- /dev/null +++ b/tests/Unit/Business/Cognitive/Report/MarkdownReporterContent_SingleTableStreaming.md @@ -0,0 +1,13 @@ +# Cognitive Metrics Report + +**Generated:** 2023-10-01 12:00:00 + +--- + +## All Methods + +| Class | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| TestClass | testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | +| TestClass | anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 185.470 | 6.250 | 1,159.188 | 2 (low) | +| AnotherClass | complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | diff --git a/tests/Unit/Business/Cognitive/Report/MarkdownReporterTest.php b/tests/Unit/Business/Cognitive/Report/MarkdownReporterTest.php index 263d49c..5576578 100644 --- a/tests/Unit/Business/Cognitive/Report/MarkdownReporterTest.php +++ b/tests/Unit/Business/Cognitive/Report/MarkdownReporterTest.php @@ -72,6 +72,28 @@ public function testExportWithConfiguration(string $configFile, string $expected ); } + #[Test] + public function testStreamingExportWithSingleTableConfiguration(): void + { + $configService = new ConfigService( + new Processor(), + new ConfigLoader() + ); + $configService->loadConfig(__DIR__ . '/../../../../Fixtures/single-table-config.yml'); + $config = $configService->getConfig(); + + $metricsCollection = $this->createTestMetricsCollection(); + $exporter = new MarkdownReport($config); + $exporter->startReport($this->filename); + $exporter->writeMetricBatch($metricsCollection); + $exporter->finalizeReport(); + + $this->assertFileEquals( + __DIR__ . '/MarkdownReporterContent_SingleTableStreaming.md', + $this->filename + ); + } + private function createTestMetricsCollection(): CognitiveMetricsCollection { $metricsCollection = new CognitiveMetricsCollection();