diff --git a/readme.md b/readme.md index 5adc8e4..5910c00 100644 --- a/readme.md +++ b/readme.md @@ -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) @@ -36,7 +36,7 @@ Cognitive Complexity Analysis bin/phpcca analyse ``` -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 --report-type json --report-file cognitive.json diff --git a/src/Business/Cognitive/Report/CheckstyleReport.php b/src/Business/Cognitive/Report/CheckstyleReport.php new file mode 100644 index 0000000..5c37dc4 --- /dev/null +++ b/src/Business/Cognitive/Report/CheckstyleReport.php @@ -0,0 +1,136 @@ +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 + */ + 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) + ); + } +} diff --git a/src/Business/Cognitive/Report/CognitiveReportFactory.php b/src/Business/Cognitive/Report/CognitiveReportFactory.php index aebba33..a1fccc3 100644 --- a/src/Business/Cognitive/Report/CognitiveReportFactory.php +++ b/src/Business/Cognitive/Report/CognitiveReportFactory.php @@ -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 */ @@ -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, }; @@ -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) ); } diff --git a/src/Business/Cognitive/Report/GitHubActionsReport.php b/src/Business/Cognitive/Report/GitHubActionsReport.php new file mode 100644 index 0000000..27d2760 --- /dev/null +++ b/src/Business/Cognitive/Report/GitHubActionsReport.php @@ -0,0 +1,83 @@ +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) + ); + } +} diff --git a/src/Business/Cognitive/Report/GitLabCodeQualityReport.php b/src/Business/Cognitive/Report/GitLabCodeQualityReport.php new file mode 100644 index 0000000..109cbbe --- /dev/null +++ b/src/Business/Cognitive/Report/GitLabCodeQualityReport.php @@ -0,0 +1,134 @@ +filterViolations($metrics); + $issues = []; + foreach ($violations as $metric) { + $issues[] = $this->buildIssue($metric); + } + + $json = json_encode($issues, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); + + if (file_put_contents($filename, $json) === 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; + } + + /** + * @return array + */ + private function buildIssue(CognitiveMetrics $metric): array + { + $threshold = $this->config->scoreThreshold; + $score = $metric->getScore(); + $description = sprintf( + 'Method %s has cognitive complexity %s (threshold: %s)', + $metric->getMethod(), + number_format($score, 1), + number_format($threshold, 1) + ); + $path = $this->normalizePath($metric->getFileName()); + + return [ + 'description' => $description, + 'check_name' => self::CHECK_NAME, + 'fingerprint' => $this->computeFingerprint($metric), + 'severity' => $this->scoreToSeverity($score), + 'location' => [ + 'path' => $path, + 'lines' => [ + 'begin' => $metric->getLine(), + ], + ], + ]; + } + + private function normalizePath(string $path): string + { + $path = str_replace('\\', '/', $path); + + return ltrim($path, './'); + } + + private function scoreToSeverity(float $score): string + { + $threshold = $this->config->scoreThreshold; + $ratio = $threshold > 0 ? $score / $threshold : 0; + + if ($ratio >= 3) { + return 'blocker'; + } + if ($ratio >= 2) { + return 'critical'; + } + if ($ratio >= 1.5) { + return 'major'; + } + if ($ratio > 1) { + return 'minor'; + } + + return 'info'; + } + + private function computeFingerprint(CognitiveMetrics $metric): string + { + $content = sprintf( + '%s:%d:%s::%s', + $metric->getFileName(), + $metric->getLine(), + $metric->getClass(), + $metric->getMethod() + ); + + return hash('sha256', $content); + } +} diff --git a/src/Business/Cognitive/Report/JUnitReport.php b/src/Business/Cognitive/Report/JUnitReport.php new file mode 100644 index 0000000..1ba132d --- /dev/null +++ b/src/Business/Cognitive/Report/JUnitReport.php @@ -0,0 +1,109 @@ +countFailures($methods); + + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + + $testsuites = $dom->createElement('testsuites'); + $testsuites->setAttribute('name', self::SUITE_NAME); + $testsuites->setAttribute('tests', (string) count($methods)); + $testsuites->setAttribute('failures', (string) $failureCount); + $testsuites->setAttribute('errors', '0'); + $dom->appendChild($testsuites); + + $testsuite = $dom->createElement('testsuite'); + $testsuite->setAttribute('name', self::SUITE_NAME); + $testsuite->setAttribute('tests', (string) count($methods)); + $testsuite->setAttribute('failures', (string) $failureCount); + $testsuite->setAttribute('errors', '0'); + $testsuites->appendChild($testsuite); + + foreach ($methods as $metric) { + $testcase = $dom->createElement('testcase'); + $testcase->setAttribute('classname', $metric->getClass()); + $testcase->setAttribute('name', $metric->getMethod()); + $testcase->setAttribute('time', '0'); + $testsuite->appendChild($testcase); + + if ($metric->getScore() <= $this->config->scoreThreshold) { + continue; + } + + $failure = $dom->createElement('failure'); + $failure->setAttribute('message', $this->buildFailureMessage($metric)); + $failure->setAttribute('type', self::FAILURE_TYPE); + $testcase->appendChild($failure); + } + + $xml = $dom->saveXML(); + if ($xml === false) { + throw new CognitiveAnalysisException('Could not generate JUnit XML'); + } + + if (file_put_contents($filename, $xml) === false) { + throw new CognitiveAnalysisException("Unable to write to file: {$filename}"); + } + } + + /** + * @param CognitiveMetrics[] $methods + */ + private function countFailures(array $methods): int + { + $count = 0; + foreach ($methods as $metric) { + if ($metric->getScore() <= $this->config->scoreThreshold) { + continue; + } + + $count++; + } + + return $count; + } + + private function buildFailureMessage(CognitiveMetrics $metric): string + { + $score = $metric->getScore(); + $threshold = $this->config->scoreThreshold; + + return sprintf( + 'Cognitive complexity %s exceeds threshold %s', + number_format($score, 1), + number_format($threshold, 1) + ); + } +} diff --git a/src/Business/Cognitive/Report/SarifReport.php b/src/Business/Cognitive/Report/SarifReport.php new file mode 100644 index 0000000..25f8d6e --- /dev/null +++ b/src/Business/Cognitive/Report/SarifReport.php @@ -0,0 +1,171 @@ +filterViolations($metrics); + $results = []; + foreach ($violations as $metric) { + $results[] = $this->buildResult($metric); + } + + $payload = [ + '$schema' => self::SCHEMA, + 'version' => self::VERSION, + 'runs' => [ + [ + 'tool' => [ + 'driver' => [ + 'name' => self::TOOL_NAME, + 'semanticVersion' => '1.0.0', + 'rules' => [ + [ + 'id' => self::RULE_ID, + 'name' => 'Cognitive Complexity', + 'shortDescription' => [ + 'text' => 'Method exceeds cognitive complexity threshold', + ], + 'defaultConfiguration' => [ + 'level' => 'warning', + ], + 'properties' => [ + 'precision' => 'high', + ], + ], + ], + ], + ], + 'results' => $results, + ], + ], + ]; + + $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); + + if (file_put_contents($filename, $json) === 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; + } + + /** + * @return array + */ + private function buildResult(CognitiveMetrics $metric): array + { + $path = $this->normalizePath($metric->getFileName()); + $line = $metric->getLine(); + $message = sprintf( + 'Method %s has cognitive complexity %s (threshold: %s)', + $metric->getMethod(), + number_format($metric->getScore(), 1), + number_format($this->config->scoreThreshold, 1) + ); + $level = $this->scoreToLevel($metric->getScore()); + $fingerprint = $this->computeFingerprint($metric); + + return [ + 'ruleId' => self::RULE_ID, + 'level' => $level, + 'message' => [ + 'text' => $message, + ], + 'locations' => [ + [ + 'physicalLocation' => [ + 'artifactLocation' => [ + 'uri' => $path, + ], + 'region' => [ + 'startLine' => $line, + ], + ], + ], + ], + 'partialFingerprints' => [ + 'primaryLocationLineHash' => $fingerprint, + ], + ]; + } + + 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'; + } + if ($score > $threshold) { + return 'warning'; + } + + return 'note'; + } + + private function computeFingerprint(CognitiveMetrics $metric): string + { + $content = sprintf( + '%s:%d:%s::%s', + $metric->getFileName(), + $metric->getLine(), + $metric->getClass(), + $metric->getMethod() + ); + + return hash('sha256', $content); + } +} diff --git a/tests/Unit/Business/Cognitive/Report/CheckstyleReporterTest.php b/tests/Unit/Business/Cognitive/Report/CheckstyleReporterTest.php new file mode 100644 index 0000000..8b560d5 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Report/CheckstyleReporterTest.php @@ -0,0 +1,130 @@ +filename = sys_get_temp_dir() . '/checkstyle_report_' . uniqid() . '.xml'; + $this->configAboveThreshold = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: true, + scoreThreshold: 10.0 + ); + $this->configBelowThreshold = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: true, + scoreThreshold: 100.0 + ); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (!file_exists($this->filename)) { + return; + } + + unlink($this->filename); + } + + #[Test] + public function testExportWithViolationsCreatesValidXml(): void + { + $metrics = $this->createMetric('App\Example', 'foo', 'src/Example.php', 42, 15.5); + $collection = new CognitiveMetricsCollection(); + $collection->add($metrics); + + $report = new CheckstyleReport($this->configAboveThreshold); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $xml = file_get_contents($this->filename); + $this->assertNotFalse($xml); + $this->assertStringContainsString('assertStringContainsString('name="src/Example.php"', $xml); + $this->assertStringContainsString('line="42"', $xml); + $this->assertStringContainsString('source="CognitiveComplexity"', $xml); + $this->assertStringContainsString('cognitive complexity', $xml); + + $dom = new \DOMDocument(); + $this->assertTrue($dom->loadXML($xml), 'Output must be valid XML'); + } + + #[Test] + public function testExportWithNoViolationsCreatesEmptyFiles(): void + { + $metrics = $this->createMetric('App\Example', 'bar', 'src/Example.php', 10, 5.0); + $collection = new CognitiveMetricsCollection(); + $collection->add($metrics); + + $report = new CheckstyleReport($this->configBelowThreshold); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $xml = file_get_contents($this->filename); + $this->assertNotFalse($xml); + $this->assertStringContainsString('assertStringNotContainsString('configAboveThreshold); + + $this->expectException(CognitiveAnalysisException::class); + $this->expectExceptionMessage('Directory /nonexistent/path does not exist'); + + $report->export($collection, '/nonexistent/path/report.xml'); + } + + private function createMetric( + string $class, + string $method, + string $file, + int $line, + float $score + ): CognitiveMetrics { + $metric = new CognitiveMetrics([ + 'class' => $class, + 'method' => $method, + 'file' => $file, + 'line' => $line, + 'lineCount' => 10, + 'argCount' => 0, + 'returnCount' => 0, + 'variableCount' => 0, + 'propertyCallCount' => 0, + 'ifCount' => 0, + 'ifNestingLevel' => 0, + 'elseCount' => 0, + ]); + $metric->setScore($score); + + return $metric; + } +} diff --git a/tests/Unit/Business/Cognitive/Report/CognitiveReporterFactoryCustomTest.php b/tests/Unit/Business/Cognitive/Report/CognitiveReporterFactoryCustomTest.php index b7d7165..a6012b4 100644 --- a/tests/Unit/Business/Cognitive/Report/CognitiveReporterFactoryCustomTest.php +++ b/tests/Unit/Business/Cognitive/Report/CognitiveReporterFactoryCustomTest.php @@ -220,7 +220,17 @@ public function testGetSupportedTypesIncludesCustomExporters(): void $factory = new CognitiveReportFactory($this->createMockConfigService($customReporters)); $supportedTypes = $factory->getSupportedTypes(); - $expectedBuiltInTypes = ['json', 'csv', 'html', 'markdown']; + $expectedBuiltInTypes = [ + 'json', + 'csv', + 'html', + 'markdown', + 'checkstyle', + 'junit', + 'sarif', + 'gitlab-codequality', + 'github-actions', + ]; $expectedCustomTypes = ['custom1', 'custom2']; foreach ($expectedBuiltInTypes as $type) { @@ -232,6 +242,51 @@ public function testGetSupportedTypesIncludesCustomExporters(): void } } + #[Test] + public function testCreateCheckstyleExporter(): void + { + $factory = new CognitiveReportFactory($this->createMockConfigService()); + $exporter = $factory->create('checkstyle'); + $this->assertInstanceOf(ReportGeneratorInterface::class, $exporter); + $this->assertInstanceOf('Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report\CheckstyleReport', $exporter); + } + + #[Test] + public function testCreateJUnitExporter(): void + { + $factory = new CognitiveReportFactory($this->createMockConfigService()); + $exporter = $factory->create('junit'); + $this->assertInstanceOf(ReportGeneratorInterface::class, $exporter); + $this->assertInstanceOf('Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report\JUnitReport', $exporter); + } + + #[Test] + public function testCreateSarifExporter(): void + { + $factory = new CognitiveReportFactory($this->createMockConfigService()); + $exporter = $factory->create('sarif'); + $this->assertInstanceOf(ReportGeneratorInterface::class, $exporter); + $this->assertInstanceOf('Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report\SarifReport', $exporter); + } + + #[Test] + public function testCreateGitLabCodeQualityExporter(): void + { + $factory = new CognitiveReportFactory($this->createMockConfigService()); + $exporter = $factory->create('gitlab-codequality'); + $this->assertInstanceOf(ReportGeneratorInterface::class, $exporter); + $this->assertInstanceOf('Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report\GitLabCodeQualityReport', $exporter); + } + + #[Test] + public function testCreateGitHubActionsExporter(): void + { + $factory = new CognitiveReportFactory($this->createMockConfigService()); + $exporter = $factory->create('github-actions'); + $this->assertInstanceOf(ReportGeneratorInterface::class, $exporter); + $this->assertInstanceOf('Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Report\GitHubActionsReport', $exporter); + } + #[Test] public function testIsSupportedWithCustomExporters(): void { diff --git a/tests/Unit/Business/Cognitive/Report/GitHubActionsReporterTest.php b/tests/Unit/Business/Cognitive/Report/GitHubActionsReporterTest.php new file mode 100644 index 0000000..fefea28 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Report/GitHubActionsReporterTest.php @@ -0,0 +1,121 @@ +filename = sys_get_temp_dir() . '/github_actions_' . uniqid() . '.txt'; + $this->configAboveThreshold = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: true, + scoreThreshold: 10.0 + ); + $this->configBelowThreshold = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: true, + scoreThreshold: 100.0 + ); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (!file_exists($this->filename)) { + return; + } + + unlink($this->filename); + } + + #[Test] + public function testExportWithViolationsWritesWorkflowCommands(): void + { + $metrics = $this->createMetric('App\Example', 'foo', 'src/Example.php', 42, 15.5); + $collection = new CognitiveMetricsCollection(); + $collection->add($metrics); + + $report = new GitHubActionsReport($this->configAboveThreshold); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $content = file_get_contents($this->filename); + $this->assertNotFalse($content); + $this->assertStringContainsString('::warning file=src/Example.php,line=42::', $content); + $this->assertStringContainsString('cognitive complexity 15.5 (threshold: 10', $content); + } + + #[Test] + public function testExportWithNoViolationsWritesEmptyFile(): void + { + $metrics = $this->createMetric('App\Example', 'bar', 'src/Example.php', 10, 5.0); + $collection = new CognitiveMetricsCollection(); + $collection->add($metrics); + + $report = new GitHubActionsReport($this->configBelowThreshold); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $this->assertSame('', file_get_contents($this->filename)); + } + + #[Test] + public function testExportThrowsWhenDirectoryMissing(): void + { + $collection = new CognitiveMetricsCollection(); + $report = new GitHubActionsReport($this->configAboveThreshold); + + $this->expectException(CognitiveAnalysisException::class); + $this->expectExceptionMessage('Directory /nonexistent/path does not exist'); + + $report->export($collection, '/nonexistent/path/report.txt'); + } + + private function createMetric( + string $class, + string $method, + string $file, + int $line, + float $score + ): CognitiveMetrics { + $metric = new CognitiveMetrics([ + 'class' => $class, + 'method' => $method, + 'file' => $file, + 'line' => $line, + 'lineCount' => 10, + 'argCount' => 0, + 'returnCount' => 0, + 'variableCount' => 0, + 'propertyCallCount' => 0, + 'ifCount' => 0, + 'ifNestingLevel' => 0, + 'elseCount' => 0, + ]); + $metric->setScore($score); + + return $metric; + } +} diff --git a/tests/Unit/Business/Cognitive/Report/GitLabCodeQualityReporterTest.php b/tests/Unit/Business/Cognitive/Report/GitLabCodeQualityReporterTest.php new file mode 100644 index 0000000..86a7fee --- /dev/null +++ b/tests/Unit/Business/Cognitive/Report/GitLabCodeQualityReporterTest.php @@ -0,0 +1,131 @@ +filename = sys_get_temp_dir() . '/gitlab_codequality_' . uniqid() . '.json'; + $this->configAboveThreshold = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: true, + scoreThreshold: 10.0 + ); + $this->configBelowThreshold = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: true, + scoreThreshold: 100.0 + ); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (!file_exists($this->filename)) { + return; + } + + unlink($this->filename); + } + + #[Test] + public function testExportWithViolationsCreatesValidJsonArray(): void + { + $metrics = $this->createMetric('App\Example', 'foo', 'src/Example.php', 42, 15.5); + $collection = new CognitiveMetricsCollection(); + $collection->add($metrics); + + $report = new GitLabCodeQualityReport($this->configAboveThreshold); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $json = file_get_contents($this->filename); + $this->assertNotFalse($json); + $data = json_decode($json, true); + $this->assertIsArray($data); + $this->assertCount(1, $data); + $issue = $data[0]; + $this->assertArrayHasKey('description', $issue); + $this->assertSame('cognitive-complexity', $issue['check_name']); + $this->assertArrayHasKey('fingerprint', $issue); + $this->assertArrayHasKey('severity', $issue); + $this->assertSame('src/Example.php', $issue['location']['path']); + $this->assertSame(42, $issue['location']['lines']['begin']); + } + + #[Test] + public function testExportWithNoViolationsCreatesEmptyArray(): void + { + $metrics = $this->createMetric('App\Example', 'bar', 'src/Example.php', 10, 5.0); + $collection = new CognitiveMetricsCollection(); + $collection->add($metrics); + + $report = new GitLabCodeQualityReport($this->configBelowThreshold); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $data = json_decode(file_get_contents($this->filename), true); + $this->assertIsArray($data); + $this->assertCount(0, $data); + } + + #[Test] + public function testExportThrowsWhenDirectoryMissing(): void + { + $collection = new CognitiveMetricsCollection(); + $report = new GitLabCodeQualityReport($this->configAboveThreshold); + + $this->expectException(CognitiveAnalysisException::class); + $this->expectExceptionMessage('Directory /nonexistent/path does not exist'); + + $report->export($collection, '/nonexistent/path/report.json'); + } + + private function createMetric( + string $class, + string $method, + string $file, + int $line, + float $score + ): CognitiveMetrics { + $metric = new CognitiveMetrics([ + 'class' => $class, + 'method' => $method, + 'file' => $file, + 'line' => $line, + 'lineCount' => 10, + 'argCount' => 0, + 'returnCount' => 0, + 'variableCount' => 0, + 'propertyCallCount' => 0, + 'ifCount' => 0, + 'ifNestingLevel' => 0, + 'elseCount' => 0, + ]); + $metric->setScore($score); + + return $metric; + } +} diff --git a/tests/Unit/Business/Cognitive/Report/JUnitReporterTest.php b/tests/Unit/Business/Cognitive/Report/JUnitReporterTest.php new file mode 100644 index 0000000..6967116 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Report/JUnitReporterTest.php @@ -0,0 +1,118 @@ +filename = sys_get_temp_dir() . '/junit_report_' . uniqid() . '.xml'; + $this->config = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: false, + scoreThreshold: 10.0 + ); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (!file_exists($this->filename)) { + return; + } + + unlink($this->filename); + } + + #[Test] + public function testExportWithFailuresCreatesValidXml(): void + { + $over = $this->createMetric('App\Example', 'foo', 15.5); + $under = $this->createMetric('App\Example', 'bar', 5.0); + $collection = new CognitiveMetricsCollection(); + $collection->add($over); + $collection->add($under); + + $report = new JUnitReport($this->config); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $xml = file_get_contents($this->filename); + $this->assertNotFalse($xml); + $this->assertStringContainsString('assertStringContainsString('tests="2"', $xml); + $this->assertStringContainsString('failures="1"', $xml); + $this->assertStringContainsString('assertStringContainsString('Cognitive complexity 15.5 exceeds threshold 10', $xml); + + $dom = new \DOMDocument(); + $this->assertTrue($dom->loadXML($xml), 'Output must be valid XML'); + } + + #[Test] + public function testExportWithNoFailuresCreatesValidXml(): void + { + $metric = $this->createMetric('App\Example', 'baz', 3.0); + $collection = new CognitiveMetricsCollection(); + $collection->add($metric); + + $report = new JUnitReport($this->config); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $xml = file_get_contents($this->filename); + $this->assertNotFalse($xml); + $this->assertStringContainsString('failures="0"', $xml); + $this->assertStringNotContainsString('config); + + $this->expectException(CognitiveAnalysisException::class); + $this->expectExceptionMessage('Directory /nonexistent/path does not exist'); + + $report->export($collection, '/nonexistent/path/report.xml'); + } + + private function createMetric(string $class, string $method, float $score): CognitiveMetrics + { + $metric = new CognitiveMetrics([ + 'class' => $class, + 'method' => $method, + 'file' => 'src/Example.php', + 'line' => 42, + 'lineCount' => 10, + 'argCount' => 0, + 'returnCount' => 0, + 'variableCount' => 0, + 'propertyCallCount' => 0, + 'ifCount' => 0, + 'ifNestingLevel' => 0, + 'elseCount' => 0, + ]); + $metric->setScore($score); + + return $metric; + } +} diff --git a/tests/Unit/Business/Cognitive/Report/SarifReporterTest.php b/tests/Unit/Business/Cognitive/Report/SarifReporterTest.php new file mode 100644 index 0000000..2b3753c --- /dev/null +++ b/tests/Unit/Business/Cognitive/Report/SarifReporterTest.php @@ -0,0 +1,135 @@ +filename = sys_get_temp_dir() . '/sarif_report_' . uniqid() . '.json'; + $this->configAboveThreshold = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: true, + scoreThreshold: 10.0 + ); + $this->configBelowThreshold = new CognitiveConfig( + excludeFilePatterns: [], + excludePatterns: [], + metrics: [], + showOnlyMethodsExceedingThreshold: true, + scoreThreshold: 100.0 + ); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (!file_exists($this->filename)) { + return; + } + + unlink($this->filename); + } + + #[Test] + public function testExportWithViolationsCreatesValidSarif(): void + { + $metrics = $this->createMetric('App\Example', 'foo', 'src/Example.php', 42, 15.5); + $collection = new CognitiveMetricsCollection(); + $collection->add($metrics); + + $report = new SarifReport($this->configAboveThreshold); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $json = file_get_contents($this->filename); + $this->assertNotFalse($json); + $data = json_decode($json, true); + $this->assertIsArray($data); + $this->assertSame('2.1.0', $data['version']); + $this->assertArrayHasKey('runs', $data); + $this->assertCount(1, $data['runs']); + $run = $data['runs'][0]; + $this->assertArrayHasKey('tool', $run); + $this->assertArrayHasKey('results', $run); + $this->assertCount(1, $run['results']); + $result = $run['results'][0]; + $this->assertSame('cognitive-complexity', $result['ruleId']); + $this->assertArrayHasKey('locations', $result); + $this->assertSame('src/Example.php', $result['locations'][0]['physicalLocation']['artifactLocation']['uri']); + $this->assertSame(42, $result['locations'][0]['physicalLocation']['region']['startLine']); + } + + #[Test] + public function testExportWithNoViolationsCreatesEmptyResults(): void + { + $metrics = $this->createMetric('App\Example', 'bar', 'src/Example.php', 10, 5.0); + $collection = new CognitiveMetricsCollection(); + $collection->add($metrics); + + $report = new SarifReport($this->configBelowThreshold); + $report->export($collection, $this->filename); + + $this->assertFileExists($this->filename); + $data = json_decode(file_get_contents($this->filename), true); + $this->assertIsArray($data); + $this->assertCount(0, $data['runs'][0]['results']); + } + + #[Test] + public function testExportThrowsWhenDirectoryMissing(): void + { + $collection = new CognitiveMetricsCollection(); + $report = new SarifReport($this->configAboveThreshold); + + $this->expectException(CognitiveAnalysisException::class); + $this->expectExceptionMessage('Directory /nonexistent/path does not exist'); + + $report->export($collection, '/nonexistent/path/report.json'); + } + + private function createMetric( + string $class, + string $method, + string $file, + int $line, + float $score + ): CognitiveMetrics { + $metric = new CognitiveMetrics([ + 'class' => $class, + 'method' => $method, + 'file' => $file, + 'line' => $line, + 'lineCount' => 10, + 'argCount' => 0, + 'returnCount' => 0, + 'variableCount' => 0, + 'propertyCallCount' => 0, + 'ifCount' => 0, + 'ifNestingLevel' => 0, + 'elseCount' => 0, + ]); + $metric->setScore($score); + + return $metric; + } +}