Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Cognitive Code Analysis is an approach to understanding and improving code by fo
* Cognitive Complexity Analysis:
* Calculates a cognitive complexity score for each class and method
* Provides detailed cognitive complexity metrics
* Generate reports in various formats (JSON, CSV, HTML)
* Generate reports in various formats (JSON, CSV, HTML, Markdown, Checkstyle XML, JUnit XML, SARIF, GitLab Code Quality, GitHub Actions)
* Baseline comparison to track complexity changes over time
* Configurable thresholds and weights for complexity analysis
* Optional result cache for faster subsequent runs (must be enabled in config)
Expand All @@ -36,7 +36,7 @@ Cognitive Complexity Analysis
bin/phpcca analyse <path-to-folder>
```

Generate a report, supported types are `json`, `csv`, `html`.
Generate a report, supported types are `json`, `csv`, `html`, `markdown`, `checkstyle`, `junit`, `sarif`, `gitlab-codequality`, `github-actions`.

```bash
bin/phpcca analyse <path-to-folder> --report-type json --report-file cognitive.json
Expand Down
136 changes: 136 additions & 0 deletions src/Business/Cognitive/Report/CheckstyleReport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

declare(strict_types=1);

namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report;

use DOMDocument;
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetrics;
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection;
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig;

/**
* Checkstyle XML report for CI (Jenkins, Maven Checkstyle Plugin, IDEs).
* Emits one violation per method that exceeds the cognitive complexity threshold.
*/
class CheckstyleReport implements ReportGeneratorInterface
{
private const SOURCE = 'CognitiveComplexity';
private const VERSION = '8.0';

public function __construct(
private readonly CognitiveConfig $config
) {
}

public function export(CognitiveMetricsCollection $metrics, string $filename): void
{
$directory = dirname($filename);
if (!is_dir($directory)) {
throw new CognitiveAnalysisException(sprintf('Directory %s does not exist', $directory));
}

$violations = $this->filterViolations($metrics);
$groupedByFile = $this->groupByFile($violations);

$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;

$root = $dom->createElement('checkstyle');
$root->setAttribute('version', self::VERSION);
$dom->appendChild($root);

foreach ($groupedByFile as $filePath => $fileMetrics) {
$normalizedPath = $this->normalizePath($filePath);
$fileEl = $dom->createElement('file');
$fileEl->setAttribute('name', $normalizedPath);
$root->appendChild($fileEl);

foreach ($fileMetrics as $metric) {
$errorEl = $dom->createElement('error');
$errorEl->setAttribute('line', (string) $metric->getLine());
$errorEl->setAttribute('column', '1');
$errorEl->setAttribute('severity', $this->scoreToSeverity($metric->getScore()));
$errorEl->setAttribute('message', $this->buildMessage($metric));
$errorEl->setAttribute('source', self::SOURCE);
$fileEl->appendChild($errorEl);
}
}

$xml = $dom->saveXML();
if ($xml === false) {
throw new CognitiveAnalysisException('Could not generate Checkstyle XML');
}

if (file_put_contents($filename, $xml) === false) {
throw new CognitiveAnalysisException("Unable to write to file: {$filename}");
}
}

/**
* @return CognitiveMetrics[]
*/
private function filterViolations(CognitiveMetricsCollection $metrics): array
{
$result = [];
foreach ($metrics as $metric) {
if ($metric->getScore() <= $this->config->scoreThreshold) {
continue;
}

$result[] = $metric;
}

return $result;
}

/**
* @param CognitiveMetrics[] $violations
* @return array<string, CognitiveMetrics[]>
*/
private function groupByFile(array $violations): array
{
$grouped = [];
foreach ($violations as $metric) {
$path = $metric->getFileName();
if (!isset($grouped[$path])) {
$grouped[$path] = [];
}
$grouped[$path][] = $metric;
}

return $grouped;
}

private function normalizePath(string $path): string
{
$path = str_replace('\\', '/', $path);

return ltrim($path, './');
}

private function scoreToSeverity(float $score): string
{
$threshold = $this->config->scoreThreshold;
if ($score >= $threshold * 2) {
return 'error';
}

return 'warning';
}

private function buildMessage(CognitiveMetrics $metric): string
{
$threshold = $this->config->scoreThreshold;
$score = $metric->getScore();
$method = $metric->getMethod();

return sprintf(
'Method %s has cognitive complexity %s (threshold: %s)',
$method,
number_format($score, 1),
number_format($threshold, 1)
);
}
}
19 changes: 17 additions & 2 deletions src/Business/Cognitive/Report/CognitiveReportFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function __construct(
/**
* Create an exporter instance based on the report type.
*
* @param string $type The type of exporter to create (json, csv, html, markdown)
* @param string $type The type of exporter to create (json, csv, html, markdown, checkstyle, junit, sarif, gitlab-codequality, github-actions)
* @return ReportGeneratorInterface
* @throws InvalidArgumentException If the type is not supported
*/
Expand All @@ -39,6 +39,11 @@ public function create(string $type): ReportGeneratorInterface
'csv' => new CsvReport(),
'html' => new HtmlReport(),
'markdown' => new MarkdownReport($config),
'checkstyle' => new CheckstyleReport($config),
'junit' => new JUnitReport($config),
'sarif' => new SarifReport($config),
'gitlab-codequality' => new GitLabCodeQualityReport($config),
'github-actions' => new GitHubActionsReport($config),
default => null,
};

