Skip to content

Commit 11597c3

Browse files
Increase analysis performance (#46)
* Optimizing the AST visitors * Update .gitignore and phpbench.json for benchmark storage configuration - Added benchmarks/storage/ and .phpbench/ to .gitignore - Updated phpbench.json to include XML storage driver and path for benchmark results * Refactor Parser class by removing unused traverseAbstractSyntaxTree method and adding type hint for class-string in clearStaticProperty method * Enhance CognitiveMetrics and Parser classes to improve complexity metrics handling - Updated CognitiveMetrics to handle both array and scalar inputs for cyclomatic complexity. - Refactored Parser to integrate cyclomatic complexity and Halstead metrics into method metrics. - Removed unused methods for cyclomatic complexity and Halstead metrics from Parser. - Added a new method to calculate risk levels based on cyclomatic complexity. * Refactor Parser and CombinedMetricsVisitor classes for improved readability and maintainability - Changed the instantiation of ReflectionClass in Parser to use a direct import for clarity. - Added suppress warnings annotations for unused parameters in beforeTraverse and afterTraverse methods of CombinedMetricsVisitor. * Refactor cyclomatic complexity handling in CognitiveMetrics class - Simplified the instantiation of CyclomaticMetrics to directly accept the complexity data, removing unnecessary conditional checks for array or scalar inputs.
1 parent 7342c00 commit 11597c3

File tree

9 files changed

+338
-61
lines changed

9 files changed

+338
-61
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
/.phpunit.cache/
77
/tmp/
88
/tools/
9+
/benchmarks/storage/
910
.idea/
1011
.env
12+
.phpbench/
1113
infection.log
1214
phive.phar
1315
phpcca.phar

phpbench.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,7 @@
33
"runner.bootstrap": "vendor/autoload.php",
44
"runner.path": "benchmarks",
55
"runner.time_unit": "milliseconds",
6-
"runner.progress": "dots"
6+
"runner.progress": "dots",
7+
"storage.driver": "xml",
8+
"storage.xml_storage_path": "benchmarks/storage"
79
}

