Skip to content

Commit 9e4ab69

Browse files
authored
Refactor version retrieval and severity normalization
Refactor severity handling and version retrieval in reporting.
1 parent aeef0e2 commit 9e4ab69

1 file changed

Lines changed: 73 additions & 57 deletions

File tree

src/pyspector/reporting.py

Lines changed: 73 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import json
22
import html as html_module
3+
import importlib.metadata
4+
35
from sarif_om import (
46
SarifLog,
57
Tool,
68
ToolComponent,
79
Run,
810
ReportingDescriptor,
11+
ReportingConfiguration,
912
MultiformatMessageString,
1013
Result,
1114
ArtifactLocation,
@@ -15,31 +18,51 @@
1518
Message,
1619
)
1720

21+
1822
# Maps internal severity levels to SARIF-compliant level strings.
1923
_SEVERITY_TO_SARIF_LEVEL = {
2024
"CRITICAL": "error",
21-
"HIGH": "error",
22-
"MEDIUM": "warning",
23-
"LOW": "note",
25+
"HIGH": "error",
26+
"MEDIUM": "warning",
27+
"LOW": "note",
2428
}
2529

26-
_PYSPECTOR_VERSION = "1.0.0"
30+
31+
def _get_version():
32+
"""Return installed PySpector version dynamically."""
33+
try:
34+
return importlib.metadata.version("pyspector")
35+
except importlib.metadata.PackageNotFoundError:
36+
return "dev"
37+
38+
39+
_PYSPECTOR_VERSION = _get_version()
40+
41+
42+
def _severity_key(issue) -> str:
43+
"""Normalize enum-like severity values."""
44+
return str(issue.severity).split(".")[-1].upper()
45+
2746

2847
def _clean(obj):
29-
"""
30-
Recursively serialize a sarif_om object to a plain dict,
31-
dropping any key whose value is None so the output stays lean.
32-
sarif_om objects expose their data via __dict__; we walk that
33-
structure and strip falsy-None leaves.
34-
"""
48+
3549
if isinstance(obj, list):
3650
return [_clean(item) for item in obj]
51+
52+
if isinstance(obj, dict):
53+
return {
54+
k: _clean(v)
55+
for k, v in obj.items()
56+
if v is not None
57+
}
58+
3759
if hasattr(obj, "__dict__"):
3860
return {
3961
k: _clean(v)
4062
for k, v in obj.__dict__.items()
4163
if v is not None
4264
}
65+
4366
return obj
4467

4568

@@ -57,9 +80,6 @@ def generate(self) -> str:
5780
return self.to_html()
5881
return self.to_console()
5982

60-
# ------------------------------------------------------------------ #
61-
# Console #
62-
# ------------------------------------------------------------------ #
6383

6484
def to_console(self) -> str:
6585
if not self.issues:
@@ -70,7 +90,7 @@ def to_console(self) -> str:
7090

7191
issues_by_severity: dict[str, list] = {}
7292
for issue in self.issues:
73-
severity = str(issue.severity).split(".")[-1].upper()
93+
severity = _severity_key(issue)
7494
issues_by_severity.setdefault(severity, []).append(issue)
7595

7696
for severity in severity_order:
@@ -81,6 +101,7 @@ def to_console(self) -> str:
81101
issues_by_severity[severity],
82102
key=lambda i: (i.file_path, i.line_number),
83103
)
104+
84105
output.append(f"\n{'='*60}")
85106
output.append(
86107
f" {severity} ({len(sorted_issues)} issue{'s' if len(sorted_issues) != 1 else ''})"
@@ -98,7 +119,7 @@ def to_console(self) -> str:
98119
return "\n".join(output)
99120

100121
# ------------------------------------------------------------------ #
101-
# JSON #
122+
# JSON #
102123
# ------------------------------------------------------------------ #
103124

104125
def to_json(self) -> str:
@@ -111,55 +132,37 @@ def to_json(self) -> str:
111132
"file_path": issue.file_path,
112133
"line_number": issue.line_number,
113134
"code": issue.code,
114-
"severity": str(issue.severity).split(".")[-1],
135+
"severity": _severity_key(issue),
115136
"remediation": issue.remediation,
116137
}
117138
for issue in self.issues
118139
],
119140
}
141+
120142
return json.dumps(report, indent=2)
121143

122144
# ------------------------------------------------------------------ #
123-
# SARIF #
145+
# SARIF #
124146
# ------------------------------------------------------------------ #
125147

126148
def to_sarif(self) -> str:
127-
"""
128-
Produces a SARIF 2.1.0 document.
129-
130-
Improvements over the previous implementation:
131-
- Uses ToolComponent (correct type for Tool.driver).
132-
- Builds a deduplicated, ordered rule list and references rules by
133-
index in each Result (rule_index), which is required for tooling
134-
that doesn't index rules by ID alone.
135-
- Maps internal severity levels to the SARIF `level` field
136-
(error / warning / note) so consumers can filter by severity
137-
without understanding PySpector-specific values.
138-
- Surfaces remediation guidance in rule.help so it appears in
139-
IDEs and dashboards that consume SARIF.
140-
- Uses proper Message / MultiformatMessageString objects instead
141-
of raw dicts.
142-
- Serialises via a custom _clean() helper that drops None-valued
143-
keys, keeping the output compact and spec-compliant.
144-
"""
145149

