Skip to content

Commit d4d8c09

Browse files
feat: Added method and class method average cyclomatic complexity insights (#670)
1 parent 0c943be commit d4d8c09

11 files changed

+567
-17
lines changed

Diff for: docs/insights/complexity.md

+35-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Complexity
22

3-
For now the Complexity section is only one Insight in one Metric:
3+
For now the Complexity section is only one Metric consisting of multiple insights:
44

55
* `NunoMaduro\PhpInsights\Domain\Metrics\Complexity\Complexity` <Badge text="Complexity" type="warn" vertical="middle"/>
66

7-
## Cyclomatic Complexity is high <Badge text="^1.0"/> <Badge text="Complexity" type="warn"/>
7+
## Class Cyclomatic Complexity is high <Badge text="^1.0"/> <Badge text="Complexity" type="warn"/>
88

9-
This insight checks complexity cyclomatic on your classes, the lower the score the easier your code is to understand. It raises an issue if complexity is over `5`.
9+
This insight checks total method cyclomatic complexity of each class, the lower the score the easier your code is to understand. It raises an issue if complexity is over `5`.
1010

1111
**Insight Class**: `NunoMaduro\PhpInsights\Domain\Insights\CyclomaticComplexityIsHigh`
1212

@@ -20,6 +20,38 @@ This insight checks complexity cyclomatic on your classes, the lower the score t
2020
```
2121
</details>
2222

23+
## Average Class Method Cyclomatic Complexity is high <Badge text="^2.12"/> <Badge text="Complexity" type="warn"/>
24+
25+
This insight checks average class method cyclomatic complexity, the lower the score the easier your code is to understand. It raises an issue if complexity is over `5.0`.
26+
27+
**Insight Class**: `NunoMaduro\PhpInsights\Domain\Insights\ClassMethodAverageCyclomaticComplexityIsHigh`
28+
29+
<details>
30+
<summary>Configuration</summary>
31+
32+
```php
33+
\NunoMaduro\PhpInsights\Domain\Insights\ClassMethodAverageCyclomaticComplexityIsHigh::class => [
34+
'maxClassMethodAverageComplexity' => 5.0,
35+
]
36+
```
37+
</details>
38+
39+
## Method Cyclomatic Complexity is high <Badge text="^2.12"/> <Badge text="Complexity" type="warn"/>
40+
41+
This insight checks cyclomatic complexity of your methods, the lower the score the easier your code is to understand. It raises an issue if complexity is over `5`.
42+
43+
**Insight Class**: `NunoMaduro\PhpInsights\Domain\Insights\MethodCyclomaticComplexityIsHigh`
44+
45+
<details>
46+
<summary>Configuration</summary>
47+
48+
```php
49+
\NunoMaduro\PhpInsights\Domain\Insights\MethodCyclomaticComplexityIsHigh::class => [
50+
'maxMethodComplexity' => 5,
51+
]
52+
```
53+
</details>
54+
2355
<!--
2456
Insight template
2557
## <Badge text="^1.0"/> <Badge text="Complexity" type="warn"/>

Diff for: src/Domain/Collector.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ final class Collector
6969
private int $totalMethodComplexity = 0;
7070

7171
/**
72-
* @var array<int>
72+
* @var array<string, int>
7373
*/
7474
private array $methodComplexity = [];
7575

@@ -486,7 +486,15 @@ public function getLogicalLines(): int
486486
return $this->logicalLines;
487487
}
488488

489-
public function getMethodComplexity(): int
489+
/**
490+
* @return array<string, int>
491+
*/
492+
public function getMethodComplexity(): array
493+
{
494+
return $this->methodComplexity;
495+
}
496+
497+
public function getTotalMethodComplexity(): int
490498
{
491499
return $this->totalMethodComplexity;
492500
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NunoMaduro\PhpInsights\Domain\Insights;
6+
7+
use NunoMaduro\PhpInsights\Domain\Contracts\GlobalInsight;
8+
use NunoMaduro\PhpInsights\Domain\Contracts\HasDetails;
9+
use NunoMaduro\PhpInsights\Domain\Details;
10+
11+
/**
12+
* @see \Tests\Domain\Insights\ClassMethodAverageCyclomaticComplexityIsHighTest
13+
*/
14+
final class ClassMethodAverageCyclomaticComplexityIsHigh extends Insight implements HasDetails, GlobalInsight
15+
{
16+
/**
17+
* @var array<Details>
18+
*/
19+
private array $details = [];
20+
21+
public function hasIssue(): bool
22+
{
23+
return $this->details !== [];
24+
}
25+
26+
public function getTitle(): string
27+
{
28+
return sprintf(
29+
'Having `classes` with average method cyclomatic complexity more than %s is prohibited - Consider refactoring',
30+
$this->getMaxComplexity()
31+
);
32+
}
33+
34+
/**
35+
* @return array<int, Details>
36+
*/
37+
public function getDetails(): array
38+
{
39+
return $this->details;
40+
}
41+
42+
public function process(): void
43+
{
44+
// Exclude in collector all excluded files
45+
if ($this->excludedFiles !== []) {
46+
$this->collector->excludeComplexityFiles($this->excludedFiles);
47+
}
48+
49+
$averageClassComplexity = $this->getAverageClassComplexity();
50+
51+
// Exclude the ones which didn't pass the threshold
52+
$complexityLimit = $this->getMaxComplexity();
53+
$averageClassComplexity = array_filter(
54+
$averageClassComplexity,
55+
static fn ($complexity): bool => $complexity > $complexityLimit
56+
);
57+
58+
$this->details = array_map(
59+
static fn ($class, $complexity): Details => Details::make()
60+
->setFile($class)
61+
->setMessage(sprintf('%.2f cyclomatic complexity', $complexity)),
62+
array_keys($averageClassComplexity),
63+
$averageClassComplexity
64+
);
65+
}
66+
67+
private function getMaxComplexity(): float
68+
{
69+
return (float) ($this->config['maxClassMethodAverageComplexity'] ?? 5.0);
70+
}
71+
72+
private function getFile(string $classMethod): string
73+
{
74+
$colonPosition = strpos($classMethod, ':');
75+
76+
if ($colonPosition !== false) {
77+
return substr($classMethod, 0, $colonPosition);
78+
}
79+
80+
return $classMethod;
81+
}
82+
83+
/**
84+
* @return array<string, float>
85+
*/
86+
private function getAverageClassComplexity(): array
87+
{
88+
// Group method complexities by files
89+
$classComplexities = [];
90+
91+
foreach ($this->collector->getMethodComplexity() as $classMethod => $complexity) {
92+
$classComplexities[$this->getFile($classMethod)][] = $complexity;
93+
}
94+
95+
// Calculate average complexity of each file
96+
$averageClassComplexity = [];
97+
98+
foreach ($classComplexities as $file => $complexities) {
99+
$averageClassComplexity[$file] = array_sum($complexities) / count($complexities);
100+
}
101+
102+
return $averageClassComplexity;
103+
}
104+
}

Diff for: src/Domain/Insights/CyclomaticComplexityIsHigh.php

+11-4
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ public function hasIssue(): bool
2626
public function getTitle(): string
2727
{
2828
return sprintf(
29-
'Having `classes` with more than %s cyclomatic complexity is prohibited - Consider refactoring',
29+
'Having `classes` with total cyclomatic complexity more than %s is prohibited - Consider refactoring',
3030
$this->getMaxComplexity()
3131
);
3232
}
3333

34+
/**
35+
* @return array<int, Details>
36+
*/
3437
public function getDetails(): array
3538
{
3639
return $this->details;
@@ -49,9 +52,13 @@ public function process(): void
4952
static fn ($complexity): bool => $complexity > $complexityLimit
5053
);
5154

52-
$this->details = array_map(static fn ($class, $complexity): Details => Details::make()
53-
->setFile($class)
54-
->setMessage("{$complexity} cyclomatic complexity"), array_keys($classesComplexity), $classesComplexity);
55+
$this->details = array_map(
56+
static fn ($class, $complexity): Details => Details::make()
57+
->setFile($class)
58+
->setMessage(sprintf('%d cyclomatic complexity', $complexity)),
59+
array_keys($classesComplexity),
60+
$classesComplexity
61+
);
5562
}
5663

5764
private function getMaxComplexity(): int
+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NunoMaduro\PhpInsights\Domain\Insights;
6+
7+
use NunoMaduro\PhpInsights\Domain\Contracts\GlobalInsight;
8+
use NunoMaduro\PhpInsights\Domain\Contracts\HasDetails;
9+
use NunoMaduro\PhpInsights\Domain\Details;
10+
11+
/**
12+
* @see \Tests\Domain\Insights\MethodCyclomaticComplexityIsHighTest
13+
*/
14+
final class MethodCyclomaticComplexityIsHigh extends Insight implements HasDetails, GlobalInsight
15+
{
16+
/**
17+
* @var array<Details>
18+
*/
19+
private array $details = [];
20+
21+
public function hasIssue(): bool
22+
{
23+
return $this->details !== [];
24+
}
25+
26+
public function getTitle(): string
27+
{
28+
return sprintf(
29+
'Having `methods` with cyclomatic complexity more than %s is prohibited - Consider refactoring',
30+
$this->getMaxComplexity()
31+
);
32+
}
33+
34+
/**
35+
* @return array<int, Details>
36+
*/
37+
public function getDetails(): array
38+
{
39+
return $this->details;
40+
}
41+
42+
public function process(): void
43+
{
44+
// Exclude in collector all excluded files
45+
if ($this->excludedFiles !== []) {
46+
$this->collector->excludeComplexityFiles($this->excludedFiles);
47+
}
48+
$complexityLimit = $this->getMaxComplexity();
49+
50+
$methodComplexity = array_filter(
51+
$this->collector->getMethodComplexity(),
52+
static fn ($complexity): bool => $complexity > $complexityLimit
53+
);
54+
55+
$this->details = array_map(
56+
fn ($class, $complexity): Details => $this->getDetailsForClassMethod($class, $complexity),
57+
array_keys($methodComplexity),
58+
$methodComplexity
59+
);
60+
}
61+
62+
private function getMaxComplexity(): int
63+
{
64+
return (int) ($this->config['maxMethodComplexity'] ?? 5);
65+
}
66+
67+
private function getDetailsForClassMethod(string $class, int $complexity): Details
68+
{
69+
$file = $class;
70+
$function = null;
71+
$colonPosition = strpos($class, ':');
72+
73+
if ($colonPosition !== false) {
74+
$file = substr($class, 0, $colonPosition);
75+
$function = substr($class, $colonPosition + 1);
76+
}
77+
78+
$details = Details::make()
79+
->setFile($file)
80+
->setMessage(sprintf('%d cyclomatic complexity', $complexity));
81+
82+
if ($function !== null) {
83+
$details->setFunction($function);
84+
}
85+
86+
return $details;
87+
}
88+
}

Diff for: src/Domain/Metrics/Complexity/Complexity.php

+4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
use NunoMaduro\PhpInsights\Domain\Collector;
88
use NunoMaduro\PhpInsights\Domain\Contracts\HasAvg;
99
use NunoMaduro\PhpInsights\Domain\Contracts\HasInsights;
10+
use NunoMaduro\PhpInsights\Domain\Insights\ClassMethodAverageCyclomaticComplexityIsHigh;
1011
use NunoMaduro\PhpInsights\Domain\Insights\CyclomaticComplexityIsHigh;
12+
use NunoMaduro\PhpInsights\Domain\Insights\MethodCyclomaticComplexityIsHigh;
1113

1214
final class Complexity implements HasAvg, HasInsights
1315
{
@@ -23,6 +25,8 @@ public function getInsights(): array
2325
{
2426
return [
2527
CyclomaticComplexityIsHigh::class,
28+
ClassMethodAverageCyclomaticComplexityIsHigh::class,
29+
MethodCyclomaticComplexityIsHigh::class,
2630
];
2731
}
2832
}

Diff for: src/Domain/Results.php

+27-8
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,7 @@ public function getCodeQuality(): float
4848
*/
4949
public function getComplexity(): float
5050
{
51-
$avg = $this->collector->getAverageComplexityPerMethod() - 1.0;
52-
53-
return (float) number_format(
54-
100.0 - max(min($avg * 100.0 / 3.0, 100.0), 0.0),
55-
1,
56-
'.',
57-
''
58-
);
51+
return $this->getPercentageForComplexity();
5952
}
6053

6154
/**
@@ -162,6 +155,32 @@ private function getPercentage(string $category): float
162155
return (float) number_format($percentage, 1, '.', '');
163156
}
164157

158+
/**
159+
* Returns the percentage of the given category.
160+
*/
161+
private function getPercentageForComplexity(): float
162+
{
163+
// Calculate total number of files multiplied by number of insights for complexity metric
164+
$complexityInsights = $this->perCategoryInsights['Complexity'] ?? [];
165+
$totalFiles = count($this->collector->getFiles()) * count($complexityInsights);
166+
167+
// For each metric count the number of files with problem
168+
$filesWithProblems = 0;
169+
170+
foreach ($complexityInsights as $insight) {
171+
if ($insight instanceof HasDetails) {
172+
$filesWithProblems += count($insight->getDetails());
173+
}
174+
}
175+
176+
// Percentage result is 100% - percentage of files with problems
177+
$percentage = $totalFiles > 0
178+
? 100 - ($filesWithProblems * 100 / $totalFiles)
179+
: 100;
180+
181+
return (float) number_format($percentage, 1, '.', '');
182+
}
183+
165184
private function getInsightByCategory(string $insightClass, string $category): Insight
166185
{
167186
foreach ($this->perCategoryInsights[$category] ?? [] as $insight) {

0 commit comments

Comments
 (0)