diff --git a/src/Business/Cognitive/Exporter/MarkdownExporter.php b/src/Business/Cognitive/Exporter/MarkdownExporter.php new file mode 100644 index 0000000..4032f91 --- /dev/null +++ b/src/Business/Cognitive/Exporter/MarkdownExporter.php @@ -0,0 +1,423 @@ +config = $config; + } + + /** + * Export metrics to a Markdown file. + * + * @param CognitiveMetricsCollection $metrics + * @param string $filename + * @return void + */ + public function export(CognitiveMetricsCollection $metrics, string $filename): void + { + $markdown = $this->generateMarkdown($metrics); + + if (file_put_contents($filename, $markdown) === false) { + throw new CognitiveAnalysisException('Could not write to file'); + } + } + + /** + * Escape markdown special characters. + */ + private function escape(string $string): string + { + return str_replace(['\\', '|'], ['\\\\', '\\|'], $string); + } + + /** + * Format a number to 3 decimal places. + */ + private function formatNumber(float $number): string + { + return number_format($number, 3); + } + + /** + * Generate a metric cell with optional delta. + */ + private function generateMetricCell(int $count, float $weight, ?Delta $delta): string + { + $cell = $count . ' (' . $this->formatNumber($weight) . ')'; + + if ($delta !== null && !$delta->hasNotChanged()) { + $deltaValue = $this->formatNumber($delta->getValue()); + $deltaSymbol = $delta->hasIncreased() ? '↑' : '↓'; + $cell .= ' ' . $deltaSymbol . ' ' . $deltaValue; + } + + return $cell; + } + + /** + * Filter metrics based on threshold setting. + * + * @param CognitiveMetricsCollection $methods + * @return CognitiveMetricsCollection + */ + private function filterMetrics(CognitiveMetricsCollection $methods): CognitiveMetricsCollection + { + if (!$this->config->showOnlyMethodsExceedingThreshold) { + return $methods; + } + + $filtered = new CognitiveMetricsCollection(); + foreach ($methods as $metric) { + if ($metric->getScore() > $this->config->scoreThreshold) { + $filtered->add($metric); + } + } + + return $filtered; + } + + /** + * Build the table header row for grouped tables (without class column). + */ + private function buildTableHeader(): string + { + $headers = ['Method']; + + if ($this->config->showDetailedCognitiveMetrics) { + $headers = array_merge($headers, [ + 'Lines', + 'Args', + 'Returns', + 'Variables', + 'Property Calls', + 'If', + 'If Nesting', + 'Else' + ]); + } + + $headers[] = 'Cognitive Complexity'; + + if ($this->config->showHalsteadComplexity) { + $headers = array_merge($headers, [ + 'Halstead Volume', + 'Halstead Difficulty', + 'Halstead Effort' + ]); + } + + if ($this->config->showCyclomaticComplexity) { + $headers[] = 'Cyclomatic Complexity'; + } + + return '| ' . implode(' | ', $headers) . ' |'; + } + + /** + * Build the table header row for single table (with class column). + */ + private function buildTableHeaderWithClass(): string + { + $headers = ['Class', 'Method']; + + if ($this->config->showDetailedCognitiveMetrics) { + $headers = array_merge($headers, [ + 'Lines', + 'Args', + 'Returns', + 'Variables', + 'Property Calls', + 'If', + 'If Nesting', + 'Else' + ]); + } + + $headers[] = 'Cognitive Complexity'; + + if ($this->config->showHalsteadComplexity) { + $headers = array_merge($headers, [ + 'Halstead Volume', + 'Halstead Difficulty', + 'Halstead Effort' + ]); + } + + if ($this->config->showCyclomaticComplexity) { + $headers[] = 'Cyclomatic Complexity'; + } + + return '| ' . implode(' | ', $headers) . ' |'; + } + + /** + * Build the table separator row for grouped tables. + */ + private function buildTableSeparator(): string + { + $count = 1; // Method column + + if ($this->config->showDetailedCognitiveMetrics) { + $count += 8; // 8 detailed metrics + } + + $count += 1; // Cognitive Complexity + + if ($this->config->showHalsteadComplexity) { + $count += 3; // 3 Halstead metrics + } + + if ($this->config->showCyclomaticComplexity) { + $count += 1; // Cyclomatic complexity + } + + return '|' . str_repeat('--------|', $count); + } + + /** + * Build the table separator row for single table. + */ + private function buildTableSeparatorWithClass(): string + { + $count = 2; // Class + Method columns + + if ($this->config->showDetailedCognitiveMetrics) { + $count += 8; // 8 detailed metrics + } + + $count += 1; // Cognitive Complexity + + if ($this->config->showHalsteadComplexity) { + $count += 3; // 3 Halstead metrics + } + + if ($this->config->showCyclomaticComplexity) { + $count += 1; // Cyclomatic complexity + } + + return '|' . str_repeat('--------|', $count); + } + + /** + * Build a table row for grouped tables (without class column). + */ + private function buildTableRow(CognitiveMetrics $data): string + { + $cells = [$this->escape($data->getMethod())]; + + if ($this->config->showDetailedCognitiveMetrics) { + $cells[] = $this->generateMetricCell($data->getLineCount(), $data->getLineCountWeight(), $data->getLineCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getArgCount(), $data->getArgCountWeight(), $data->getArgCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getReturnCount(), $data->getReturnCountWeight(), $data->getReturnCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getVariableCount(), $data->getVariableCountWeight(), $data->getVariableCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getPropertyCallCount(), $data->getPropertyCallCountWeight(), $data->getPropertyCallCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getIfCount(), $data->getIfCountWeight(), $data->getIfCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getIfNestingLevel(), $data->getIfNestingLevelWeight(), $data->getIfNestingLevelWeightDelta()); + $cells[] = $this->generateMetricCell($data->getElseCount(), $data->getElseCountWeight(), $data->getElseCountWeightDelta()); + } + + $cells[] = $this->formatNumber($data->getScore()); + + $cells = $this->addHalsteadCells($cells, $data); + $cells = $this->addCyclomaticCell($cells, $data); + + return '| ' . implode(' | ', $cells) . ' |'; + } + + /** + * Build a table row for single table (with class column). + */ + private function buildTableRowWithClass(CognitiveMetrics $data): string + { + $cells = [ + $this->escape($data->getClass()), + $this->escape($data->getMethod()) + ]; + + if ($this->config->showDetailedCognitiveMetrics) { + $cells[] = $this->generateMetricCell($data->getLineCount(), $data->getLineCountWeight(), $data->getLineCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getArgCount(), $data->getArgCountWeight(), $data->getArgCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getReturnCount(), $data->getReturnCountWeight(), $data->getReturnCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getVariableCount(), $data->getVariableCountWeight(), $data->getVariableCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getPropertyCallCount(), $data->getPropertyCallCountWeight(), $data->getPropertyCallCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getIfCount(), $data->getIfCountWeight(), $data->getIfCountWeightDelta()); + $cells[] = $this->generateMetricCell($data->getIfNestingLevel(), $data->getIfNestingLevelWeight(), $data->getIfNestingLevelWeightDelta()); + $cells[] = $this->generateMetricCell($data->getElseCount(), $data->getElseCountWeight(), $data->getElseCountWeightDelta()); + } + + $cells[] = $this->formatNumber($data->getScore()); + + $cells = $this->addHalsteadCells($cells, $data); + $cells = $this->addCyclomaticCell($cells, $data); + + return '| ' . implode(' | ', $cells) . ' |'; + } + + /** + * Add Halstead complexity cells to the row. + * + * @param array $cells + * @return array + */ + private function addHalsteadCells(array $cells, CognitiveMetrics $data): array + { + if (!$this->config->showHalsteadComplexity) { + return $cells; + } + + $halstead = $data->getHalstead(); + if ($halstead === null) { + $cells[] = 'N/A'; + $cells[] = 'N/A'; + $cells[] = 'N/A'; + return $cells; + } + + $cells[] = $this->formatNumber($halstead->volume); + $cells[] = $this->formatNumber($halstead->difficulty); + $cells[] = $this->formatNumber($halstead->effort); + + return $cells; + } + + /** + * Add Cyclomatic complexity cell to the row. + * + * @param array $cells + * @return array + */ + private function addCyclomaticCell(array $cells, CognitiveMetrics $data): array + { + if (!$this->config->showCyclomaticComplexity) { + return $cells; + } + + $cyclomatic = $data->getCyclomatic(); + if ($cyclomatic === null) { + $cells[] = 'N/A'; + return $cells; + } + + $cells[] = $cyclomatic->complexity . ' (' . $cyclomatic->riskLevel . ')'; + + return $cells; + } + + + /** + * Generate Markdown content using the metrics data. + * + * @param CognitiveMetricsCollection $metrics + * @return string + */ + private function generateMarkdown(CognitiveMetricsCollection $metrics): string + { + if ($this->config->groupByClass) { + return $this->generateGroupedMarkdown($metrics); + } + + return $this->generateSingleTableMarkdown($metrics); + } + + /** + * Generate Markdown with methods grouped by class. + */ + private function generateGroupedMarkdown(CognitiveMetricsCollection $metrics): string + { + $groupedByClass = $metrics->groupBy('class'); + $datetime = (new Datetime())->format('Y-m-d H:i:s'); + + $markdown = "# Cognitive Metrics Report\n\n"; + $markdown .= "**Generated:** {$datetime}\n\n"; + $markdown .= "**Total Classes:** " . count($groupedByClass) . "\n\n"; + + if ($this->config->showOnlyMethodsExceedingThreshold && $this->config->scoreThreshold > 0) { + $markdown .= "**Note:** Only showing methods exceeding threshold of " . $this->formatNumber($this->config->scoreThreshold) . "\n\n"; + } + + $markdown .= "---\n\n"; + + foreach ($groupedByClass as $class => $methods) { + $filteredMethods = $this->filterMetrics($methods); + + if (count($filteredMethods) === 0) { + continue; + } + + // Get file path from first method in the collection + $firstMethod = null; + foreach ($filteredMethods as $method) { + $firstMethod = $method; + break; + } + + $markdown .= "* **Class:** " . $this->escape((string)$class) . "\n"; + if ($firstMethod !== null) { + $markdown .= "* **File:** " . $this->escape($firstMethod->getFileName()) . "\n"; + } + $markdown .= "\n"; + + // Table header and separator + $markdown .= $this->buildTableHeader() . "\n"; + $markdown .= $this->buildTableSeparator() . "\n"; + + // Table rows + foreach ($filteredMethods as $data) { + $markdown .= $this->buildTableRow($data) . "\n"; + } + + $markdown .= "\n---\n\n"; + } + + return $markdown; + } + + /** + * Generate Markdown with all methods in a single table. + */ + private function generateSingleTableMarkdown(CognitiveMetricsCollection $metrics): string + { + $datetime = (new Datetime())->format('Y-m-d H:i:s'); + $filteredMetrics = $this->filterMetrics($metrics); + $totalMethods = count($filteredMetrics); + + $markdown = "# Cognitive Metrics Report\n\n"; + $markdown .= "**Generated:** {$datetime}\n\n"; + + if ($this->config->showOnlyMethodsExceedingThreshold && $this->config->scoreThreshold > 0) { + $markdown .= "**Note:** Only showing methods exceeding threshold of " . $this->formatNumber($this->config->scoreThreshold) . "\n\n"; + } + + $markdown .= "## All Methods ({$totalMethods} total)\n\n"; + + if ($totalMethods > 0) { + // Table header and separator + $markdown .= $this->buildTableHeaderWithClass() . "\n"; + $markdown .= $this->buildTableSeparatorWithClass() . "\n"; + + // Table rows + foreach ($filteredMetrics as $data) { + $markdown .= $this->buildTableRowWithClass($data) . "\n"; + } + } + + return $markdown; + } +} diff --git a/src/Business/MetricsFacade.php b/src/Business/MetricsFacade.php index 506f86c..c73d5f4 100644 --- a/src/Business/MetricsFacade.php +++ b/src/Business/MetricsFacade.php @@ -13,6 +13,7 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Exporter\CsvExporter; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Exporter\HtmlExporter; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Exporter\JsonExporter; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Exporter\MarkdownExporter; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\ScoreCalculator; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; @@ -170,10 +171,13 @@ public function exportMetricsReport( string $reportType, string $filename ): void { + $config = $this->configService->getConfig(); + match ($reportType) { 'json' => (new JsonExporter())->export($metricsCollection, $filename), 'csv' => (new CsvExporter())->export($metricsCollection, $filename), 'html' => (new HtmlExporter())->export($metricsCollection, $filename), + 'markdown' => (new MarkdownExporter($config))->export($metricsCollection, $filename), default => null, }; } diff --git a/src/Command/Handler/CognitiveMetricsReportHandler.php b/src/Command/Handler/CognitiveMetricsReportHandler.php index fc6bcf6..e78f56f 100644 --- a/src/Command/Handler/CognitiveMetricsReportHandler.php +++ b/src/Command/Handler/CognitiveMetricsReportHandler.php @@ -56,7 +56,7 @@ private function hasIncompleteReportOptions(?string $reportType, ?string $report private function isValidReportType(?string $reportType): bool { - return in_array($reportType, ['json', 'csv', 'html']); + return in_array($reportType, ['json', 'csv', 'html', 'markdown']); } /** @@ -80,7 +80,7 @@ private function handleExceptions(Exception $exception): int public function handleInvalidReporType(?string $reportType): int { $this->output->writeln(sprintf( - 'Invalid report type `%s` provided. Only `json`, `csv`, and `html` are accepted.', + 'Invalid report type `%s` provided. Only `json`, `csv`, `html`, and `markdown` are accepted.', $reportType )); diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_AllMetrics.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_AllMetrics.md new file mode 100644 index 0000000..70ef966 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_AllMetrics.md @@ -0,0 +1,27 @@ +# Cognitive Metrics Report + +**Generated:** 2023-10-01 12:00:00 + +**Total Classes:** 2 + +--- + +* **Class:** TestClass +* **File:** TestClass.php + +| Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | +| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 185.470 | 6.250 | 1,159.188 | 2 (low) | + +--- + +* **Class:** AnotherClass +* **File:** AnotherClass.php + +| Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | + +--- + diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_CyclomaticOnly.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_CyclomaticOnly.md new file mode 100644 index 0000000..8670297 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_CyclomaticOnly.md @@ -0,0 +1,27 @@ +# Cognitive Metrics Report + +**Generated:** 2023-10-01 12:00:00 + +**Total Classes:** 2 + +--- + +* **Class:** TestClass +* **File:** TestClass.php + +| Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 5 (low) | +| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 2 (low) | + +--- + +* **Class:** AnotherClass +* **File:** AnotherClass.php + +| Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 12 (medium) | + +--- + diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_HalsteadOnly.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_HalsteadOnly.md new file mode 100644 index 0000000..43b64de --- /dev/null +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_HalsteadOnly.md @@ -0,0 +1,27 @@ +# Cognitive Metrics Report + +**Generated:** 2023-10-01 12:00:00 + +**Total Classes:** 2 + +--- + +* **Class:** TestClass +* **File:** TestClass.php + +| Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | +| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 185.470 | 6.250 | 1,159.188 | + +--- + +* **Class:** AnotherClass +* **File:** AnotherClass.php + +| Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | + +--- + diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Minimal.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Minimal.md new file mode 100644 index 0000000..7f6ee37 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Minimal.md @@ -0,0 +1,27 @@ +# Cognitive Metrics Report + +**Generated:** 2023-10-01 12:00:00 + +**Total Classes:** 2 + +--- + +* **Class:** TestClass +* **File:** TestClass.php + +| Method | Cognitive Complexity | +|--------|--------| +| testMethod | 0.300 | +| anotherMethod | 0.050 | + +--- + +* **Class:** AnotherClass +* **File:** AnotherClass.php + +| Method | Cognitive Complexity | +|--------|--------| +| complexMethod | 0.800 | + +--- + diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_NoDetailedMetrics.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_NoDetailedMetrics.md new file mode 100644 index 0000000..3c21cf5 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_NoDetailedMetrics.md @@ -0,0 +1,27 @@ +# Cognitive Metrics Report + +**Generated:** 2023-10-01 12:00:00 + +**Total Classes:** 2 + +--- + +* **Class:** TestClass +* **File:** TestClass.php + +| Method | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------| +| testMethod | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | +| anotherMethod | 0.050 | 185.470 | 6.250 | 1,159.188 | 2 (low) | + +--- + +* **Class:** AnotherClass +* **File:** AnotherClass.php + +| Method | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------| +| complexMethod | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | + +--- + diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_SingleTable.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_SingleTable.md new file mode 100644 index 0000000..e8b8c99 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_SingleTable.md @@ -0,0 +1,11 @@ +# Cognitive Metrics Report + +**Generated:** 2023-10-01 12:00:00 + +## All Methods (3 total) + +| Class | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| TestClass | testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | +| TestClass | anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 185.470 | 6.250 | 1,159.188 | 2 (low) | +| AnotherClass | complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Threshold.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Threshold.md new file mode 100644 index 0000000..a944ba3 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Threshold.md @@ -0,0 +1,28 @@ +# Cognitive Metrics Report + +**Generated:** 2023-10-01 12:00:00 + +**Total Classes:** 2 + +**Note:** Only showing methods exceeding threshold of 0.100 + +--- + +* **Class:** TestClass +* **File:** TestClass.php + +| Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | + +--- + +* **Class:** AnotherClass +* **File:** AnotherClass.php + +| Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | +|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| +| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | + +--- + diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterTest.php b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterTest.php new file mode 100644 index 0000000..a97f9f5 --- /dev/null +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterTest.php @@ -0,0 +1,261 @@ +filename = sys_get_temp_dir() . '/test_metrics.md'; + Datetime::$fixedDate = '2023-10-01 12:00:00'; + } + + protected function tearDown(): void + { + parent::tearDown(); + if (file_exists($this->filename)) { + unlink($this->filename); + } + DateTime::$fixedDate = null; + } + + /** + * @return array + */ + public static function configurationProvider(): array + { + return [ + 'All metrics' => ['all-metrics-config.yml', 'MarkdownExporterContent_AllMetrics.md'], + 'Minimal' => ['minimal-config.yml', 'MarkdownExporterContent_Minimal.md'], + 'Single table' => ['single-table-config.yml', 'MarkdownExporterContent_SingleTable.md'], + 'Halstead only' => ['halstead-only-config.yml', 'MarkdownExporterContent_HalsteadOnly.md'], + 'Cyclomatic only' => ['cyclomatic-only-config.yml', 'MarkdownExporterContent_CyclomaticOnly.md'], + 'No detailed metrics' => ['no-detailed-metrics-config.yml', 'MarkdownExporterContent_NoDetailedMetrics.md'], + 'Threshold' => ['threshold-config.yml', 'MarkdownExporterContent_Threshold.md'], + ]; + } + + #[Test] + #[DataProvider('configurationProvider')] + public function testExportWithConfiguration(string $configFile, string $expectedFile): void + { + $configService = new ConfigService( + new Processor(), + new ConfigLoader() + ); + $configService->loadConfig(__DIR__ . '/../../../../Fixtures/' . $configFile); + $config = $configService->getConfig(); + + $metricsCollection = $this->createTestMetricsCollection(); + $exporter = new MarkdownExporter($config); + $exporter->export($metricsCollection, $this->filename); + + $this->assertFileEquals( + __DIR__ . '/' . $expectedFile, + $this->filename + ); + } + + private function createTestMetricsCollection(): CognitiveMetricsCollection + { + $metricsCollection = new CognitiveMetricsCollection(); + + // First metric with all data + $metrics1 = new CognitiveMetrics([ + 'class' => 'TestClass', + 'method' => 'testMethod', + 'file' => 'TestClass.php', + 'lineCount' => 10, + 'argCount' => 2, + 'returnCount' => 1, + 'variableCount' => 5, + 'propertyCallCount' => 3, + 'ifCount' => 4, + 'ifNestingLevel' => 2, + 'elseCount' => 1, + 'lineCountWeight' => 0.5, + 'argCountWeight' => 0.3, + 'returnCountWeight' => 0.2, + 'variableCountWeight' => 0.4, + 'propertyCallCountWeight' => 0.3, + 'ifCountWeight' => 0.6, + 'ifNestingLevelWeight' => 0.5, + 'elseCountWeight' => 0.2, + 'halstead' => [ + 'n1' => 10, + 'n2' => 15, + 'N1' => 50, + 'N2' => 75, + 'programLength' => 125, + 'programVocabulary' => 25, + 'volume' => 573.211, + 'difficulty' => 12.5, + 'effort' => 7165.138, + 'fqName' => 'TestClass::testMethod' + ], + 'cyclomatic_complexity' => [ + 'complexity' => 5, + 'risk_level' => 'low', + 'breakdown' => [ + 'total' => 5, + 'base' => 1, + 'if' => 2, + 'elseif' => 0, + 'else' => 1, + 'switch' => 0, + 'case' => 0, + 'default' => 0, + 'while' => 0, + 'do_while' => 0, + 'for' => 1, + 'foreach' => 0, + 'catch' => 0, + 'logical_and' => 0, + 'logical_or' => 0, + 'logical_xor' => 0, + 'ternary' => 0, + ] + ] + ]); + $metrics1->setScore(0.3); + $metricsCollection->add($metrics1); + + // Second metric in same class + $metrics2 = new CognitiveMetrics([ + 'class' => 'TestClass', + 'method' => 'anotherMethod', + 'file' => 'TestClass.php', + 'lineCount' => 5, + 'argCount' => 1, + 'returnCount' => 1, + 'variableCount' => 2, + 'propertyCallCount' => 1, + 'ifCount' => 1, + 'ifNestingLevel' => 1, + 'elseCount' => 0, + 'lineCountWeight' => 0.2, + 'argCountWeight' => 0.1, + 'returnCountWeight' => 0.1, + 'variableCountWeight' => 0.1, + 'propertyCallCountWeight' => 0.1, + 'ifCountWeight' => 0.2, + 'ifNestingLevelWeight' => 0.1, + 'elseCountWeight' => 0.0, + 'halstead' => [ + 'n1' => 5, + 'n2' => 8, + 'N1' => 20, + 'N2' => 30, + 'programLength' => 50, + 'programVocabulary' => 13, + 'volume' => 185.47, + 'difficulty' => 6.25, + 'effort' => 1159.188, + 'fqName' => 'TestClass::anotherMethod' + ], + 'cyclomatic_complexity' => [ + 'complexity' => 2, + 'risk_level' => 'low', + 'breakdown' => [ + 'total' => 2, + 'base' => 1, + 'if' => 1, + 'elseif' => 0, + 'else' => 0, + 'switch' => 0, + 'case' => 0, + 'default' => 0, + 'while' => 0, + 'do_while' => 0, + 'for' => 0, + 'foreach' => 0, + 'catch' => 0, + 'logical_and' => 0, + 'logical_or' => 0, + 'logical_xor' => 0, + 'ternary' => 0, + ] + ] + ]); + $metrics2->setScore(0.05); + $metricsCollection->add($metrics2); + + // Third metric in different class + $metrics3 = new CognitiveMetrics([ + 'class' => 'AnotherClass', + 'method' => 'complexMethod', + 'file' => 'AnotherClass.php', + 'lineCount' => 20, + 'argCount' => 4, + 'returnCount' => 3, + 'variableCount' => 10, + 'propertyCallCount' => 5, + 'ifCount' => 8, + 'ifNestingLevel' => 3, + 'elseCount' => 4, + 'lineCountWeight' => 1.0, + 'argCountWeight' => 0.6, + 'returnCountWeight' => 0.5, + 'variableCountWeight' => 0.8, + 'propertyCallCountWeight' => 0.5, + 'ifCountWeight' => 1.2, + 'ifNestingLevelWeight' => 1.0, + 'elseCountWeight' => 0.6, + 'halstead' => [ + 'n1' => 20, + 'n2' => 25, + 'N1' => 100, + 'N2' => 150, + 'programLength' => 250, + 'programVocabulary' => 45, + 'volume' => 1357.824, + 'difficulty' => 25.0, + 'effort' => 33945.6, + 'fqName' => 'AnotherClass::complexMethod' + ], + 'cyclomatic_complexity' => [ + 'complexity' => 12, + 'risk_level' => 'medium', + 'breakdown' => [ + 'total' => 12, + 'base' => 1, + 'if' => 5, + 'elseif' => 2, + 'else' => 2, + 'switch' => 1, + 'case' => 0, + 'default' => 0, + 'while' => 1, + 'do_while' => 0, + 'for' => 0, + 'foreach' => 0, + 'catch' => 0, + 'logical_and' => 0, + 'logical_or' => 0, + 'logical_xor' => 0, + 'ternary' => 0, + ] + ] + ]); + $metrics3->setScore(0.8); + $metricsCollection->add($metrics3); + + return $metricsCollection; + } +}