rust_diff_analyzer/output/
comment.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use crate::{
5    config::Config,
6    types::{AnalysisResult, SemanticUnitKind},
7};
8
9const COMMENT_MARKER: &str = "<!-- rust-diff-analyzer-comment -->";
10
11/// Formats analysis result as a markdown PR comment
12///
13/// # Arguments
14///
15/// * `result` - Analysis result to format
16/// * `config` - Configuration for formatting
17///
18/// # Returns
19///
20/// Formatted markdown string for PR comment
21///
22/// # Examples
23///
24/// ```
25/// use rust_diff_analyzer::{
26///     config::Config,
27///     output::comment::format_comment,
28///     types::{AnalysisResult, Summary},
29/// };
30///
31/// let result = AnalysisResult::new(vec![], Summary::default());
32/// let config = Config::default();
33/// let output = format_comment(&result, &config);
34/// assert!(output.contains("Rust Diff Analysis"));
35/// ```
36pub fn format_comment(result: &AnalysisResult, config: &Config) -> String {
37    let summary = &result.summary;
38    let status = if summary.exceeds_limit { "❌" } else { "✅" };
39
40    let mut output = String::new();
41
42    output.push_str(COMMENT_MARKER);
43    output.push('\n');
44    output.push_str("## Rust Diff Analysis\n\n");
45
46    output.push_str("| Metric | Production | Test |\n");
47    output.push_str("|--------|------------|------|\n");
48    output.push_str(&format!("| Functions | {} | - |\n", summary.prod_functions));
49    output.push_str(&format!(
50        "| Structs/Enums | {} | - |\n",
51        summary.prod_structs
52    ));
53    output.push_str(&format!("| Other | {} | - |\n", summary.prod_other));
54    output.push_str(&format!(
55        "| Lines added | {} | {} |\n",
56        summary.prod_lines_added, summary.test_lines_added
57    ));
58    output.push_str(&format!(
59        "| Lines removed | {} | {} |\n",
60        summary.prod_lines_removed, summary.test_lines_removed
61    ));
62    output.push_str(&format!(
63        "| Total units | {} | {} |\n",
64        summary.total_prod_units(),
65        summary.test_units
66    ));
67
68    output.push_str("\n### Score\n\n");
69    output.push_str(&format!(
70        "**{}** / {} {}\n",
71        summary.weighted_score, config.limits.max_weighted_score, status
72    ));
73
74    if config.output.include_details && !result.changes.is_empty() {
75        output.push_str("\n<details>\n");
76        output.push_str(&format!(
77            "<summary>Changed units ({})</summary>\n\n",
78            result.changes.len()
79        ));
80
81        let prod_changes: Vec<_> = result.production_changes().collect();
82        let test_changes: Vec<_> = result.test_changes().collect();
83
84        if !prod_changes.is_empty() {
85            output.push_str(&format!("#### Production ({})\n\n", prod_changes.len()));
86            for change in prod_changes {
87                let kind = match change.unit.kind {
88                    SemanticUnitKind::Function => "function",
89                    SemanticUnitKind::Struct => "struct",
90                    SemanticUnitKind::Enum => "enum",
91                    SemanticUnitKind::Trait => "trait",
92                    SemanticUnitKind::Impl => "impl",
93                    _ => "other",
94                };
95                output.push_str(&format!(
96                    "- `{}` → `{}` ({})\n",
97                    change.file_path.display(),
98                    change.unit.name,
99                    kind
100                ));
101            }
102            output.push('\n');
103        }
104
105        if !test_changes.is_empty() {
106            output.push_str(&format!("#### Test ({})\n\n", test_changes.len()));
107            for change in test_changes {
108                output.push_str(&format!(
109                    "- `{}` → `{}`\n",
110                    change.file_path.display(),
111                    change.unit.name
112                ));
113            }
114            output.push('\n');
115        }
116
117        output.push_str("</details>\n");
118    }
119
120    output.push_str("\n---\n");
121    output.push_str(
122        "<sub>[Rust Diff Analyzer](https://github.com/RAprogramm/rust-prod-diff-checker)</sub>\n",
123    );
124
125    output
126}
127
128/// Returns the comment marker for finding existing comments
129///
130/// # Returns
131///
132/// The marker string used to identify analyzer comments
133///
134/// # Examples
135///
136/// ```
137/// use rust_diff_analyzer::output::comment::get_comment_marker;
138///
139/// let marker = get_comment_marker();
140/// assert!(marker.contains("rust-diff-analyzer"));
141/// ```
142pub fn get_comment_marker() -> &'static str {
143    COMMENT_MARKER
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::types::Summary;
150
151    #[test]
152    fn test_format_comment() {
153        let result = AnalysisResult::new(vec![], Summary::default());
154        let config = Config::default();
155        let output = format_comment(&result, &config);
156
157        assert!(output.contains(COMMENT_MARKER));
158        assert!(output.contains("Rust Diff Analysis"));
159        assert!(output.contains("Production"));
160        assert!(output.contains("Test"));
161    }
162
163    #[test]
164    fn test_format_comment_with_exceeded_limit() {
165        let summary = Summary {
166            exceeds_limit: true,
167            ..Default::default()
168        };
169        let result = AnalysisResult::new(vec![], summary);
170        let config = Config::default();
171        let output = format_comment(&result, &config);
172
173        assert!(output.contains("❌"));
174    }
175
176    #[test]
177    fn test_get_comment_marker() {
178        let marker = get_comment_marker();
179        assert!(marker.contains("rust-diff-analyzer"));
180    }
181}