| 75 | |
| 76 | |
| 77 | def render_alert_rows(alerts: Iterable[dict[str, object]]) -> str: |
| 78 | normalized: list[dict[str, str]] = [] |
| 79 | for alert in alerts: |
| 80 | if not isinstance(alert, dict): |
| 81 | continue |
| 82 | span = alert.get("Span") |
| 83 | line = column = "" |
| 84 | if isinstance(span, dict): |
| 85 | start = span.get("Start") |
| 86 | if isinstance(start, dict): |
| 87 | line = str(start.get("Line", "")) |
| 88 | column = str(start.get("Column", "")) |
| 89 | elif isinstance(span, list) and span: |
| 90 | line = str(span[0]) |
| 91 | if len(span) > 1: |
| 92 | column = str(span[1]) |
| 93 | normalized.append( |
| 94 | { |
| 95 | "file": str(alert.get("Path", "")), |
| 96 | "line": line, |
| 97 | "column": column, |
| 98 | "severity": str(alert.get("Severity", "")), |
| 99 | "rule": str(alert.get("Check", "")), |
| 100 | "message": str(alert.get("Message", "")), |
| 101 | } |
| 102 | ) |
| 103 | |
| 104 | if not normalized: |
| 105 | return "<tr><td colspan='6'>No alerts found.</td></tr>" |
| 106 | |
| 107 | def sort_key(entry: dict[str, str]) -> tuple: |
| 108 | def as_int(value: str) -> int: |
| 109 | try: |
| 110 | return int(value) |
| 111 | except (TypeError, ValueError): |
| 112 | return 0 |
| 113 | |
| 114 | return ( |
| 115 | entry["file"], |
| 116 | as_int(entry["line"]), |
| 117 | as_int(entry["column"]), |
| 118 | entry["rule"], |
| 119 | ) |
| 120 | |
| 121 | normalized.sort(key=sort_key) |
| 122 | |
| 123 | rows: list[str] = [] |
| 124 | for entry in normalized: |
| 125 | severity_value = entry["severity"] |
| 126 | severity = html.escape(severity_value) |
| 127 | severity_class = f"severity-{severity_value.lower()}" if severity_value else "" |
| 128 | rows.append( |
| 129 | "<tr>" |
| 130 | f"<td>{html.escape(entry['file'])}</td>" |
| 131 | f"<td>{html.escape(entry['line'])}</td>" |
| 132 | f"<td>{html.escape(entry['column'])}</td>" |
| 133 | f"<td class='{severity_class}'>{severity}</td>" |
| 134 | f"<td>{html.escape(entry['rule'])}</td>" |