diff --git a/src/Business/Cognitive/Parser.php b/src/Business/Cognitive/Parser.php index 9016504..6430004 100644 --- a/src/Business/Cognitive/Parser.php +++ b/src/Business/Cognitive/Parser.php @@ -4,6 +4,7 @@ namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive; +use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetricsCalculator; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\PhpParser\AnnotationVisitor; use Phauthentic\CognitiveCodeAnalysis\PhpParser\CognitiveMetricsVisitor; @@ -25,6 +26,7 @@ class Parser protected CyclomaticComplexityVisitor $cyclomaticComplexityVisitor; protected HalsteadMetricsVisitor $halsteadMetricsVisitor; protected CombinedMetricsVisitor $combinedVisitor; + protected HalsteadMetricsCalculator $halsteadCalculator; public function __construct( ParserFactory $parserFactory, @@ -44,7 +46,8 @@ public function __construct( $this->cyclomaticComplexityVisitor->setAnnotationVisitor($this->annotationVisitor); $this->traverser->addVisitor($this->cyclomaticComplexityVisitor); - $this->halsteadMetricsVisitor = new HalsteadMetricsVisitor(); + $this->halsteadCalculator = new HalsteadMetricsCalculator(); + $this->halsteadMetricsVisitor = new HalsteadMetricsVisitor($this->halsteadCalculator); $this->halsteadMetricsVisitor->setAnnotationVisitor($this->annotationVisitor); $this->traverser->addVisitor($this->halsteadMetricsVisitor); diff --git a/src/Business/Halstead/HalsteadMetricsCalculator.php b/src/Business/Halstead/HalsteadMetricsCalculator.php new file mode 100644 index 0000000..39e7f05 --- /dev/null +++ b/src/Business/Halstead/HalsteadMetricsCalculator.php @@ -0,0 +1,106 @@ + $operators Array of operator types + * @param array $operands Array of operand values + * @param string $identifier Identifier for the metrics (class name or method FQN) + * @return array Array containing all Halstead metrics + */ + public function calculateMetrics(array $operators, array $operands, string $identifier): array + { + // Step 1: Count distinct and total occurrences of operators and operands + $distinctOperators = count(array_unique($operators)); + $distinctOperands = count(array_unique($operands)); + $totalOperators = count($operators); + $totalOperands = count($operands); + + // Step 2: Calculate basic metrics + $programLength = $this->calculateProgramLength($totalOperators, $totalOperands); + $programVocabulary = $this->calculateProgramVocabulary($distinctOperators, $distinctOperands); + + // Step 3: Calculate advanced metrics + $volume = $this->calculateVolume($programLength, $programVocabulary); + $difficulty = $this->calculateDifficulty($distinctOperators, $totalOperands, $distinctOperands); + $effort = $difficulty * $volume; + + // Step 4: Prepare the results array + return [ + 'n1' => $distinctOperators, + 'n2' => $distinctOperands, + 'N1' => $totalOperators, + 'N2' => $totalOperands, + 'programLength' => $programLength, + 'programVocabulary' => $programVocabulary, + 'volume' => $volume, + 'difficulty' => $difficulty, + 'effort' => $effort, + 'fqName' => $identifier, + ]; + } + + /** + * Calculate the program length. + * + * @param int $N1 The total occurrences of operators + * @param int $N2 The total occurrences of operands + * @return int The program length + */ + public function calculateProgramLength(int $N1, int $N2): int + { + return $N1 + $N2; + } + + /** + * Calculate the program vocabulary. + * + * @param int $n1 The count of distinct operators + * @param int $n2 The count of distinct operands + * @return int The program vocabulary + */ + public function calculateProgramVocabulary(int $n1, int $n2): int + { + return $n1 + $n2; + } + + /** + * Calculate the volume of the program. + * + * @param int $programLength The length of the program + * @param int $programVocabulary The vocabulary of the program + * @return float The volume of the program + */ + public function calculateVolume(int $programLength, int $programVocabulary): float + { + if ($programVocabulary <= 0) { + return 0.0; + } + return $programLength * log($programVocabulary, 2); + } + + /** + * Calculate the difficulty of the program. + * + * @param int $n1 The count of distinct operators + * @param int $N2 The total occurrences of operands + * @param int $n2 The count of distinct operands + * @return float The difficulty of the program + */ + public function calculateDifficulty(int $n1, int $N2, int $n2): float + { + if ($n2 === 0) { + return 0.0; + } + return ($n1 / 2) * ($N2 / $n2); + } +} diff --git a/src/Business/Halstead/HalsteadMetricsCalculatorInterface.php b/src/Business/Halstead/HalsteadMetricsCalculatorInterface.php new file mode 100644 index 0000000..4a0e04a --- /dev/null +++ b/src/Business/Halstead/HalsteadMetricsCalculatorInterface.php @@ -0,0 +1,58 @@ + $operators Array of operator types + * @param array $operands Array of operand values + * @param string $identifier Identifier for the metrics (class name or method FQN) + * @return array Array containing all Halstead metrics + */ + public function calculateMetrics(array $operators, array $operands, string $identifier): array; + + /** + * Calculate the volume of the program. + * + * @param int $programLength The length of the program + * @param int $programVocabulary The vocabulary of the program + * @return float The volume of the program + */ + public function calculateVolume(int $programLength, int $programVocabulary): float; + + /** + * Calculate the difficulty of the program. + * + * @param int $n1 The count of distinct operators + * @param int $N2 The total occurrences of operands + * @param int $n2 The count of distinct operands + * @return float The difficulty of the program + */ + public function calculateDifficulty(int $n1, int $N2, int $n2): float; + + /** + * Calculate the program length. + * + * @param int $N1 The total occurrences of operators + * @param int $N2 The total occurrences of operands + * @return int The program length + */ + public function calculateProgramLength(int $N1, int $N2): int; + + /** + * Calculate the program vocabulary. + * + * @param int $n1 The count of distinct operators + * @param int $n2 The count of distinct operands + * @return int The program vocabulary + */ + public function calculateProgramVocabulary(int $n1, int $n2): int; +} diff --git a/src/PhpParser/CombinedMetricsVisitor.php b/src/PhpParser/CombinedMetricsVisitor.php index 87ace32..57e30df 100644 --- a/src/PhpParser/CombinedMetricsVisitor.php +++ b/src/PhpParser/CombinedMetricsVisitor.php @@ -4,6 +4,7 @@ namespace Phauthentic\CognitiveCodeAnalysis\PhpParser; +use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetricsCalculator; use PhpParser\Node; use PhpParser\NodeVisitor; @@ -18,13 +19,15 @@ class CombinedMetricsVisitor implements NodeVisitor private CognitiveMetricsVisitor $cognitiveVisitor; private CyclomaticComplexityVisitor $cyclomaticVisitor; private HalsteadMetricsVisitor $halsteadVisitor; + private HalsteadMetricsCalculator $halsteadCalculator; public function __construct() { $this->annotationVisitor = new AnnotationVisitor(); $this->cognitiveVisitor = new CognitiveMetricsVisitor(); $this->cyclomaticVisitor = new CyclomaticComplexityVisitor(); - $this->halsteadVisitor = new HalsteadMetricsVisitor(); + $this->halsteadCalculator = new HalsteadMetricsCalculator(); + $this->halsteadVisitor = new HalsteadMetricsVisitor($this->halsteadCalculator); } /** diff --git a/src/PhpParser/HalsteadMetricsVisitor.php b/src/PhpParser/HalsteadMetricsVisitor.php index c838f5c..69bec5c 100644 --- a/src/PhpParser/HalsteadMetricsVisitor.php +++ b/src/PhpParser/HalsteadMetricsVisitor.php @@ -4,6 +4,7 @@ namespace Phauthentic\CognitiveCodeAnalysis\PhpParser; +use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetricsCalculatorInterface; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Trait_; @@ -13,8 +14,6 @@ /** * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ShortVariable) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ class HalsteadMetricsVisitor extends NodeVisitorAbstract { @@ -38,6 +37,21 @@ class HalsteadMetricsVisitor extends NodeVisitorAbstract */ private ?AnnotationVisitor $annotationVisitor = null; + /** + * @var HalsteadMetricsCalculatorInterface The calculator for Halstead metrics + */ + private HalsteadMetricsCalculatorInterface $calculator; + + /** + * Constructor for HalsteadMetricsVisitor. + * + * @param HalsteadMetricsCalculatorInterface $calculator The calculator for Halstead metrics + */ + public function __construct(HalsteadMetricsCalculatorInterface $calculator) + { + $this->calculator = $calculator; + } + /** * Set the annotation visitor to check for ignored items. */ @@ -59,6 +73,7 @@ public function setAnnotationVisitor(AnnotationVisitor $annotationVisitor): void * The node processing is part of gathering metrics for the Halstead complexity measurement. Operators and operands * are collected to later calculate metrics such as program length, vocabulary, volume, difficulty, and effort. * + * @SuppressWarnings("PHPMD.CyclomaticComplexity") */ public function enterNode(Node $node) { @@ -170,7 +185,7 @@ public function leaveNode(Node $node) // Store metrics for the method before resetting if ($this->currentClassName !== null && $this->currentMethodName !== null) { $methodKey = $this->currentClassName . '::' . $this->currentMethodName; - $this->methodMetrics[$methodKey] = $this->calculateMetricsFor($this->methodOperators, $this->methodOperands, $methodKey); + $this->methodMetrics[$methodKey] = $this->calculator->calculateMetrics($this->methodOperators, $this->methodOperands, $methodKey); } $this->currentMethodName = null; $this->methodOperators = []; @@ -229,7 +244,7 @@ private function storeClassMetrics(): void return; } - $this->classMetrics[$this->currentClassName] = $this->calculateMetrics(); + $this->classMetrics[$this->currentClassName] = $this->calculator->calculateMetrics($this->operators, $this->operands, $this->currentClassName); } public function resetMetrics(): void @@ -261,156 +276,6 @@ public function resetAll(): void $this->methodMetrics = []; } - private function calculateMetrics(): array - { - // Step 1: Count distinct and total occurrences of operators and operands - $distinctOperators = $this->countDistinctOperators(); - $distinctOperands = $this->countDistinctOperands(); - $totalOperators = $this->countTotalOperators(); - $totalOperands = $this->countTotalOperands(); - - // Step 2: Calculate basic metrics - $programLength = $this->calculateProgramLength($totalOperators, $totalOperands); - $programVocabulary = $this->calculateProgramVocabulary($distinctOperators, $distinctOperands); - - // Step 3: Calculate advanced metrics - $volume = $this->calculateVolume($programLength, $programVocabulary); - $difficulty = $this->calculateDifficulty($distinctOperators, $totalOperands, $distinctOperands); - $effort = $difficulty * $volume; - - // Step 4: Prepare the results array - return [ - 'n1' => $distinctOperators, - 'n2' => $distinctOperands, - 'N1' => $totalOperators, - 'N2' => $totalOperands, - 'programLength' => $programLength, - 'programVocabulary' => $programVocabulary, - 'volume' => $volume, - 'difficulty' => $difficulty, - 'effort' => $effort, - 'className' => $this->currentClassName, // Include the FQCN - ]; - } - - private function calculateMetricsFor(array $operators, array $operands, string $fqName): array - { - $distinctOperators = count(array_unique($operators)); - $distinctOperands = count(array_unique($operands)); - $totalOperators = count($operators); - $totalOperands = count($operands); - $programLength = $totalOperators + $totalOperands; - $programVocabulary = $distinctOperators + $distinctOperands; - $volume = $programVocabulary > 0 ? $programLength * log($programVocabulary, 2) : 0.0; - $difficulty = ($distinctOperators > 0 && $distinctOperands > 0) ? ($distinctOperators / 2) * ($totalOperands / $distinctOperands) : 0.0; - $effort = $difficulty * $volume; - - return [ - 'n1' => $distinctOperators, - 'n2' => $distinctOperands, - 'N1' => $totalOperators, - 'N2' => $totalOperands, - 'programLength' => $programLength, - 'programVocabulary' => $programVocabulary, - 'volume' => $volume, - 'difficulty' => $difficulty, - 'effort' => $effort, - 'fqName' => $fqName, - ]; - } - - /** - * Calculate the program length. - * - * @param int $N1 The total occurrences of operators. - * @param int $N2 The total occurrences of operands. - * @return int The program length. - */ - private function calculateProgramLength(int $N1, int $N2): int - { - return $N1 + $N2; - } - - /** - * Calculate the program vocabulary. - * - * @param int $n1 The count of distinct operators. - * @param int $n2 The count of distinct operands. - * @return int The program vocabulary. - */ - private function calculateProgramVocabulary(int $n1, int $n2): int - { - return $n1 + $n2; - } - - /** - * Calculate the volume of the program. - * - * @param int $programLength The length of the program. - * @param int $programVocabulary The vocabulary of the program. - * @return float The volume of the program. - */ - private function calculateVolume(int $programLength, int $programVocabulary): float - { - return $programLength * log($programVocabulary, 2); - } - - /** - * Calculate the difficulty of the program. - * - * @param int $n1 The count of distinct operators. - * @param int $N2 The total occurrences of operands. - * @param int $n2 The count of distinct operands. - * @return float The difficulty of the program. - */ - private function calculateDifficulty(int $n1, int $N2, int $n2): float - { - if ($n2 === 0) { - return 0.0; - } - return ($n1 / 2) * ($N2 / $n2); - } - - /** - * Count the number of distinct operators. - * - * @return int The count of distinct operators. - */ - private function countDistinctOperators(): int - { - return count(array_unique($this->operators)); - } - - /** - * Count the number of distinct operands. - * - * @return int The count of distinct operands. - */ - private function countDistinctOperands(): int - { - return count(array_unique($this->operands)); - } - - /** - * Count the total occurrences of operators. - * - * @return int The total occurrences of operators. - */ - private function countTotalOperators(): int - { - return count($this->operators); - } - - /** - * Count the total occurrences of operands. - * - * @return int The total occurrences of operands. - */ - private function countTotalOperands(): int - { - return count($this->operands); - } - public function getMetrics(): array { // In case we haven't left the last class diff --git a/tests/Unit/Business/Halstead/HalsteadMetricsCalculatorTest.php b/tests/Unit/Business/Halstead/HalsteadMetricsCalculatorTest.php new file mode 100644 index 0000000..c20b32d --- /dev/null +++ b/tests/Unit/Business/Halstead/HalsteadMetricsCalculatorTest.php @@ -0,0 +1,166 @@ +calculator = new HalsteadMetricsCalculator(); + } + + public function testCalculateProgramLength(): void + { + $this->assertEquals(5, $this->calculator->calculateProgramLength(2, 3)); + $this->assertEquals(0, $this->calculator->calculateProgramLength(0, 0)); + $this->assertEquals(10, $this->calculator->calculateProgramLength(7, 3)); + } + + public function testCalculateProgramVocabulary(): void + { + $this->assertEquals(5, $this->calculator->calculateProgramVocabulary(2, 3)); + $this->assertEquals(0, $this->calculator->calculateProgramVocabulary(0, 0)); + $this->assertEquals(10, $this->calculator->calculateProgramVocabulary(7, 3)); + } + + public function testCalculateVolume(): void + { + // Test with normal values: 10 * log(4, 2) = 10 * 2 = 20 + $this->assertEquals(20.0, $this->calculator->calculateVolume(10, 4), 'Volume calculation with normal values'); + + // Test with zero vocabulary (edge case) - should return 0 + $this->assertEquals(0.0, $this->calculator->calculateVolume(5, 0), 'Volume calculation with zero vocabulary'); + + // Test with single vocabulary: 5 * log(1, 2) = 5 * 0 = 0 + $this->assertEquals(0.0, $this->calculator->calculateVolume(5, 1), 'Volume calculation with single vocabulary'); + } + + public function testCalculateDifficulty(): void + { + // Test normal case + $this->assertEquals(2.0, $this->calculator->calculateDifficulty(2, 4, 2), 'Difficulty calculation with normal values'); + + // Test edge case: zero distinct operands + $this->assertEquals(0.0, $this->calculator->calculateDifficulty(2, 4, 0), 'Difficulty calculation with zero distinct operands'); + + // Test edge case: zero operators + $this->assertEquals(0.0, $this->calculator->calculateDifficulty(0, 4, 2), 'Difficulty calculation with zero operators'); + + // Test edge case: zero total operands + $this->assertEquals(0.0, $this->calculator->calculateDifficulty(2, 0, 2), 'Difficulty calculation with zero total operands'); + } + + public function testCalculateMetricsWithEmptyArrays(): void + { + $result = $this->calculator->calculateMetrics([], [], 'EmptyTest'); + + $this->assertEquals(0, $result['n1'], 'Distinct operators should be 0'); + $this->assertEquals(0, $result['n2'], 'Distinct operands should be 0'); + $this->assertEquals(0, $result['N1'], 'Total operators should be 0'); + $this->assertEquals(0, $result['N2'], 'Total operands should be 0'); + $this->assertEquals(0, $result['programLength'], 'Program length should be 0'); + $this->assertEquals(0, $result['programVocabulary'], 'Program vocabulary should be 0'); + $this->assertEquals(0.0, $result['volume'], 'Volume should be 0'); + $this->assertEquals(0.0, $result['difficulty'], 'Difficulty should be 0'); + $this->assertEquals(0.0, $result['effort'], 'Effort should be 0'); + $this->assertEquals('EmptyTest', $result['fqName'], 'Identifier should be preserved'); + } + + public function testCalculateMetricsWithSimpleData(): void + { + $operators = ['+', '=', '+']; + $operands = ['$a', '$b', '$c', '$a']; + $identifier = 'TestClass::testMethod'; + + $result = $this->calculator->calculateMetrics($operators, $operands, $identifier); + + // Check basic counts + $this->assertEquals(2, $result['n1'], 'Should have 2 distinct operators'); + $this->assertEquals(3, $result['n2'], 'Should have 3 distinct operands'); + $this->assertEquals(3, $result['N1'], 'Should have 3 total operators'); + $this->assertEquals(4, $result['N2'], 'Should have 4 total operands'); + + // Check calculated metrics + $this->assertEquals(7, $result['programLength'], 'Program length should be 7'); + $this->assertEquals(5, $result['programVocabulary'], 'Program vocabulary should be 5'); + + // Check advanced metrics + $expectedVolume = 7 * log(5, 2); + $this->assertEquals($expectedVolume, $result['volume'], 'Volume calculation should be correct'); + + $expectedDifficulty = (2 / 2) * (4 / 3); + $this->assertEquals($expectedDifficulty, $result['difficulty'], 'Difficulty calculation should be correct'); + + $expectedEffort = $expectedDifficulty * $expectedVolume; + $this->assertEquals($expectedEffort, $result['effort'], 'Effort calculation should be correct'); + + $this->assertEquals($identifier, $result['fqName'], 'Identifier should be preserved'); + } + + public function testCalculateMetricsWithDuplicateOperatorsAndOperands(): void + { + $operators = ['+', '+', '+', '+']; + $operands = ['$a', '$a', '$a', '$a']; + $identifier = 'TestClass::duplicateMethod'; + + $result = $this->calculator->calculateMetrics($operators, $operands, $identifier); + + // Check basic counts + $this->assertEquals(1, $result['n1'], 'Should have 1 distinct operator'); + $this->assertEquals(1, $result['n2'], 'Should have 1 distinct operand'); + $this->assertEquals(4, $result['N1'], 'Should have 4 total operators'); + $this->assertEquals(4, $result['N2'], 'Should have 4 total operands'); + + // Check calculated metrics + $this->assertEquals(8, $result['programLength'], 'Program length should be 8'); + $this->assertEquals(2, $result['programVocabulary'], 'Program vocabulary should be 2'); + + // Check advanced metrics + $expectedVolume = 8 * log(2, 2); + $this->assertEquals($expectedVolume, $result['volume'], 'Volume calculation should be correct'); + + $expectedDifficulty = (1 / 2) * (4 / 1); + $this->assertEquals($expectedDifficulty, $result['difficulty'], 'Difficulty calculation should be correct'); + + $expectedEffort = $expectedDifficulty * $expectedVolume; + $this->assertEquals($expectedEffort, $result['effort'], 'Effort calculation should be correct'); + } + + public function testCalculateMetricsWithComplexData(): void + { + $operators = ['+', '-', '*', '/', '=', '==', '!=', '+', '-']; + $operands = ['$a', '$b', '$c', '$d', '$e', '$f', '$a', '$b', '$c', '$d', '$e']; + $identifier = 'ComplexClass::complexMethod'; + + $result = $this->calculator->calculateMetrics($operators, $operands, $identifier); + + // Check basic counts + $this->assertEquals(7, $result['n1'], 'Should have 7 distinct operators'); + $this->assertEquals(6, $result['n2'], 'Should have 6 distinct operands'); // $a, $b, $c, $d, $e, $f + $this->assertEquals(9, $result['N1'], 'Should have 9 total operators'); + $this->assertEquals(11, $result['N2'], 'Should have 11 total operands'); + + // Check calculated metrics + $this->assertEquals(20, $result['programLength'], 'Program length should be 20'); + $this->assertEquals(13, $result['programVocabulary'], 'Program vocabulary should be 13'); + + // Check advanced metrics + $expectedVolume = 20 * log(13, 2); + $this->assertEquals($expectedVolume, $result['volume'], 'Volume calculation should be correct'); + + $expectedDifficulty = (7 / 2) * (11 / 6); + $this->assertEquals($expectedDifficulty, $result['difficulty'], 'Difficulty calculation should be correct'); + + $expectedEffort = $expectedDifficulty * $expectedVolume; + $this->assertEquals($expectedEffort, $result['effort'], 'Effort calculation should be correct'); + + $this->assertEquals($identifier, $result['fqName'], 'Identifier should be preserved'); + } +} diff --git a/tests/Unit/PhpParser/AnnotationVisitorTest.php b/tests/Unit/PhpParser/AnnotationVisitorTest.php index ce24dec..de3cb01 100644 --- a/tests/Unit/PhpParser/AnnotationVisitorTest.php +++ b/tests/Unit/PhpParser/AnnotationVisitorTest.php @@ -4,18 +4,16 @@ namespace Phauthentic\CognitiveCodeAnalysis\Tests\Unit\PhpParser; +use PhpParser\Parser; use PhpParser\ParserFactory; use PhpParser\NodeTraverser; use Phauthentic\CognitiveCodeAnalysis\PhpParser\AnnotationVisitor; use PHPUnit\Framework\TestCase; -/** - * Test for AnnotationVisitor class. - */ class AnnotationVisitorTest extends TestCase { private AnnotationVisitor $visitor; - private $parser; + private Parser $parser; private NodeTraverser $traverser; protected function setUp(): void diff --git a/tests/Unit/PhpParser/HalsteadMetricsVisitorTest.php b/tests/Unit/PhpParser/HalsteadMetricsVisitorTest.php index d3ab759..8b62499 100644 --- a/tests/Unit/PhpParser/HalsteadMetricsVisitorTest.php +++ b/tests/Unit/PhpParser/HalsteadMetricsVisitorTest.php @@ -4,6 +4,8 @@ namespace Phauthentic\CognitiveCodeAnalysis\Tests\Unit\PhpParser; +use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetricsCalculator; +use Phauthentic\CognitiveCodeAnalysis\PhpParser\HalsteadMetricsVisitor; use PhpParser\ParserFactory; use PhpParser\NodeTraverser; use PHPUnit\Framework\TestCase; @@ -26,7 +28,8 @@ public function add($a, $b) { $parser = (new ParserFactory())->createForHostVersion(); $ast = $parser->parse($code); $traverser = new NodeTraverser(); - $visitor = new \Phauthentic\CognitiveCodeAnalysis\PhpParser\HalsteadMetricsVisitor(); + $calculator = new HalsteadMetricsCalculator(); + $visitor = new HalsteadMetricsVisitor($calculator); $traverser->addVisitor($visitor); $traverser->traverse($ast); $metrics = $visitor->getMetrics();