146-
# ── 1. Build an ordered, deduplicated rule list ──────────────────
147150
rule_index_map: dict[str, int] = {}
148151
rules: list[ReportingDescriptor] = []
149152

150153
for issue in self.issues:
154+
151155
if issue.rule_id in rule_index_map:
152156
continue
153157

154-
severity_key = str(issue.severity).split(".")[-1].upper()
158+
severity_key = _severity_key(issue)
155159

156160
rule = ReportingDescriptor(
157161
id=issue.rule_id,
158-
name=issue.rule_id, # human-friendly CamelCase id is conventional
162+
name=issue.rule_id,
159163
short_description=MultiformatMessageString(
160164
text=issue.description
161165
),
162-
# help surfaces remediation in GitHub Advanced Security, VS Code, etc.
163166
help=MultiformatMessageString(
164167
text=issue.remediation or issue.description,
165168
markdown=(
@@ -168,40 +171,47 @@ def to_sarif(self) -> str:
168171
else None
169172
),
170173
),
171-
# default_configuration carries the base severity level for the rule
172-
default_configuration={"level": _SEVERITY_TO_SARIF_LEVEL.get(severity_key, "warning")},
174+
default_configuration=ReportingConfiguration(
175+
level=_SEVERITY_TO_SARIF_LEVEL.get(
176+
severity_key,
177+
"warning",
178+
)
179+
),
173180
)
174181

175182
rule_index_map[issue.rule_id] = len(rules)
176183
rules.append(rule)
177184

178-
# ── 2. Assemble the Tool ─────────────────────────────────────────
179185
driver = ToolComponent(
180186
name="PySpector",
181187
version=_PYSPECTOR_VERSION,
182188
information_uri="https://github.com/your-org/pyspector",
183189
rules=rules,
184190
)
191+
185192
tool = Tool(driver=driver)
186193

187-
# ── 3. Build Results ─────────────────────────────────────────────
188194
results: list[Result] = []
189195

190196
for issue in self.issues:
191-
severity_key = str(issue.severity).split(".")[-1].upper()
192-
level = _SEVERITY_TO_SARIF_LEVEL.get(severity_key, "warning")
197+
198+
severity_key = _severity_key(issue)
199+
level = _SEVERITY_TO_SARIF_LEVEL.get(
200+
severity_key,
201+
"warning",
202+
)
193203

194204
region = Region(
195205
start_line=issue.line_number,
196-
# Snippet lets viewers show the offending code inline
197-
snippet=MultiformatMessageString(text=issue.code.strip()),
206+
snippet=MultiformatMessageString(
207+
text=issue.code.strip()
208+
),
198209
)
199210

200211
location = Location(
201212
physical_location=PhysicalLocation(
202213
artifact_location=ArtifactLocation(
203214
uri=issue.file_path,
204-
# uri_base_id makes paths relative to the repo root,
205215
uri_base_id="%SRCROOT%",
206216
),
207217
region=region,
@@ -218,20 +228,23 @@ def to_sarif(self) -> str:
218228

219229
results.append(result)
220230

221-
# ── 4. Compose the log ───────────────────────────────────────────
222231
run = Run(tool=tool, results=results)
232+
223233
log = SarifLog(
224234
version="2.1.0",
225235
schema_uri=(
226-
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/"
227-
"master/Schemata/sarif-schema-2.1.0.json"
236+
"https://raw.githubusercontent.com/oasis-tcs/"
237+
"sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
228238
),
229239
runs=[run],
230240
)
231241

232-
# ── 5. Serialise, stripping None values ──────────────────────────
233242
return json.dumps(_clean(log), indent=2)
234243

244+
# ------------------------------------------------------------------ #
245+
# HTML #
246+
# ------------------------------------------------------------------ #
247+
235248
def to_html(self) -> str:
236249
html = f"""
237250
<html>
@@ -241,13 +254,14 @@ def to_html(self) -> str:
241254
<h2>Found {len(self.issues)} issues.</h2>
242255
<table border='1' style='border-collapse: collapse; width: 100%;'>
243256
<tr style='background-color: #f2f2f2;'>
244-
<th style='padding: 8px; text-align: left;'>File</th>
245-
<th style='padding: 8px; text-align: left;'>Line</th>
246-
<th style='padding: 8px; text-align: left;'>Severity</th>
247-
<th style='padding: 8px; text-align: left;'>Description</th>
248-
<th style='padding: 8px; text-align: left;'>Code</th>
257+
<th style='padding: 8px;'>File</th>
258+
<th style='padding: 8px;'>Line</th>
259+
<th style='padding: 8px;'>Severity</th>
260+
<th style='padding: 8px;'>Description</th>
261+
<th style='padding: 8px;'>Code</th>
249262
</tr>
250263
"""
264+
251265
for issue in self.issues:
252266
html += f"""
253267
<tr>
@@ -258,5 +272,7 @@ def to_html(self) -> str:
258272
<td style='padding: 8px;'><pre><code>{html_module.escape(issue.code)}</code></pre></td>
259273
</tr>
260274
"""
275+
261276
html += "</table></body></html>"
277+
262278
return html

0 commit comments

Comments
 (0)