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#[derive(Serialize)]
15pub struct ReportData {
16 pub title: String,
18 pub timestamp: String,
20 pub lint_results: Vec<LintFileResult>,
22 pub test_results: Vec<TestResultData>,
24 pub format_results: Vec<FormatFileResult>,
26 pub audit_results: Vec<AuditIssue>,
28 pub coverage_summary: Option<CoverageSummary>,
30 pub artifact_analysis: Option<serde_json::Value>,
32}
33
34#[derive(Serialize)]
36pub struct LintFileResult {
37 pub file: String,
39 pub diagnostics: Vec<Diagnostic>,
41}
42
43#[derive(Serialize)]
45pub struct TestResultData {
46 pub file: String,
48 pub success: bool,
50 pub message: Option<String>,
52}
53
54#[derive(Serialize)]
56pub struct FormatFileResult {
57 pub file: String,
59 pub formatted: bool,
61}
62
63#[derive(Serialize)]
65pub struct CoverageSummary {
66 pub total_statements: usize,
68 pub covered_statements: usize,
70 pub percentage: f64,
72}
73
74pub struct NargoReporter {
78 hb: Handlebars<'static>,
79}
80
81impl NargoReporter {
82 pub fn new() -> Self {
84 let mut hb = Handlebars::new();
85
86 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 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}