Skip to content

Commit 1af43fa

Browse files
authored
Add helpUri to HTML report (#76) (#88)
1 parent 5b12d29 commit 1af43fa

File tree

6 files changed

+111
-36
lines changed

6 files changed

+111
-36
lines changed

CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Pull requests are welcome.
1919

2020
1. Fork the repository.
2121
2. Make and test your changes (see Developer Guide below).
22-
3. Run `poetry run black sarif` to format the code.
22+
3. Run `poetry run ruff format` and `poetry run black sarif` to format the code.
2323
4. Run `poetry run pylint sarif` and check for no new errors or warnings.
2424
5. Raise Pull Request in GitHub.com.
2525

@@ -54,8 +54,8 @@ To check that the right versions are being run:
5454

5555
```bash
5656
poetry run python --version
57-
poetry run sarif --version
58-
poetry run python -m sarif --version
57+
poetry run sarif --version --debug
58+
poetry run python -m sarif --version --debug
5959
```
6060

6161
To see which executable is being run:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ The data in each `result` object can then be used for filtering via the `--filte
676676

677677
```yaml
678678
# Lines beginning with # are interpreted as comments and ignored.
679-
# Optional description for the filter. If no title is specified, the filter file name is used.
679+
# Optional description for the filter. If not specified, the filter file name is used.
680680
description: Example filter from README.md
681681

682682
# Optional configuration section to override default values.

sarif/operations/html_op.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99

1010
from jinja2 import Environment, FileSystemLoader, select_autoescape
1111

12-
from sarif import charts
13-
from sarif import sarif_file
12+
from sarif import charts, sarif_file
1413

1514
_THIS_MODULE_PATH = os.path.dirname(__file__)
1615

@@ -90,7 +89,7 @@ def _generate_single_html(
9089
total_distinct_issue_codes += distinct_issue_codes
9190

9291
severity_details = _enrich_details(
93-
report.get_issues_grouped_by_type_for_severity(severity)
92+
report.get_issues_grouped_by_type_for_severity(severity), input_file
9493
)
9594

9695
severity_section = {
@@ -129,8 +128,30 @@ def _generate_single_html(
129128
file_out.write(html_content)
130129

131130

132-
def _enrich_details(records_of_severity):
133-
return [
134-
{"code": key, "count": len(records), "details": records}
135-
for (key, records) in records_of_severity.items()
136-
]
131+
def _extract_help_links_from_rules(rules, link_to_desc, key):
132+
for rule in rules:
133+
if "helpUri" in rule:
134+
uri = rule["helpUri"]
135+
if uri not in link_to_desc:
136+
desc = rule.get("fullDescription", {}).get("text")
137+
if not desc:
138+
desc = rule.get("name")
139+
if not desc:
140+
desc = key
141+
link_to_desc[uri] = desc
142+
143+
144+
def _enrich_details(records_of_severity, input_file):
145+
ret = []
146+
147+
for key, records in records_of_severity.items():
148+
link_to_desc = {}
149+
for record in records:
150+
rule_id = record["Code"]
151+
rules = input_file.get_rules_by_id(rule_id)
152+
_extract_help_links_from_rules(rules, link_to_desc, key)
153+
links = [(desc, uri) for (uri, desc) in link_to_desc.items()]
154+
ret.append(
155+
{"code": key, "count": len(records), "links": links, "details": records}
156+
)
157+
return ret

sarif/operations/templates/sarif_summary.html

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,24 +101,27 @@ <h4>Total number of distinct issues of all severities ({{ severities }}): <b>{{
101101
{% for problem in problems %}
102102
<h3>Severity : {{ problem.type }} [ {{ problem.count }} ]</h3>
103103
<ul>
104-
{% for error in problem.details -%}
104+
{%- for error in problem.details %}
105105
<li>
106106
<button class="collapsible">{{- error.code }}: <b>{{ error.count -}}</b></button>
107107
<div class="content">
108108
<ul>
109-
{%- for line in error.details -%}
110-
{%- if line.Location %}
111-
<li>{{ line.Location }}:{{ line.Line }}</li>
112-
{%- else %}
113-
<li>{{ line.Description }}</li>
114-
{%- endif %}
115-
{%- endfor %}
109+
{%- for link in error.links %}
110+
<li><a href="{{ link.1 }}" target="_blank">{{ link.0 }}</a></li>
111+
{%- endfor %}
112+
{%- for line in error.details %}
113+
{%- if line.Location %}
114+
<li>{{ line.Location }}:{{ line.Line }}</li>
115+
{%- else %}
116+
<li>{{ line.Description }}</li>
117+
{%- endif %}
118+
{%- endfor %}
116119
</ul>
117120
</div>
118121
</li>
119-
{% endfor %}
122+
{%- endfor %}
120123
</ul>
121-
{% endfor -%}
124+
{%- endfor %}
122125

123126
<script>
124127
var coll = document.getElementsByClassName("collapsible");

sarif/sarif_file.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,19 @@ def get_conversion_tool_name(self) -> Optional[str]:
193193
)
194194
return None
195195

196+
def get_rules_by_id(self, rule_id: str) -> List[Dict]:
197+
"""
198+
Get the sarif rule for the given ID, if it exists in this run.
199+
"""
200+
ret = []
201+
rule_id = rule_id.strip()
202+
if not rule_id:
203+
return ret
204+
for rule in self.run_data.get("tool", {}).get("driver", {}).get("rules", []):
205+
if rule.get("id", "") == rule_id:
206+
ret.append(rule)
207+
return ret
208+
196209
def get_results(self) -> List[Dict]:
197210
"""
198211
Get the results from this run. These are the Result objects as defined in the SARIF
@@ -443,6 +456,15 @@ def get_distinct_tool_names(self):
443456
"""
444457
return sorted(list(set(run.get_tool_name() for run in self.runs)))
445458

459+
def get_rules_by_id(self, rule_id: str) -> List[Dict]:
460+
"""
461+
Get the sarif rule(s) for the given ID.
462+
"""
463+
ret = []
464+
for run in self.runs:
465+
ret.extend(run.get_rules_by_id(rule_id))
466+
return ret
467+
446468
def get_results(self) -> List[Dict]:
447469
"""
448470
Get the results from all runs in this file. These are the Result objects as defined in the
@@ -625,6 +647,17 @@ def get_distinct_tool_names(self) -> List[str]:
625647

626648
return sorted(list(all_tool_names))
627649

650+
def get_rules_by_id(self, rule_id: str) -> List[Dict]:
651+
"""
652+
Get the sarif rule(s) for the given ID.
653+
"""
654+
ret = []
655+
for subdir in self.subdirs:
656+
ret.extend(subdir.get_rules_by_id(rule_id))
657+
for input_file in self.files:
658+
ret.extend(input_file.get_rules_by_id(rule_id))
659+
return ret
660+
628661
def get_results(self) -> List[Dict]:
629662
"""
630663
Get the results from all runs in all files. These are the Result objects as defined in the

tests/ops/html/test_html.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,18 @@
1010
"version": "2.1.0",
1111
"runs": [
1212
{
13-
"tool": {"driver": {"name": "unit test"}},
13+
"tool": {
14+
"driver": {
15+
"name": "unit test",
16+
"rules": [
17+
{
18+
"id": "CA2101",
19+
"name": "Specify <marshalling> for P/Invoke string arguments",
20+
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2101",
21+
}
22+
],
23+
}
24+
},
1425
"results": [
1526
{
1627
"ruleId": "CA2101",
@@ -33,7 +44,8 @@
3344
}
3445

3546

36-
EXPECTED_OUTPUT_TXT = """<head>
47+
EXPECTED_OUTPUT_TXT = """
48+
<head>
3749
<style>
3850
#pageContainer {
3951
margin: auto;
@@ -110,37 +122,32 @@
110122
</style>
111123
</head>
112124
113-
114-
115125
<h3>Sarif Summary: <b>unit test</b></h3>
116126
<h4>Document generated on: <b><date_val></b></h4>
117127
<h4>Total number of distinct issues of all severities (error, warning, note): <b>1</b></h4>
118128
119-
120-
121-
122-
123129
<h3>Severity : error [ 1 ]</h3>
124130
<ul>
125131
<li>
126132
<button class="collapsible">CA2101: <b>1</b></button>
127133
<div class="content">
128134
<ul>
129-
<li>file:///C:/Code/main.c:24</li>
135+
<li><a href="https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2101" target="_blank">Specify &lt;marshalling&gt; for P/Invoke string arguments</a></li>
136+
<li>file:///C:/Code/main.c:24</li>
130137
</ul>
131138
</div>
132139
</li>
133-
140+
134141
</ul>
135142
136143
<h3>Severity : warning [ 0 ]</h3>
137144
<ul>
138-
145+
139146
</ul>
140147
141148
<h3>Severity : note [ 0 ]</h3>
142149
<ul>
143-
150+
144151
</ul>
145152
<script>
146153
var coll = document.getElementsByClassName("collapsible");
@@ -185,6 +192,17 @@ def test_html():
185192
pie_chart_end = output.find("/>", pie_chart_start) + 2
186193
output = output[:pie_chart_start] + output[pie_chart_end:]
187194

188-
assert output == EXPECTED_OUTPUT_TXT.replace("\n", os.linesep).replace(
195+
# Check the output line-by-line, ignoring whitespace around and between lines.
196+
output_split = output.splitlines()
197+
for check_line in EXPECTED_OUTPUT_TXT.replace(
189198
"<date_val>", mtime.strftime("%Y-%m-%d %H:%M:%S.%f")
190-
)
199+
).splitlines():
200+
expected = check_line.strip()
201+
if not expected:
202+
continue
203+
actual = ""
204+
while output_split:
205+
actual = output_split.pop(0).strip()
206+
if actual:
207+
break
208+
assert actual == expected

0 commit comments

Comments
 (0)