Skip to content
Merged
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
51 changes: 51 additions & 0 deletions docs/Cyclomatic-Complexity-Analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Cyclomatic Complexity

This tool calculates cyclomatic complexity for PHP classes and methods. Cyclomatic complexity is a software metric that measures the complexity of a program by counting the number of linearly independent paths through the source code.

## What is Cyclomatic Complexity?

Cyclomatic complexity is calculated as:
- **Base complexity**: 1 (for the entry point)
- **+1 for each decision point**: if statements, loops, switch cases, catch blocks, etc.
- **+1 for each logical operator**: &&, ||, and, or, xor, ternary operators

## Risk Levels

- **Low (1-5)**: Simple, easy to understand and maintain
- **Medium (6-10)**: Moderately complex, may need some refactoring
- **High (11-15)**: Complex, should be refactored
- **Very High (16+)**: Very complex, difficult to maintain and test

## Complexity Factors

The calculator counts the following complexity factors:

### Control Structures
- `if` statements
- `elseif` statements
- `switch` statements
- `case` statements
- `while` loops
- `do-while` loops
- `for` loops
- `foreach` loops

### Exception Handling
- `catch` blocks

### Logical Operators
- `&&` (logical AND)
- `||` (logical OR)
- `and` (logical AND)
- `or` (logical OR)
- `xor` (logical XOR)
- Ternary operators (`? :`)

## Best Practices

