Skip to content

Commit ed8df17

Browse files
Code Coverage Support for the CognitiveMetrics Command (#57)
* Add code coverage support to CognitiveMetrics command * Add code coverage integration and improve metrics display
1 parent 72b54e8 commit ed8df17

15 files changed

+828
-209
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"bin/phpbench run benchmarks --report=aggregate"
8383
],
8484
"all": [
85+
"@csfix",
8586
"@cscheck",
8687
"@analyze",
8788
"@phpmd",

src/Application.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChangeCounter\ChangeCounterFactory;
88
use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator;
9+
use Phauthentic\CognitiveCodeAnalysis\Business\CodeCoverage\CodeCoverageFactory;
910
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Baseline;
1011
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector;
1112
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsSorter;
@@ -100,6 +101,9 @@ private function registerServices(): void
100101
$this->containerBuilder->register(CognitiveMetricsSorter::class, CognitiveMetricsSorter::class)
101102
->setPublic(true);
102103

104+
$this->containerBuilder->register(CodeCoverageFactory::class, CodeCoverageFactory::class)
105+
->setPublic(true);
106+
103107
$this->containerBuilder->register(Processor::class, Processor::class)
104108
->setPublic(true);
105109

@@ -222,6 +226,7 @@ private function registerCommands(): void
222226
new Reference(Baseline::class),
223227
new Reference(CognitiveMetricsReportHandler::class),
224228
new Reference(CognitiveMetricsSorter::class),
229+
new Reference(CodeCoverageFactory::class),
225230
])
226231
->setPublic(true);
227232

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\CognitiveCodeAnalysis\Business\CodeCoverage;
6+
7+
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
8+
9+
class CodeCoverageFactory
10+
{
11+
/**
12+
* @throws CognitiveAnalysisException
13+
*/
14+
public function createFromName(string $name, string $filePath): CoverageReportReaderInterface
15+
{
16+
return match (strtolower($name)) {
17+
'clover' => new CloverReader($filePath),
18+
'cobertura' => new CoberturaReader($filePath),
19+
default => throw new CognitiveAnalysisException("Unknown code coverage implementation: {$name}"),
20+
};
21+
}
22+
}

src/Business/Cognitive/CognitiveMetrics.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class CognitiveMetrics implements JsonSerializable
6464

6565
private ?HalsteadMetrics $halstead = null;
6666
private ?CyclomaticMetrics $cyclomatic = null;
67+
private ?float $coverage = null;
6768

