Skip to content

Commit 97f7857

Browse files
Working on adding additional metrics to see how they align with cognitive metrics.
1 parent 1181775 commit 97f7857

14 files changed

Lines changed: 1891 additions & 14 deletions
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Cyclomatic Complexity Calculator
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+
## Usage
20+
21+
### Command Line Interface
22+
23+
```bash
24+
# Basic usage
25+
bin/phpcca complexity src/
26+
27+
# With threshold (only show methods with complexity >= 5)
28+
bin/phpcca complexity src/ --threshold=5
29+
30+
# Detailed breakdown
31+
bin/phpcca complexity src/ --detailed
32+
33+
# Output to JSON file
34+
bin/phpcca complexity src/ --format=json --output=complexity.json
35+
36+
# Output to CSV file
37+
bin/phpcca complexity src/ --format=csv --output=complexity.csv
38+
```
39+
40+
### Command Options
41+
42+
- `--format, -f`: Output format (text, json, csv) - default: text
43+
- `--output, -o`: Output file path
44+
- `--threshold, -t`: Minimum complexity threshold to report (default: 1)
45+
- `--detailed, -d`: Show detailed breakdown of complexity factors
46+
47+
### Programmatic Usage
48+
49+
```php
50+
<?php
51+
52+
use PhpParser\ParserFactory;
53+
use PhpParser\NodeTraverser;
54+
use Phauthentic\CognitiveCodeAnalysis\PhpParser\CyclomaticComplexityVisitor;
55+
56+
// Create parser and traverser
57+
$parser = (new ParserFactory())->createForNewestSupportedVersion();
58+
$traverser = new NodeTraverser();
59+
$visitor = new CyclomaticComplexityVisitor();
60+
$traverser->addVisitor($visitor);
61+
62+
// Parse your PHP code
63+
$code = file_get_contents('your-file.php');
64+
$ast = $parser->parse($code);
65+
$traverser->traverse($ast);
66+
67+
// Get results
68+
$classComplexity = $visitor->getClassComplexity();
69+
$methodComplexity = $visitor->getMethodComplexity();
70+
$methodBreakdown = $visitor->getMethodComplexityBreakdown();
71+
$summary = $visitor->getComplexitySummary();
72+
```
73+
74+
## Complexity Factors
75+
76+
The calculator counts the following complexity factors:
77+
78+
### Control Structures
79+
- `if` statements
80+
- `elseif` statements
81+
- `switch` statements
82+
- `case` statements
83+
- `while` loops
84+
- `do-while` loops
85+
- `for` loops
86+
- `foreach` loops
87+
88+
### Exception Handling
89+
- `catch` blocks
90+
91+
### Logical Operators
92+
- `&&` (logical AND)
93+
- `||` (logical OR)
94+
- `and` (logical AND)
95+
- `or` (logical OR)
96+
- `xor` (logical XOR)
97+
- Ternary operators (`? :`)
98+
99+
## Example Output
100+
101+
### Text Format
102+
```
103+
Cyclomatic Complexity Analysis
104+
Files analyzed: 15
105+
106+
Class Complexity:
107+
Test\ComplexityTest: 45 (high)
108+
Test\AnotherComplexityTest: 6 (medium)
109+
110+
Method Complexity:
111+
Test\ComplexityTest::simpleMethod: 1 (low)
112+
Test\ComplexityTest::methodWithIf: 2 (low)
113+
Test\ComplexityTest::highComplexityMethod: 12 (high)
114+
Test\ComplexityTest::veryHighComplexityMethod: 18 (very_high)
115+
116+
High Risk Methods (≥10):
117+
Test\ComplexityTest::highComplexityMethod: 12
118+
Test\ComplexityTest::veryHighComplexityMethod: 18
119+
120+
Summary Statistics:
121+
Average complexity: 4.2
122+
Maximum complexity: 18
123+
Minimum complexity: 1
124+
Total methods: 10
125+
```
126+
127+
### JSON Format
128+
```json
129+
{
130+
"summary": {
131+
"classes": {
132+
"Test\\ComplexityTest": {
133+
"complexity": 45,
134+
"risk_level": "high"
135+
}
136+
},
137+
"methods": {
138+
"Test\\ComplexityTest::highComplexityMethod": {
139+
"complexity": 12,
140+
"risk_level": "high",
141+
"breakdown": {
142+
"total": 12,
143+
"base": 1,
144+
"if": 3,
145+
"switch": 1,
146+
"case": 3,
147+
"foreach": 1,
148+
"logical_and": 1
149+
}
150+
}
151+
},
152+
"high_risk_methods": {
153+
"Test\\ComplexityTest::highComplexityMethod": 12,
154+
"Test\\ComplexityTest::veryHighComplexityMethod": 18
155+
}
156+
},
157+
"files_analyzed": 15
158+
}
159+
```
160+
161+
## Best Practices
162+
163+
1. **Keep methods simple**: Aim for complexity ≤ 10
164+
2. **Refactor complex methods**: Break down methods with complexity > 15
165+
3. **Use early returns**: Reduce nesting and complexity
166+
4. **Extract conditions**: Move complex conditions to separate methods
167+
5. **Use strategy pattern**: Replace complex switch statements
168+
6. **Limit logical operators**: Avoid deeply nested AND/OR conditions
169+
170+
## Integration with CI/CD
171+
172+
Add complexity checks to your CI pipeline:
173+
174+
```bash
175+
# Fail if any method has complexity > 15
176+
bin/phpcca complexity src/ --threshold=15 --format=json | jq '.summary.very_high_risk_methods | length == 0'
177+
178+
# Generate complexity report
179+
bin/phpcca complexity src/ --format=json --output=complexity-report.json
180+
```
181+
182+
## Testing
183+
184+
Run the example to see the complexity calculator in action:
185+
186+
```bash
187+
php example_complexity_usage.php
188+
```
189+
190+
This will analyze the `test_complexity.php` file and show detailed complexity metrics for all classes and methods.

