From 9bc4e0a083659735138249706c2aa5321e4b7d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Fri, 22 May 2026 00:11:59 +0200 Subject: [PATCH 1/5] Add Understandability Metrics and Reporting (#70) This commit introduces a new set of metrics focused on understandability, enhancing the cognitive analysis capabilities. Key changes include: - Added `UnderstandabilityMetrics` and `UnderstandabilityCalculator` for calculating and summarizing understandability metrics. - Integrated understandability metrics into the existing cognitive metrics framework. - Updated configuration options to enable the display of understandability metrics in reports. - Enhanced the reporting system to include understandability in the output format. - Added tests to ensure the correctness of the new understandability features. These changes aim to provide deeper insights into code complexity and maintainability, aligning with modern coding standards. --- composer.lock | 312 ++++++++++--- config.yml | 1 + src/Business/Cognitive/CognitiveMetrics.php | 32 +- src/Business/Cognitive/Parser.php | 15 + .../UnderstandabilityCalculator.php | 68 +++ .../UnderstandabilityCalculatorInterface.php | 28 ++ .../UnderstandabilityMetrics.php | 37 ++ src/Command/Presentation/MetricFormatter.php | 28 +- .../Presentation/TableHeaderBuilder.php | 15 + src/Command/Presentation/TableRowBuilder.php | 15 + src/Config/CognitiveConfig.php | 2 + src/Config/ConfigFactory.php | 1 + src/Config/ConfigLoader.php | 3 + src/PhpParser/CombinedMetricsVisitor.php | 23 +- src/PhpParser/UnderstandabilityVisitor.php | 431 ++++++++++++++++++ stan.json | 1 + tests/Fixtures/understandability-config.yml | 10 + .../Churn/Report/TestCognitiveConfig.php | 2 + .../UnderstandabilityCalculatorTest.php | 55 +++ .../Command/CognitiveMetricsCommandTest.php | 5 + .../Command/OutputWithUnderstandability.txt | 55 +++ .../UnderstandabilityVisitorTest.php | 260 +++++++++++ 22 files changed, 1336 insertions(+), 63 deletions(-) create mode 100644 src/Business/Understandability/UnderstandabilityCalculator.php create mode 100644 src/Business/Understandability/UnderstandabilityCalculatorInterface.php create mode 100644 src/Business/Understandability/UnderstandabilityMetrics.php create mode 100644 src/PhpParser/UnderstandabilityVisitor.php create mode 100644 stan.json create mode 100644 tests/Fixtures/understandability-config.yml create mode 100644 tests/Unit/Business/Understandability/UnderstandabilityCalculatorTest.php create mode 100644 tests/Unit/Command/OutputWithUnderstandability.txt create mode 100644 tests/Unit/PhpParser/UnderstandabilityVisitorTest.php diff --git a/composer.lock b/composer.lock index 5de4bb4..0041e78 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "822640c462f0d98b33c92e0f7b9231ad", + "content-hash": "331a3bfd67cb0b4f7c8ebd4a472626c9", "packages": [ { "name": "nikic/php-parser", @@ -64,6 +64,55 @@ }, "time": "2024-07-01T20:03:41+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -1469,6 +1518,102 @@ ], "time": "2021-04-09T19:40:06+00:00" }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-05-06T08:26:05+00:00" + }, { "name": "doctrine/annotations", "version": "2.0.2", @@ -2655,6 +2800,53 @@ ], "time": "2023-12-11T08:22:20+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.33.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" + }, + "time": "2024-10-13T11:25:22+00:00" + }, { "name": "phpstan/phpstan", "version": "2.1.0", @@ -3136,55 +3328,6 @@ ], "time": "2024-08-02T03:56:43+00:00" }, - { - "name": "psr/cache", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" - }, - "time": "2021-02-03T23:26:27+00:00" - }, { "name": "roave/security-advisories", "version": "dev-latest", @@ -5252,6 +5395,71 @@ ], "time": "2024-07-11T14:55:45+00:00" }, + { + "name": "slevomat/coding-standard", + "version": "8.15.0", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "7d1d957421618a3803b593ec31ace470177d7817" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", + "reference": "7d1d957421618a3803b593ec31ace470177d7817", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": "^1.23.1", + "squizlabs/php_codesniffer": "^3.9.0" + }, + "require-dev": { + "phing/phing": "2.17.4", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.10.60", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.16", + "phpstan/phpstan-strict-rules": "1.5.2", + "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2024-03-09T15:20:58+00:00" + }, { "name": "squizlabs/php_codesniffer", "version": "3.10.0", @@ -5926,6 +6134,8 @@ "platform": { "php": "^8.1" }, - "platform-dev": {}, + "platform-dev": { + "ext-dom": "*" + }, "plugin-api-version": "2.6.0" } diff --git a/config.yml b/config.yml index 2064475..8224b9d 100644 --- a/config.yml +++ b/config.yml @@ -5,6 +5,7 @@ cognitive: showOnlyMethodsExceedingThreshold: false showHalsteadComplexity: false showCyclomaticComplexity: false + showUnderstandability: false showDetailedCognitiveMetrics: true groupByClass: true metrics: diff --git a/src/Business/Cognitive/CognitiveMetrics.php b/src/Business/Cognitive/CognitiveMetrics.php index ce58123..e6f36dc 100644 --- a/src/Business/Cognitive/CognitiveMetrics.php +++ b/src/Business/Cognitive/CognitiveMetrics.php @@ -6,6 +6,7 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetrics; use Phauthentic\CognitiveCodeAnalysis\Business\Cyclomatic\CyclomaticMetrics; +use Phauthentic\CognitiveCodeAnalysis\Business\Understandability\UnderstandabilityMetrics; use InvalidArgumentException; use JsonSerializable; @@ -69,6 +70,7 @@ class CognitiveMetrics implements JsonSerializable private ?HalsteadMetrics $halstead = null; private ?CyclomaticMetrics $cyclomatic = null; + private ?UnderstandabilityMetrics $understandability = null; private ?float $coverage = null; /** @@ -103,18 +105,23 @@ public function __construct(array $metrics) ]); } - if (!isset($metrics['cyclomatic_complexity'])) { - // Handle baseline format with individual cyclomatic fields - if (isset($metrics['cyclomaticComplexity']) && !isset($metrics['cyclomatic_complexity'])) { - $this->cyclomatic = new CyclomaticMetrics([ - 'complexity' => $metrics['cyclomaticComplexity'], - 'riskLevel' => $metrics['cyclomaticRiskLevel'] ?? 'unknown' - ]); - } - return; + if (isset($metrics['cyclomatic_complexity'])) { + $this->cyclomatic = new CyclomaticMetrics($metrics['cyclomatic_complexity']); + } elseif (isset($metrics['cyclomaticComplexity'])) { + $this->cyclomatic = new CyclomaticMetrics([ + 'complexity' => $metrics['cyclomaticComplexity'], + 'riskLevel' => $metrics['cyclomaticRiskLevel'] ?? 'unknown', + ]); } - $this->cyclomatic = new CyclomaticMetrics($metrics['cyclomatic_complexity']); + if (isset($metrics['understandability'])) { + $this->understandability = new UnderstandabilityMetrics($metrics['understandability']); + } elseif (isset($metrics['understandabilityComplexity'])) { + $this->understandability = new UnderstandabilityMetrics([ + 'complexity' => $metrics['understandabilityComplexity'], + 'riskLevel' => $metrics['understandabilityRiskLevel'] ?? 'unknown', + ]); + } } /** @@ -527,4 +534,9 @@ public function getCyclomatic(): ?CyclomaticMetrics { return $this->cyclomatic; } + + public function getUnderstandability(): ?UnderstandabilityMetrics + { + return $this->understandability; + } } diff --git a/src/Business/Cognitive/Parser.php b/src/Business/Cognitive/Parser.php index c256c8a..453461f 100644 --- a/src/Business/Cognitive/Parser.php +++ b/src/Business/Cognitive/Parser.php @@ -75,6 +75,7 @@ public function parse(string $code): array $methodMetrics = $this->combinedVisitor->getMethodMetrics(); $cyclomaticMetrics = $this->combinedVisitor->getMethodComplexity(); $halsteadMetrics = $this->combinedVisitor->getHalsteadMethodMetrics(); + $understandabilityMetrics = $this->combinedVisitor->getMethodUnderstandability(); // Now reset the combined visitor $this->combinedVisitor->resetAll(); @@ -102,6 +103,20 @@ public function parse(string $code): array $methodMetrics[$method]['halstead'] = $metrics; } + foreach ($understandabilityMetrics as $method => $complexityData) { + if (!isset($methodMetrics[$method])) { + continue; + } + + $complexity = $complexityData['complexity'] ?? $complexityData; + $riskLevel = $complexityData['risk_level'] ?? 'unknown'; + $methodMetrics[$method]['understandability'] = [ + 'complexity' => $complexity, + 'risk_level' => $riskLevel, + 'breakdown' => $complexityData['breakdown'] ?? [], + ]; + } + return $methodMetrics; } diff --git a/src/Business/Understandability/UnderstandabilityCalculator.php b/src/Business/Understandability/UnderstandabilityCalculator.php new file mode 100644 index 0000000..20f7e9f --- /dev/null +++ b/src/Business/Understandability/UnderstandabilityCalculator.php @@ -0,0 +1,68 @@ + $incrementCounts + */ + public function calculateComplexity(array $incrementCounts): int + { + return $incrementCounts['total'] ?? 0; + } + + /** + * @param array $incrementCounts + * @return array + */ + public function createBreakdown(array $incrementCounts, int $totalComplexity): array + { + return array_merge(['total' => $totalComplexity], $incrementCounts); + } + + public function getRiskLevel(int $complexity): string + { + return match (true) { + $complexity <= 5 => 'low', + $complexity <= 10 => 'medium', + $complexity <= 15 => 'high', + default => 'very_high', + }; + } + + /** + * @param array $methodComplexities + * @param array> $methodBreakdowns + * @return array + */ + public function createSummary(array $methodComplexities, array $methodBreakdowns): array + { + $summary = [ + 'methods' => [], + 'high_risk_methods' => [], + 'very_high_risk_methods' => [], + ]; + + foreach ($methodComplexities as $methodKey => $complexity) { + $riskLevel = $this->getRiskLevel($complexity); + $summary['methods'][$methodKey] = [ + 'complexity' => $complexity, + 'risk_level' => $riskLevel, + 'breakdown' => $methodBreakdowns[$methodKey] ?? [], + ]; + + if ($complexity >= 10) { + $summary['high_risk_methods'][$methodKey] = $complexity; + } + + if ($complexity >= 15) { + $summary['very_high_risk_methods'][$methodKey] = $complexity; + } + } + + return $summary; + } +} diff --git a/src/Business/Understandability/UnderstandabilityCalculatorInterface.php b/src/Business/Understandability/UnderstandabilityCalculatorInterface.php new file mode 100644 index 0000000..2b5c3b5 --- /dev/null +++ b/src/Business/Understandability/UnderstandabilityCalculatorInterface.php @@ -0,0 +1,28 @@ + $incrementCounts + */ + public function calculateComplexity(array $incrementCounts): int; + + /** + * @param array $incrementCounts + * @return array + */ + public function createBreakdown(array $incrementCounts, int $totalComplexity): array; + + public function getRiskLevel(int $complexity): string; + + /** + * @param array $methodComplexities + * @param array> $methodBreakdowns + * @return array + */ + public function createSummary(array $methodComplexities, array $methodBreakdowns): array; +} diff --git a/src/Business/Understandability/UnderstandabilityMetrics.php b/src/Business/Understandability/UnderstandabilityMetrics.php new file mode 100644 index 0000000..64a1349 --- /dev/null +++ b/src/Business/Understandability/UnderstandabilityMetrics.php @@ -0,0 +1,37 @@ + $data + */ + public function __construct(array $data) + { + $this->complexity = $data['complexity'] ?? 0; + $this->riskLevel = (string)($data['risk_level'] ?? $data['riskLevel'] ?? 'unknown'); + $breakdown = $data['breakdown'] ?? $data; + $this->structuralCount = $breakdown['structural'] ?? 0; + $this->hybridCount = $breakdown['hybrid'] ?? 0; + $this->fundamentalCount = $breakdown['fundamental'] ?? 0; + $this->nestingCount = $breakdown['nesting'] ?? 0; + $this->recursionCount = $breakdown['recursion'] ?? 0; + } +} diff --git a/src/Command/Presentation/MetricFormatter.php b/src/Command/Presentation/MetricFormatter.php index 55678a0..8981db0 100644 --- a/src/Command/Presentation/MetricFormatter.php +++ b/src/Command/Presentation/MetricFormatter.php @@ -6,6 +6,7 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetrics; use Phauthentic\CognitiveCodeAnalysis\Business\Cyclomatic\CyclomaticMetrics; +use Phauthentic\CognitiveCodeAnalysis\Business\Understandability\UnderstandabilityMetrics; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; /** @@ -82,11 +83,36 @@ public function formatCyclomaticComplexity(?CyclomaticMetrics $cyclomatic): stri return $complexity . ' (' . $riskColored . ')'; } + public function formatUnderstandability(?UnderstandabilityMetrics $understandability): string + { + if (!$understandability) { + return '-'; + } + + $complexity = $understandability->complexity; + $risk = $understandability->riskLevel; + if ($risk === '' || $risk === 'unknown') { + return (string)$complexity; + } + + return $complexity . ' (' . $this->colorUnderstandabilityRisk($risk) . ')'; + } + private function colorCyclomaticRisk(string $risk): string + { + return $this->colorRisk($risk); + } + + private function colorUnderstandabilityRisk(string $risk): string + { + return $this->colorRisk($risk); + } + + private function colorRisk(string $risk): string { return match (strtolower($risk)) { 'medium' => '' . $risk . '', - 'high' => '' . $risk . '', + 'high', 'very_high' => '' . $risk . '', default => $risk, }; } diff --git a/src/Command/Presentation/TableHeaderBuilder.php b/src/Command/Presentation/TableHeaderBuilder.php index 1fd79ae..75a1be0 100644 --- a/src/Command/Presentation/TableHeaderBuilder.php +++ b/src/Command/Presentation/TableHeaderBuilder.php @@ -37,6 +37,7 @@ public function getGroupedTableHeaders(): array $fields = $this->addHalsteadHeaders($fields); $fields = $this->addCyclomaticHeaders($fields); + $fields = $this->addUnderstandabilityHeaders($fields); $fields = $this->addCoverageHeader($fields); return $fields; @@ -75,6 +76,7 @@ public function getSingleTableHeaders(): array $fields = $this->addHalsteadHeaders($fields); $fields = $this->addCyclomaticHeaders($fields); + $fields = $this->addUnderstandabilityHeaders($fields); if ($this->hasCoverage) { $fields[] = "Coverage"; @@ -115,6 +117,19 @@ private function addCyclomaticHeaders(array $fields): array return $fields; } + /** + * @param array $fields + * @return array + */ + private function addUnderstandabilityHeaders(array $fields): array + { + if ($this->config->showUnderstandability) { + $fields[] = "Understandability"; + } + + return $fields; + } + /** * @param array $fields * @return array diff --git a/src/Command/Presentation/TableRowBuilder.php b/src/Command/Presentation/TableRowBuilder.php index 795342a..5d693d4 100644 --- a/src/Command/Presentation/TableRowBuilder.php +++ b/src/Command/Presentation/TableRowBuilder.php @@ -8,6 +8,7 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Delta; use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetrics; use Phauthentic\CognitiveCodeAnalysis\Business\Cyclomatic\CyclomaticMetrics; +use Phauthentic\CognitiveCodeAnalysis\Business\Understandability\UnderstandabilityMetrics; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; @@ -139,6 +140,19 @@ private function addCyclomaticFields(array $fields, ?CyclomaticMetrics $cyclomat return $fields; } + /** + * @param array $fields + * @return array + */ + private function addUnderstandabilityFields(array $fields, ?UnderstandabilityMetrics $understandability): array + { + if ($this->config->showUnderstandability) { + $fields['understandability'] = $this->formatter->formatUnderstandability($understandability); + } + + return $fields; + } + /** * Add weighted value to the row * @@ -256,6 +270,7 @@ private function extracted(array $fields, CognitiveMetrics $metrics): array $fields = $this->addHalsteadFields($fields, $metrics->getHalstead()); $fields = $this->addCyclomaticFields($fields, $metrics->getCyclomatic()); + $fields = $this->addUnderstandabilityFields($fields, $metrics->getUnderstandability()); return $fields; } diff --git a/src/Config/CognitiveConfig.php b/src/Config/CognitiveConfig.php index cf8cf06..9542894 100644 --- a/src/Config/CognitiveConfig.php +++ b/src/Config/CognitiveConfig.php @@ -25,6 +25,7 @@ public function __construct( public readonly float $scoreThreshold, public readonly bool $showHalsteadComplexity = false, public readonly bool $showCyclomaticComplexity = false, + public readonly bool $showUnderstandability = false, public readonly bool $groupByClass = false, public readonly bool $showDetailedCognitiveMetrics = true, public readonly ?CacheConfig $cache = null, @@ -53,6 +54,7 @@ public function toArray(): array 'scoreThreshold' => $this->scoreThreshold, 'showHalsteadComplexity' => $this->showHalsteadComplexity, 'showCyclomaticComplexity' => $this->showCyclomaticComplexity, + 'showUnderstandability' => $this->showUnderstandability, 'groupByClass' => $this->groupByClass, 'showDetailedCognitiveMetrics' => $this->showDetailedCognitiveMetrics, 'cache' => $this->cache?->toArray(), diff --git a/src/Config/ConfigFactory.php b/src/Config/ConfigFactory.php index fe4055f..92332a6 100644 --- a/src/Config/ConfigFactory.php +++ b/src/Config/ConfigFactory.php @@ -43,6 +43,7 @@ public function fromArray(array $config): CognitiveConfig scoreThreshold: $config['cognitive']['scoreThreshold'], showHalsteadComplexity: $config['cognitive']['showHalsteadComplexity'] ?? false, showCyclomaticComplexity: $config['cognitive']['showCyclomaticComplexity'] ?? false, + showUnderstandability: $config['cognitive']['showUnderstandability'] ?? false, groupByClass: $config['cognitive']['groupByClass'] ?? true, showDetailedCognitiveMetrics: $config['cognitive']['showDetailedCognitiveMetrics'] ?? true, cache: $cacheConfig, diff --git a/src/Config/ConfigLoader.php b/src/Config/ConfigLoader.php index 29cbbf0..aa0cbee 100644 --- a/src/Config/ConfigLoader.php +++ b/src/Config/ConfigLoader.php @@ -105,6 +105,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->booleanNode('showCyclomaticComplexity') ->defaultValue(false) ->end() + ->booleanNode('showUnderstandability') + ->defaultValue(false) + ->end() ->booleanNode('groupByClass') ->defaultValue(true) ->end() diff --git a/src/PhpParser/CombinedMetricsVisitor.php b/src/PhpParser/CombinedMetricsVisitor.php index 3e920a6..8555363 100644 --- a/src/PhpParser/CombinedMetricsVisitor.php +++ b/src/PhpParser/CombinedMetricsVisitor.php @@ -6,6 +6,7 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cyclomatic\CyclomaticComplexityCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\Halstead\HalsteadMetricsCalculator; +use Phauthentic\CognitiveCodeAnalysis\Business\Understandability\UnderstandabilityCalculator; use PhpParser\Node; use PhpParser\NodeVisitor; @@ -22,6 +23,8 @@ class CombinedMetricsVisitor implements NodeVisitor private HalsteadMetricsVisitor $halsteadVisitor; private HalsteadMetricsCalculator $halsteadCalculator; private CyclomaticComplexityCalculator $cyclomaticCalculator; + private UnderstandabilityVisitor $understandabilityVisitor; + private UnderstandabilityCalculator $understandabilityCalculator; public function __construct() { @@ -29,8 +32,12 @@ public function __construct() $this->cognitiveVisitor = new CognitiveMetricsVisitor(); $this->cyclomaticCalculator = new CyclomaticComplexityCalculator(); $this->cyclomaticVisitor = new CyclomaticComplexityVisitor($this->cyclomaticCalculator); + $this->cyclomaticVisitor->setAnnotationVisitor($this->annotationVisitor); $this->halsteadCalculator = new HalsteadMetricsCalculator(); $this->halsteadVisitor = new HalsteadMetricsVisitor($this->halsteadCalculator); + $this->understandabilityCalculator = new UnderstandabilityCalculator(); + $this->understandabilityVisitor = new UnderstandabilityVisitor($this->understandabilityCalculator); + $this->understandabilityVisitor->setAnnotationVisitor($this->annotationVisitor); } /** @@ -51,13 +58,15 @@ public function enterNode(Node $node): int|Node|null $result2 = $this->cognitiveVisitor->enterNode($node); $result3 = $this->cyclomaticVisitor->enterNode($node); $result4 = $this->halsteadVisitor->enterNode($node); + $result5 = $this->understandabilityVisitor->enterNode($node); // If any visitor wants to skip children, respect that if ( $result1 === NodeVisitor::DONT_TRAVERSE_CHILDREN || $result2 === NodeVisitor::DONT_TRAVERSE_CHILDREN || $result3 === NodeVisitor::DONT_TRAVERSE_CHILDREN || - $result4 === NodeVisitor::DONT_TRAVERSE_CHILDREN + $result4 === NodeVisitor::DONT_TRAVERSE_CHILDREN || + $result5 === NodeVisitor::DONT_TRAVERSE_CHILDREN ) { return NodeVisitor::DONT_TRAVERSE_CHILDREN; } @@ -72,6 +81,7 @@ public function leaveNode(Node $node): int|Node|null $this->cognitiveVisitor->leaveNode($node); $this->cyclomaticVisitor->leaveNode($node); $this->halsteadVisitor->leaveNode($node); + $this->understandabilityVisitor->leaveNode($node); return null; } @@ -93,6 +103,7 @@ public function resetAll(): void $this->cognitiveVisitor->resetValues(); $this->cyclomaticVisitor->resetAll(); $this->halsteadVisitor->resetMetrics(); + $this->understandabilityVisitor->resetAll(); } /** @@ -104,6 +115,7 @@ public function resetAllBetweenFiles(): void $this->cognitiveVisitor->resetAll(); $this->cyclomaticVisitor->resetAll(); $this->halsteadVisitor->resetAll(); + $this->understandabilityVisitor->resetAll(); } /** @@ -132,6 +144,15 @@ public function getHalsteadMethodMetrics(): array return $metrics['methods'] ?? []; } + /** + * Get method understandability (Sonar cognitive complexity) from the understandability visitor. + */ + public function getMethodUnderstandability(): array + { + $summary = $this->understandabilityVisitor->getComplexitySummary(); + return $summary['methods'] ?? []; + } + /** * Get ignored items from the annotation visitor. */ diff --git a/src/PhpParser/UnderstandabilityVisitor.php b/src/PhpParser/UnderstandabilityVisitor.php new file mode 100644 index 0000000..dca8ac4 --- /dev/null +++ b/src/PhpParser/UnderstandabilityVisitor.php @@ -0,0 +1,431 @@ + + */ + private array $methodComplexity = []; + + /** + * @var array> + */ + private array $methodBreakdown = []; + + private string $currentNamespace = ''; + private string $currentClassName = ''; + private string $currentMethodName = ''; + private string $currentMethodKey = ''; + + private int $nestingLevel = 0; + private int $score = 0; + private int $structuralCount = 0; + private int $hybridCount = 0; + private int $fundamentalCount = 0; + private int $nestingIncrementCount = 0; + private int $recursionCount = 0; + private bool $hasRecursion = false; + + /** + * @var array + */ + private static array $fqcnCache = []; + + private ?AnnotationVisitor $annotationVisitor = null; + + public function __construct( + private readonly UnderstandabilityCalculatorInterface $calculator, + ) { + } + + public function setAnnotationVisitor(AnnotationVisitor $annotationVisitor): void + { + $this->annotationVisitor = $annotationVisitor; + } + + public function resetMethodCounters(): void + { + $this->nestingLevel = 0; + $this->score = 0; + $this->structuralCount = 0; + $this->hybridCount = 0; + $this->fundamentalCount = 0; + $this->nestingIncrementCount = 0; + $this->recursionCount = 0; + $this->hasRecursion = false; + } + + public function resetAll(): void + { + $this->methodComplexity = []; + $this->methodBreakdown = []; + $this->currentNamespace = ''; + $this->currentClassName = ''; + $this->currentMethodName = ''; + $this->currentMethodKey = ''; + $this->resetMethodCounters(); + } + + public function enterNode(Node $node): void + { + $this->setCurrentNamespaceOnEnterNode($node); + $this->setCurrentClassOnEnterNode($node); + $this->handleClassMethodEnter($node); + + if ($this->currentMethodKey === '') { + return; + } + + $this->countRecursion($node); + $this->countLogicalOperator($node); + $this->countControlFlowIncrements($node); + } + + public function leaveNode(Node $node): void + { + $this->leaveControlFlowNesting($node); + $this->handleClassMethodLeave($node); + $this->checkNamespaceLeave($node); + $this->checkClassLeave($node); + } + + /** + * @return array + */ + public function getMethodComplexity(): array + { + return $this->methodComplexity; + } + + /** + * @return array + */ + public function getComplexitySummary(): array + { + return $this->calculator->createSummary($this->methodComplexity, $this->methodBreakdown); + } + + private function setCurrentNamespaceOnEnterNode(Node $node): void + { + if (!$node instanceof Stmt\Namespace_) { + return; + } + + $this->currentNamespace = $node->name instanceof Node\Name ? $node->name->toString() : ''; + } + + private function setCurrentClassOnEnterNode(Node $node): void + { + if (!$node instanceof Stmt\Class_ && !$node instanceof Stmt\Trait_) { + return; + } + + if ($node->name === null) { + return; + } + + $fqcn = $this->currentNamespace . '\\' . $node->name->toString(); + $this->currentClassName = $this->normalizeFqcn($fqcn); + + if ($this->annotationVisitor !== null && $this->annotationVisitor->isClassIgnored($this->currentClassName)) { + $this->currentClassName = ''; + } + } + + private function normalizeFqcn(string $fqcn): string + { + if (!isset(self::$fqcnCache[$fqcn])) { + self::$fqcnCache[$fqcn] = str_starts_with($fqcn, '\\') ? $fqcn : '\\' . $fqcn; + } + + return self::$fqcnCache[$fqcn]; + } + + private function handleClassMethodEnter(Node $node): void + { + if (!$node instanceof Stmt\ClassMethod) { + return; + } + + if ($this->currentClassName === '') { + return; + } + + $methodKey = $this->currentClassName . '::' . $node->name->toString(); + + if ($this->annotationVisitor !== null && $this->annotationVisitor->isMethodIgnored($methodKey)) { + return; + } + + $this->currentMethodName = $node->name->toString(); + $this->currentMethodKey = $methodKey; + $this->resetMethodCounters(); + } + + private function handleClassMethodLeave(Node $node): void + { + if (!$node instanceof Stmt\ClassMethod) { + return; + } + + if ($this->currentMethodKey === '') { + return; + } + + if ($this->hasRecursion) { + $this->addFundamental(); + $this->recursionCount++; + } + + $incrementCounts = [ + 'total' => $this->score, + 'structural' => $this->structuralCount, + 'hybrid' => $this->hybridCount, + 'fundamental' => $this->fundamentalCount, + 'nesting' => $this->nestingIncrementCount, + 'recursion' => $this->recursionCount, + ]; + + $this->methodComplexity[$this->currentMethodKey] = $this->score; + $this->methodBreakdown[$this->currentMethodKey] = $this->calculator->createBreakdown( + $incrementCounts, + $this->score, + ); + + $this->currentMethodName = ''; + $this->currentMethodKey = ''; + $this->resetMethodCounters(); + } + + private function countControlFlowIncrements(Node $node): void + { + match (true) { + $node instanceof Stmt\If_ => $this->addStructuralAndIncreaseNesting(), + $node instanceof Stmt\ElseIf_ => $this->addHybridAndIncreaseNesting(), + $node instanceof Stmt\Else_ => $this->addHybridAndIncreaseNesting(), + $node instanceof Expr\Ternary => $this->addStructuralAndIncreaseNesting(), + $node instanceof Stmt\Switch_ => $this->enterSwitch(), + $node instanceof Expr\Match_ => $this->enterMatch(), + $node instanceof Stmt\For_, + $node instanceof Stmt\Foreach_, + $node instanceof Stmt\While_, + $node instanceof Stmt\Do_ => $this->addStructuralAndIncreaseNesting(), + $node instanceof Stmt\Catch_ => $this->addStructuralAndIncreaseNesting(), + $node instanceof Expr\Closure, + $node instanceof Expr\ArrowFunction => $this->enterClosure(), + $node instanceof Stmt\Goto_ => $this->addFundamental(), + $node instanceof Stmt\Break_, + $node instanceof Stmt\Continue_ => $this->countJumpStatement($node), + default => null, + }; + } + + private function leaveControlFlowNesting(Node $node): void + { + match (true) { + $node instanceof Stmt\If_, + $node instanceof Stmt\ElseIf_, + $node instanceof Stmt\Else_, + $node instanceof Expr\Ternary, + $node instanceof Stmt\For_, + $node instanceof Stmt\Foreach_, + $node instanceof Stmt\While_, + $node instanceof Stmt\Do_, + $node instanceof Stmt\Catch_ => $this->decreaseNesting(), + $node instanceof Stmt\Switch_ => $this->leaveSwitch(), + $node instanceof Expr\Match_ => $this->leaveMatch(), + $node instanceof Expr\Closure, + $node instanceof Expr\ArrowFunction => $this->leaveClosure(), + default => null, + }; + } + + private function enterSwitch(): void + { + $this->addStructuralAndIncreaseNesting(); + } + + private function leaveSwitch(): void + { + $this->decreaseNesting(); + } + + private function enterMatch(): void + { + $this->addStructuralAndIncreaseNesting(); + } + + private function leaveMatch(): void + { + $this->decreaseNesting(); + } + + private function enterClosure(): void + { + $this->nestingLevel++; + } + + private function leaveClosure(): void + { + $this->nestingLevel--; + } + + private function addStructuralAndIncreaseNesting(): void + { + $this->score += 1 + $this->nestingLevel; + $this->structuralCount++; + if ($this->nestingLevel > 0) { + $this->nestingIncrementCount++; + } + $this->nestingLevel++; + } + + private function addHybridAndIncreaseNesting(): void + { + $this->score += 1; + $this->hybridCount++; + $this->nestingLevel++; + } + + private function addFundamental(): void + { + $this->score += 1; + $this->fundamentalCount++; + } + + private function decreaseNesting(): void + { + if ($this->nestingLevel > 0) { + $this->nestingLevel--; + } + } + + private function countJumpStatement(Node $node): void + { + if ($node instanceof Stmt\Goto_) { + $this->addFundamental(); + return; + } + + if (!$node instanceof Stmt\Break_ && !$node instanceof Stmt\Continue_) { + return; + } + + if ($node->num !== null) { + $this->addFundamental(); + } + } + + private function countLogicalOperator(Node $node): void + { + if (!$this->isLogicalBinaryOp($node)) { + return; + } + + /** @var Expr\BinaryOp $node */ + if ($this->leftOperandIsSameLogicalOp($node)) { + return; + } + + $this->addFundamental(); + } + + private function isLogicalBinaryOp(Node $node): bool + { + return $node instanceof Expr\BinaryOp\BooleanAnd + || $node instanceof Expr\BinaryOp\BooleanOr + || $node instanceof Expr\BinaryOp\LogicalAnd + || $node instanceof Expr\BinaryOp\LogicalOr; + } + + private function leftOperandIsSameLogicalOp(Expr\BinaryOp $node): bool + { + $left = $node->left; + $operatorClass = $node::class; + + return $left::class === $operatorClass; + } + + private function countRecursion(Node $node): void + { + if ($this->hasRecursion || $this->currentMethodName === '') { + return; + } + + if ($node instanceof Expr\MethodCall) { + $name = $this->resolveMethodName($node->name); + if ($name === $this->currentMethodName) { + $this->hasRecursion = true; + } + return; + } + + if (!$node instanceof Expr\StaticCall) { + return; + } + + $name = $this->resolveMethodName($node->name); + if ($name !== $this->currentMethodName) { + return; + } + + if ($this->isSelfStaticCall($node->class)) { + $this->hasRecursion = true; + } + } + + private function resolveMethodName(Node\Expr|string|Node\Identifier $name): ?string + { + if ($name instanceof Node\Identifier) { + return $name->toString(); + } + + if (is_string($name)) { + return $name; + } + + return null; + } + + private function isSelfStaticCall(Node\Name|Expr $class): bool + { + if (!$class instanceof Node\Name) { + return false; + } + + $parts = $class->getParts(); + $last = end($parts); + + return in_array($last, ['self', 'static', 'parent'], true); + } + + private function checkNamespaceLeave(Node $node): void + { + if ($node instanceof Stmt\Namespace_) { + $this->currentNamespace = ''; + } + } + + private function checkClassLeave(Node $node): void + { + if ($node instanceof Stmt\Class_ || $node instanceof Stmt\Trait_) { + $this->currentClassName = ''; + } + } +} diff --git a/stan.json b/stan.json new file mode 100644 index 0000000..5650b5c --- /dev/null +++ b/stan.json @@ -0,0 +1 @@ +{"totals":{"errors":0,"file_errors":0},"files":[],"errors":[]} \ No newline at end of file diff --git a/tests/Fixtures/understandability-config.yml b/tests/Fixtures/understandability-config.yml new file mode 100644 index 0000000..d522d53 --- /dev/null +++ b/tests/Fixtures/understandability-config.yml @@ -0,0 +1,10 @@ +cognitive: + excludeFilePatterns: + excludePatterns: + scoreThreshold: 0.5 + showOnlyMethodsExceedingThreshold: false + showHalsteadComplexity: false + showCyclomaticComplexity: false + showUnderstandability: true + showDetailedCognitiveMetrics: true + groupByClass: true diff --git a/tests/Unit/Business/Churn/Report/TestCognitiveConfig.php b/tests/Unit/Business/Churn/Report/TestCognitiveConfig.php index 3531074..f88b806 100644 --- a/tests/Unit/Business/Churn/Report/TestCognitiveConfig.php +++ b/tests/Unit/Business/Churn/Report/TestCognitiveConfig.php @@ -19,6 +19,7 @@ public function __construct( float $scoreThreshold = 0.5, bool $showHalsteadComplexity = false, bool $showCyclomaticComplexity = false, + bool $showUnderstandability = false, bool $groupByClass = false, bool $showDetailedCognitiveMetrics = true, array $customReporters = [] @@ -31,6 +32,7 @@ public function __construct( scoreThreshold: $scoreThreshold, showHalsteadComplexity: $showHalsteadComplexity, showCyclomaticComplexity: $showCyclomaticComplexity, + showUnderstandability: $showUnderstandability, groupByClass: $groupByClass, showDetailedCognitiveMetrics: $showDetailedCognitiveMetrics, customReporters: $customReporters diff --git a/tests/Unit/Business/Understandability/UnderstandabilityCalculatorTest.php b/tests/Unit/Business/Understandability/UnderstandabilityCalculatorTest.php new file mode 100644 index 0000000..4160dd9 --- /dev/null +++ b/tests/Unit/Business/Understandability/UnderstandabilityCalculatorTest.php @@ -0,0 +1,55 @@ +calculator = new UnderstandabilityCalculator(); + } + + #[DataProvider('riskLevelProvider')] + public function testGetRiskLevel(int $complexity, string $expected): void + { + $this->assertSame($expected, $this->calculator->getRiskLevel($complexity)); + } + + /** + * @return array + */ + public static function riskLevelProvider(): array + { + return [ + 'zero' => [0, 'low'], + 'low boundary' => [5, 'low'], + 'medium' => [10, 'medium'], + 'high' => [15, 'high'], + 'very high' => [16, 'very_high'], + ]; + } + + public function testCreateSummaryFlagsHighRiskMethods(): void + { + $summary = $this->calculator->createSummary( + [ + '\\App\\A::low' => 3, + '\\App\\B::high' => 12, + '\\App\\C::veryHigh' => 20, + ], + [], + ); + + $this->assertArrayHasKey('\\App\\B::high', $summary['high_risk_methods']); + $this->assertArrayHasKey('\\App\\C::veryHigh', $summary['very_high_risk_methods']); + $this->assertSame('high', $summary['methods']['\\App\\B::high']['risk_level']); + } +} diff --git a/tests/Unit/Command/CognitiveMetricsCommandTest.php b/tests/Unit/Command/CognitiveMetricsCommandTest.php index 91650f6..a7529cc 100644 --- a/tests/Unit/Command/CognitiveMetricsCommandTest.php +++ b/tests/Unit/Command/CognitiveMetricsCommandTest.php @@ -228,6 +228,11 @@ public static function configurationOutputProvider(): array 'OutputWithCyclomaticOnly.txt', 'Should show only Cyclomatic complexity metrics' ], + 'understandability only' => [ + 'understandability-config.yml', + 'OutputWithUnderstandability.txt', + 'Should show Sonar-style Understandability metric' + ], 'no detailed metrics' => [ 'no-detailed-metrics-config.yml', 'OutputWithoutDetailedMetrics.txt', diff --git a/tests/Unit/Command/OutputWithUnderstandability.txt b/tests/Unit/Command/OutputWithUnderstandability.txt new file mode 100644 index 0000000..a813709 --- /dev/null +++ b/tests/Unit/Command/OutputWithUnderstandability.txt @@ -0,0 +1,55 @@ +Class: \ClassOne +File: tests/TestCode/FileWithTwoClasses.php +┌─────────────┬───────┬───────────┬─────────┬───────────┬──────────┬───────┬────────────┬───────┬────────────┬───────────────────┐ +│ Method Name │ Lines │ Arguments │ Returns │ Variables │ Property │ If │ If Nesting │ Else │ Cognitive │ Understandability │ +│ │ │ │ │ │ Accesses │ │ Level │ │ Complexity │ │ +├─────────────┼───────┼───────────┼─────────┼───────────┼──────────┼───────┼────────────┼───────┼────────────┼───────────────────┤ +│ add │ 4 (0) │ 2 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +└─────────────┴───────┴───────────┴─────────┴───────────┴──────────┴───────┴────────────┴───────┴────────────┴───────────────────┘ + +Class: \ClassTwo +File: tests/TestCode/FileWithTwoClasses.php +┌─────────────┬───────┬───────────┬─────────┬───────────┬──────────┬───────┬────────────┬───────┬────────────┬───────────────────┐ +│ Method Name │ Lines │ Arguments │ Returns │ Variables │ Property │ If │ If Nesting │ Else │ Cognitive │ Understandability │ +│ │ │ │ │ │ Accesses │ │ Level │ │ Complexity │ │ +├─────────────┼───────┼───────────┼─────────┼───────────┼──────────┼───────┼────────────┼───────┼────────────┼───────────────────┤ +│ add │ 4 (0) │ 2 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +└─────────────┴───────┴───────────┴─────────┴───────────┴──────────┴───────┴────────────┴───────┴────────────┴───────────────────┘ + +Class: \Doctrine\ORM\Tools\Pagination\Paginator +File: tests/TestCode/Paginator.php +┌───────────────────────────────────────────┬────────┬───────────┬─────────┬───────────┬──────────┬───────┬────────────┬───────────┬────────────┬───────────────────┐ +│ Method Name │ Lines │ Arguments │ Returns │ Variables │ Property │ If │ If Nesting │ Else │ Cognitive │ Understandability │ +│ │ │ │ │ │ Accesses │ │ Level │ │ Complexity │ │ +├───────────────────────────────────────────┼────────┼───────────┼─────────┼───────────┼──────────┼───────┼────────────┼───────────┼────────────┼───────────────────┤ +│ __construct │ 10 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 1 (low) │ +│ getQuery │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ getFetchJoinCollection │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ getUseOutputWalkers │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ setUseOutputWalkers │ 6 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ count │ 12 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 3 (low) │ +│ getIterator │ 46 (0) │ 0 (0) │ 2 (0) │ 9 (0.693) │ 2 (0) │ 3 (0) │ 2 (0.693) │ 2 (0.693) │ 2.079 │ 8 (medium) │ +│ cloneQuery │ 13 (0) │ 1 (0) │ 1 (0) │ 3 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 1 (low) │ +│ useOutputWalker │ 8 (0) │ 1 (0) │ 2 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 1 (low) │ +│ appendTreeWalker │ 11 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 1 (low) │ +│ getCountQuery │ 25 (0) │ 0 (0) │ 1 (0) │ 4 (0) │ 1 (0) │ 2 (0) │ 1 (0) │ 1 (0) │ 0 │ 3 (low) │ +│ unbindUnusedQueryParams │ 17 (0) │ 1 (0) │ 0 (0) │ 6 (0.336) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0.336 │ 4 (low) │ +│ convertWhereInIdentifiersToDatabaseValues │ 11 (0) │ 1 (0) │ 1 (0) │ 5 (0.182) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0.182 │ 0 (low) │ +└───────────────────────────────────────────┴────────┴───────────┴─────────┴───────────┴──────────┴───────┴────────────┴───────────┴────────────┴───────────────────┘ + +Class: \WP_Debug_Data +File: tests/TestCode/WpDebugData.php +┌───────────────────┬──────────────┬───────────┬─────────┬─────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬───────────────────┐ +│ Method Name │ Lines │ Arguments │ Returns │ Variables │ Property │ If │ If Nesting │ Else │ Cognitive │ Understandability │ +│ │ │ │ │ │ Accesses │ │ Level │ │ Complexity │ │ +├───────────────────┼──────────────┼───────────┼─────────┼─────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼───────────────────┤ +│ check_for_updates │ 6 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ debug_data │ 1230 (3.867) │ 0 (0) │ 1 (0) │ 105 (3.054) │ 20 (0.726) │ 58 (4.025) │ 3 (1.099) │ 33 (3.497) │ 16.267 │ 230 (very_high) │ +│ get_wp_constants │ 144 (1.472) │ 0 (0) │ 1 (0) │ 9 (0.693) │ 0 (0) │ 5 (1.099) │ 1 (0) │ 5 (1.609) │ 4.874 │ 36 (very_high) │ +│ get_wp_filesystem │ 60 (0) │ 0 (0) │ 1 (0) │ 9 (0.693) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0.693 │ 18 (very_high) │ +│ get_mysql_var │ 15 (0) │ 1 (0) │ 2 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 2 (low) │ +│ format │ 60 (0) │ 2 (0) │ 1 (0) │ 11 (0.875) │ 0 (0) │ 5 (1.099) │ 1 (0) │ 5 (1.609) │ 3.584 │ 41 (very_high) │ +│ get_database_size │ 14 (0) │ 0 (0) │ 1 (0) │ 4 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 3 (low) │ +│ get_sizes │ 125 (1.281) │ 0 (0) │ 1 (0) │ 14 (1.099) │ 0 (0) │ 9 (1.946) │ 2 (0.693) │ 5 (1.609) │ 6.628 │ 23 (very_high) │ +└───────────────────┴──────────────┴───────────┴─────────┴─────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴───────────────────┘ + diff --git a/tests/Unit/PhpParser/UnderstandabilityVisitorTest.php b/tests/Unit/PhpParser/UnderstandabilityVisitorTest.php new file mode 100644 index 0000000..1df8a35 --- /dev/null +++ b/tests/Unit/PhpParser/UnderstandabilityVisitorTest.php @@ -0,0 +1,260 @@ +createForHostVersion(); + $ast = $parser->parse($code); + $calculator = new UnderstandabilityCalculator(); + $visitor = new UnderstandabilityVisitor($calculator); + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + $traverser->traverse($ast); + + $methodKey = '\\MyNamespace\\MyClass::' . $methodName; + $complexity = $visitor->getMethodComplexity(); + + $this->assertArrayHasKey($methodKey, $complexity, 'Method not found: ' . $methodKey); + + return $complexity[$methodKey]; + } + + public function testSwitchOnlyScoresOneLikeGetWords(): void + { + $code = <<<'CODE' + assertSame(1, $this->analyzeMethod($code, 'getWords')); + } + + public function testNestedLoopsAndContinueMatchSumOfPrimes(): void + { + $code = <<<'CODE' + assertSame(7, $this->analyzeMethod($code, 'sumOfPrimes')); + } + + public function testNestedIfForWhileCatchScoresNine(): void + { + $code = <<<'CODE' + assertSame(9, $this->analyzeMethod($code)); + } + + public function testClosureNestingScoresTwo(): void + { + $code = <<<'CODE' + $condition1 ? 1 : 0; + } + } + CODE; + + // arrow fn with ternary inside: closure nesting + ternary structural (+1+1) + $this->assertSame(2, $this->analyzeMethod($code)); + } + + public function testIfInsideClosureScoresTwo(): void + { + $code = <<<'CODE' + assertSame(2, $this->analyzeMethod($code)); + } + + public function testLogicalOperatorSequencesInCondition(): void + { + $code = <<<'CODE' + assertSame(2, $this->analyzeMethod($code)); + } + + public function testMixedLogicalOperatorsIncrementPerSequence(): void + { + $code = <<<'CODE' + assertSame(4, $this->analyzeMethod($code)); + } + + public function testSwitchLowerThanIfElseIfChain(): void + { + $switchCode = <<<'CODE' + analyzeMethod($switchCode, 'viaSwitch'); + $ifScore = $this->analyzeMethod($ifCode, 'viaIf'); + + $this->assertLessThan($ifScore, $switchScore); + $this->assertSame(1, $switchScore); + } + + public function testDirectRecursionAddsFundamentalIncrement(): void + { + $code = <<<'CODE' + run($n - 1); + } + } + CODE; + + // if +1, recursion +1 + $this->assertSame(2, $this->analyzeMethod($code)); + } + + public function testIgnoredMethodIsSkippedWhenAnnotationVisitorWired(): void + { + $code = <<<'CODE' + createForHostVersion(); + $ast = $parser->parse($code); + + $annotationVisitor = new AnnotationVisitor(); + $annotationTraverser = new NodeTraverser(); + $annotationTraverser->addVisitor($annotationVisitor); + $annotationTraverser->traverse($ast); + + $visitor = new UnderstandabilityVisitor(new UnderstandabilityCalculator()); + $visitor->setAnnotationVisitor($annotationVisitor); + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + $traverser->traverse($ast); + + $this->assertEmpty($visitor->getMethodComplexity()); + } +} From f81fe27a71870788f4180eb25ecd7d8c37a9171c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Mon, 25 May 2026 20:30:01 +0200 Subject: [PATCH 2/5] Enhance understandability metrics handling and improve type validation - Added new methods in `UnderstandabilityMetrics` for resolving risk levels and count values, improving data handling and robustness. - Updated `Parser` class to include understandability metrics in the analysis process. - Enhanced `CombinedMetricsVisitor` with type annotations for the `getMethodUnderstandability` method. - Added suppression warnings in `UnderstandabilityVisitor` for excessive complexity and method count, promoting cleaner code practices. --- src/Business/Cognitive/Parser.php | 1 + .../UnderstandabilityMetrics.php | 54 ++++++++++++++++--- src/PhpParser/CombinedMetricsVisitor.php | 3 ++ src/PhpParser/UnderstandabilityVisitor.php | 2 + 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/Business/Cognitive/Parser.php b/src/Business/Cognitive/Parser.php index 2bc0c2f..8c54ca0 100644 --- a/src/Business/Cognitive/Parser.php +++ b/src/Business/Cognitive/Parser.php @@ -78,6 +78,7 @@ public function parse(string $code): array $cyclomaticMetrics = $this->combinedVisitor->getMethodComplexity(); /** @var array $halsteadMetrics */ $halsteadMetrics = $this->combinedVisitor->getHalsteadMethodMetrics(); + /** @var array $understandabilityMetrics */ $understandabilityMetrics = $this->combinedVisitor->getMethodUnderstandability(); // Now reset the combined visitor diff --git a/src/Business/Understandability/UnderstandabilityMetrics.php b/src/Business/Understandability/UnderstandabilityMetrics.php index 64a1349..f5139ff 100644 --- a/src/Business/Understandability/UnderstandabilityMetrics.php +++ b/src/Business/Understandability/UnderstandabilityMetrics.php @@ -25,13 +25,51 @@ class UnderstandabilityMetrics */ public function __construct(array $data) { - $this->complexity = $data['complexity'] ?? 0; - $this->riskLevel = (string)($data['risk_level'] ?? $data['riskLevel'] ?? 'unknown'); - $breakdown = $data['breakdown'] ?? $data; - $this->structuralCount = $breakdown['structural'] ?? 0; - $this->hybridCount = $breakdown['hybrid'] ?? 0; - $this->fundamentalCount = $breakdown['fundamental'] ?? 0; - $this->nestingCount = $breakdown['nesting'] ?? 0; - $this->recursionCount = $breakdown['recursion'] ?? 0; + $breakdown = $data['breakdown'] ?? null; + if (!is_array($breakdown)) { + $breakdown = $data; + } + + /** @var array $breakdown */ + $this->complexity = $this->resolveIntValue($data['complexity'] ?? null, 0); + $this->riskLevel = $this->resolveRiskLevel($data); + $this->structuralCount = $this->resolveCountValue($data, $breakdown, 'structuralCount', 'structural', 0); + $this->hybridCount = $this->resolveCountValue($data, $breakdown, 'hybridCount', 'hybrid', 0); + $this->fundamentalCount = $this->resolveCountValue($data, $breakdown, 'fundamentalCount', 'fundamental', 0); + $this->nestingCount = $this->resolveCountValue($data, $breakdown, 'nestingCount', 'nesting', 0); + $this->recursionCount = $this->resolveCountValue($data, $breakdown, 'recursionCount', 'recursion', 0); + } + + /** + * @param array $data + */ + private function resolveRiskLevel(array $data): string + { + $value = $data['risk_level'] ?? $data['riskLevel'] ?? 'unknown'; + + return is_string($value) ? $value : 'unknown'; + } + + /** + * @param array $data + * @param array $breakdown + */ + private function resolveCountValue( + array $data, + array $breakdown, + string $dataKey, + string $breakdownKey, + int $default + ): int { + if (array_key_exists($dataKey, $data)) { + return $this->resolveIntValue($data[$dataKey], $default); + } + + return $this->resolveIntValue($breakdown[$breakdownKey] ?? null, $default); + } + + private function resolveIntValue(mixed $value, int $default): int + { + return is_int($value) ? $value : $default; } } diff --git a/src/PhpParser/CombinedMetricsVisitor.php b/src/PhpParser/CombinedMetricsVisitor.php index 8555363..369bc09 100644 --- a/src/PhpParser/CombinedMetricsVisitor.php +++ b/src/PhpParser/CombinedMetricsVisitor.php @@ -147,6 +147,9 @@ public function getHalsteadMethodMetrics(): array /** * Get method understandability (Sonar cognitive complexity) from the understandability visitor. */ + /** + * @return array + */ public function getMethodUnderstandability(): array { $summary = $this->understandabilityVisitor->getComplexitySummary(); diff --git a/src/PhpParser/UnderstandabilityVisitor.php b/src/PhpParser/UnderstandabilityVisitor.php index dca8ac4..e1f555d 100644 --- a/src/PhpParser/UnderstandabilityVisitor.php +++ b/src/PhpParser/UnderstandabilityVisitor.php @@ -15,6 +15,8 @@ * * @see https://www.sonarsource.com/resources/cognitive-complexity/ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.TooManyMethods) */ class UnderstandabilityVisitor extends NodeVisitorAbstract { From 89c3244f217b816adc959cd55306da9b2c64f3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Mon, 25 May 2026 20:49:50 +0200 Subject: [PATCH 3/5] Add Understandability documentation for Sonar Cognitive Complexity - Introduced a new document, Understandability.md, detailing the Sonar Cognitive Complexity metric. - Explained the calculation method, risk levels, and configuration options for enabling understandability metrics. - Provided references to relevant literature and resources for further reading on cognitive complexity. --- docs/Understandability.md | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/Understandability.md diff --git a/docs/Understandability.md b/docs/Understandability.md new file mode 100644 index 0000000..0b0e2c4 --- /dev/null +++ b/docs/Understandability.md @@ -0,0 +1,63 @@ +# Understandability (Sonar Cognitive Complexity) + +Understandability measures how hard it is for a human to follow a method’s control flow. It implements **Sonar Cognitive Complexity** as described in SonarSource’s 2023 white paper (*Cognitive Complexity: a new way of measuring understandability*, v1.7). + +This metric is **separate** from this tool’s weighted **Cognitive Complexity** score, which sums logarithmic weights over structural metrics (lines, arguments, `if` count, and so on). Understandability follows Sonar’s rule-based control-flow model instead. + +## Why use it? + +Cyclomatic complexity counts paths through code but treats structures like `switch` and nested loops similarly even when one is much harder to read. Sonar Cognitive Complexity is designed to better match maintainer intuition: it penalizes nested flow breaks, treats `switch` as a single decision, and ignores method calls that shorthand logic. + +## How it is calculated + +Per method, the score follows three rules from the Sonar spec: + +1. **Ignore shorthand** — method calls and null-coalescing are not counted. +2. **Increment for flow breaks** — loops, `if`, ternary, `catch`, `switch`/`match`, logical-operator sequences, recursion, and multi-level `break`/`continue`/`goto`. +3. **Increment for nesting** — each nested flow-breaking structure adds its current nesting depth to the score. + +Increments fall into four categories (each adds to the total, but categories clarify nesting behavior): + +| Category | Examples | +|-------------|-----------------------------------------------| +| Structural | `if`, loops, ternary, `catch`, `switch` | +| Hybrid | `elseif`, `else` (no nesting penalty, but increase nesting level) | +| Fundamental | Logical-operator sequences, recursion, jumps | +| Nesting | Extra points when structures are nested | + +Structural increments use `1 + nestingLevel`; hybrid increments add `1` only. + +## Risk levels + +| Score | Risk | +|-------|-------------| +| 0–5 | low | +| 6–10 | medium | +| 11–15 | high | +| 16+ | very high | + +Console output shows `score (risk)`, for example `7 (medium)`. + +## Configuration + +Understandability is **off by default**. Enable it in `phpcca.yaml`: + +```yaml +cognitive: + showUnderstandability: true +``` + +When enabled, an **Understandability** column appears in console output. It does not affect baselines, file reports, or sorting unless those features are extended separately. + +## Interpretation + +- **Low (≤5)** — easy to follow; usually fine as-is. +- **Medium (6–10)** — worth a closer look during review. +- **High / very high (≥11)** — nested or branching logic is taxing; consider extracting methods or simplifying control flow. + +Use as an indicator, not an absolute rule. Domain logic, parsers, and constructors may legitimately score higher. + +## References + +- [Sonar Cognitive Complexity white paper (2023, v1.7)](https://www.sonarsource.com/resources/cognitive-complexity/) +- [An Empirical Validation of Cognitive Complexity as a Measure of Source Code Understandability](https://arxiv.org/pdf/2007.12520) — Muñoz Barón, Wyrich, Wagner From 4c060626f075c46dd488033412b0c042f1bb69d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Mon, 25 May 2026 20:53:03 +0200 Subject: [PATCH 4/5] Refactor understandability calculations and visitor logic for improved clarity - Updated `UnderstandabilityCalculator` to skip low complexity methods, enhancing risk categorization. - Refined `UnderstandabilityVisitor` logic to return early for ignored classes and non-relevant nodes, improving code readability and maintainability. - Adjusted nesting and logical operator handling to streamline flow and reduce unnecessary checks. --- .../UnderstandabilityCalculator.php | 6 ++-- src/PhpParser/UnderstandabilityVisitor.php | 36 ++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/Business/Understandability/UnderstandabilityCalculator.php b/src/Business/Understandability/UnderstandabilityCalculator.php index 20f7e9f..8e66795 100644 --- a/src/Business/Understandability/UnderstandabilityCalculator.php +++ b/src/Business/Understandability/UnderstandabilityCalculator.php @@ -58,9 +58,11 @@ public function createSummary(array $methodComplexities, array $methodBreakdowns $summary['high_risk_methods'][$methodKey] = $complexity; } - if ($complexity >= 15) { - $summary['very_high_risk_methods'][$methodKey] = $complexity; + if ($complexity < 15) { + continue; } + + $summary['very_high_risk_methods'][$methodKey] = $complexity; } return $summary; diff --git a/src/PhpParser/UnderstandabilityVisitor.php b/src/PhpParser/UnderstandabilityVisitor.php index e1f555d..ced7108 100644 --- a/src/PhpParser/UnderstandabilityVisitor.php +++ b/src/PhpParser/UnderstandabilityVisitor.php @@ -145,9 +145,11 @@ private function setCurrentClassOnEnterNode(Node $node): void $fqcn = $this->currentNamespace . '\\' . $node->name->toString(); $this->currentClassName = $this->normalizeFqcn($fqcn); - if ($this->annotationVisitor !== null && $this->annotationVisitor->isClassIgnored($this->currentClassName)) { - $this->currentClassName = ''; + if ($this->annotationVisitor === null || !$this->annotationVisitor->isClassIgnored($this->currentClassName)) { + return; } + + $this->currentClassName = ''; } private function normalizeFqcn(string $fqcn): string @@ -313,9 +315,11 @@ private function addFundamental(): void private function decreaseNesting(): void { - if ($this->nestingLevel > 0) { - $this->nestingLevel--; + if ($this->nestingLevel <= 0) { + return; } + + $this->nestingLevel--; } private function countJumpStatement(Node $node): void @@ -329,9 +333,11 @@ private function countJumpStatement(Node $node): void return; } - if ($node->num !== null) { - $this->addFundamental(); + if ($node->num === null) { + return; } + + $this->addFundamental(); } private function countLogicalOperator(Node $node): void @@ -387,9 +393,11 @@ private function countRecursion(Node $node): void return; } - if ($this->isSelfStaticCall($node->class)) { - $this->hasRecursion = true; + if (!$this->isSelfStaticCall($node->class)) { + return; } + + $this->hasRecursion = true; } private function resolveMethodName(Node\Expr|string|Node\Identifier $name): ?string @@ -419,15 +427,19 @@ private function isSelfStaticCall(Node\Name|Expr $class): bool private function checkNamespaceLeave(Node $node): void { - if ($node instanceof Stmt\Namespace_) { - $this->currentNamespace = ''; + if (!($node instanceof Stmt\Namespace_)) { + return; } + + $this->currentNamespace = ''; } private function checkClassLeave(Node $node): void { - if ($node instanceof Stmt\Class_ || $node instanceof Stmt\Trait_) { - $this->currentClassName = ''; + if (!($node instanceof Stmt\Class_) && !($node instanceof Stmt\Trait_)) { + return; } + + $this->currentClassName = ''; } } From e29b789c56bd7e539b664bc7fcba1e8303108529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Mon, 25 May 2026 21:07:39 +0200 Subject: [PATCH 5/5] Update cache versioning in CognitiveMetricsCollector and enhance understandability output - Introduced a new constant `CACHE_VERSION` in `CognitiveMetricsCollector` to manage cache versioning effectively. - Updated cache item versioning to use the new constant, ensuring stale entries are ignored when the analysis result shape changes. - Enhanced the understandability output in various test files by updating complexity ratings, improving clarity in metrics reporting. --- .../Cognitive/CognitiveMetricsCollector.php | 15 +++++- .../Command/OutputWithUnderstandability.txt | 46 +++++++++---------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index 202228e..1d7f0c9 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -23,6 +23,11 @@ */ class CognitiveMetricsCollector { + /** + * Bump when cached analysis_result shape changes so stale entries are ignored. + */ + private const CACHE_VERSION = '1.1'; + /** * @var array */ @@ -322,7 +327,7 @@ private function cacheResult( string $configHash ): void { $cacheItem->set([ - 'version' => '1.0', + 'version' => self::CACHE_VERSION, 'file_path' => $file->getRealPath(), 'file_mtime' => $file->getMTime(), 'config_hash' => $configHash, @@ -363,6 +368,14 @@ private function getCachedMetrics(SplFileInfo $file, string $configHash, bool $u return ['metrics' => null, 'cacheItem' => $cacheItem]; } + if (($cachedData['version'] ?? null) !== self::CACHE_VERSION) { + return ['metrics' => null, 'cacheItem' => $cacheItem]; + } + + if (($cachedData['config_hash'] ?? null) !== $configHash) { + return ['metrics' => null, 'cacheItem' => $cacheItem]; + } + $ignoredItems = $cachedData['ignored_items'] ?? []; $this->ignoredItems = $this->normalizeIgnoredItems($ignoredItems); $this->messageBus->dispatch(new FileProcessed($file)); diff --git a/tests/Unit/Command/OutputWithUnderstandability.txt b/tests/Unit/Command/OutputWithUnderstandability.txt index 1d0a934..93da945 100644 --- a/tests/Unit/Command/OutputWithUnderstandability.txt +++ b/tests/Unit/Command/OutputWithUnderstandability.txt @@ -7,7 +7,7 @@ File: tests/TestCode/FileWithTwoClasses.php │ Method Name │ Lines │ Arguments │ Returns │ Variables │ Property │ If │ If Nesting │ Else │ Cognitive │ Understandability │ │ │ │ │ │ │ Accesses │ │ Level │ │ Complexity │ │ ├─────────────┼───────┼───────────┼─────────┼───────────┼──────────┼───────┼────────────┼───────┼────────────┼───────────────────┤ -│ add │ 4 (0) │ 2 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ - │ +│ add │ 4 (0) │ 2 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ └─────────────┴───────┴───────────┴─────────┴───────────┴──────────┴───────┴────────────┴───────┴────────────┴───────────────────┘ Class: \ClassTwo @@ -16,7 +16,7 @@ File: tests/TestCode/FileWithTwoClasses.php │ Method Name │ Lines │ Arguments │ Returns │ Variables │ Property │ If │ If Nesting │ Else │ Cognitive │ Understandability │ │ │ │ │ │ │ Accesses │ │ Level │ │ Complexity │ │ ├─────────────┼───────┼───────────┼─────────┼───────────┼──────────┼───────┼────────────┼───────┼────────────┼───────────────────┤ -│ add │ 4 (0) │ 2 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ - │ +│ add │ 4 (0) │ 2 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ └─────────────┴───────┴───────────┴─────────┴───────────┴──────────┴───────┴────────────┴───────┴────────────┴───────────────────┘ Class: \Doctrine\ORM\Tools\Pagination\Paginator @@ -25,19 +25,19 @@ File: tests/TestCode/Paginator.php │ Method Name │ Lines │ Arguments │ Returns │ Variables │ Property │ If │ If Nesting │ Else │ Cognitive │ Understandability │ │ │ │ │ │ │ Accesses │ │ Level │ │ Complexity │ │ ├───────────────────────────────────────────┼────────┼───────────┼─────────┼───────────┼──────────┼───────┼────────────┼───────────┼────────────┼───────────────────┤ -│ __construct │ 10 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ - │ -│ getQuery │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ - │ -│ getFetchJoinCollection │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ - │ -│ getUseOutputWalkers │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ - │ -│ setUseOutputWalkers │ 6 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ - │ -│ count │ 12 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ - │ -│ getIterator │ 46 (0) │ 0 (0) │ 2 (0) │ 9 (0.693) │ 2 (0) │ 3 (0) │ 2 (0.693) │ 2 (0.693) │ 2.079 │ - │ -│ cloneQuery │ 13 (0) │ 1 (0) │ 1 (0) │ 3 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ - │ -│ useOutputWalker │ 8 (0) │ 1 (0) │ 2 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ - │ -│ appendTreeWalker │ 11 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ - │ -│ getCountQuery │ 25 (0) │ 0 (0) │ 1 (0) │ 4 (0) │ 1 (0) │ 2 (0) │ 1 (0) │ 1 (0) │ 0 │ - │ -│ unbindUnusedQueryParams │ 17 (0) │ 1 (0) │ 0 (0) │ 6 (0.336) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0.336 │ - │ -│ convertWhereInIdentifiersToDatabaseValues │ 11 (0) │ 1 (0) │ 1 (0) │ 5 (0.182) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0.182 │ - │ +│ __construct │ 10 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 1 (low) │ +│ getQuery │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ getFetchJoinCollection │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ getUseOutputWalkers │ 4 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ setUseOutputWalkers │ 6 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ count │ 12 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 3 (low) │ +│ getIterator │ 46 (0) │ 0 (0) │ 2 (0) │ 9 (0.693) │ 2 (0) │ 3 (0) │ 2 (0.693) │ 2 (0.693) │ 2.079 │ 8 (medium) │ +│ cloneQuery │ 13 (0) │ 1 (0) │ 1 (0) │ 3 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 1 (low) │ +│ useOutputWalker │ 8 (0) │ 1 (0) │ 2 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 1 (low) │ +│ appendTreeWalker │ 11 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 1 (low) │ +│ getCountQuery │ 25 (0) │ 0 (0) │ 1 (0) │ 4 (0) │ 1 (0) │ 2 (0) │ 1 (0) │ 1 (0) │ 0 │ 3 (low) │ +│ unbindUnusedQueryParams │ 17 (0) │ 1 (0) │ 0 (0) │ 6 (0.336) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0.336 │ 4 (low) │ +│ convertWhereInIdentifiersToDatabaseValues │ 11 (0) │ 1 (0) │ 1 (0) │ 5 (0.182) │ 1 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0.182 │ 0 (low) │ └───────────────────────────────────────────┴────────┴───────────┴─────────┴───────────┴──────────┴───────┴────────────┴───────────┴────────────┴───────────────────┘ Class: \WP_Debug_Data @@ -46,13 +46,13 @@ File: tests/TestCode/WpDebugData.php │ Method Name │ Lines │ Arguments │ Returns │ Variables │ Property │ If │ If Nesting │ Else │ Cognitive │ Understandability │ │ │ │ │ │ │ Accesses │ │ Level │ │ Complexity │ │ ├───────────────────┼──────────────┼───────────┼─────────┼─────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼───────────────────┤ -│ check_for_updates │ 6 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ - │ -│ debug_data │ 1230 (3.867) │ 0 (0) │ 1 (0) │ 105 (3.054) │ 20 (0.726) │ 58 (4.025) │ 3 (1.099) │ 33 (3.497) │ 16.267 │ - │ -│ get_wp_constants │ 144 (1.472) │ 0 (0) │ 1 (0) │ 9 (0.693) │ 0 (0) │ 5 (1.099) │ 1 (0) │ 5 (1.609) │ 4.874 │ - │ -│ get_wp_filesystem │ 60 (0) │ 0 (0) │ 1 (0) │ 9 (0.693) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0.693 │ - │ -│ get_mysql_var │ 15 (0) │ 1 (0) │ 2 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ - │ -│ format │ 60 (0) │ 2 (0) │ 1 (0) │ 11 (0.875) │ 0 (0) │ 5 (1.099) │ 1 (0) │ 5 (1.609) │ 3.584 │ - │ -│ get_database_size │ 14 (0) │ 0 (0) │ 1 (0) │ 4 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ - │ -│ get_sizes │ 125 (1.281) │ 0 (0) │ 1 (0) │ 14 (1.099) │ 0 (0) │ 9 (1.946) │ 2 (0.693) │ 5 (1.609) │ 6.628 │ - │ +│ check_for_updates │ 6 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 (0) │ 0 │ 0 (low) │ +│ debug_data │ 1230 (3.867) │ 0 (0) │ 1 (0) │ 105 (3.054) │ 20 (0.726) │ 58 (4.025) │ 3 (1.099) │ 33 (3.497) │ 16.267 │ 230 (very_high) │ +│ get_wp_constants │ 144 (1.472) │ 0 (0) │ 1 (0) │ 9 (0.693) │ 0 (0) │ 5 (1.099) │ 1 (0) │ 5 (1.609) │ 4.874 │ 36 (very_high) │ +│ get_wp_filesystem │ 60 (0) │ 0 (0) │ 1 (0) │ 9 (0.693) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0.693 │ 18 (very_high) │ +│ get_mysql_var │ 15 (0) │ 1 (0) │ 2 (0) │ 2 (0) │ 0 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 2 (low) │ +│ format │ 60 (0) │ 2 (0) │ 1 (0) │ 11 (0.875) │ 0 (0) │ 5 (1.099) │ 1 (0) │ 5 (1.609) │ 3.584 │ 41 (very_high) │ +│ get_database_size │ 14 (0) │ 0 (0) │ 1 (0) │ 4 (0) │ 1 (0) │ 1 (0) │ 1 (0) │ 0 (0) │ 0 │ 3 (low) │ +│ get_sizes │ 125 (1.281) │ 0 (0) │ 1 (0) │ 14 (1.099) │ 0 (0) │ 9 (1.946) │ 2 (0.693) │ 5 (1.609) │ 6.628 │ 23 (very_high) │ └───────────────────┴──────────────┴───────────┴─────────┴─────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴───────────────────┘