Skip to main content

nargo_report/
lib.rs

1#![warn(missing_docs)]
2
3use handlebars::Handlebars;
4use nargo_types::{Error, Result};
5use serde::Serialize;
6use std::path::Path;
7
8use nargo_audit::AuditIssue;
9use nargo_linter::Diagnostic;
10
11/// 报告数据结构
12///
13/// 包含所有类型的报告结果,用于生成 HTML 报告。
14#[derive(Serialize)]
15pub struct ReportData {
16    /// 报告标题
17    pub title: String,
18    /// 生成时间戳
19    pub timestamp: String,
20    ///  lint 结果
21    pub lint_results: Vec<LintFileResult>,
22    /// 测试结果
23    pub test_results: Vec<TestResultData>,
24    /// 格式化结果
25    pub format_results: Vec<FormatFileResult>,
26    /// 安全审计结果
27    pub audit_results: Vec<AuditIssue>,
28    /// 覆盖率摘要
29    pub coverage_summary: Option<CoverageSummary>,
30    /// 构建产物分析
31    pub artifact_analysis: Option<serde_json::Value>,
32}
33
34/// 单个文件的 lint 结果
35#[derive(Serialize)]
36pub struct LintFileResult {
37    /// 文件路径
38    pub file: String,
39    /// 诊断信息
40    pub diagnostics: Vec<Diagnostic>,
41}
42
43/// 测试结果数据
44#[derive(Serialize)]
45pub struct TestResultData {
46    /// 测试文件路径
47    pub file: String,
48    /// 是否测试通过
49    pub success: bool,
50    /// 测试消息(如果有)
51    pub message: Option<String>,
52}
53
54/// 格式化结果
55#[derive(Serialize)]
56pub struct FormatFileResult {
57    /// 文件路径
58    pub file: String,
59    /// 是否已格式化
60    pub formatted: bool,
61}
62
63/// 覆盖率摘要
64#[derive(Serialize)]
65pub struct CoverageSummary {
66    /// 总语句数
67    pub total_statements: usize,
68    /// 覆盖的语句数
69    pub covered_statements: usize,
70    /// 覆盖率百分比
71    pub percentage: f64,
72}
73
74/// Nargo 报告生成器
75///
76/// 用于生成 HTML 格式的报告,包含各种分析结果。
77pub struct NargoReporter {
78    hb: Handlebars<'static>,
79}
80
81impl NargoReporter {
82    /// 创建一个新的报告生成器
83    pub fn new() -> Self {
84        let mut hb = Handlebars::new();
85
86        // Define a simple HTML template
87        let template = r#"
88<!DOCTYPE html>
89<html>
90<head>
91    <title>{{title}}</title>
92    <style>
93        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f4f7f6; }
94        .card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
95        h1 { color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; }
96        h2 { color: #34495e; margin-top: 30px; }
97        .summary { display: flex; gap: 20px; flex-wrap: wrap; }
98        .stat { flex: 1; min-width: 150px; text-align: center; padding: 15px; border-radius: 8px; color: white; }
99        .stat-success { background: #27ae60; }
100        .stat-error { background: #e74c3c; }
101        .stat-info { background: #3498db; }
102        .stat-value { font-size: 24px; font-weight: bold; display: block; }
103        .stat-label { font-size: 14px; opacity: 0.9; }
104        table { width: 100%; border-collapse: collapse; margin-top: 10px; }
105        th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
106        th { background: #f8f9fa; font-weight: 600; }
107        .success { color: #27ae60; font-weight: bold; }
108        .error { color: #e74c3c; font-weight: bold; }
109        .warning { color: #f39c12; font-weight: bold; }
110        pre { background: #f8f9fa; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px; }
111        .progress-bar { height: 10px; background: #eee; border-radius: 5px; overflow: hidden; margin-top: 5px; }
112        .progress-fill { height: 100%; background: #27ae60; }
113    </style>
114</head>
115<body>
116    <h1>{{title}}</h1>
117    <p>Generated at: {{timestamp}}</p>
118
119    {{#if artifact_analysis}}
120    <div class="card">
121        <h2>🏗️ Artifact Analysis</h2>
122        <div class="summary">
123            <div class="stat stat-info">
124                <span class="stat-value">{{artifact_analysis.file_count}}</span>
125                <span class="stat-label">Total Files</span>
126            </div>
127            <div class="stat stat-info">
128                <span class="stat-value">{{artifact_analysis.total_size}} B</span>
129                <span class="stat-label">Total Size</span>
130            </div>
131        </div>
132        <table>
133            <thead>
134                <tr>
135                    <th>File Path</th>
136                    <th>Size (Bytes)</th>
137                    <th>Type</th>
138                </tr>
139            </thead>
140            <tbody>
141                {{#each artifact_analysis.files}}
142                <tr>
143                    <td>{{path}}</td>
144                    <td>{{size}}</td>
145                    <td>{{file_type}}</td>
146                </tr>
147                {{/each}}
148            </tbody>
149        </table>
150    </div>
151    {{/if}}
152
153    {{#if test_results}}
154    <div class="card">
155        <h2>🧪 Test Results</h2>
156        <table>
157            <thead>
158                <tr>
159                    <th>Test File</th>
160                    <th>Status</th>
161                    <th>Message</th>
162                </tr>
163            </thead>
164            <tbody>
165                {{#each test_results}}
166                <tr>
167                    <td>{{file}}</td>
168                    <td><span class="{{#if success}}success{{else}}error{{/if}}">{{#if success}}PASSED{{else}}FAILED{{/if}}</span></td>
169                    <td>{{#if message}}<pre>{{message}}</pre>{{/if}}</td>
170                </tr>
171                {{/each}}
172            </tbody>
173        </table>
174    </div>
175    {{/if}}
176
177    {{#if coverage_summary}}
178    <div class="card">
179        <h2>📊 Coverage Summary</h2>
180        <div class="stat-value">{{coverage_summary.percentage}}%</div>
181        <div class="progress-bar">
182            <div class="progress-fill" style="width: {{coverage_summary.percentage}}%"></div>
183        </div>
184        <p>{{coverage_summary.covered_statements}} / {{coverage_summary.total_statements}} statements covered</p>
185    </div>
186    {{/if}}
187
188    {{#if lint_results}}
189    <div class="card">
190        <h2>🔍 Lint Results</h2>
191        <table>
192            <thead>
193                <tr>
194                    <th>File</th>
195                    <th>Diagnostics</th>
196                </tr>
197            </thead>
198            <tbody>
199                {{#each lint_results}}
200                <tr>
201                    <td>{{file}}</td>
202                    <td>
203                        {{#each diagnostics}}
204                        <div class="{{severity}}">[{{severity}}] {{message}} (line {{line}}, col {{column}})</div>
205                        {{/each}}
206                    </td>
207                </tr>
208                {{/each}}
209            </tbody>
210        </table>
211    </div>
212    {{/if}}
213
214    {{#if format_results}}
215    <div class="card">
216        <h2>🎨 Formatting</h2>
217        <table>
218            <thead>
219                <tr>
220                    <th>File</th>
221                    <th>Status</th>
222                </tr>
223            </thead>
224            <tbody>
225                {{#each format_results}}
226                <tr>
227                    <td>{{file}}</td>
228                    <td><span class="{{#if formatted}}success{{else}}error{{/if}}">{{#if formatted}}FORMATTED{{else}}UNFORMATTED{{/if}}</span></td>
229                </tr>
230                {{/each}}
231            </tbody>
232        </table>
233    </div>
234    {{/if}}
235
236    {{#if audit_results}}
237    <div class="card">
238        <h2>🛡️ Security Audit</h2>
239        <table>
240            <thead>
241                <tr>
242                    <th>Category</th>
243                    <th>File</th>
244                    <th>Issue</th>
245                    <th>Severity</th>
246                </tr>
247            </thead>
248            <tbody>
249                {{#each audit_results}}
250                <tr>
251                    <td>{{category}}</td>
252                    <td>{{file}}</td>
253                    <td>{{message}} (line {{line}})</td>
254                    <td><span class="{{severity}}">[{{severity}}]</span></td>
255                </tr>
256                {{/each}}
257            </tbody>
258        </table>
259    </div>
260    {{/if}}
261</body>
262</html>
263"#;
264
265        hb.register_template_string("report", template).unwrap();
266        Self { hb }
267    }
268
269    /// 生成 HTML 报告
270    ///
271    /// # Arguments
272    ///
273    /// * `data` - 报告数据
274    /// * `output_path` - 输出文件路径
275    ///
276    /// # Returns
277    ///
278    /// 生成结果
279    pub fn generate_html(&self, data: &ReportData, output_path: &Path) -> Result<()> {
280        let html = self.hb.render("report", data).map_err(|e| Error::external_error("handlebars".to_string(), format!("Failed to render report: {}", e), nargo_types::Span::unknown()))?;
281        std::fs::write(output_path, html)?;
282        Ok(())
283    }
284}