@sun-asterisk/sunlint 1.3.24 → 1.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rules/enhanced-rules-registry.json +32 -0
- package/core/artifact-upload-service.js +107 -0
- package/core/cli-program.js +5 -5
- package/core/github-annotate-service.js +68 -6
- package/core/github-step-summary-generator.js +277 -0
- package/core/html-report-generator.js +839 -0
- package/core/output-service.js +170 -0
- package/package.json +1 -1
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +40 -11
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +104 -28
- package/rules/common/C019_log_level_usage/analyzer.js +30 -27
- package/rules/common/C019_log_level_usage/config.json +4 -2
- package/rules/common/C019_log_level_usage/ts-morph-analyzer.js +274 -0
- package/rules/common/C020_unused_imports/analyzer.js +88 -0
- package/rules/common/C020_unused_imports/config.json +64 -0
- package/rules/common/C020_unused_imports/ts-morph-analyzer.js +358 -0
- package/rules/common/C021_import_organization/analyzer.js +88 -0
- package/rules/common/C021_import_organization/config.json +77 -0
- package/rules/common/C021_import_organization/ts-morph-analyzer.js +373 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +106 -31
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +377 -87
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Report Generator
|
|
3
|
+
* Generate standalone HTML report with embedded CSS for SunLint results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate HTML report from violations data
|
|
8
|
+
* @param {Array} violations - Array of violation objects
|
|
9
|
+
* @param {Object} metadata - Report metadata
|
|
10
|
+
* @param {Object} metadata.score - Scoring summary
|
|
11
|
+
* @param {Object} metadata.gitInfo - Git information
|
|
12
|
+
* @param {string} metadata.timestamp - Report timestamp
|
|
13
|
+
* @returns {string} Complete HTML report
|
|
14
|
+
*/
|
|
15
|
+
function generateHTMLReport(violations, metadata = {}) {
|
|
16
|
+
const {
|
|
17
|
+
score = {},
|
|
18
|
+
gitInfo = {},
|
|
19
|
+
timestamp = new Date().toISOString()
|
|
20
|
+
} = metadata;
|
|
21
|
+
|
|
22
|
+
// Calculate statistics
|
|
23
|
+
const stats = calculateStatistics(violations);
|
|
24
|
+
|
|
25
|
+
// Generate HTML sections
|
|
26
|
+
const html = `<!DOCTYPE html>
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<head>
|
|
29
|
+
<meta charset="UTF-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
31
|
+
<title>SunLint Report - ${gitInfo.repository_name || 'Project'}</title>
|
|
32
|
+
${embedCSS()}
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div class="container">
|
|
36
|
+
${generateHeader(gitInfo, timestamp)}
|
|
37
|
+
${generateSummary(stats, score)}
|
|
38
|
+
${generateByFileSection(violations, stats.fileGroups)}
|
|
39
|
+
${generateByRuleSection(violations, stats.ruleGroups)}
|
|
40
|
+
${generateFooter(timestamp)}
|
|
41
|
+
</div>
|
|
42
|
+
${embedJavaScript()}
|
|
43
|
+
</body>
|
|
44
|
+
</html>`;
|
|
45
|
+
|
|
46
|
+
return html;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Calculate statistics from violations
|
|
51
|
+
* @param {Array} violations - Violations array
|
|
52
|
+
* @returns {Object} Statistics object
|
|
53
|
+
*/
|
|
54
|
+
function calculateStatistics(violations) {
|
|
55
|
+
const totalViolations = violations.length;
|
|
56
|
+
const errorCount = violations.filter(v => v.severity === 'error').length;
|
|
57
|
+
const warningCount = violations.filter(v => v.severity === 'warning').length;
|
|
58
|
+
|
|
59
|
+
// Group by file
|
|
60
|
+
const fileGroups = {};
|
|
61
|
+
for (const v of violations) {
|
|
62
|
+
if (!fileGroups[v.file]) {
|
|
63
|
+
fileGroups[v.file] = [];
|
|
64
|
+
}
|
|
65
|
+
fileGroups[v.file].push(v);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Group by rule
|
|
69
|
+
const ruleGroups = {};
|
|
70
|
+
for (const v of violations) {
|
|
71
|
+
if (!ruleGroups[v.rule]) {
|
|
72
|
+
ruleGroups[v.rule] = [];
|
|
73
|
+
}
|
|
74
|
+
ruleGroups[v.rule].push(v);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const filesWithIssues = Object.keys(fileGroups).length;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
totalViolations,
|
|
81
|
+
errorCount,
|
|
82
|
+
warningCount,
|
|
83
|
+
filesWithIssues,
|
|
84
|
+
fileGroups,
|
|
85
|
+
ruleGroups
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generate HTML header
|
|
91
|
+
* @param {Object} gitInfo - Git information
|
|
92
|
+
* @param {string} timestamp - Report timestamp
|
|
93
|
+
* @returns {string} Header HTML
|
|
94
|
+
*/
|
|
95
|
+
function generateHeader(gitInfo, timestamp) {
|
|
96
|
+
const repoName = gitInfo.repository_name || 'Project';
|
|
97
|
+
const branch = gitInfo.branch || 'Unknown';
|
|
98
|
+
const commit = gitInfo.commit_hash ? gitInfo.commit_hash.substring(0, 7) : 'N/A';
|
|
99
|
+
|
|
100
|
+
return `
|
|
101
|
+
<header class="header">
|
|
102
|
+
<div class="header-content">
|
|
103
|
+
<h1>
|
|
104
|
+
<span class="logo">🌟</span>
|
|
105
|
+
SunLint Code Quality Report
|
|
106
|
+
</h1>
|
|
107
|
+
<div class="header-meta">
|
|
108
|
+
<div class="meta-item">
|
|
109
|
+
<span class="meta-label">Repository:</span>
|
|
110
|
+
<span class="meta-value">${escapeHTML(repoName)}</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="meta-item">
|
|
113
|
+
<span class="meta-label">Branch:</span>
|
|
114
|
+
<span class="meta-value">${escapeHTML(branch)}</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="meta-item">
|
|
117
|
+
<span class="meta-label">Commit:</span>
|
|
118
|
+
<span class="meta-value">${escapeHTML(commit)}</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="meta-item">
|
|
121
|
+
<span class="meta-label">Generated:</span>
|
|
122
|
+
<span class="meta-value">${new Date(timestamp).toLocaleString()}</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</header>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate summary section
|
|
131
|
+
* @param {Object} stats - Statistics object
|
|
132
|
+
* @param {Object} score - Score information
|
|
133
|
+
* @returns {string} Summary HTML
|
|
134
|
+
*/
|
|
135
|
+
function generateSummary(stats, score) {
|
|
136
|
+
const { totalViolations, errorCount, warningCount, filesWithIssues } = stats;
|
|
137
|
+
const status = errorCount > 0 ? 'failed' : warningCount > 0 ? 'warning' : 'passed';
|
|
138
|
+
const statusIcon = errorCount > 0 ? '❌' : warningCount > 0 ? '⚠️' : '✅';
|
|
139
|
+
const statusText = errorCount > 0 ? 'Failed' : warningCount > 0 ? 'Passed with Warnings' : 'Passed';
|
|
140
|
+
|
|
141
|
+
const scoreValue = score.score !== undefined ? score.score : 'N/A';
|
|
142
|
+
const grade = score.grade || 'N/A';
|
|
143
|
+
|
|
144
|
+
return `
|
|
145
|
+
<section class="summary">
|
|
146
|
+
<div class="summary-header">
|
|
147
|
+
<div class="status-badge status-${status}">
|
|
148
|
+
<span class="status-icon">${statusIcon}</span>
|
|
149
|
+
<span class="status-text">${statusText}</span>
|
|
150
|
+
</div>
|
|
151
|
+
${scoreValue !== 'N/A' ? `
|
|
152
|
+
<div class="score-display">
|
|
153
|
+
<div class="score-value">${scoreValue}</div>
|
|
154
|
+
<div class="score-grade">${grade}</div>
|
|
155
|
+
</div>
|
|
156
|
+
` : ''}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="summary-stats">
|
|
160
|
+
<div class="stat-card">
|
|
161
|
+
<div class="stat-value">${totalViolations}</div>
|
|
162
|
+
<div class="stat-label">Total Violations</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="stat-card stat-error">
|
|
165
|
+
<div class="stat-value">${errorCount}</div>
|
|
166
|
+
<div class="stat-label">Errors</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="stat-card stat-warning">
|
|
169
|
+
<div class="stat-value">${warningCount}</div>
|
|
170
|
+
<div class="stat-label">Warnings</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="stat-card">
|
|
173
|
+
<div class="stat-value">${filesWithIssues}</div>
|
|
174
|
+
<div class="stat-label">Files with Issues</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</section>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generate violations by file section
|
|
182
|
+
* @param {Array} violations - Violations array
|
|
183
|
+
* @param {Object} fileGroups - Grouped by file
|
|
184
|
+
* @returns {string} By file HTML
|
|
185
|
+
*/
|
|
186
|
+
function generateByFileSection(violations, fileGroups) {
|
|
187
|
+
if (violations.length === 0) {
|
|
188
|
+
return `
|
|
189
|
+
<section class="section">
|
|
190
|
+
<h2>✅ Great Job!</h2>
|
|
191
|
+
<p class="no-violations">No coding standard violations found.</p>
|
|
192
|
+
</section>`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const sortedFiles = Object.entries(fileGroups)
|
|
196
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
197
|
+
|
|
198
|
+
let html = `
|
|
199
|
+
<section class="section">
|
|
200
|
+
<h2>📁 Violations by File</h2>
|
|
201
|
+
<div class="controls">
|
|
202
|
+
<input type="text" id="fileSearch" class="search-box" placeholder="Search files...">
|
|
203
|
+
<select id="fileFilter" class="filter-select">
|
|
204
|
+
<option value="all">All Severities</option>
|
|
205
|
+
<option value="error">Errors Only</option>
|
|
206
|
+
<option value="warning">Warnings Only</option>
|
|
207
|
+
</select>
|
|
208
|
+
</div>
|
|
209
|
+
<table class="violations-table" id="fileTable">
|
|
210
|
+
<thead>
|
|
211
|
+
<tr>
|
|
212
|
+
<th onclick="sortTable('fileTable', 0)">File <span class="sort-icon">↕</span></th>
|
|
213
|
+
<th onclick="sortTable('fileTable', 1)">Errors <span class="sort-icon">↕</span></th>
|
|
214
|
+
<th onclick="sortTable('fileTable', 2)">Warnings <span class="sort-icon">↕</span></th>
|
|
215
|
+
<th onclick="sortTable('fileTable', 3)">Total <span class="sort-icon">↕</span></th>
|
|
216
|
+
<th>Details</th>
|
|
217
|
+
</tr>
|
|
218
|
+
</thead>
|
|
219
|
+
<tbody>`;
|
|
220
|
+
|
|
221
|
+
for (const [file, fileViolations] of sortedFiles) {
|
|
222
|
+
const fileErrors = fileViolations.filter(v => v.severity === 'error').length;
|
|
223
|
+
const fileWarnings = fileViolations.filter(v => v.severity === 'warning').length;
|
|
224
|
+
const total = fileViolations.length;
|
|
225
|
+
const fileId = `file-${Buffer.from(file).toString('base64').replace(/=/g, '')}`;
|
|
226
|
+
|
|
227
|
+
html += `
|
|
228
|
+
<tr class="file-row" data-errors="${fileErrors}" data-warnings="${fileWarnings}">
|
|
229
|
+
<td class="file-path"><code>${escapeHTML(file)}</code></td>
|
|
230
|
+
<td class="count-cell ${fileErrors > 0 ? 'has-errors' : ''}">${fileErrors}</td>
|
|
231
|
+
<td class="count-cell ${fileWarnings > 0 ? 'has-warnings' : ''}">${fileWarnings}</td>
|
|
232
|
+
<td class="count-cell">${total}</td>
|
|
233
|
+
<td>
|
|
234
|
+
<button class="details-btn" onclick="toggleDetails('${fileId}')">
|
|
235
|
+
Show Details <span class="arrow">▼</span>
|
|
236
|
+
</button>
|
|
237
|
+
</td>
|
|
238
|
+
</tr>
|
|
239
|
+
<tr class="details-row" id="${fileId}" style="display: none;">
|
|
240
|
+
<td colspan="5">
|
|
241
|
+
<div class="details-content">
|
|
242
|
+
${generateFileDetails(fileViolations)}
|
|
243
|
+
</div>
|
|
244
|
+
</td>
|
|
245
|
+
</tr>`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
html += `
|
|
249
|
+
</tbody>
|
|
250
|
+
</table>
|
|
251
|
+
</section>`;
|
|
252
|
+
|
|
253
|
+
return html;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Generate file details
|
|
258
|
+
* @param {Array} violations - Violations for a file
|
|
259
|
+
* @returns {string} Details HTML
|
|
260
|
+
*/
|
|
261
|
+
function generateFileDetails(violations) {
|
|
262
|
+
let html = '<ul class="violation-list">';
|
|
263
|
+
|
|
264
|
+
for (const v of violations) {
|
|
265
|
+
const severityClass = v.severity === 'error' ? 'severity-error' : 'severity-warning';
|
|
266
|
+
const severityIcon = v.severity === 'error' ? '🔴' : '🟡';
|
|
267
|
+
|
|
268
|
+
html += `
|
|
269
|
+
<li class="violation-item">
|
|
270
|
+
<span class="${severityClass}">${severityIcon} ${v.severity.toUpperCase()}</span>
|
|
271
|
+
<span class="violation-line">Line ${v.line}</span>
|
|
272
|
+
<span class="violation-rule">[${escapeHTML(v.rule)}]</span>
|
|
273
|
+
<span class="violation-message">${escapeHTML(v.message)}</span>
|
|
274
|
+
</li>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
html += '</ul>';
|
|
278
|
+
return html;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Generate violations by rule section
|
|
283
|
+
* @param {Array} violations - Violations array
|
|
284
|
+
* @param {Object} ruleGroups - Grouped by rule
|
|
285
|
+
* @returns {string} By rule HTML
|
|
286
|
+
*/
|
|
287
|
+
function generateByRuleSection(violations, ruleGroups) {
|
|
288
|
+
if (violations.length === 0) {
|
|
289
|
+
return '';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const sortedRules = Object.entries(ruleGroups)
|
|
293
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
294
|
+
|
|
295
|
+
let html = `
|
|
296
|
+
<section class="section">
|
|
297
|
+
<h2>📋 Violations by Rule</h2>
|
|
298
|
+
<table class="violations-table" id="ruleTable">
|
|
299
|
+
<thead>
|
|
300
|
+
<tr>
|
|
301
|
+
<th onclick="sortTable('ruleTable', 0)">Rule <span class="sort-icon">↕</span></th>
|
|
302
|
+
<th onclick="sortTable('ruleTable', 1)">Errors <span class="sort-icon">↕</span></th>
|
|
303
|
+
<th onclick="sortTable('ruleTable', 2)">Warnings <span class="sort-icon">↕</span></th>
|
|
304
|
+
<th onclick="sortTable('ruleTable', 3)">Total <span class="sort-icon">↕</span></th>
|
|
305
|
+
<th>Locations</th>
|
|
306
|
+
</tr>
|
|
307
|
+
</thead>
|
|
308
|
+
<tbody>`;
|
|
309
|
+
|
|
310
|
+
for (const [ruleId, ruleViolations] of sortedRules) {
|
|
311
|
+
const ruleErrors = ruleViolations.filter(v => v.severity === 'error').length;
|
|
312
|
+
const ruleWarnings = ruleViolations.filter(v => v.severity === 'warning').length;
|
|
313
|
+
const total = ruleViolations.length;
|
|
314
|
+
const ruleIdSafe = `rule-${Buffer.from(ruleId).toString('base64').replace(/=/g, '')}`;
|
|
315
|
+
|
|
316
|
+
html += `
|
|
317
|
+
<tr class="rule-row">
|
|
318
|
+
<td class="rule-id"><code>${escapeHTML(ruleId)}</code></td>
|
|
319
|
+
<td class="count-cell ${ruleErrors > 0 ? 'has-errors' : ''}">${ruleErrors}</td>
|
|
320
|
+
<td class="count-cell ${ruleWarnings > 0 ? 'has-warnings' : ''}">${ruleWarnings}</td>
|
|
321
|
+
<td class="count-cell">${total}</td>
|
|
322
|
+
<td>
|
|
323
|
+
<button class="details-btn" onclick="toggleDetails('${ruleIdSafe}')">
|
|
324
|
+
Show Locations <span class="arrow">▼</span>
|
|
325
|
+
</button>
|
|
326
|
+
</td>
|
|
327
|
+
</tr>
|
|
328
|
+
<tr class="details-row" id="${ruleIdSafe}" style="display: none;">
|
|
329
|
+
<td colspan="5">
|
|
330
|
+
<div class="details-content">
|
|
331
|
+
${generateRuleDetails(ruleViolations)}
|
|
332
|
+
</div>
|
|
333
|
+
</td>
|
|
334
|
+
</tr>`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
html += `
|
|
338
|
+
</tbody>
|
|
339
|
+
</table>
|
|
340
|
+
</section>`;
|
|
341
|
+
|
|
342
|
+
return html;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Generate rule details
|
|
347
|
+
* @param {Array} violations - Violations for a rule
|
|
348
|
+
* @returns {string} Details HTML
|
|
349
|
+
*/
|
|
350
|
+
function generateRuleDetails(violations) {
|
|
351
|
+
let html = '<ul class="location-list">';
|
|
352
|
+
|
|
353
|
+
for (const v of violations) {
|
|
354
|
+
const severityClass = v.severity === 'error' ? 'severity-error' : 'severity-warning';
|
|
355
|
+
const severityIcon = v.severity === 'error' ? '🔴' : '🟡';
|
|
356
|
+
|
|
357
|
+
html += `
|
|
358
|
+
<li class="location-item">
|
|
359
|
+
<span class="${severityClass}">${severityIcon}</span>
|
|
360
|
+
<code class="location-path">${escapeHTML(v.file)}:${v.line}</code>
|
|
361
|
+
<span class="location-message">${escapeHTML(v.message)}</span>
|
|
362
|
+
</li>`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
html += '</ul>';
|
|
366
|
+
return html;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Generate footer
|
|
371
|
+
* @param {string} timestamp - Report timestamp
|
|
372
|
+
* @returns {string} Footer HTML
|
|
373
|
+
*/
|
|
374
|
+
function generateFooter(timestamp) {
|
|
375
|
+
return `
|
|
376
|
+
<footer class="footer">
|
|
377
|
+
<p>
|
|
378
|
+
Generated by <a href="https://github.com/sun-asterisk/engineer-excellence" target="_blank">SunLint</a>
|
|
379
|
+
on ${new Date(timestamp).toLocaleString()}
|
|
380
|
+
</p>
|
|
381
|
+
</footer>`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Embed CSS styles
|
|
386
|
+
* @returns {string} Style tag
|
|
387
|
+
*/
|
|
388
|
+
function embedCSS() {
|
|
389
|
+
return `<style>
|
|
390
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
391
|
+
|
|
392
|
+
body {
|
|
393
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
394
|
+
line-height: 1.6;
|
|
395
|
+
color: #333;
|
|
396
|
+
background: #f5f5f5;
|
|
397
|
+
padding: 20px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.container {
|
|
401
|
+
max-width: 1400px;
|
|
402
|
+
margin: 0 auto;
|
|
403
|
+
background: white;
|
|
404
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
405
|
+
border-radius: 8px;
|
|
406
|
+
overflow: hidden;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.header {
|
|
410
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
411
|
+
color: white;
|
|
412
|
+
padding: 30px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.header-content h1 {
|
|
416
|
+
font-size: 28px;
|
|
417
|
+
margin-bottom: 20px;
|
|
418
|
+
display: flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
gap: 10px;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.logo { font-size: 32px; }
|
|
424
|
+
|
|
425
|
+
.header-meta {
|
|
426
|
+
display: grid;
|
|
427
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
428
|
+
gap: 15px;
|
|
429
|
+
margin-top: 20px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.meta-item {
|
|
433
|
+
background: rgba(255,255,255,0.1);
|
|
434
|
+
padding: 10px 15px;
|
|
435
|
+
border-radius: 5px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.meta-label {
|
|
439
|
+
display: block;
|
|
440
|
+
font-size: 12px;
|
|
441
|
+
opacity: 0.8;
|
|
442
|
+
margin-bottom: 5px;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.meta-value {
|
|
446
|
+
display: block;
|
|
447
|
+
font-size: 14px;
|
|
448
|
+
font-weight: 600;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.summary {
|
|
452
|
+
padding: 30px;
|
|
453
|
+
background: #f9fafb;
|
|
454
|
+
border-bottom: 1px solid #e5e7eb;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.summary-header {
|
|
458
|
+
display: flex;
|
|
459
|
+
justify-content: space-between;
|
|
460
|
+
align-items: center;
|
|
461
|
+
margin-bottom: 30px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.status-badge {
|
|
465
|
+
display: inline-flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
gap: 10px;
|
|
468
|
+
padding: 12px 24px;
|
|
469
|
+
border-radius: 8px;
|
|
470
|
+
font-size: 20px;
|
|
471
|
+
font-weight: 600;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.status-passed { background: #d1fae5; color: #065f46; }
|
|
475
|
+
.status-warning { background: #fef3c7; color: #92400e; }
|
|
476
|
+
.status-failed { background: #fee2e2; color: #991b1b; }
|
|
477
|
+
|
|
478
|
+
.score-display {
|
|
479
|
+
text-align: center;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.score-value {
|
|
483
|
+
font-size: 48px;
|
|
484
|
+
font-weight: bold;
|
|
485
|
+
color: #667eea;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.score-grade {
|
|
489
|
+
font-size: 24px;
|
|
490
|
+
color: #666;
|
|
491
|
+
margin-top: -5px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.summary-stats {
|
|
495
|
+
display: grid;
|
|
496
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
497
|
+
gap: 20px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.stat-card {
|
|
501
|
+
background: white;
|
|
502
|
+
padding: 20px;
|
|
503
|
+
border-radius: 8px;
|
|
504
|
+
text-align: center;
|
|
505
|
+
border: 2px solid #e5e7eb;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.stat-card.stat-error { border-color: #fca5a5; }
|
|
509
|
+
.stat-card.stat-warning { border-color: #fcd34d; }
|
|
510
|
+
|
|
511
|
+
.stat-value {
|
|
512
|
+
font-size: 36px;
|
|
513
|
+
font-weight: bold;
|
|
514
|
+
color: #667eea;
|
|
515
|
+
margin-bottom: 5px;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.stat-card.stat-error .stat-value { color: #dc2626; }
|
|
519
|
+
.stat-card.stat-warning .stat-value { color: #f59e0b; }
|
|
520
|
+
|
|
521
|
+
.stat-label {
|
|
522
|
+
font-size: 14px;
|
|
523
|
+
color: #666;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.section {
|
|
527
|
+
padding: 30px;
|
|
528
|
+
border-bottom: 1px solid #e5e7eb;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.section h2 {
|
|
532
|
+
font-size: 22px;
|
|
533
|
+
margin-bottom: 20px;
|
|
534
|
+
color: #1f2937;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.controls {
|
|
538
|
+
display: flex;
|
|
539
|
+
gap: 15px;
|
|
540
|
+
margin-bottom: 20px;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.search-box, .filter-select {
|
|
544
|
+
padding: 10px 15px;
|
|
545
|
+
border: 2px solid #e5e7eb;
|
|
546
|
+
border-radius: 6px;
|
|
547
|
+
font-size: 14px;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.search-box {
|
|
551
|
+
flex: 1;
|
|
552
|
+
max-width: 400px;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.filter-select {
|
|
556
|
+
min-width: 150px;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.violations-table {
|
|
560
|
+
width: 100%;
|
|
561
|
+
border-collapse: collapse;
|
|
562
|
+
background: white;
|
|
563
|
+
border: 1px solid #e5e7eb;
|
|
564
|
+
border-radius: 8px;
|
|
565
|
+
overflow: hidden;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.violations-table th {
|
|
569
|
+
background: #f9fafb;
|
|
570
|
+
padding: 12px;
|
|
571
|
+
text-align: left;
|
|
572
|
+
font-weight: 600;
|
|
573
|
+
border-bottom: 2px solid #e5e7eb;
|
|
574
|
+
cursor: pointer;
|
|
575
|
+
user-select: none;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.violations-table th:hover { background: #f3f4f6; }
|
|
579
|
+
|
|
580
|
+
.sort-icon {
|
|
581
|
+
font-size: 10px;
|
|
582
|
+
opacity: 0.5;
|
|
583
|
+
margin-left: 5px;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.violations-table td {
|
|
587
|
+
padding: 12px;
|
|
588
|
+
border-bottom: 1px solid #e5e7eb;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.file-path, .rule-id {
|
|
592
|
+
font-family: 'Courier New', monospace;
|
|
593
|
+
font-size: 13px;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.count-cell {
|
|
597
|
+
text-align: center;
|
|
598
|
+
font-weight: 600;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.count-cell.has-errors { color: #dc2626; }
|
|
602
|
+
.count-cell.has-warnings { color: #f59e0b; }
|
|
603
|
+
|
|
604
|
+
.details-btn {
|
|
605
|
+
background: #667eea;
|
|
606
|
+
color: white;
|
|
607
|
+
border: none;
|
|
608
|
+
padding: 6px 12px;
|
|
609
|
+
border-radius: 4px;
|
|
610
|
+
cursor: pointer;
|
|
611
|
+
font-size: 13px;
|
|
612
|
+
display: inline-flex;
|
|
613
|
+
align-items: center;
|
|
614
|
+
gap: 5px;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.details-btn:hover { background: #5568d3; }
|
|
618
|
+
|
|
619
|
+
.arrow {
|
|
620
|
+
font-size: 10px;
|
|
621
|
+
transition: transform 0.2s;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.details-row td {
|
|
625
|
+
background: #f9fafb;
|
|
626
|
+
padding: 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.details-content {
|
|
630
|
+
padding: 20px;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.violation-list, .location-list {
|
|
634
|
+
list-style: none;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.violation-item, .location-item {
|
|
638
|
+
padding: 10px;
|
|
639
|
+
margin-bottom: 8px;
|
|
640
|
+
background: white;
|
|
641
|
+
border-left: 3px solid #e5e7eb;
|
|
642
|
+
border-radius: 4px;
|
|
643
|
+
display: flex;
|
|
644
|
+
gap: 10px;
|
|
645
|
+
align-items: center;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.severity-error { color: #dc2626; font-weight: 600; }
|
|
649
|
+
.severity-warning { color: #f59e0b; font-weight: 600; }
|
|
650
|
+
|
|
651
|
+
.violation-line {
|
|
652
|
+
background: #e5e7eb;
|
|
653
|
+
padding: 2px 8px;
|
|
654
|
+
border-radius: 3px;
|
|
655
|
+
font-size: 12px;
|
|
656
|
+
font-family: monospace;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.violation-rule {
|
|
660
|
+
color: #667eea;
|
|
661
|
+
font-weight: 600;
|
|
662
|
+
font-size: 12px;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.violation-message, .location-message {
|
|
666
|
+
flex: 1;
|
|
667
|
+
color: #666;
|
|
668
|
+
font-size: 14px;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.location-path {
|
|
672
|
+
font-family: 'Courier New', monospace;
|
|
673
|
+
background: #f3f4f6;
|
|
674
|
+
padding: 2px 8px;
|
|
675
|
+
border-radius: 3px;
|
|
676
|
+
font-size: 12px;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.no-violations {
|
|
680
|
+
text-align: center;
|
|
681
|
+
padding: 40px;
|
|
682
|
+
font-size: 18px;
|
|
683
|
+
color: #10b981;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.footer {
|
|
687
|
+
padding: 20px;
|
|
688
|
+
text-align: center;
|
|
689
|
+
color: #666;
|
|
690
|
+
font-size: 14px;
|
|
691
|
+
background: #f9fafb;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.footer a {
|
|
695
|
+
color: #667eea;
|
|
696
|
+
text-decoration: none;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.footer a:hover { text-decoration: underline; }
|
|
700
|
+
|
|
701
|
+
@media print {
|
|
702
|
+
body { background: white; padding: 0; }
|
|
703
|
+
.container { box-shadow: none; }
|
|
704
|
+
.details-btn { display: none; }
|
|
705
|
+
.details-row { display: table-row !important; }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
@media (max-width: 768px) {
|
|
709
|
+
.header-meta { grid-template-columns: 1fr; }
|
|
710
|
+
.summary-stats { grid-template-columns: 1fr; }
|
|
711
|
+
.controls { flex-direction: column; }
|
|
712
|
+
.search-box { max-width: 100%; }
|
|
713
|
+
}
|
|
714
|
+
</style>`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Embed JavaScript
|
|
719
|
+
* @returns {string} Script tag
|
|
720
|
+
*/
|
|
721
|
+
function embedJavaScript() {
|
|
722
|
+
return `<script>
|
|
723
|
+
// Toggle details row
|
|
724
|
+
function toggleDetails(id) {
|
|
725
|
+
const row = document.getElementById(id);
|
|
726
|
+
const btn = event.target.closest('.details-btn');
|
|
727
|
+
const arrow = btn.querySelector('.arrow');
|
|
728
|
+
|
|
729
|
+
if (row.style.display === 'none') {
|
|
730
|
+
row.style.display = 'table-row';
|
|
731
|
+
arrow.style.transform = 'rotate(180deg)';
|
|
732
|
+
btn.innerHTML = 'Hide Details <span class="arrow" style="transform: rotate(180deg);">▼</span>';
|
|
733
|
+
} else {
|
|
734
|
+
row.style.display = 'none';
|
|
735
|
+
arrow.style.transform = 'rotate(0deg)';
|
|
736
|
+
btn.innerHTML = 'Show Details <span class="arrow">▼</span>';
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Sort table
|
|
741
|
+
function sortTable(tableId, columnIndex) {
|
|
742
|
+
const table = document.getElementById(tableId);
|
|
743
|
+
const tbody = table.querySelector('tbody');
|
|
744
|
+
const rows = Array.from(tbody.querySelectorAll('tr')).filter(r => !r.classList.contains('details-row'));
|
|
745
|
+
|
|
746
|
+
const isNumeric = columnIndex > 0;
|
|
747
|
+
rows.sort((a, b) => {
|
|
748
|
+
const aVal = a.cells[columnIndex].textContent.trim();
|
|
749
|
+
const bVal = b.cells[columnIndex].textContent.trim();
|
|
750
|
+
|
|
751
|
+
if (isNumeric) {
|
|
752
|
+
return parseInt(bVal) - parseInt(aVal);
|
|
753
|
+
}
|
|
754
|
+
return aVal.localeCompare(bVal);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
rows.forEach(row => {
|
|
758
|
+
const detailsRow = row.nextElementSibling;
|
|
759
|
+
tbody.appendChild(row);
|
|
760
|
+
if (detailsRow && detailsRow.classList.contains('details-row')) {
|
|
761
|
+
tbody.appendChild(detailsRow);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// File search
|
|
767
|
+
const fileSearch = document.getElementById('fileSearch');
|
|
768
|
+
if (fileSearch) {
|
|
769
|
+
fileSearch.addEventListener('input', function() {
|
|
770
|
+
const searchTerm = this.value.toLowerCase();
|
|
771
|
+
const rows = document.querySelectorAll('#fileTable tbody tr.file-row');
|
|
772
|
+
|
|
773
|
+
rows.forEach(row => {
|
|
774
|
+
const file = row.querySelector('.file-path').textContent.toLowerCase();
|
|
775
|
+
const detailsRow = row.nextElementSibling;
|
|
776
|
+
|
|
777
|
+
if (file.includes(searchTerm)) {
|
|
778
|
+
row.style.display = '';
|
|
779
|
+
if (detailsRow && detailsRow.style.display !== 'none') {
|
|
780
|
+
detailsRow.style.display = '';
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
row.style.display = 'none';
|
|
784
|
+
if (detailsRow) {
|
|
785
|
+
detailsRow.style.display = 'none';
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// File filter
|
|
793
|
+
const fileFilter = document.getElementById('fileFilter');
|
|
794
|
+
if (fileFilter) {
|
|
795
|
+
fileFilter.addEventListener('change', function() {
|
|
796
|
+
const filter = this.value;
|
|
797
|
+
const rows = document.querySelectorAll('#fileTable tbody tr.file-row');
|
|
798
|
+
|
|
799
|
+
rows.forEach(row => {
|
|
800
|
+
const errors = parseInt(row.dataset.errors);
|
|
801
|
+
const warnings = parseInt(row.dataset.warnings);
|
|
802
|
+
const detailsRow = row.nextElementSibling;
|
|
803
|
+
|
|
804
|
+
let show = true;
|
|
805
|
+
if (filter === 'error' && errors === 0) show = false;
|
|
806
|
+
if (filter === 'warning' && warnings === 0) show = false;
|
|
807
|
+
|
|
808
|
+
if (show) {
|
|
809
|
+
row.style.display = '';
|
|
810
|
+
} else {
|
|
811
|
+
row.style.display = 'none';
|
|
812
|
+
if (detailsRow) {
|
|
813
|
+
detailsRow.style.display = 'none';
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
</script>`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Escape HTML special characters
|
|
824
|
+
* @param {string} text - Text to escape
|
|
825
|
+
* @returns {string} Escaped text
|
|
826
|
+
*/
|
|
827
|
+
function escapeHTML(text) {
|
|
828
|
+
if (!text) return '';
|
|
829
|
+
return String(text)
|
|
830
|
+
.replace(/&/g, '&')
|
|
831
|
+
.replace(/</g, '<')
|
|
832
|
+
.replace(/>/g, '>')
|
|
833
|
+
.replace(/"/g, '"')
|
|
834
|
+
.replace(/'/g, ''');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
module.exports = {
|
|
838
|
+
generateHTMLReport
|
|
839
|
+
};
|