src/Application.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Phauthentic\CognitiveCodeAnalysis\Business\MetricsFacade;
1818
use Phauthentic\CognitiveCodeAnalysis\Command\ChurnCommand;
1919
use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsCommand;
20+
use Phauthentic\CognitiveCodeAnalysis\Command\CyclomaticComplexityCommand;
2021
use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\ParserErrorHandler;
2122
use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\ProgressBarHandler;
2223
use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\VerboseHandler;
@@ -232,14 +233,18 @@ private function registerCommands(): void
232233
new Reference(ChurnReportHandler::class),
233234
])
234235
->setPublic(true);
236+
237+
$this->containerBuilder->register(CyclomaticComplexityCommand::class, CyclomaticComplexityCommand::class)
238+
->setPublic(true);
235239
}
236240

237241
private function configureApplication(): void
238242
{
239243
$this->containerBuilder->register(SymfonyApplication::class, SymfonyApplication::class)
240244
->setPublic(true)
241245
->addMethodCall('add', [new Reference(CognitiveMetricsCommand::class)])
242-
->addMethodCall('add', [new Reference(ChurnCommand::class)]);
246+
->addMethodCall('add', [new Reference(ChurnCommand::class)])
247+
->addMethodCall('add', [new Reference(CyclomaticComplexityCommand::class)]);
243248
}
244249

245250
public function run(): void

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: 33 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,25 @@ 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+
$cyclomatic = $this->cyclomaticComplexityVisitor->getComplexitySummary();
54+
foreach ($cyclomatic['methods'] as $method => $complexity) {
55+
$methodMetrics[$method]['cyclomatic_complexity'] = $complexity;
56+
}
57+
58+
$halstead = $this->halsteadMetricsVisitor->getMetrics();
59+
//dd(array_keys($methodMetrics));
60+
//dd(array_keys($halstead['methods']));
61+
//dd(array_diff(array_keys($halstead['methods']), array_keys($methodMetrics)));
62+
foreach ($halstead['methods'] as $method => $metrics) {
63+
if (!isset( $methodMetrics[$method])) {
64+
//dd( $methodMetrics[$method]);
65+
//dd($method);
66+
}
67+
$methodMetrics[$method]['halstead'] = $metrics;
68+
}
4169

4270
return $methodMetrics;
4371
}

0 commit comments

Comments
 (0)