Skip to content

Commit

Permalink
feat: Added method and class method average cyclomatic complexity ins…
Browse files Browse the repository at this point in the history
…ights
  • Loading branch information
nikolicaleksa committed Dec 31, 2023
1 parent f476219 commit 56d61bd
Show file tree
Hide file tree
Showing 11 changed files with 567 additions and 17 deletions.
38 changes: 35 additions & 3 deletions docs/insights/complexity.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Complexity

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

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

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

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`.
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`.

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

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

## Average Class Method Cyclomatic Complexity is high <Badge text="^2.12"/> <Badge text="Complexity" type="warn"/>

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`.

**Insight Class**: `NunoMaduro\PhpInsights\Domain\Insights\ClassMethodAverageCyclomaticComplexityIsHigh`

<details>
<summary>Configuration</summary>

```php
\NunoMaduro\PhpInsights\Domain\Insights\ClassMethodAverageCyclomaticComplexityIsHigh::class => [
'maxClassMethodAverageComplexity' => 5.0,
]
```
</details>

## Method Cyclomatic Complexity is high <Badge text="^2.12"/> <Badge text="Complexity" type="warn"/>

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`.

**Insight Class**: `NunoMaduro\PhpInsights\Domain\Insights\MethodCyclomaticComplexityIsHigh`

<details>
<summary>Configuration</summary>

```php
\NunoMaduro\PhpInsights\Domain\Insights\MethodCyclomaticComplexityIsHigh::class => [
'maxMethodComplexity' => 5,
]
```
</details>

<!--
Insight template
## <Badge text="^1.0"/> <Badge text="Complexity" type="warn"/>
Expand Down
12 changes: 10 additions & 2 deletions src/Domain/Collector.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ final class Collector
private int $totalMethodComplexity = 0;

/**
* @var array<int>
* @var array<string, int>
*/
private array $methodComplexity = [];

Expand Down Expand Up @@ -486,7 +486,15 @@ public function getLogicalLines(): int
return $this->logicalLines;
}

public function getMethodComplexity(): int
/**
* @return array<string, int>
*/
public function getMethodComplexity(): array
{
return $this->methodComplexity;
}

public function getTotalMethodComplexity(): int
{
return $this->totalMethodComplexity;
}
Expand Down
104 changes: 104 additions & 0 deletions src/Domain/Insights/ClassMethodAverageCyclomaticComplexityIsHigh.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\PhpInsights\Domain\Insights;

use NunoMaduro\PhpInsights\Domain\Contracts\GlobalInsight;
use NunoMaduro\PhpInsights\Domain\Contracts\HasDetails;
use NunoMaduro\PhpInsights\Domain\Details;

/**
* @see \Tests\Domain\Insights\ClassMethodAverageCyclomaticComplexityIsHighTest
*/
final class ClassMethodAverageCyclomaticComplexityIsHigh extends Insight implements HasDetails, GlobalInsight
{
/**
* @var array<Details>
*/
private array $details = [];

public function hasIssue(): bool
{
return $this->details !== [];
}

public function getTitle(): string
{
return sprintf(
'Having `classes` with average method cyclomatic complexity more than %s is prohibited - Consider refactoring',
$this->getMaxComplexity()
);
}

/**
* @return array<int, Details>
*/
public function getDetails(): array
{
return $this->details;
}

public function process(): void
{
// Exclude in collector all excluded files
if ($this->excludedFiles !== []) {
$this->collector->excludeComplexityFiles($this->excludedFiles);
}

$averageClassComplexity = $this->getAverageClassComplexity();

// Exclude the ones which didn't pass the threshold
$complexityLimit = $this->getMaxComplexity();
$averageClassComplexity = array_filter(
$averageClassComplexity,
static fn ($complexity): bool => $complexity > $complexityLimit
);

$this->details = array_map(
static fn ($class, $complexity): Details => Details::make()
->setFile($class)
->setMessage(sprintf('%.2f cyclomatic complexity', $complexity)),
array_keys($averageClassComplexity),
$averageClassComplexity
);
}

private function getMaxComplexity(): float
{
return (float) ($this->config['maxClassMethodAverageComplexity'] ?? 5.0);
}

private function getFile(string $classMethod): string
{
$colonPosition = strpos($classMethod, ':');

if ($colonPosition !== false) {
return substr($classMethod, 0, $colonPosition);
}

return $classMethod;
}

/**
* @return array<string, float>
*/
private function getAverageClassComplexity(): array
{
// Group method complexities by files
$classComplexities = [];

foreach ($this->collector->getMethodComplexity() as $classMethod => $complexity) {
$classComplexities[$this->getFile($classMethod)][] = $complexity;
}

// Calculate average complexity of each file
$averageClassComplexity = [];

foreach ($classComplexities as $file => $complexities) {
$averageClassComplexity[$file] = array_sum($complexities) / count($complexities);
}

return $averageClassComplexity;
}
}
15 changes: 11 additions & 4 deletions src/Domain/Insights/CyclomaticComplexityIsHigh.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ public function hasIssue(): bool
public function getTitle(): string
{
return sprintf(
'Having `classes` with more than %s cyclomatic complexity is prohibited - Consider refactoring',
'Having `classes` with total cyclomatic complexity more than %s is prohibited - Consider refactoring',
$this->getMaxComplexity()
);
}