src/Business/Cognitive/CognitiveMetricsCollector.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ private function getCodeFromFile(SplFileInfo $file): string
8181
private function findMetrics(iterable $files): CognitiveMetricsCollection
8282
{
8383
$metricsCollection = new CognitiveMetricsCollection();
84+
$fileCount = 0;
8485

8586
foreach ($files as $file) {
8687
try {
@@ -90,6 +91,14 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection
9091

9192
// Store ignored items from the parser
9293
$this->ignoredItems = $this->parser->getIgnored();
94+
95+
$fileCount++;
96+
97+
// Clear memory periodically to prevent memory leaks
98+
if ($fileCount % 50 === 0) {
99+
$this->parser->clearStaticCaches();
100+
gc_collect_cycles();
101+
}
93102
} catch (Throwable $exception) {
94103
$this->messageBus->dispatch(new ParserFailed(
95104
$file,

src/Business/Cognitive/Parser.php

Lines changed: 90 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
use Phauthentic\CognitiveCodeAnalysis\PhpParser\CognitiveMetricsVisitor;
1010
use Phauthentic\CognitiveCodeAnalysis\PhpParser\CyclomaticComplexityVisitor;
1111
use Phauthentic\CognitiveCodeAnalysis\PhpParser\HalsteadMetricsVisitor;
12+
use Phauthentic\CognitiveCodeAnalysis\PhpParser\CombinedMetricsVisitor;
1213
use PhpParser\NodeTraverserInterface;
1314
use PhpParser\Parser as PhpParser;
1415
use PhpParser\NodeTraverser;
1516
use PhpParser\Error;
1617
use PhpParser\ParserFactory;
18+
use ReflectionClass;
1719

1820
/**
1921
*
@@ -25,6 +27,7 @@ class Parser
2527
protected CognitiveMetricsVisitor $cognitiveMetricsVisitor;
2628
protected CyclomaticComplexityVisitor $cyclomaticComplexityVisitor;
2729
protected HalsteadMetricsVisitor $halsteadMetricsVisitor;
30+
protected CombinedMetricsVisitor $combinedVisitor;
2831

2932
public function __construct(
3033
ParserFactory $parserFactory,
@@ -47,6 +50,10 @@ public function __construct(
4750
$this->halsteadMetricsVisitor = new HalsteadMetricsVisitor();
4851
$this->halsteadMetricsVisitor->setAnnotationVisitor($this->annotationVisitor);
4952
$this->traverser->addVisitor($this->halsteadMetricsVisitor);
53+
54+
// Create the combined visitor for performance optimization
55+
$this->combinedVisitor = new CombinedMetricsVisitor();
56+
$this->combinedVisitor->setAnnotationVisitor();
5057
}
5158

5259
/**
@@ -58,14 +65,35 @@ public function parse(string $code): array
5865
// First, scan for annotations to collect ignored items
5966
$this->scanForAnnotations($code);
6067

61-
// Then parse for metrics
62-
$this->traverseAbstractSyntaxTree($code);
68+
// Then parse for metrics using the combined visitor for better performance
69+
$this->traverseAbstractSyntaxTreeWithCombinedVisitor($code);
70+
71+
// Get all metrics before resetting
72+
$methodMetrics = $this->combinedVisitor->getMethodMetrics();
73+
$cyclomaticMetrics = $this->combinedVisitor->getMethodComplexity();
74+
$halsteadMetrics = $this->combinedVisitor->getHalsteadMethodMetrics();
6375

64-
$methodMetrics = $this->cognitiveMetricsVisitor->getMethodMetrics();
65-
$this->cognitiveMetricsVisitor->resetValues();
76+
// Now reset the combined visitor
77+
$this->combinedVisitor->resetAll();
6678

67-
$methodMetrics = $this->getCyclomaticComplexityVisitor($methodMetrics);
68-
$methodMetrics = $this->getHalsteadMetricsVisitor($methodMetrics);
79+
// Add cyclomatic complexity to method metrics
80+
foreach ($cyclomaticMetrics as $method => $complexityData) {
81+
if (isset($methodMetrics[$method])) {
82+
$complexity = $complexityData['complexity'] ?? $complexityData;
83+
$riskLevel = $complexityData['risk_level'] ?? $this->getRiskLevel($complexity);
84+
$methodMetrics[$method]['cyclomatic_complexity'] = [
85+
'complexity' => $complexity,
86+
'risk_level' => $riskLevel
87+
];
88+
}
89+
}
90+
91+
// Add Halstead metrics to method metrics
92+
foreach ($halsteadMetrics as $method => $metrics) {
93+
if (isset($methodMetrics[$method])) {
94+
$methodMetrics[$method]['halstead'] = $metrics;
95+
}
96+
}
6997

7098
return $methodMetrics;
7199
}
@@ -95,9 +123,10 @@ private function scanForAnnotations(string $code): void
95123
}
96124

97125
/**
126+
* Traverse the AST using the combined visitor for better performance.
98127
* @throws CognitiveAnalysisException
99128
*/
100-
private function traverseAbstractSyntaxTree(string $code): void
129+
private function traverseAbstractSyntaxTreeWithCombinedVisitor(string $code): void
101130
{
102131
try {
103132
$ast = $this->parser->parse($code);
@@ -109,58 +138,12 @@ private function traverseAbstractSyntaxTree(string $code): void
109138
throw new CognitiveAnalysisException("Could not parse the code.");
110139
}
111140

112-
$this->traverser->traverse($ast);
141+
// Create a new traverser for the combined visitor
142+
$combinedTraverser = new NodeTraverser();
143+
$combinedTraverser->addVisitor($this->combinedVisitor);
144+
$combinedTraverser->traverse($ast);
113145
}
114146

115-
/**
116-
* @param array<string, array<string, int>> $methodMetrics
117-
* @return array<string, array<string, int>>
118-
*/
119-
private function getHalsteadMetricsVisitor(array $methodMetrics): array
120-
{
121-
$halstead = $this->halsteadMetricsVisitor->getMetrics();
122-
foreach ($halstead['methods'] as $method => $metrics) {
123-
// Skip ignored methods
124-
if ($this->annotationVisitor->isMethodIgnored($method)) {
125-
continue;
126-
}
127-
// Skip malformed method keys (ClassName::)
128-
if (str_ends_with($method, '::')) {
129-
continue;
130-
}
131-
// Only add Halstead metrics to methods that were processed by CognitiveMetricsVisitor
132-
if (isset($methodMetrics[$method])) {
133-
$methodMetrics[$method]['halstead'] = $metrics;
134-
}
135-
}
136-
137-
return $methodMetrics;
138-
}
139-
140-
/**
141-
* @param array<string, array<string, int>> $methodMetrics
142-
* @return array<string, array<string, int>>
143-
*/
144-
private function getCyclomaticComplexityVisitor(array $methodMetrics): array
145-
{
146-
$cyclomatic = $this->cyclomaticComplexityVisitor->getComplexitySummary();
147-
foreach ($cyclomatic['methods'] as $method => $complexity) {
148-
// Skip ignored methods
149-
if ($this->annotationVisitor->isMethodIgnored($method)) {
150-
continue;
151-
}
152-
// Skip malformed method keys (ClassName::)
153-
if (str_ends_with($method, '::')) {
154-
continue;
155-
}
156-
// Only add cyclomatic complexity to methods that were processed by CognitiveMetricsVisitor
157-
if (isset($methodMetrics[$method])) {
158-
$methodMetrics[$method]['cyclomatic_complexity'] = $complexity;
159-
}
160-
}
161-
162-
return $methodMetrics;
163-
}
164147

165148
/**
166149
* Get all ignored classes and methods.
@@ -191,4 +174,54 @@ public function getIgnoredMethods(): array
191174
{
192175
return $this->annotationVisitor->getIgnoredMethods();
193176
}
177+
178+
/**
179+
* Clear static caches to prevent memory leaks during long-running processes.
180+
*/
181+
public function clearStaticCaches(): void
182+
{
183+
// Clear FQCN caches from all visitors
184+
$this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\PhpParser\CognitiveMetricsVisitor', 'fqcnCache');
185+
$this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\PhpParser\CyclomaticComplexityVisitor', 'fqcnCache');
186+
$this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\PhpParser\HalsteadMetricsVisitor', 'fqcnCache');
187+
$this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\PhpParser\AnnotationVisitor', 'fqcnCache');
188+
189+
// Clear regex pattern caches
190+
$this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner', 'compiledPatterns');
191+
$this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector', 'compiledPatterns');
192+
193+
// Clear accumulated data in visitors
194+
$this->combinedVisitor->resetAllBetweenFiles();
195+
}
196+
197+
/**
198+
* Clear a static property using reflection.
199+
*/
200+
private function clearStaticProperty(string $className, string $propertyName): void
201+
{
202+
try {
203+
/** @var class-string $className */
204+
$reflection = new ReflectionClass($className);
205+
if ($reflection->hasProperty($propertyName)) {
206+
$property = $reflection->getProperty($propertyName);
207+
$property->setAccessible(true);
208+
$property->setValue(null, []);
209+
}
210+
} catch (\ReflectionException $e) {
211+
// Ignore reflection errors
212+
}
213+
}
214+
215+
/**
216+
* Calculate risk level based on cyclomatic complexity.
217+
*/
218+
private function getRiskLevel(int $complexity): string
219+
{
220+
return match (true) {
221+
$complexity <= 5 => 'low',
222+
$complexity <= 10 => 'medium',
223+
$complexity <= 15 => 'high',
224+
default => 'very_high',
225+
};
226+
}
194227
}

src/PhpParser/AnnotationVisitor.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,13 @@ public function reset(): void
195195
$this->currentNamespace = '';
196196
$this->currentClassName = '';
197197
}
198+
199+
/**
200+
* Reset only the current context (for between-file cleanup).
201+
*/
202+
public function resetContext(): void
203+
{
204+
$this->currentNamespace = '';
205+
$this->currentClassName = '';
206+
}
198207
}

src/PhpParser/CognitiveMetricsVisitor.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ class CognitiveMetricsVisitor extends NodeVisitorAbstract
2525
private string $currentMethod = '';
2626
private int $currentReturnCount = 0;
2727

28+
/**
29+
* @var array<string, string> Cache for normalized FQCNs
30+
*/
31+
private static array $fqcnCache = [];
32+
2833
/**
2934
* @var AnnotationVisitor|null The annotation visitor to check for ignored items
3035
*/
@@ -73,6 +78,19 @@ public function resetValues(): void
7378
$this->ifCount = 0;
7479
}
7580

81+
/**
82+
* Reset all data including method metrics (for memory cleanup between files).
83+
*/
84+
public function resetAll(): void
85+
{
86+
// Clear all accumulated data to prevent memory leaks
87+
$this->methodMetrics = [];
88+
$this->currentNamespace = '';
89+
$this->currentClassName = '';
90+
$this->currentMethod = '';
91+
$this->resetValues();
92+
}
93+
7694
/**
7795
* Create the initial metrics array for a method.
7896
*/
@@ -237,10 +255,15 @@ private function setCurrentClassOnEnterNode(Node $node): bool
237255

238256
/**
239257
* Ensures the FQCN always starts with a backslash.
258+
* Uses caching to avoid repeated string operations.
240259
*/
241260
private function normalizeFqcn(string $fqcn): string
242261
{
243-
return str_starts_with($fqcn, '\\') ? $fqcn : '\\' . $fqcn;
262+
if (!isset(self::$fqcnCache[$fqcn])) {
263+
self::$fqcnCache[$fqcn] = str_starts_with($fqcn, '\\') ? $fqcn : '\\' . $fqcn;
264+
}
265+
266+
return self::$fqcnCache[$fqcn];
244267
}
245268

246269
public function enterNode(Node $node): int|Node|null

0 commit comments

Comments
 (0)