Skip to content

Commit 5194050

Browse files
Calculate Churn with Code Coverage (#49)
1 parent 63384f5 commit 5194050

18 files changed

Lines changed: 8090 additions & 40 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
infection.log
1414
phive.phar
1515
phpcca.phar
16+
/*.xml

composer.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
"name": "Florian Krämer"
2727
}
2828
],
29+
"suggest": {
30+
"ext-dom": "Required for some rules and code analysis features."
31+
},
2932
"require-dev": {
33+
"ext-dom": "*",
3034
"roave/security-advisories": "dev-latest",
3135
"phpunit/phpunit": "^11.3",
3236
"infection/infection": "^0.29.6",
@@ -40,7 +44,8 @@
4044
"bin-dir": "bin",
4145
"allow-plugins": {
4246
"infection/extension-installer": true
43-
}
47+
},
48+
"process-timeout": 0
4449
},
4550
"bin": [
4651
"bin/phpcca"
@@ -53,10 +58,10 @@
5358
"infection"
5459
],
5560
"test-coverage": [
56-
"phpunit --coverage-text"
61+
"phpunit --coverage-text --path-coverage"
5762
],
5863
"test-coverage-html": [
59-
"phpunit --coverage-html tmp/coverage/"
64+
"phpunit --coverage-html tmp/coverage/ --path-coverage"
6065
],
6166
"cscheck": [
6267
"phpcs src/ tests/ -s"

coverage.xml

Lines changed: 6395 additions & 0 deletions
Large diffs are not rendered by default.

docs/Churn-Finding-Hotspots.md

Lines changed: 133 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,61 +5,174 @@ Churn is a measure of how much code has changed over time. The `churn` command h
55
## Usage
66

77
```bash
8-
bin/phpcca churn <path-to-folder> [--config=<file>] [--git=<vcs>] [--debug]
8+
bin/phpcca churn <path-to-folder> [options]
99
```
1010

11+
**Options:**
1112
- `<path-to-folder>`: **Required.** Path to the PHP file or directory to analyze.
1213
- `--config, -c`: Path to a configuration file (optional).
1314
- `--vcs, -s`: Version control system to use for change detection (default: `git`).
15+
- `--since`: Start date for counting changes (default: `2000-01-01`).
1416
- `--report-type, -r`: Type of report to generate (`json`, `csv`, `html`).
1517
- `--report-file, -f`: File to save the report (default: `phpcca-churn-report.html`).
18+
- `--coverage-cobertura`: Path to Cobertura XML coverage file to include coverage analysis.
1619
- `--debug`: Enables debug output.
1720

1821
## What it does
1922

2023
1. **Collects cognitive metrics** for each class in the given path.
2124
2. **Counts how many times each file/class has changed** using your VCS (default: Git).
22-
3. **Calculates a churn score**:
23-
`churn = timesChanged * score`
24-
4. **Ranks classes by churn score** so you can focus on the most critical hotspots.
25+
3. **Calculates churn scores**:
26+
- **Standard Churn**: `churn = timesChanged × cognitiveScore`
27+
- **Risk Churn** (with coverage): `riskChurn = timesChanged × cognitiveScore × (1 - coverage)`
28+
4. **Assigns risk levels** based on churn and coverage (when coverage data is provided).
29+
5. **Ranks classes by churn score** so you can focus on the most critical hotspots.
2530

2631
⚠️ **For the time being only Git is supported as the VCS backend!** ⚠️
2732

28-
## Example Output
33+
## Basic Example Output
2934

3035
```
31-
+----------------------+--------------+--------+-------+
32-
| Class | TimesChanged | Score | Churn |
33-
+----------------------+--------------+--------+-------+
34-
| App\Service\Foo | 12 | 8 | 96 |
35-
| App\Controller\Bar | 7 | 10 | 70 |
36-
+----------------------+--------------+--------+-------+
36+
+-------------------------+-------+--------+---------------+
37+
| Class | Score | Churn | Times Changed |
38+
+-------------------------+-------+--------+---------------+
39+
| App\Service\Foo | 8.0 | 96.0 | 12 |
40+
| App\Controller\Bar | 10.0 | 70.0 | 7 |
41+
+-------------------------+-------+--------+---------------+
3742
```
3843

44+
## Coverage-Weighted Churn Analysis
45+
46+
### Generating Coverage Data
47+
48+
The tool supports both **Cobertura** and **Clover** XML coverage formats.
49+
50+
**Generate Cobertura coverage:**
51+
```bash
52+
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-cobertura=coverage.xml
53+
```
54+
55+
**Generate Clover coverage:**
56+
```bash
57+
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover=coverage.xml
58+
```
59+
60+
### Running Churn with Coverage
61+
62+
**Using Cobertura format:**
63+
```bash
64+
bin/phpcca churn src --coverage-cobertura=coverage.xml
65+
```
66+
67+
**Using Clover format:**
68+
```bash
69+
bin/phpcca churn src --coverage-clover=coverage.xml
70+
```
71+
72+
**Note:** The tool will auto-detect the format if you use a standard filename, but explicit format options are recommended for clarity.
73+
74+
### Enhanced Output with Coverage
75+
76+
When coverage data is provided, the output includes additional risk analysis columns:
77+
78+
```
79+
+-------------------------+-------+--------+------------+---------------+----------+------------+
80+
| Class | Score | Churn | Risk Churn | Times Changed | Coverage | Risk Level |
81+
+-------------------------+-------+--------+------------+---------------+----------+------------+
82+
| App\Service\Foo | 8.0 | 96.0 | 86.4 | 12 | 10.00% | CRITICAL |
83+
| App\Controller\Bar | 10.0 | 70.0 | 3.5 | 7 | 95.00% | LOW |
84+
| App\Model\Baz | 6.5 | 26.0 | 13.0 | 4 | 50.00% | MEDIUM |
85+
+-------------------------+-------+--------+------------+---------------+----------+------------+
86+
```
87+
88+
### How Risk Churn is Calculated
89+
90+
**Risk Churn** multiplies standard churn by the coverage gap:
91+
92+
```
93+
Risk Churn = Times Changed × Cognitive Score × (1 - Coverage)
94+
```
95+
96+
**Example:**
97+
- Class with 12 changes, score 8.0, and 10% coverage:
98+
- Standard Churn: `12 × 8.0 = 96.0`
99+
- Risk Churn: `12 × 8.0 × (1 - 0.10) = 86.4`
100+
101+
This formula prioritizes classes that are:
102+
- Frequently changed (high `timesChanged`)
103+
- Complex (high `cognitiveScore`)
104+
- Poorly tested (low `coverage`)
105+
106+
### Risk Level Thresholds
107+
108+
Risk levels help you prioritize which classes need immediate attention:
109+
110+
| Risk Level | Criteria | Action Required |
111+
|-----------|----------|-----------------|
112+
| **CRITICAL** | Churn > 30 AND Coverage < 50% | Urgent: Add tests and refactor immediately |
113+
| **HIGH** | Churn > 20 AND Coverage < 70% | High Priority: Increase test coverage |
114+
| **MEDIUM** | Churn > 10 AND Coverage < 80% | Medium Priority: Consider adding tests |
115+
| **LOW** | Everything else | Low Priority: Monitor |
116+
117+
### Interpreting the Results
118+
119+
**High Risk Churn** indicates:
120+
- Code that changes frequently without adequate test protection
121+
- Areas where bugs are most likely to be introduced
122+
- Prime candidates for refactoring and test coverage improvement
123+
124+
**Low Risk Churn** indicates:
125+
- Well-tested code that changes frequently (good!)
126+
- Stable code with high coverage (safe to modify)
127+
128+
### Use Cases
129+
130+
1. **Prioritize Testing Efforts**: Focus on CRITICAL and HIGH risk classes first
131+
2. **Refactoring Planning**: Target high churn + low coverage areas
132+
3. **Code Review Focus**: Extra scrutiny for changes in high-risk classes
133+
4. **Technical Debt Tracking**: Monitor risk levels over time
134+
39135
## Exporting Reports
40136

41-
You can export the churn report in various formats. The command supports three report types: `html`, `json`, and `csv`.
137+
You can export the churn report in various formats. The command supports four report types: `html`, `json`, `csv`, and `svg-treemap`.
42138

43139
You must use the `--report-type` option to specify the format and the `--report-file` option to specify the output file name together to generate a report.
44140

45141
```bash
46142
bin/phpcca churn <path-to-folder> --report-type=<report-type> --report-file=<filename>
47143
```
48144

49-
Supported report types:
145+
### Supported Report Types
146+
147+
| Format | Description | Use Case |
148+
|--------|-------------|----------|
149+
| `html` | Interactive HTML report | Easy sharing and viewing in browsers |
150+
| `json` | Structured JSON data | Integration with other tools, APIs |
151+
| `csv` | Comma-separated values | Import into spreadsheets, data analysis |
152+
| `svg-treemap` | Visual treemap representation | Visual overview of churn distribution |
50153

51-
- `html`: Generates an HTML report.
52-
- `json`: Generates a JSON report.
53-
- `csv`: Generates a CSV report.
154+
### Examples
54155

55-
## When to use
156+
```bash
157+
# Generate HTML report
158+
bin/phpcca churn src --report-type=html --report-file=churn-report.html
159+
160+
# Generate JSON report for CI/CD integration
161+
bin/phpcca churn src --report-type=json --report-file=churn-report.json
162+
163+
# Generate CSV for spreadsheet analysis
164+
bin/phpcca churn src --report-type=csv --report-file=churn-report.csv
165+
166+
# Generate SVG treemap for visual analysis
167+
bin/phpcca churn src --report-type=svg-treemap --report-file=churn-treemap.svg
168+
```
56169

57-
- To prioritize refactoring or testing efforts.
58-
- To identify risky or unstable parts of your codebase.
59-
- To monitor the impact of frequent changes on complex code.
170+
**Note:** Coverage data is currently only available in console output, not in exported reports.
60171

61172
## Notes
62173

63174
- Only classes with a valid class name are included in the results.
64175
- The command supports extensible VCS backends (default is Git).
65176
- For now only Git is supported.
177+
- Coverage data is optional; the command works with or without it.
178+
- When coverage is not found for a class, it assumes 0% coverage for risk calculation.

src/Business/Churn/ChurnCalculator.php

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

55
namespace Phauthentic\CognitiveCodeAnalysis\Business\Churn;
66

7+
use Phauthentic\CognitiveCodeAnalysis\Business\CodeCoverage\CoverageReportReaderInterface;
78
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection;
89

910
/**
@@ -15,13 +16,16 @@ class ChurnCalculator
1516
* Calculate the churn for each class based on the metrics collection.
1617
*
1718
* @param CognitiveMetricsCollection $metricsCollection
19+
* @param CoverageReportReaderInterface|null $coverageReader
1820
* @return array<string, array<string, mixed>>
1921
*/
20-
public function calculate(CognitiveMetricsCollection $metricsCollection): array
21-
{
22+
public function calculate(
23+
CognitiveMetricsCollection $metricsCollection,
24+
?CoverageReportReaderInterface $coverageReader = null
25+
): array {
2226
$classes = [];
2327
$classes = $this->groupByClasses($metricsCollection, $classes);
24-
$classes = $this->calculateChurn($classes);
28+
$classes = $this->calculateChurn($classes, $coverageReader);
2529

2630
return $this->sortClassesByChurnDescending($classes);
2731
}
@@ -41,12 +45,29 @@ public function sortClassesByChurnDescending(array $classes): array
4145

4246
/**
4347
* @param array<string, array<string, mixed>> $classes
48+
* @param CoverageReportReaderInterface|null $coverageReader
4449
* @return array<string, array<string, mixed>>
4550
*/
46-
public function calculateChurn(array $classes): array
51+
public function calculateChurn(array $classes, ?CoverageReportReaderInterface $coverageReader = null): array
4752
{
4853
foreach ($classes as $className => $data) {
54+
// Calculate standard churn
4955
$classes[$className]['churn'] = $data['timesChanged'] * $data['score'];
56+
57+
// Add coverage information if available
58+
$coverage = null;
59+
$riskChurn = null;
60+
$riskLevel = null;
61+
62+
if ($coverageReader !== null) {
63+
$coverage = $this->getCoverageForClass($className, $coverageReader);
64+
$riskChurn = $data['timesChanged'] * $data['score'] * (1 - $coverage);
65+
$riskLevel = $this->calculateRiskLevel($classes[$className]['churn'], $coverage);
66+
}
67+
68+
$classes[$className]['coverage'] = $coverage;
69+
$classes[$className]['riskChurn'] = $riskChurn;
70+
$classes[$className]['riskLevel'] = $riskLevel;
5071
}
5172

5273
return $classes;
@@ -77,4 +98,45 @@ public function groupByClasses(CognitiveMetricsCollection $metricsCollection, ar
7798
}
7899
return $classes;
79100
}
101+
102+
/**
103+
* Get coverage for a class, normalizing the class name
104+
*
105+
* @param string $className
106+
* @param CoverageReportReaderInterface $coverageReader
107+
* @return float Coverage value between 0.0 and 1.0
108+
*/
109+
private function getCoverageForClass(string $className, CoverageReportReaderInterface $coverageReader): float
110+
{
111+
// Remove leading backslash for coverage lookup
112+
$lookupClassName = ltrim($className, '\\');
113+
$coverage = $coverageReader->getLineCoverage($lookupClassName);
114+
115+
// Return coverage or 0.0 if not found
116+
return $coverage ?? 0.0;
117+
}
118+
119+
/**
120+
* Calculate risk level based on churn and coverage
121+
*
122+
* @param float $churn
123+
* @param float $coverage
124+
* @return string Risk level: CRITICAL, HIGH, MEDIUM, or LOW
125+
*/
126+
private function calculateRiskLevel(float $churn, float $coverage): string
127+
{
128+
if ($churn > 30 && $coverage < 0.5) {
129+
return 'CRITICAL';
130+
}
131+
132+
if ($churn > 20 && $coverage < 0.7) {
133+
return 'HIGH';
134+
}
135+
136+
if ($churn > 10 && $coverage < 0.8) {
137+
return 'MEDIUM';
138+
}
139+
140+
return 'LOW';
141+
}
80142
}

0 commit comments

Comments
 (0)