Skip to content

Commit

Permalink
Added class and method exclusion by regex (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
floriankraemer authored Sep 26, 2024
1 parent 814232f commit ff3a9e3
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 146 deletions.
7 changes: 1 addition & 6 deletions config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
cognitive:
excludedClasses: [],
excludedMethods: [],
excludePatterns: [],
excludePatterns:
metrics:
lineCount:
threshold: 60
Expand Down Expand Up @@ -29,9 +27,6 @@ cognitive:
scale: 1.0

halstead:
excludedClasses: [],
excludedMethods: [],
excludePatterns: [],
threshold:
difficulty: 0.0
effort: 0.0
Expand Down
66 changes: 40 additions & 26 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,52 @@ You can specify another configuration file by passing it to the config options:
php analyse.php metrics:cognitive <path-to-folder> --config=<path-to-config-file>
```

## 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.
7 changes: 6 additions & 1 deletion src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
75 changes: 0 additions & 75 deletions src/Business/AbstractMetricCollector.php

This file was deleted.

88 changes: 84 additions & 4 deletions src/Business/Cognitive/CognitiveMetricsCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, FindMetricsPluginInterface> $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<string, mixed> $config
* @return array<int, string>
*/
protected function getExcludePatternsFromConfig(array $config): array
{
if (isset($config['excludePatterns'])) {
return $config['excludePatterns'];
}

return [];
}

/**
* Collect cognitive metrics from the given path
*
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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, [
Expand All @@ -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<int, string> $exclude List of regx to exclude
* @return iterable<mixed, SplFileInfo> 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);
}
}
58 changes: 56 additions & 2 deletions src/Business/Halstead/HalsteadMetricsCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed> $config
* @return array<int, string>
*/
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<int, string> $exclude List of regx to exclude
* @return iterable<mixed, SplFileInfo> 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.
*
Expand Down
Loading

0 comments on commit ff3a9e3

Please sign in to comment.