1. **Keep methods simple**: Aim for complexity ≤ 10
2. **Refactor complex methods**: Break down methods with complexity > 15
3. **Use early returns**: Reduce nesting and complexity
4. **Extract conditions**: Move complex conditions to separate methods
5. **Use strategy pattern**: Replace complex switch statements
6. **Limit logical operators**: Avoid deeply nested AND/OR conditions
4 changes: 3 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ These pages and papers provide more information on cognitive limitations and rea
* [Code Readability Testing, an Empirical Study](https://www.researchgate.net/publication/299412540_Code_Readability_Testing_an_Empirical_Study) by Todd Sedano.
* [An Empirical Validation of Cognitive Complexity as a Measure of Source Code Understandability](https://arxiv.org/pdf/2007.12520) by Marvin Muñoz Barón, Marvin Wyrich, and Stefan Wagner.
* **Halstead Complexity**
* [Halstead Complexity Measures](https://en.wikipedia.org/wiki/Halstead_complexity_measures)
* [Halstead Complexity](https://en.wikipedia.org/wiki/Halstead_complexity_measures)
* **Cyclomatic Complexity**
* [Cyclomatic Complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity)

## Examples 📖

Expand Down
31 changes: 30 additions & 1 deletion src/Business/Cognitive/CognitiveMetrics.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive;

use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetrics;
use Phauthentic\CognitiveCodeAnalysis\Business\Cyclomatic\CyclomaticMetrics;
use InvalidArgumentException;
use JsonSerializable;

Expand Down Expand Up @@ -59,6 +61,9 @@ class CognitiveMetrics implements JsonSerializable
private ?Delta $ifNestingLevelWeightDelta = null;
private ?Delta $elseCountWeightDelta = null;

private ?HalsteadMetrics $halstead = null;
private ?CyclomaticMetrics $cyclomatic = null;

/**
* @param array<string, mixed> $metrics
*/
Expand All @@ -73,6 +78,14 @@ public function __construct(array $metrics)

$this->setRequiredMetricProperties($metrics);
$this->setOptionalMetricProperties($metrics);

if (isset($metrics['halstead'])) {
$this->halstead = new HalsteadMetrics($metrics['halstead']);
}

if (isset($metrics['cyclomatic_complexity'])) {
$this->cyclomatic = new CyclomaticMetrics($metrics['cyclomatic_complexity']);
}
}

/**
Expand All @@ -83,7 +96,7 @@ private function setRequiredMetricProperties(array $metrics): void
{
$missingKeys = array_diff_key($this->metrics, $metrics);
if (!empty($missingKeys)) {
throw new InvalidArgumentException('Missing required keys');
throw new InvalidArgumentException('Missing required keys: ' . implode(', ', $missingKeys));
}

// Not pretty to set each but more efficient than using a loop and $this->metrics
Expand Down Expand Up @@ -411,4 +424,20 @@ public function jsonSerialize(): array
{
return $this->toArray();
}

/**
* @return HalsteadMetrics|null
*/
public function getHalstead(): ?HalsteadMetrics
{
return $this->halstead;
}

/**
* @return CyclomaticMetrics|null
*/
public function getCyclomatic(): ?CyclomaticMetrics
{
return $this->cyclomatic;
}
}
52 changes: 47 additions & 5 deletions src/Business/Cognitive/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
use Phauthentic\CognitiveCodeAnalysis\PhpParser\CognitiveMetricsVisitor;
use Phauthentic\CognitiveCodeAnalysis\PhpParser\CyclomaticComplexityVisitor;
use Phauthentic\CognitiveCodeAnalysis\PhpParser\HalsteadMetricsVisitor;
use PhpParser\NodeTraverserInterface;
use PhpParser\Parser as PhpParser;
use PhpParser\Error;
Expand All @@ -17,15 +19,24 @@
class Parser
{
protected PhpParser $parser;
protected CognitiveMetricsVisitor $visitor;
protected CognitiveMetricsVisitor $cognitiveMetricsVisitor;
protected CyclomaticComplexityVisitor $cyclomaticComplexityVisitor;
protected HalsteadMetricsVisitor $halsteadMetricsVisitor;

public function __construct(
ParserFactory $parserFactory,
protected readonly NodeTraverserInterface $traverser,
) {
$this->parser = $parserFactory->createForHostVersion();
$this->visitor = new CognitiveMetricsVisitor();
$this->traverser->addVisitor($this->visitor);

$this->cognitiveMetricsVisitor = new CognitiveMetricsVisitor();
$this->traverser->addVisitor($this->cognitiveMetricsVisitor);

$this->cyclomaticComplexityVisitor = new CyclomaticComplexityVisitor();
$this->traverser->addVisitor($this->cyclomaticComplexityVisitor);

$this->halsteadMetricsVisitor = new HalsteadMetricsVisitor();
$this->traverser->addVisitor($this->halsteadMetricsVisitor);
}

/**
Expand All @@ -36,8 +47,11 @@ public function parse(string $code): array
{
$this->traverseAbstractSyntaxTree($code);

$methodMetrics = $this->visitor->getMethodMetrics();
$this->visitor->resetValues();
$methodMetrics = $this->cognitiveMetricsVisitor->getMethodMetrics();
$this->cognitiveMetricsVisitor->resetValues();

$methodMetrics = $this->getCyclomaticComplexityVisitor($methodMetrics);
$methodMetrics = $this->getHalsteadMetricsVisitor($methodMetrics);

return $methodMetrics;
}
Expand All @@ -59,4 +73,32 @@ private function traverseAbstractSyntaxTree(string $code): void

$this->traverser->traverse($ast);
}

/**
* @param array<string, array<string, int>> $methodMetrics
* @return array<string, array<string, int>>
*/
private function getHalsteadMetricsVisitor(array $methodMetrics): array
{
$halstead = $this->halsteadMetricsVisitor->getMetrics();
foreach ($halstead['methods'] as $method => $metrics) {
$methodMetrics[$method]['halstead'] = $metrics;
}

return $methodMetrics;
}

/**
* @param array<string, array<string, int>> $methodMetrics
* @return array<string, array<string, int>>
*/
private function getCyclomaticComplexityVisitor(array $methodMetrics): array
{
$cyclomatic = $this->cyclomaticComplexityVisitor->getComplexitySummary();
foreach ($cyclomatic['methods'] as $method => $complexity) {
$methodMetrics[$method]['cyclomatic_complexity'] = $complexity;
}

return $methodMetrics;
}
}
152 changes: 152 additions & 0 deletions src/Business/Cyclomatic/CyclomaticMetrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

declare(strict_types=1);

namespace Phauthentic\CognitiveCodeAnalysis\Business\Cyclomatic;

/**
* @SuppressWarnings(TooManyFields)
*/
class CyclomaticMetrics
{
/**
* The cyclomatic complexity value.
* @var int
*/
public int $complexity;

/**
* The risk level associated with the complexity.
* @var string
*/
public string $riskLevel;

/**
* The total number of decision points.
* @var int
*/
public int $totalCount;

/**
* The base complexity (usually 1).
* @var int
*/
public int $baseCount;

/**
* Number of if statements.
* @var int
*/
public int $ifCount;

/**
* Number of elseif statements.
* @var int
*/
public int $elseifCount;

/**
* Number of else statements.
* @var int
*/
public int $elseCount;

/**
* Number of switch statements.
* @var int
*/
public int $switchCount;

/**
* Number of case statements.
* @var int
*/
public int $caseCount;

/**
* Number of default statements.
* @var int
*/
public int $defaultCount;

/**
* Number of while loops.
* @var int
*/
public int $whileCount;

/**
* Number of do-while loops.
* @var int
*/
public int $doWhileCount;

/**
* Number of for loops.
* @var int
*/
public int $forCount;

/**
* Number of foreach loops.
* @var int
*/
public int $foreachCount;

/**
* Number of catch blocks.
* @var int
*/
public int $catchCount;

/**
* Number of logical AND (&&) operations.
* @var int
*/
public int $logicalAndCount;

/**
* Number of logical OR (||) operations.
* @var int
*/
public int $logicalOrCount;

/**
* Number of logical XOR operations.
* @var int
*/
public int $logicalXorCount;

/**
* Number of ternary operations.
* @var int
*/
public int $ternaryCount;

/**
* @param array<string, mixed> $data
*/
public function __construct(array $data)
{

$this->complexity = $data['complexity'] ?? 1;
$this->riskLevel = (string)($data['risk_level'] ?? $data['riskLevel'] ?? 'unknown');
$this->totalCount = $data['totalCount'] ?? $data['breakdown']['total'] ?? 0;
$this->baseCount = $data['baseCount'] ?? $data['breakdown']['base'] ?? 1;
$this->ifCount = $data['ifCount'] ?? $data['breakdown']['if'] ?? 0;
$this->elseifCount = $data['elseifCount'] ?? $data['breakdown']['elseif'] ?? 0;
$this->elseCount = $data['elseCount'] ?? $data['breakdown']['else'] ?? 0;
$this->switchCount = $data['switchCount'] ?? $data['breakdown']['switch'] ?? 0;
$this->caseCount = $data['caseCount'] ?? $data['breakdown']['case'] ?? 0;
$this->defaultCount = $data['defaultCount'] ?? $data['breakdown']['default'] ?? 0;
$this->whileCount = $data['whileCount'] ?? $data['breakdown']['while'] ?? 0;
$this->doWhileCount = $data['doWhileCount'] ?? $data['breakdown']['do_while'] ?? 0;
$this->forCount = $data['forCount'] ?? $data['breakdown']['for'] ?? 0;
$this->foreachCount = $data['foreachCount'] ?? $data['breakdown']['foreach'] ?? 0;
$this->catchCount = $data['catchCount'] ?? $data['breakdown']['catch'] ?? 0;
$this->logicalAndCount = $data['logicalAndCount'] ?? $data['breakdown']['logical_and'] ?? 0;
$this->logicalOrCount = $data['logicalOrCount'] ?? $data['breakdown']['logical_or'] ?? 0;
$this->logicalXorCount = $data['logicalXorCount'] ?? $data['breakdown']['logical_xor'] ?? 0;
$this->ternaryCount = $data['ternaryCount'] ?? $data['breakdown']['ternary'] ?? 0;
}
}
Loading