Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/php/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion src/Business/Cognitive/Report/CognitiveReportFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
169 changes: 126 additions & 43 deletions src/Business/Cognitive/Report/HtmlReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
{
Expand Down Expand Up @@ -69,17 +72,68 @@ public function generateMetricRow(int $count, float $weight, ?Delta $delta): str
return $metricRow . '</td>';
}

/**
* 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) {
?>
<table class="table table-bordered">
<thead>
<tr>
<th colspan="<?php echo count($this->header); ?>" class="table-primary"><?php echo $this->escape((string)$class); ?></th>
</tr>
<?php echo $this->renderHeaderRow(false); ?>
</thead>
<tbody>
<?php foreach ($methods as $data) : ?>
<?php echo $this->renderMethodRow($data, false); ?>
<?php endforeach; ?>
</tbody>
</table>
<?php
}
$this->renderDocumentEnd();

return $this->captureOutput();
}

private function generateSingleTableHtml(CognitiveMetricsCollection $metrics): string
{
$groupedByClass = $metrics->groupBy('class');
$totalMethods = count($metrics);

ob_start();
$this->renderDocumentStart(count($groupedByClass));
?>
<h2 class="h5 mb-3">All Methods (<?php echo $totalMethods; ?> total)</h2>
<table class="table table-bordered">
<thead>
<?php echo $this->renderHeaderRow(true); ?>
</thead>
<tbody>
<?php foreach ($metrics as $data) : ?>
<?php echo $this->renderMethodRow($data, true); ?>
<?php endforeach; ?>
</tbody>
</table>
<?php
$this->renderDocumentEnd();

return $this->captureOutput();
}

private function renderDocumentStart(int $classCount): void
{
?>
<!DOCTYPE html>
<html lang="en">
Expand All @@ -93,44 +147,73 @@ private function generateHtml(CognitiveMetricsCollection $metrics): string
<div class="container-fluid">
<h1 class="mb-4">Cognitive Metrics Report - <?php echo (new Datetime())->format('Y-m-d H:i:s') ?></h1>
<p>
This report contains the cognitive complexity metrics for the analyzed code in <?php echo count($groupedByClass); ?> classes.
This report contains the cognitive complexity metrics for the analyzed code in <?php echo $classCount; ?> classes.
</p>
<?php
}

<?php foreach ($groupedByClass as $class => $methods) : ?>
<table class="table table-bordered">
<thead>
<tr>
<th colspan="10" class="table-primary"><?php echo $this->escape((string)$class); ?></th>
</tr>
<tr>
<?php foreach ($this->header as $column) : ?>
<th class="table-secondary"><?php echo $this->escape($column); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($methods as $data) : ?>
<tr>
<td><?php echo $this->escape($data->getMethod()); ?></td>
<?php echo $this->generateMetricRow($data->getLineCount(), $data->getLineCountWeight(), $data->getLineCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getArgCount(), $data->getArgCountWeight(), $data->getArgCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getIfCount(), $data->getIfCountWeight(), $data->getIfCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getIfNestingLevel(), $data->getIfNestingLevelWeight(), $data->getIfNestingLevelWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getElseCount(), $data->getElseCountWeight(), $data->getElseCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getReturnCount(), $data->getReturnCountWeight(), $data->getReturnCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getVariableCount(), $data->getVariableCountWeight(), $data->getVariableCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getPropertyCallCount(), $data->getPropertyCallCountWeight(), $data->getPropertyCallCountWeightDelta()); ?>
<td><?php echo $this->formatNumber($data->getScore()); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach; ?>

private function renderDocumentEnd(): void
{
?>
</div>
</body>
</html>
<?php
}

private function renderHeaderRow(bool $includeClass): string
{
ob_start();
?>
<tr>
<?php if ($includeClass) : ?>
<th class="table-secondary">Class</th>
<?php endif; ?>
<?php foreach ($this->header as $column) : ?>
<th class="table-secondary"><?php echo $this->escape($column); ?></th>
<?php endforeach; ?>
</tr>
<?php
$result = ob_get_clean();

if ($result === false) {
throw new CognitiveAnalysisException('Could not generate HTML header row');
}

return $result;
}

private function renderMethodRow(CognitiveMetrics $data, bool $includeClass): string
{
ob_start();
?>
<tr>
<?php if ($includeClass) : ?>
<td><?php echo $this->escape($data->getClass()); ?></td>
<?php endif; ?>
<td><?php echo $this->escape($data->getMethod()); ?></td>
<?php echo $this->generateMetricRow($data->getLineCount(), $data->getLineCountWeight(), $data->getLineCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getArgCount(), $data->getArgCountWeight(), $data->getArgCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getIfCount(), $data->getIfCountWeight(), $data->getIfCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getIfNestingLevel(), $data->getIfNestingLevelWeight(), $data->getIfNestingLevelWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getElseCount(), $data->getElseCountWeight(), $data->getElseCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getReturnCount(), $data->getReturnCountWeight(), $data->getReturnCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getVariableCount(), $data->getVariableCountWeight(), $data->getVariableCountWeightDelta()); ?>
<?php echo $this->generateMetricRow($data->getPropertyCallCount(), $data->getPropertyCallCountWeight(), $data->getPropertyCallCountWeightDelta()); ?>
<td><?php echo $this->formatNumber($data->getScore()); ?></td>
</tr>
<?php
$result = ob_get_clean();

if ($result === false) {
throw new CognitiveAnalysisException('Could not generate HTML method row');
}

return $result;
}

private function captureOutput(): string
{
$result = ob_get_clean();

if ($result === false) {
Expand Down
99 changes: 66 additions & 33 deletions src/Business/Cognitive/Report/MarkdownReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class MarkdownReport implements ReportGeneratorInterface, StreamableReportInterf
use MarkdownFormatterTrait;

private CognitiveConfig $config;
/** @var resource|false|null */
/** @var resource|null */
private $fileHandle = null;
private bool $isStreaming = false;
/** @var array<int|string> */
Expand Down Expand Up @@ -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 = [];

Expand All @@ -447,21 +448,38 @@ 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);
}

/**
* @throws CognitiveAnalysisException
*/
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) {
Expand All @@ -471,54 +489,69 @@ 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);
}

/**
* @throws CognitiveAnalysisException
*/
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;
Expand Down
Loading
Loading