diff --git a/config.yml b/config.yml index 0bd3f65..7ef89c2 100644 --- a/config.yml +++ b/config.yml @@ -1,7 +1,5 @@ cognitive: - excludedClasses: [], - excludedMethods: [], - excludePatterns: [], + excludePatterns: metrics: lineCount: threshold: 60 @@ -29,9 +27,6 @@ cognitive: scale: 1.0 halstead: - excludedClasses: [], - excludedMethods: [], - excludePatterns: [], threshold: difficulty: 0.0 effort: 0.0 diff --git a/docs/Configuration.md b/docs/Configuration.md index 89781e9..5bfef72 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -8,38 +8,52 @@ You can specify another configuration file by passing it to the config options: php analyse.php metrics:cognitive --config= ``` +## Excluding Classes and Methods + +You can exclude classes and methods via a regex in the configuration. + +The following configuration will exclude all constructors and all methods of classes that end with `Transformer`. + +```yaml +cognitive: + excludePatterns: + - '(.*)::__construct' + - '(.*)Transformer::(.*)' +``` + ## Tuning the calculation The configuration file can contain the following settings for the calculation of cognitive complexity. Feel free to adjust the values to your match your opinion on what makes code complex. -``` -metrics: - lineCount: - threshold: 60 - scale: 2.0 - argCount: - threshold: 4 - scale: 1.0 - returnCount: - threshold: 2 - scale: 5.0 - variableCount: - threshold: 2 - scale: 5.0 - propertyCallCount: - threshold: 2 - scale: 15.0 - ifCount: - threshold: 3 - scale: 1.0 - ifNestingLevel: - threshold: 1 - scale: 1.0 - elseCount: - threshold: 1 - scale: 1.0 +```yaml +cognitive: + metrics: + lineCount: + threshold: 60 + scale: 2.0 + argCount: + threshold: 4 + scale: 1.0 + returnCount: + threshold: 2 + scale: 5.0 + variableCount: + threshold: 2 + scale: 5.0 + propertyCallCount: + threshold: 2 + scale: 15.0 + ifCount: + threshold: 3 + scale: 1.0 + ifNestingLevel: + threshold: 1 + scale: 1.0 + elseCount: + threshold: 1 + scale: 1.0 ``` It is recommended to play with the values until you get weights that you are comfortable with. The default values are a good starting point. diff --git a/src/Application.php b/src/Application.php index c49a49c..96b6174 100644 --- a/src/Application.php +++ b/src/Application.php @@ -6,6 +6,7 @@ use Phauthentic\CodeQualityMetrics\Business\Cognitive\BaselineService; use Phauthentic\CodeQualityMetrics\Business\Cognitive\CognitiveMetricsCollector; +use Phauthentic\CodeQualityMetrics\Business\Cognitive\FindMetricsPluginInterface; use Phauthentic\CodeQualityMetrics\Business\Cognitive\ScoreCalculator; use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; use Phauthentic\CodeQualityMetrics\Business\Halstead\HalsteadMetricsCollector; @@ -80,6 +81,9 @@ private function registerServices(): void $this->containerBuilder->register(NodeTraverserInterface::class, NodeTraverser::class) ->setPublic(true); + $this->containerBuilder->register(NodeTraverserInterface::class, NodeTraverser::class) + ->setPublic(true); + $this->containerBuilder->register(OutputInterface::class, ConsoleOutput::class) ->setPublic(true); @@ -111,8 +115,9 @@ private function bootstrapMetricsCollectors(): void new Reference(ParserFactory::class), new Reference(NodeTraverserInterface::class), new Reference(DirectoryScanner::class), + new Reference(ConfigService::class), [ - $this->containerBuilder->get(CognitiveCollectorShellOutputPlugin::class) + $this->containerBuilder->get(CognitiveCollectorShellOutputPlugin::class), ] ]) ->setPublic(true); diff --git a/src/Business/AbstractMetricCollector.php b/src/Business/AbstractMetricCollector.php deleted file mode 100644 index c974e9f..0000000 --- a/src/Business/AbstractMetricCollector.php +++ /dev/null @@ -1,75 +0,0 @@ - $config - * @return array - */ - protected function getExcludePatternsFromConfig(array $config): array - { - if (isset($config['excludePatterns'])) { - return $config['excludePatterns']; - } - - return []; - } - - /** - * @param array $findMetricsPlugins - */ - public function __construct( - protected readonly ParserFactory $parserFactory, - protected readonly NodeTraverserInterface $traverser, - protected readonly DirectoryScanner $directoryScanner, - protected readonly array $findMetricsPlugins = [] - ) { - $this->parser = $this->parserFactory->createForHostVersion(); - } - - /** - * Find source files using DirectoryScanner - * - * @param string $path Path to the directory or file to scan - * @param array $exclude List of regx to exclude - * @return Generator An iterable of SplFileInfo objects - */ - protected function findSourceFiles(string $path, array $exclude = []): iterable - { - return $this->directoryScanner->scan([$path], ['^(?!.*\.php$).+'] + $exclude); // Exclude non-PHP files - } - - - protected function traverseAbstractSyntaxTree(string $code): void - { - try { - $ast = $this->parser->parse($code); - } catch (Error $e) { - throw new RuntimeException("Parse error: {$e->getMessage()}", 0, $e); - } - - if ($ast === null) { - throw new RuntimeException("Could not parse the code."); - } - - $this->traverser->traverse($ast); - } -} diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index a4f2559..a65ef24 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -4,16 +4,49 @@ namespace Phauthentic\CodeQualityMetrics\Business\Cognitive; -use Phauthentic\CodeQualityMetrics\Business\AbstractMetricCollector; +use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; +use Phauthentic\CodeQualityMetrics\CognitiveAnalysisException; +use Phauthentic\CodeQualityMetrics\Config\ConfigService; use Phauthentic\CodeQualityMetrics\PhpParser\CognitiveMetricsVisitor; -use RuntimeException; +use PhpParser\Error; +use PhpParser\NodeTraverserInterface; +use PhpParser\Parser; +use PhpParser\ParserFactory; use SplFileInfo; /** * CognitiveMetricsCollector class that collects cognitive metrics from source files */ -class CognitiveMetricsCollector extends AbstractMetricCollector +class CognitiveMetricsCollector { + protected Parser $parser; + + /** + * @param array $findMetricsPlugins + */ + public function __construct( + protected readonly ParserFactory $parserFactory, + protected readonly NodeTraverserInterface $traverser, + protected readonly DirectoryScanner $directoryScanner, + protected readonly ConfigService $configService, + protected readonly array $findMetricsPlugins = [] + ) { + $this->parser = $parserFactory->createForHostVersion(); + } + + /** + * @param array $config + * @return array + */ + protected function getExcludePatternsFromConfig(array $config): array + { + if (isset($config['excludePatterns'])) { + return $config['excludePatterns']; + } + + return []; + } + /** * Collect cognitive metrics from the given path * @@ -51,7 +84,7 @@ protected function findMetrics(iterable $files): CognitiveMetricsCollection $code = file_get_contents($file->getRealPath()); if ($code === false) { - throw new RuntimeException("Could not read file: {$file->getRealPath()}"); + throw new CognitiveAnalysisException("Could not read file: {$file->getRealPath()}"); } $this->traverser->addVisitor($visitor); @@ -85,6 +118,10 @@ private function processMethodMetrics( CognitiveMetricsCollection $metricsCollection ): void { foreach ($methodMetrics as $classAndMethod => $metrics) { + if ($this->isExcluded($classAndMethod)) { + continue; + } + [$class, $method] = explode('::', $classAndMethod); $metricsArray = array_merge($metrics, [ @@ -99,4 +136,47 @@ private function processMethodMetrics( } } } + + public function isExcluded(string $classAndMethod): bool + { + $regexes = $this->configService->getConfig()['cognitive']['excludePatterns']; + + foreach ($regexes as $regex) { + if (preg_match('/' . $regex . '/', $classAndMethod, $matches)) { + return true; + } + } + + return false; + } + + /** + * Find source files using DirectoryScanner + * + * @param string $path Path to the directory or file to scan + * @param array $exclude List of regx to exclude + * @return iterable An iterable of SplFileInfo objects + */ + protected function findSourceFiles(string $path, array $exclude = []): iterable + { + return $this->directoryScanner->scan([$path], ['^(?!.*\.php$).+'] + $exclude); // Exclude non-PHP files + } + + /** + * @throws CognitiveAnalysisException + */ + protected function traverseAbstractSyntaxTree(string $code): void + { + try { + $ast = $this->parser->parse($code); + } catch (Error $e) { + throw new CognitiveAnalysisException("Parse error: {$e->getMessage()}", 0, $e); + } + + if ($ast === null) { + throw new CognitiveAnalysisException("Could not parse the code."); + } + + $this->traverser->traverse($ast); + } } diff --git a/src/Business/Halstead/HalsteadMetricsCollector.php b/src/Business/Halstead/HalsteadMetricsCollector.php index 4cd27e5..345648d 100644 --- a/src/Business/Halstead/HalsteadMetricsCollector.php +++ b/src/Business/Halstead/HalsteadMetricsCollector.php @@ -4,16 +4,70 @@ namespace Phauthentic\CodeQualityMetrics\Business\Halstead; -use Phauthentic\CodeQualityMetrics\Business\AbstractMetricCollector; +use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; use Phauthentic\CodeQualityMetrics\PhpParser\HalsteadMetricsVisitor; +use PhpParser\Error; +use PhpParser\NodeTraverserInterface; +use PhpParser\Parser; +use PhpParser\ParserFactory; use RuntimeException; use SplFileInfo; /** * HalsteadMetricsCollector class that collects Halstead metrics from source files. */ -class HalsteadMetricsCollector extends AbstractMetricCollector +class HalsteadMetricsCollector { + protected Parser $parser; + + public function __construct( + protected readonly ParserFactory $parserFactory, + protected readonly NodeTraverserInterface $traverser, + protected readonly DirectoryScanner $directoryScanner, + ) { + $this->parser = $parserFactory->createForHostVersion(); + } + + /** + * @param array $config + * @return array + */ + protected function getExcludePatternsFromConfig(array $config): array + { + if (isset($config['excludePatterns'])) { + return $config['excludePatterns']; + } + + return []; + } + + /** + * Find source files using DirectoryScanner + * + * @param string $path Path to the directory or file to scan + * @param array $exclude List of regx to exclude + * @return iterable An iterable of SplFileInfo objects + */ + protected function findSourceFiles(string $path, array $exclude = []): iterable + { + return $this->directoryScanner->scan([$path], ['^(?!.*\.php$).+'] + $exclude); // Exclude non-PHP files + } + + protected function traverseAbstractSyntaxTree(string $code): void + { + try { + $ast = $this->parser->parse($code); + } catch (Error $e) { + throw new RuntimeException("Parse error: {$e->getMessage()}", 0, $e); + } + + if ($ast === null) { + throw new RuntimeException("Could not parse the code."); + } + + $this->traverser->traverse($ast); + } + /** * Collect Halstead metrics from the given path. * diff --git a/src/CognitiveAnalysisException.php b/src/CognitiveAnalysisException.php new file mode 100644 index 0000000..b58c532 --- /dev/null +++ b/src/CognitiveAnalysisException.php @@ -0,0 +1,14 @@ +children() ->arrayNode('cognitive') ->children() - ->arrayNode('excludedClasses') - ->scalarPrototype() - ->defaultValue([]) - ->end() - ->end() - ->arrayNode('excludedMethods') - ->scalarPrototype() - ->defaultValue([]) - ->end() - ->end() ->arrayNode('excludePatterns') ->scalarPrototype() ->defaultValue([]) @@ -120,15 +110,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->arrayNode('halstead') ->children() - ->arrayNode('excludedClasses') - ->scalarPrototype()->end() - ->end() - ->arrayNode('excludedMethods') - ->scalarPrototype()->end() - ->end() - ->arrayNode('excludePatterns') - ->scalarPrototype()->end() - ->end() ->arrayNode('threshold') ->children() ->floatNode('difficulty')->end() diff --git a/tests/Fixtures/config-with-exclude-patterns.yml b/tests/Fixtures/config-with-exclude-patterns.yml new file mode 100644 index 0000000..6a03c86 --- /dev/null +++ b/tests/Fixtures/config-with-exclude-patterns.yml @@ -0,0 +1,3 @@ +cognitive: + excludePatterns: + - '(.*)::__construct' diff --git a/tests/Fixtures/config-with-one-metric.yml b/tests/Fixtures/config-with-one-metric.yml index 8419bef..aabf15c 100644 --- a/tests/Fixtures/config-with-one-metric.yml +++ b/tests/Fixtures/config-with-one-metric.yml @@ -1,8 +1,5 @@ cognitive: - excludedClasses: [] - excludedMethods: - - '*.test' - excludePatterns: [] + excludePatterns: metrics: lineCount: threshold: 1000 diff --git a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php index a57e9bf..5ca4fa0 100644 --- a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php +++ b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php @@ -8,17 +8,20 @@ use Phauthentic\CodeQualityMetrics\Business\Cognitive\CognitiveMetricsCollection; use Phauthentic\CodeQualityMetrics\Business\Cognitive\CognitiveMetricsCollector; use Phauthentic\CodeQualityMetrics\Business\DirectoryScanner; +use Phauthentic\CodeQualityMetrics\Config\ConfigLoader; +use Phauthentic\CodeQualityMetrics\Config\ConfigService; use PhpParser\NodeTraverser; use PhpParser\ParserFactory; use PHPUnit\Framework\TestCase; use RuntimeException; +use Symfony\Component\Config\Definition\Processor; /** * */ class CognitiveMetricsCollectorTest extends TestCase { - private AbstractMetricCollector $metricsCollector; + private CognitiveMetricsCollector $metricsCollector; protected function setUp(): void { @@ -26,7 +29,11 @@ protected function setUp(): void $this->metricsCollector = new CognitiveMetricsCollector( new ParserFactory(), new NodeTraverser(), - new DirectoryScanner() + new DirectoryScanner(), + new ConfigService( + new Processor(), + new ConfigLoader(), + ) ); } @@ -40,6 +47,31 @@ public function testCollectWithValidDirectoryPath(): void $this->assertCount(23, $metricsCollection); } + public function testCollectWithExcludedClasses(): void + { + $configService = new ConfigService( + new Processor(), + new ConfigLoader(), + ); + + // It will exclude just the constructor methods + $configService->loadConfig(__DIR__ . '/../../../Fixtures/config-with-exclude-patterns.yml'); + + $metricsCollector = new CognitiveMetricsCollector( + new ParserFactory(), + new NodeTraverser(), + new DirectoryScanner(), + $configService, + ); + + $path = './tests/TestCode'; + + $metricsCollection = $metricsCollector->collect($path); + + $this->assertInstanceOf(CognitiveMetricsCollection::class, $metricsCollection); + $this->assertCount(22, $metricsCollection); + } + public function testCollectWithValidFilePath(): void { $path = './tests/TestCode/Paginator.php'; diff --git a/tests/Unit/Business/Halstead/HalsteadMetricsCollectorTest.php b/tests/Unit/Business/Halstead/HalsteadMetricsCollectorTest.php index 0d13455..c85b7ec 100644 --- a/tests/Unit/Business/Halstead/HalsteadMetricsCollectorTest.php +++ b/tests/Unit/Business/Halstead/HalsteadMetricsCollectorTest.php @@ -20,7 +20,7 @@ public function testCount() $collector = new HalsteadMetricsCollector( new ParserFactory(), new NodeTraverser(), - new DirectoryScanner() + new DirectoryScanner(), ); $collection = $collector->collect('./tests/TestCode'); diff --git a/tests/Unit/Business/MetricsFacadeTest.php b/tests/Unit/Business/MetricsFacadeTest.php index 79e0b50..4807826 100644 --- a/tests/Unit/Business/MetricsFacadeTest.php +++ b/tests/Unit/Business/MetricsFacadeTest.php @@ -5,10 +5,6 @@ namespace Phauthentic\CodeQualityMetrics\Tests\Unit\Business; use Phauthentic\CodeQualityMetrics\Application; -use Phauthentic\CodeQualityMetrics\Business\Cognitive\ScoreCalculator; -use Phauthentic\CodeQualityMetrics\Business\Halstead\HalsteadMetricsCollector; -use Phauthentic\CodeQualityMetrics\Config\ConfigService; -use PHP_CodeSniffer\Config; use PHPUnit\Framework\TestCase; use Phauthentic\CodeQualityMetrics\Business\MetricsFacade; use Symfony\Component\Yaml\Exception\ParseException; diff --git a/tests/Unit/Config/ConfigLoaderTest.php b/tests/Unit/Config/ConfigLoaderTest.php index 2e32409..9e163d2 100644 --- a/tests/Unit/Config/ConfigLoaderTest.php +++ b/tests/Unit/Config/ConfigLoaderTest.php @@ -25,8 +25,6 @@ public function testConfigTreeBuilder(): void $config = [ 'cognitive' => [ - 'excludedClasses' => ['Class1', 'Class2'], - 'excludedMethods' => ['method1', 'method2'], 'metrics' => [ 'lineCount' => [ 'threshold' => 60.0,