Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added method and class method average cyclomatic complexity ins… #670

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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
Loading