Expand Down Expand Up @@ -86,7 +91,17 @@ public function getSupportedTypes(): array
$customReporters = $config->customReporters['cognitive'] ?? [];

return array_merge(
['json', 'csv', 'html', 'markdown'],
[
'json',
'csv',
'html',
'markdown',
'checkstyle',
'junit',
'sarif',
'gitlab-codequality',
'github-actions',
],
array_keys($customReporters)
);
}
Expand Down
83 changes: 83 additions & 0 deletions src/Business/Cognitive/Report/GitHubActionsReport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report;

use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetrics;
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection;
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig;

/**
* GitHub Actions workflow command report (::warning / ::error).
* Writes one line per method over threshold for CI log annotations.
*/
class GitHubActionsReport implements ReportGeneratorInterface
{
public function __construct(
private readonly CognitiveConfig $config
) {
}

public function export(CognitiveMetricsCollection $metrics, string $filename): void
{
$directory = dirname($filename);
if (!is_dir($directory)) {
throw new CognitiveAnalysisException(sprintf('Directory %s does not exist', $directory));
}

$lines = [];
foreach ($metrics as $metric) {
if ($metric->getScore() <= $this->config->scoreThreshold) {
continue;
}

$level = $this->scoreToLevel($metric->getScore());
$path = $this->normalizePath($metric->getFileName());
$line = $metric->getLine();
$message = $this->buildMessage($metric);
$lines[] = sprintf('::%s file=%s,line=%d::%s', $level, $path, $line, $message);
}

$content = implode("\n", $lines);
if ($lines !== []) {
$content .= "\n";
}

if (file_put_contents($filename, $content) === false) {
throw new CognitiveAnalysisException("Unable to write to file: {$filename}");
}
}

private function normalizePath(string $path): string
{
$path = str_replace('\\', '/', $path);

return ltrim($path, './');
}

private function scoreToLevel(float $score): string
{
$threshold = $this->config->scoreThreshold;
if ($score >= $threshold * 2) {
return 'error';
}

return 'warning';
}

private function buildMessage(CognitiveMetrics $metric): string
{
$score = $metric->getScore();
$threshold = $this->config->scoreThreshold;
$method = $metric->getMethod();

return sprintf(
'Method %s has cognitive complexity %s (threshold: %s)',
$method,
number_format($score, 1),
number_format($threshold, 1)
);
}
}
Loading