Skip to content

Commit 1371d60

Browse files
Adding Halstead and Cyclomatic Complexity to the output (#37)
#31
1 parent 3b5a554 commit 1371d60

14 files changed

Lines changed: 1281 additions & 21 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Cyclomatic Complexity
2+
3+
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.
4+
5+
## What is Cyclomatic Complexity?
6+
7+
Cyclomatic complexity is calculated as:
8+
- **Base complexity**: 1 (for the entry point)
9+
- **+1 for each decision point**: if statements, loops, switch cases, catch blocks, etc.
10+
- **+1 for each logical operator**: &&, ||, and, or, xor, ternary operators
11+
12+
## Risk Levels
13+
14+
- **Low (1-5)**: Simple, easy to understand and maintain
15+
- **Medium (6-10)**: Moderately complex, may need some refactoring
16+
- **High (11-15)**: Complex, should be refactored
17+
- **Very High (16+)**: Very complex, difficult to maintain and test
18+
19+
## Complexity Factors
20+
21+
The calculator counts the following complexity factors:
22+
23+
### Control Structures
24+
- `if` statements
25+
- `elseif` statements
26+
- `switch` statements
27+
- `case` statements
28+
- `while` loops
29+
- `do-while` loops
30+
- `for` loops
31+
- `foreach` loops
32+
33+
### Exception Handling
34+
- `catch` blocks
35+
36+
### Logical Operators
37+
- `&&` (logical AND)
38+
- `||` (logical OR)
39+
- `and` (logical AND)
40+
- `or` (logical OR)
41+
- `xor` (logical XOR)
42+
- Ternary operators (`? :`)
43+
44+
## Best Practices
45+
46+
1. **Keep methods simple**: Aim for complexity ≤ 10
47+
2. **Refactor complex methods**: Break down methods with complexity > 15
48+
3. **Use early returns**: Reduce nesting and complexity
49+
4. **Extract conditions**: Move complex conditions to separate methods
50+
5. **Use strategy pattern**: Replace complex switch statements
51+
6. **Limit logical operators**: Avoid deeply nested AND/OR conditions

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ These pages and papers provide more information on cognitive limitations and rea
7171
* [Code Readability Testing, an Empirical Study](https://www.researchgate.net/publication/299412540_Code_Readability_Testing_an_Empirical_Study) by Todd Sedano.
7272
* [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.
7373
* **Halstead Complexity**
74-
* [Halstead Complexity Measures](https://en.wikipedia.org/wiki/Halstead_complexity_measures)
74+
* [Halstead Complexity](https://en.wikipedia.org/wiki/Halstead_complexity_measures)
75+
* **Cyclomatic Complexity**
76+
* [Cyclomatic Complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity)
7577

7678
## Examples 📖
7779

src/Business/Cognitive/CognitiveMetrics.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive;
66

7+
use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetrics;
8+
use Phauthentic\CognitiveCodeAnalysis\Business\Cyclomatic\CyclomaticMetrics;
79
use InvalidArgumentException;
810
use JsonSerializable;
911

@@ -59,6 +61,9 @@ class CognitiveMetrics implements JsonSerializable
5961
private ?Delta $ifNestingLevelWeightDelta = null;
6062
private ?Delta $elseCountWeightDelta = null;
6163

64+
private ?HalsteadMetrics $halstead = null;
65+
private ?CyclomaticMetrics $cyclomatic = null;
66+
6267
/**
6368
* @param array<string, mixed> $metrics
6469
*/
@@ -73,6 +78,14 @@ public function __construct(array $metrics)
7378

7479
$this->setRequiredMetricProperties($metrics);
7580
$this->setOptionalMetricProperties($metrics);
81+
82+
if (isset($metrics['halstead'])) {
83+
$this->halstead = new HalsteadMetrics($metrics['halstead']);
84+
}
85+
86+
if (isset($metrics['cyclomatic_complexity'])) {
87+
$this->cyclomatic = new CyclomaticMetrics($metrics['cyclomatic_complexity']);
88+
}
7689
}
7790

7891
/**
@@ -83,7 +96,7 @@ private function setRequiredMetricProperties(array $metrics): void
8396
{
8497
$missingKeys = array_diff_key($this->metrics, $metrics);
8598
if (!empty($missingKeys)) {
86-
throw new InvalidArgumentException('Missing required keys');
99+
throw new InvalidArgumentException('Missing required keys: ' . implode(', ', $missingKeys));
87100
}
88101

89102
// Not pretty to set each but more efficient than using a loop and $this->metrics
@@ -411,4 +424,20 @@ public function jsonSerialize(): array
411424
{
412425
return $this->toArray();
413426
}
427+
428+
/**
429+
* @return HalsteadMetrics|null
430+
*/
431+
public function getHalstead(): ?HalsteadMetrics
432+
{
433+
return $this->halstead;
434+
}
435+
436+
/**
437+
* @return CyclomaticMetrics|null
438+
*/
439+
public function getCyclomatic(): ?CyclomaticMetrics
440+
{
441+
return $this->cyclomatic;
442+
}
414443
}

src/Business/Cognitive/Parser.php

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
88
use Phauthentic\CognitiveCodeAnalysis\PhpParser\CognitiveMetricsVisitor;
9+
use Phauthentic\CognitiveCodeAnalysis\PhpParser\CyclomaticComplexityVisitor;
10+
use Phauthentic\CognitiveCodeAnalysis\PhpParser\HalsteadMetricsVisitor;
911
use PhpParser\NodeTraverserInterface;
1012
use PhpParser\Parser as PhpParser;
1113
use PhpParser\Error;
@@ -17,15 +19,24 @@
1719
class Parser
1820
{
1921
protected PhpParser $parser;
20-
protected CognitiveMetricsVisitor $visitor;
22+
protected CognitiveMetricsVisitor $cognitiveMetricsVisitor;
23+
protected CyclomaticComplexityVisitor $cyclomaticComplexityVisitor;
24+
protected HalsteadMetricsVisitor $halsteadMetricsVisitor;
2125

2226
public function __construct(
2327
ParserFactory $parserFactory,
2428
protected readonly NodeTraverserInterface $traverser,
2529
) {
2630
$this->parser = $parserFactory->createForHostVersion();
27-
$this->visitor = new CognitiveMetricsVisitor();
28-
$this->traverser->addVisitor($this->visitor);
31+
32+
$this->cognitiveMetricsVisitor = new CognitiveMetricsVisitor();
33+
$this->traverser->addVisitor($this->cognitiveMetricsVisitor);
34+
35+
$this->cyclomaticComplexityVisitor = new CyclomaticComplexityVisitor();
36+
$this->traverser->addVisitor($this->cyclomaticComplexityVisitor);
37+
38+
$this->halsteadMetricsVisitor = new HalsteadMetricsVisitor();
39+
$this->traverser->addVisitor($this->halsteadMetricsVisitor);
2940
}
3041

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

39-
$methodMetrics = $this->visitor->getMethodMetrics();
40-
$this->visitor->resetValues();
50+
$methodMetrics = $this->cognitiveMetricsVisitor->getMethodMetrics();
51+
$this->cognitiveMetricsVisitor->resetValues();
52+
53+
$methodMetrics = $this->getCyclomaticComplexityVisitor($methodMetrics);
54+
$methodMetrics = $this->getHalsteadMetricsVisitor($methodMetrics);
4155

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

6074
$this->traverser->traverse($ast);
6175
}
76+
77+
/**
78+
* @param array<string, array<string, int>> $methodMetrics
79+
* @return array<string, array<string, int>>
80+
*/
81+
private function getHalsteadMetricsVisitor(array $methodMetrics): array
82+
{
83+
$halstead = $this->halsteadMetricsVisitor->getMetrics();
84+
foreach ($halstead['methods'] as $method => $metrics) {
85+
$methodMetrics[$method]['halstead'] = $metrics;
86+
}
87+
88+
return $methodMetrics;
89+
}
90+
91+
/**
92+
* @param array<string, array<string, int>> $methodMetrics
93+
* @return array<string, array<string, int>>
94+
*/
95+
private function getCyclomaticComplexityVisitor(array $methodMetrics): array
96+
{
97+
$cyclomatic = $this->cyclomaticComplexityVisitor->getComplexitySummary();
98+
foreach ($cyclomatic['methods'] as $method => $complexity) {
99+
$methodMetrics[$method]['cyclomatic_complexity'] = $complexity;
100+
}
101+
102+
return $methodMetrics;
103+
}
62104
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\CognitiveCodeAnalysis\Business\Cyclomatic;
6+
7+
/**
8+
* @SuppressWarnings(TooManyFields)
9+
*/
10+
class CyclomaticMetrics
11+
{
12+
/**
13+
* The cyclomatic complexity value.
14+
* @var int
15+
*/
16+
public int $complexity;
17+
18+
/**
19+
* The risk level associated with the complexity.
20+
* @var string
21+
*/
22+
public string $riskLevel;
23+
24+
/**
25+
* The total number of decision points.
26+
* @var int
27+
*/
28+
public int $totalCount;
29+
30+
/**
31+
* The base complexity (usually 1).
32+
* @var int
33+
*/
34+
public int $baseCount;
35+
36+
/**
37+
* Number of if statements.
38+
* @var int
39+
*/
40+
public int $ifCount;
41+
42+
/**
43+
* Number of elseif statements.
44+
* @var int
45+
*/
46+
public int $elseifCount;
47+
48+
/**
49+
* Number of else statements.
50+
* @var int
51+
*/
52+
public int $elseCount;
53+
54+
/**
55+
* Number of switch statements.
56+
* @var int
57+
*/
58+
public int $switchCount;
59+
60+
/**
61+
* Number of case statements.
62+
* @var int
63+
*/
64+
public int $caseCount;
65+
66+
/**
67+
* Number of default statements.
68+
* @var int
69+
*/
70+
public int $defaultCount;
71+
72+
/**
73+
* Number of while loops.
74+
* @var int
75+
*/
76+
public int $whileCount;
77+
78+
/**
79+
* Number of do-while loops.
80+
* @var int
81+
*/
82+
public int $doWhileCount;
83+
84+
/**
85+
* Number of for loops.
86+
* @var int
87+
*/
88+
public int $forCount;
89+
90+
/**
91+
* Number of foreach loops.
92+
* @var int
93+
*/
94+
public int $foreachCount;
95+
96+
/**
97+
* Number of catch blocks.
98+
* @var int
99+
*/
100+
public int $catchCount;
101+
102+
/**
103+
* Number of logical AND (&&) operations.
104+
* @var int
105+
*/
106+
public int $logicalAndCount;
107+
108+
/**
109+
* Number of logical OR (||) operations.
110+
* @var int
111+
*/
112+
public int $logicalOrCount;
113+
114+
/**
115+
* Number of logical XOR operations.
116+
* @var int
117+
*/
118+
public int $logicalXorCount;
119+
120+
/**
121+
* Number of ternary operations.
122+
* @var int
123+
*/
124+
public int $ternaryCount;
125+
126+
/**
127+
* @param array<string, mixed> $data
128+
*/
129+
public function __construct(array $data)
130+
{
131+
132+
$this->complexity = $data['complexity'] ?? 1;
133+
$this->riskLevel = (string)($data['risk_level'] ?? $data['riskLevel'] ?? 'unknown');
134+
$this->totalCount = $data['totalCount'] ?? $data['breakdown']['total'] ?? 0;
135+
$this->baseCount = $data['baseCount'] ?? $data['breakdown']['base'] ?? 1;
136+
$this->ifCount = $data['ifCount'] ?? $data['breakdown']['if'] ?? 0;
137+
$this->elseifCount = $data['elseifCount'] ?? $data['breakdown']['elseif'] ?? 0;
138+
$this->elseCount = $data['elseCount'] ?? $data['breakdown']['else'] ?? 0;
139+
$this->switchCount = $data['switchCount'] ?? $data['breakdown']['switch'] ?? 0;
140+
$this->caseCount = $data['caseCount'] ?? $data['breakdown']['case'] ?? 0;
141+
$this->defaultCount = $data['defaultCount'] ?? $data['breakdown']['default'] ?? 0;
142+
$this->whileCount = $data['whileCount'] ?? $data['breakdown']['while'] ?? 0;
143+
$this->doWhileCount = $data['doWhileCount'] ?? $data['breakdown']['do_while'] ?? 0;
144+
$this->forCount = $data['forCount'] ?? $data['breakdown']['for'] ?? 0;
145+
$this->foreachCount = $data['foreachCount'] ?? $data['breakdown']['foreach'] ?? 0;
146+
$this->catchCount = $data['catchCount'] ?? $data['breakdown']['catch'] ?? 0;
147+
$this->logicalAndCount = $data['logicalAndCount'] ?? $data['breakdown']['logical_and'] ?? 0;
148+
$this->logicalOrCount = $data['logicalOrCount'] ?? $data['breakdown']['logical_or'] ?? 0;
149+
$this->logicalXorCount = $data['logicalXorCount'] ?? $data['breakdown']['logical_xor'] ?? 0;
150+
$this->ternaryCount = $data['ternaryCount'] ?? $data['breakdown']['ternary'] ?? 0;
151+
}
152+
}

0 commit comments

Comments
 (0)