Skip to content

Commit 922ac2b

Browse files
committed
Improve developer guide lint reporting
1 parent d39e80b commit 922ac2b

File tree

3 files changed

+207
-10
lines changed

3 files changed

+207
-10
lines changed

.github/workflows/developer-guide-docs.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,15 @@ jobs:
188188
set -euo pipefail
189189
REPORT_DIR="build/developer-guide/reports"
190190
REPORT_FILE="${REPORT_DIR}/vale-report.json"
191+
HTML_REPORT="${REPORT_DIR}/vale-report.html"
191192
mkdir -p "$REPORT_DIR"
192193
set +e
193194
vale --config docs/developer-guide/.vale.ini --output=JSON docs/developer-guide > "$REPORT_FILE"
194195
STATUS=$?
195196
set -e
197+
python3 scripts/developer-guide/vale_report_to_html.py --input "$REPORT_FILE" --output "$HTML_REPORT"
196198
echo "VALE_REPORT=$REPORT_FILE" >> "$GITHUB_ENV"
199+
echo "VALE_HTML_REPORT=$HTML_REPORT" >> "$GITHUB_ENV"
197200
echo "VALE_STATUS=$STATUS" >> "$GITHUB_ENV"
198201
if [ "$STATUS" -ne 0 ]; then
199202
echo "Vale exited with status $STATUS" >&2
@@ -260,7 +263,9 @@ jobs:
260263
uses: actions/upload-artifact@v4
261264
with:
262265
name: developer-guide-vale-report
263-
path: ${{ env.VALE_REPORT }}
266+
path: |
267+
${{ env.VALE_REPORT }}
268+
${{ env.VALE_HTML_REPORT }}
264269
if-no-files-found: warn
265270

266271
- name: Upload unused image report

scripts/developer-guide/summarize_reports.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,30 @@ def write_output(lines: Iterable[str], output: Path | None) -> None:
2020
print(content, end="")
2121

2222

23+
def _normalize_status(status: str) -> str:
24+
return status.strip()
25+
26+
27+
def _has_nonzero_status(status: str) -> bool:
28+
status = _normalize_status(status)
29+
return bool(status and status != "0")
30+
31+
2332
def summarize_asciidoc(report: Path, status: str, summary_key: str, output: Path | None) -> None:
2433
text = ""
2534
if report.is_file():
2635
text = report.read_text(encoding="utf-8", errors="ignore")
2736
issues = re.findall(r"\b(?:WARN|ERROR|SEVERE)\b", text)
28-
summary = f"{len(issues)} issue(s) flagged" if issues else "No issues found"
29-
status = status.strip()
30-
if status and status != "0":
31-
summary += f" (exit code {status})"
37+
38+
if issues:
39+
summary = f"{len(issues)} issue(s) flagged"
40+
if _has_nonzero_status(status):
41+
summary += f" (exit code {_normalize_status(status)})"
42+
elif _has_nonzero_status(status):
43+
summary = f"Linter failed (exit code {_normalize_status(status)})"
44+
else:
45+
summary = "No issues found"
46+
3247
write_output([f"{summary_key}={summary}"], output)
3348

3449