/**
* @return array<int, Details>
*/
public function getDetails(): array
{
return $this->details;
Expand All @@ -49,9 +52,13 @@ public function process(): void
static fn ($complexity): bool => $complexity > $complexityLimit
);

$this->details = array_map(static fn ($class, $complexity): Details => Details::make()
->setFile($class)
->setMessage("{$complexity} cyclomatic complexity"), array_keys($classesComplexity), $classesComplexity);
$this->details = array_map(
static fn ($class, $complexity): Details => Details::make()
->setFile($class)
->setMessage(sprintf('%d cyclomatic complexity', $complexity)),
array_keys($classesComplexity),
$classesComplexity
);
}

private function getMaxComplexity(): int
Expand Down
88 changes: 88 additions & 0 deletions src/Domain/Insights/MethodCyclomaticComplexityIsHigh.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\PhpInsights\Domain\Insights;

use NunoMaduro\PhpInsights\Domain\Contracts\GlobalInsight;
use NunoMaduro\PhpInsights\Domain\Contracts\HasDetails;
use NunoMaduro\PhpInsights\Domain\Details;

/**
* @see \Tests\Domain\Insights\MethodCyclomaticComplexityIsHighTest
*/
final class MethodCyclomaticComplexityIsHigh extends Insight implements HasDetails, GlobalInsight
{
/**
* @var array<Details>
*/
private array $details = [];

public function hasIssue(): bool
{
return $this->details !== [];
}

public function getTitle(): string
{
return sprintf(
'Having `methods` with cyclomatic complexity more than %s is prohibited - Consider refactoring',
$this->getMaxComplexity()
);
}

/**
* @return array<int, Details>
*/
public function getDetails(): array
{
return $this->details;
}

public function process(): void
{
// Exclude in collector all excluded files
if ($this->excludedFiles !== []) {
$this->collector->excludeComplexityFiles($this->excludedFiles);
}
$complexityLimit = $this->getMaxComplexity();

$methodComplexity = array_filter(
$this->collector->getMethodComplexity(),
static fn ($complexity): bool => $complexity > $complexityLimit
);

$this->details = array_map(
fn ($class, $complexity): Details => $this->getDetailsForClassMethod($class, $complexity),
array_keys($methodComplexity),
$methodComplexity
);
}

private function getMaxComplexity(): int
{
return (int) ($this->config['maxMethodComplexity'] ?? 5);
}

private function getDetailsForClassMethod(string $class, int $complexity): Details
{
$file = $class;
$function = null;
$colonPosition = strpos($class, ':');

if ($colonPosition !== false) {
$file = substr($class, 0, $colonPosition);
$function = substr($class, $colonPosition + 1);
}

$details = Details::make()
->setFile($file)
->setMessage(sprintf('%d cyclomatic complexity', $complexity));

if ($function !== null) {
$details->setFunction($function);
}

return $details;
}
}
4 changes: 4 additions & 0 deletions src/Domain/Metrics/Complexity/Complexity.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use NunoMaduro\PhpInsights\Domain\Collector;
use NunoMaduro\PhpInsights\Domain\Contracts\HasAvg;
use NunoMaduro\PhpInsights\Domain\Contracts\HasInsights;
use NunoMaduro\PhpInsights\Domain\Insights\ClassMethodAverageCyclomaticComplexityIsHigh;
use NunoMaduro\PhpInsights\Domain\Insights\CyclomaticComplexityIsHigh;
use NunoMaduro\PhpInsights\Domain\Insights\MethodCyclomaticComplexityIsHigh;

final class Complexity implements HasAvg, HasInsights
{
Expand All @@ -23,6 +25,8 @@ public function getInsights(): array
{
return [
CyclomaticComplexityIsHigh::class,
ClassMethodAverageCyclomaticComplexityIsHigh::class,
MethodCyclomaticComplexityIsHigh::class,
];
}
}
35 changes: 27 additions & 8 deletions src/Domain/Results.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,7 @@ public function getCodeQuality(): float
*/
public function getComplexity(): float
{
$avg = $this->collector->getAverageComplexityPerMethod() - 1.0;

return (float) number_format(
100.0 - max(min($avg * 100.0 / 3.0, 100.0), 0.0),
1,
'.',
''
);
return $this->getPercentageForComplexity();
}

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

/**
* Returns the percentage of the given category.
*/
private function getPercentageForComplexity(): float
{
// Calculate total number of files multiplied by number of insights for complexity metric
$complexityInsights = $this->perCategoryInsights['Complexity'] ?? [];
$totalFiles = count($this->collector->getFiles()) * count($complexityInsights);

// For each metric count the number of files with problem
$filesWithProblems = 0;

foreach ($complexityInsights as $insight) {
if ($insight instanceof HasDetails) {
$filesWithProblems += count($insight->getDetails());
}
}

// Percentage result is 100% - percentage of files with problems
$percentage = $totalFiles > 0
? 100 - ($filesWithProblems * 100 / $totalFiles)
: 100;

return (float) number_format($percentage, 1, '.', '');
}

private function getInsightByCategory(string $insightClass, string $category): Insight
{
foreach ($this->perCategoryInsights[$category] ?? [] as $insight) {
Expand Down
Loading

0 comments on commit 56d61bd

Please sign in to comment.