6869
/**
6970
* @param array<string, mixed> $metrics
@@ -340,6 +341,16 @@ public function getScore(): float
340341
return $this->score;
341342
}
342343

344+
public function setCoverage(?float $coverage): void
345+
{
346+
$this->coverage = $coverage;
347+
}
348+
349+
public function getCoverage(): ?float
350+
{
351+
return $this->coverage;
352+
}
353+
343354
public function getLineCountWeightDelta(): ?Delta
344355
{
345356
return $this->lineCountWeightDelta;
@@ -435,6 +446,7 @@ public function toArray(): array
435446
'ifCountWeightDelta' => $this->ifCountWeightDelta,
436447
'ifNestingLevelWeightDelta' => $this->ifNestingLevelWeightDelta,
437448
'elseCountWeightDelta' => $this->elseCountWeightDelta,
449+
'coverage' => $this->coverage,
438450
];
439451
}
440452

src/Business/Cognitive/CognitiveMetricsCollector.php

Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig;
1313
use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService;
1414
use SplFileInfo;
15-
use Symfony\Component\Messenger\Exception\ExceptionInterface;
1615
use Symfony\Component\Messenger\MessageBusInterface;
1716
use Throwable;
1817

@@ -21,11 +20,6 @@
2120
*/
2221
class CognitiveMetricsCollector
2322
{
24-
/**
25-
* @var array<string, array<string, string>>|null Cached ignored items from the last parsing operation
26-
*/
27-
private ?array $ignoredItems = null;
28-
2923
public function __construct(
3024
protected readonly Parser $parser,
3125
protected readonly DirectoryScanner $directoryScanner,
@@ -40,7 +34,7 @@ public function __construct(
4034
* @param string $path
4135
* @param CognitiveConfig $config
4236
* @return CognitiveMetricsCollection
43-
* @throws CognitiveAnalysisException|ExceptionInterface
37+
* @throws CognitiveAnalysisException
4438
*/
4539
public function collect(string $path, CognitiveConfig $config): CognitiveMetricsCollection
4640
{
@@ -53,7 +47,7 @@ public function collect(string $path, CognitiveConfig $config): CognitiveMetrics
5347
* @param array<string> $paths Array of paths to process
5448
* @param CognitiveConfig $config
5549
* @return CognitiveMetricsCollection Merged collection of metrics from all paths
56-
* @throws CognitiveAnalysisException|ExceptionInterface
50+
* @throws CognitiveAnalysisException
5751
*/
5852
public function collectFromPaths(array $paths, CognitiveConfig $config): CognitiveMetricsCollection
5953
{
@@ -88,7 +82,6 @@ private function getCodeFromFile(SplFileInfo $file): string
8882
*
8983
* @param iterable<SplFileInfo> $files
9084
* @return CognitiveMetricsCollection
91-
* @throws ExceptionInterface
9285
*/
9386
private function findMetrics(iterable $files): CognitiveMetricsCollection
9487
{
@@ -101,9 +94,6 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection
10194
$this->getCodeFromFile($file)
10295
);
10396

104-
// Store ignored items from the parser
105-
$this->ignoredItems = $this->parser->getIgnored();
106-
10797
$fileCount++;
10898

10999
// Clear memory periodically to prevent memory leaks
@@ -197,6 +187,7 @@ private function isExcluded(string $classAndMethod): bool
197187
* @param string $path Path to the directory or file to scan
198188
* @param array<int, string> $exclude List of regx to exclude
199189
* @return iterable<mixed, SplFileInfo> An iterable of SplFileInfo objects
190+
* @throws CognitiveAnalysisException
200191
*/
201192
private function findSourceFiles(string $path, array $exclude = []): iterable
202193
{
@@ -206,36 +197,6 @@ private function findSourceFiles(string $path, array $exclude = []): iterable
206197
);
207198
}
208199

209-
/**
210-
* Get all ignored classes and methods from the last parsing operation.
211-
*
212-
* @return array<string, array<string, string>> Array with 'classes' and 'methods' keys
213-
*/
214-
public function getIgnored(): array
215-
{
216-
return $this->ignoredItems ?? ['classes' => [], 'methods' => []];
217-
}
218-
219-
/**
220-
* Get ignored classes from the last parsing operation.
221-
*
222-
* @return array<string, string> Array of ignored class FQCNs
223-
*/
224-
public function getIgnoredClasses(): array
225-
{
226-
return $this->ignoredItems['classes'] ?? [];
227-
}
228-
229-
/**
230-
* Get ignored methods from the last parsing operation.
231-
*
232-
* @return array<string, string> Array of ignored method keys (ClassName::methodName)
233-
*/
234-
public function getIgnoredMethods(): array
235-
{
236-
return $this->ignoredItems['methods'] ?? [];
237-
}
238-
239200
/**
240201
* Get the project root directory path.
241202
*

src/Business/MetricsFacade.php

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44

55
namespace Phauthentic\CognitiveCodeAnalysis\Business;
66

7-
use JsonException;
87
use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChangeCounter\ChangeCounterFactory;
98
use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator;
109
use Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter\ChurnExporterFactory;
1110
use Phauthentic\CognitiveCodeAnalysis\Business\CodeCoverage\CoverageReportReaderInterface;
11+
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetrics;
1212
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection;
1313
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector;
1414
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Exporter\CognitiveExporterFactory;
1515
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\ScoreCalculator;
16-
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
1716
use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig;
1817
use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService;
1918

@@ -81,14 +80,20 @@ public function getCognitiveMetrics(string $path): CognitiveMetricsCollection
8180
* Collects and returns cognitive metrics for multiple paths.
8281
*
8382
* @param array<string> $paths Array of file or directory paths to collect metrics from.
83+
* @param CoverageReportReaderInterface|null $coverageReader Optional coverage reader for coverage data.
8484
* @return CognitiveMetricsCollection The collected cognitive metrics from all paths.
8585
*/
86-
public function getCognitiveMetricsFromPaths(array $paths): CognitiveMetricsCollection
86+
public function getCognitiveMetricsFromPaths(array $paths, ?CoverageReportReaderInterface $coverageReader = null): CognitiveMetricsCollection
8787
{
8888
$metricsCollection = $this->cognitiveMetricsCollector->collectFromPaths($paths, $this->configService->getConfig());
8989

9090
foreach ($metricsCollection as $metric) {
9191
$this->scoreCalculator->calculate($metric, $this->configService->getConfig());
92+
93+
// Add coverage data if reader is provided
94+
if ($coverageReader !== null) {
95+
$this->addCoverageToMetric($metric, $coverageReader);
96+
}
9297
}
9398

9499
return $metricsCollection;
@@ -136,36 +141,6 @@ public function getConfig(): CognitiveConfig
136141
return $this->configService->getConfig();
137142
}
138143

139-
/**
140-
* Get all ignored classes and methods from the last metrics collection.
141-
*
142-
* @return array<string, array<string, string>> Array with 'classes' and 'methods' keys
143-
*/
144-
public function getIgnored(): array
145-
{
146-
return $this->cognitiveMetricsCollector->getIgnored();
147-
}
148-
149-
/**
150-
* Get ignored classes from the last metrics collection.
151-
*
152-
* @return array<string, string> Array of ignored class FQCNs
153-
*/
154-
public function getIgnoredClasses(): array
155-
{
156-
return $this->cognitiveMetricsCollector->getIgnoredClasses();
157-
}
158-
159-
/**
160-
* Get ignored methods from the last metrics collection.
161-
*
162-
* @return array<string, string> Array of ignored method keys (ClassName::methodName)
163-
*/
164-
public function getIgnoredMethods(): array
165-
{
166-
return $this->cognitiveMetricsCollector->getIgnoredMethods();
167-
}
168-
169144
/**
170145
* @param array<string, array<string, mixed>> $classes
171146
*/
@@ -188,4 +163,48 @@ public function exportMetricsReport(
188163
$exporter = $this->getCognitiveExporterFactory()->create($reportType);
189164
$exporter->export($metricsCollection, $filename);
190165
}
166+
167+
/**
168+
* Add coverage data to a metric
169+
*/
170+
private function addCoverageToMetric(
171+
CognitiveMetrics $metric,
172+
CoverageReportReaderInterface $coverageReader
173+
): void {
174+
// Strip leading backslash from class name for coverage lookup
175+
$className = ltrim($metric->getClass(), '\\');
176+
177+
// Try to get method-level coverage first
178+
$coverageDetails = $coverageReader->getCoverageDetails($className);
179+
if ($coverageDetails !== null) {
180+
$this->addMethodLevelCoverage($metric, $coverageDetails);
181+
return;
182+
}
183+
184+
// Fall back to class-level coverage if details not available
185+
$coverage = $coverageReader->getLineCoverage($className);
186+
if ($coverage !== null) {
187+
$metric->setCoverage($coverage);
188+
}
189+
}
190+
191+
/**
192+
* Add method-level coverage from coverage details
193+
*/
194+
private function addMethodLevelCoverage(
195+
CognitiveMetrics $metric,
196+
CodeCoverage\CoverageDetails $coverageDetails
197+
): void {
198+
$methods = $coverageDetails->getMethods();
199+
$methodName = $metric->getMethod();
200+
201+
if (isset($methods[$methodName])) {
202+
$methodCoverage = $methods[$methodName];
203+
$metric->setCoverage($methodCoverage->getLineRate());
204+
return;
205+
}
206+
207+
// Fall back to class-level coverage if method not found
208+
$metric->setCoverage($coverageDetails->getLineRate());
209+
}
191210
}

0 commit comments

Comments
 (0)