@@ -41,7 +56,17 @@ def summarize_vale(
4156
data = json.loads(report.read_text(encoding="utf-8"))
4257
except json.JSONDecodeError:
4358
data = {}
44-
alerts = data.get("alerts", []) if isinstance(data, dict) else []
59+
if isinstance(data, dict):
60+
if isinstance(data.get("alerts"), list):
61+
alerts.extend(data["alerts"])
62+
files = data.get("files")
63+
if isinstance(files, dict):
64+
for file_result in files.values():
65+
if not isinstance(file_result, dict):
66+
continue
67+
file_alerts = file_result.get("alerts")
68+
if isinstance(file_alerts, list):
69+
alerts.extend(file_alerts)
4570

4671
counts = {"error": 0, "warning": 0, "suggestion": 0}
4772
total = 0
@@ -60,13 +85,13 @@ def summarize_vale(
6085
f"{counts['suggestion']} suggestions",
6186
]
6287
summary = f"{total} alert(s) ({', '.join(parts)})"
88+
if _has_nonzero_status(status):
89+
summary += f" (exit code {_normalize_status(status)})"
90+
elif _has_nonzero_status(status):
91+
summary = f"Vale failed (exit code {_normalize_status(status)})"
6392
else:
6493
summary = "No alerts found"
6594

66-
status = status.strip()
67-
if status and status != "0":
68-
summary += f" (exit code {status})"
69-
7095
write_output([f"{summary_key}={summary}"], output)
7196

7297

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
"""Convert a Vale JSON report into a standalone HTML file."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import html
8+
import json
9+
from pathlib import Path
10+
from typing import Iterable
11+
12+
13+
def parse_args() -> argparse.Namespace:
14+
parser = argparse.ArgumentParser(description=__doc__)
15+
parser.add_argument("--input", type=Path, required=True, help="Path to the Vale JSON report.")
16+
parser.add_argument("--output", type=Path, required=True, help="Destination path for the HTML report.")
17+
return parser.parse_args()
18+
19+
20+
def load_alerts(report: Path) -> list[dict[str, object]]:
21+
if not report.is_file():
22+
return []
23+
try:
24+
data = json.loads(report.read_text(encoding="utf-8"))
25+
except json.JSONDecodeError:
26+
return []
27+
28+
alerts: list[dict[str, object]] = []
29+
if isinstance(data, dict):
30+
if isinstance(data.get("alerts"), list):
31+
alerts.extend(data["alerts"])
32+
files = data.get("files")
33+
if isinstance(files, dict):
34+
for file_result in files.values():
35+
if not isinstance(file_result, dict):
36+
continue
37+
file_alerts = file_result.get("alerts")
38+
if isinstance(file_alerts, list):
39+
alerts.extend(file_alerts)
40+
return alerts
41+
42+
43+
def render_alert_rows(alerts: Iterable[dict[str, object]]) -> str:
44+
normalized: list[dict[str, str]] = []
45+
for alert in alerts:
46+
if not isinstance(alert, dict):
47+
continue
48+
span = alert.get("Span")
49+
line = column = ""
50+
if isinstance(span, dict):
51+
start = span.get("Start")
52+
if isinstance(start, dict):
53+
line = str(start.get("Line", ""))
54+
column = str(start.get("Column", ""))
55+
elif isinstance(span, list) and span:
56+
line = str(span[0])
57+
if len(span) > 1:
58+
column = str(span[1])
59+
normalized.append(
60+
{
61+
"file": str(alert.get("Path", "")),
62+
"line": line,
63+
"column": column,
64+
"severity": str(alert.get("Severity", "")),
65+
"rule": str(alert.get("Check", "")),
66+
"message": str(alert.get("Message", "")),
67+
}
68+
)
69+
70+
if not normalized:
71+
return "<tr><td colspan='6'>No alerts found.</td></tr>"
72+
73+
def sort_key(entry: dict[str, str]) -> tuple:
74+
def as_int(value: str) -> int:
75+
try:
76+
return int(value)
77+
except (TypeError, ValueError):
78+
return 0
79+
80+
return (
81+
entry["file"],
82+
as_int(entry["line"]),
83+
as_int(entry["column"]),
84+
entry["rule"],
85+
)
86+
87+
normalized.sort(key=sort_key)
88+
89+
rows: list[str] = []
90+
for entry in normalized:
91+
severity_value = entry["severity"]
92+
severity = html.escape(severity_value)
93+
severity_class = f"severity-{severity_value.lower()}" if severity_value else ""
94+
rows.append(
95+
"<tr>"
96+
f"<td>{html.escape(entry['file'])}</td>"
97+
f"<td>{html.escape(entry['line'])}</td>"
98+
f"<td>{html.escape(entry['column'])}</td>"
99+
f"<td class='{severity_class}'>{severity}</td>"
100+
f"<td>{html.escape(entry['rule'])}</td>"
101+
f"<td>{html.escape(entry['message'])}</td>"
102+
"</tr>"
103+
)
104+
105+
return "\n".join(rows)
106+
107+
108+
def main() -> None:
109+
args = parse_args()
110+
alerts = load_alerts(args.input)
111+
counts = {"error": 0, "warning": 0, "suggestion": 0}
112+
for alert in alerts:
113+
if not isinstance(alert, dict):
114+
continue
115+
severity = str(alert.get("Severity", "")).lower()
116+
if severity in counts:
117+
counts[severity] += 1
118+
args.output.parent.mkdir(parents=True, exist_ok=True)
119+
table_rows = render_alert_rows(alerts)
120+
html_content = f"""
121+
<!DOCTYPE html>
122+
<html lang="en">
123+
<head>
124+
<meta charset="utf-8" />
125+
<title>Vale Report</title>
126+
<style>
127+
body {{ font-family: Arial, sans-serif; margin: 2rem; }}
128+
table {{ border-collapse: collapse; width: 100%; }}
129+
th, td {{ border: 1px solid #ccc; padding: 0.5rem; text-align: left; }}
130+
th {{ background-color: #f0f0f0; }}
131+
tbody tr:nth-child(even) {{ background-color: #fafafa; }}
132+
.severity-error {{ color: #c62828; font-weight: bold; }}
133+
.severity-warning {{ color: #ef6c00; font-weight: bold; }}
134+
.severity-suggestion {{ color: #1565c0; font-weight: bold; }}
135+
</style>
136+
</head>
137+
<body>
138+
<h1>Vale Report</h1>
139+
<p>Total alerts: {len(alerts)}</p>
140+
<ul>
141+
<li>Errors: {counts['error']}</li>
142+
<li>Warnings: {counts['warning']}</li>
143+
<li>Suggestions: {counts['suggestion']}</li>
144+
</ul>
145+
<table>
146+
<thead>
147+
<tr>
148+
<th>File</th>
149+
<th>Line</th>
150+
<th>Column</th>
151+
<th>Severity</th>
152+
<th>Rule</th>
153+
<th>Message</th>
154+
</tr>
155+
</thead>
156+
<tbody>
157+
{table_rows}
158+
</tbody>
159+
</table>
160+
</body>
161+
</html>
162+
"""
163+
args.output.write_text(html_content, encoding="utf-8")
164+
165+
166+
if __name__ == "__main__":
167+
main()

0 commit comments

Comments
 (0)