blob: 7b428b76bfd7e894248c1ffbaf9f61ae883b31f3 [file] [log] [blame]
Fathi Boudra422bf772019-12-02 11:10:16 +02001#!/usr/bin/env python3
2#
Zelalemc9531f82020-08-04 15:37:08 -05003# Copyright (c) 2019-2020, Arm Limited. All rights reserved.
Fathi Boudra422bf772019-12-02 11:10:16 +02004#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8import argparse
Zelalemc9531f82020-08-04 15:37:08 -05009import collections
Fathi Boudra422bf772019-12-02 11:10:16 +020010import json
11import re
12import shutil
13import sys
14
15
16_rule_exclusions = [
Zelalem219df412020-05-17 19:21:20 -050017 "MISRA C-2012 Rule 2.4",
18 "MISRA C-2012 Rule 2.5",
19 "MISRA C-2012 Rule 2.7",
20 "MISRA C-2012 Rule 5.1",
21 "MISRA C-2012 Rule 5.8",
Fathi Boudra422bf772019-12-02 11:10:16 +020022 "MISRA C-2012 Rule 8.6",
Zelalem219df412020-05-17 19:21:20 -050023 "MISRA C-2012 Rule 8.7",
24 "MISRA C-2012 Rule 11.4",
25 "MISRA C-2012 Rule 11.5",
26 "MISRA C-2012 Rule 15.1",
27 "MISRA C-2012 Rule 15.5",
28 "MISRA C-2012 Rule 15.6",
29 "MISRA C-2012 Rule 16.1",
30 "MISRA C-2012 Rule 16.3",
31 "MISRA C-2012 Rule 17.1",
32 "MISRA C-2012 Rule 21.6",
33 "MISRA C-2012 Directive 4.6",
34 "MISRA C-2012 Directive 4.8",
35 "MISRA C-2012 Directive 4.9"
Fathi Boudra422bf772019-12-02 11:10:16 +020036]
37
38# The following classification of rules and directives include 'MISRA C:2012
39# Amendment 1'
40
41# Directives
42_dir_required = set(["1.1", "2.1", "3.1", "4.1", "4.3", "4.7", "4.10", "4.11",
43 "4.12", "4.14"])
44
45_dir_advisory = set(["4.2", "4.4", "4.5", "4.6", "4.8", "4.9", "4.13"])
46
47# Rules
48_rule_mandatory = set(["9.1", "9.2", "9.3", "12.5", "13.6", "17.3", "17.4",
49 "17.6", "19.1", "21.13", "21.17", "21.18", "21.19", "21.20", "22.2", "22.5",
50 "22.6"])
51
52_rule_required = set(["1.1", "1.3", "2.1", "2.2", "3.1", "3.2", "4.1", "5.1",
53 "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "6.1", "6.2", "7.1", "7.2",
54 "7.3", "7.4", "8.1", "8.2", "8.3", "8.4", "8.5", "8.6", "8.7", "8.8",
55 "8.10", "8.12", "8.14", "9.2", "9.3", "9.4", "9.5", "10.1", "10.2", "10.3",
56 "10.4", "10.6", "10.7", "10.8", "11.1", "11.2", "11.3", "11.6", "11.7",
57 "11.8", "11.9", "12.2", "13.1", "13.2", "13.5", "14.1", "14.2", "14.3",
58 "14.4", "15.2", "15.3", "15.6", "15.7", "16.1", "16.2", "16.3", "16.4",
59 "16.5", "16.6", "16.7", "17.1", "17.2", "17.7", "18.1", "18.2", "18.3",
60 "18.6", "18.7", "18.8", "20.3", "20.4", "20.6", "20.7", "20.8", "20.9",
61 "20.11", "20.12", "20.13", "20.14", "21.1", "21.2", "21.3", "21.4", "21.5",
62 "21.6", "21.7", "21.8", "21.9", "21.10", "21.11", "21.14", "21.15", "21.16",
63 "22.1", "22.3", "22.4", "22.7", "22.8", "22.9", "22.10"])
64
65_rule_advisory = set(["1.2", "2.3", "2.4", "2.5", "2.6", "2.7", "4.2", "5.9",
66 "8.9", "8.11", "8.13", "10.5", "11.4", "11.5", "12.1", "12.3", "12.4",
67 "13.3", "13.4", "15.1", "15.4", "15.5", "17.5", "17.8", "18.4", "18.5",
68 "19.2", "20.1", "20.2", "20.5", "20.10", "21.12"])
69
70
71_checker_lookup = {
72 "Directive": {
73 "required": _dir_required,
74 "advisory": _dir_advisory
75 },
76 "Rule": {
77 "mandatory": _rule_mandatory,
78 "required": _rule_required,
79 "advisory": _rule_advisory
80 }
81 }
82
83_checker_re = re.compile(r"""(?P<kind>\w+) (?P<number>[\d\.]+)$""")
84
85
86def _classify_checker(checker):
87 match = _checker_re.search(checker)
88 if match:
89 kind, number = match.group("kind"), match.group("number")
90 for classification, class_set in _checker_lookup[kind].items():
91 if number in class_set:
92 return classification
93
94 return "unknown"
95
96
97# Return a copy of the original issue description. Update file path to strip
98# heading '/', and also insert CID.
99def _new_issue(cid, orig_issue):
100 checker = orig_issue["checker"]
101 classification = _classify_checker(checker)
102
103 return {
104 "cid": cid,
105 "file": orig_issue["file"].lstrip("/"),
106 "line": orig_issue["mainEventLineNumber"],
107 "checker": checker,
108 "classification": classification,
109 "description": orig_issue["mainEventDescription"]
110 }
111
Zelalemc9531f82020-08-04 15:37:08 -0500112def _new_issue_v7(cid, checker, issue):
113 return {
114 "cid": cid,
115 "file": issue["strippedFilePathname"],
116 "line": issue["lineNumber"],
117 "checker": checker,
118 "classification": _classify_checker(checker),
119 "description": issue["eventDescription"],
120 }
121
Fathi Boudra422bf772019-12-02 11:10:16 +0200122
123def _cls_string(issue):
124 cls = issue["classification"]
125
126 return " (" + cls + ")" if cls != "unknown" else ""
127
128
129# Given an issue, make a string formed of file name, line number, checker, and
130# the CID. This could be used as a dictionary key to identify unique defects
131# across the scan. Convert inegers to zero-padded strings for proper sorting.
132def make_key(i):
133 return (i["file"] + str(i["line"]).zfill(5) + i["checker"] +
134 str(i["cid"]).zfill(5))
135
136
Fathi Boudra422bf772019-12-02 11:10:16 +0200137
Fathi Boudra422bf772019-12-02 11:10:16 +0200138
Zelalemc9531f82020-08-04 15:37:08 -0500139class Issues(object):
140 """An iterator over issue events that collects a summary
Fathi Boudra422bf772019-12-02 11:10:16 +0200141
Zelalemc9531f82020-08-04 15:37:08 -0500142 After using this object as an iterator, the totals member will contain a
143 dict that maps defect types to their totals, and a "total" key with the
144 total number of defects in this scan.
145 """
146 def __init__(self, path, show_all):
147 self.path = path
148 self.show_all = show_all
149 self.iterated = False
150 self.totals = collections.defaultdict(int)
151 self.gen = None
Fathi Boudra422bf772019-12-02 11:10:16 +0200152
Zelalemc9531f82020-08-04 15:37:08 -0500153 def iter_issues_v1(self, report):
154 # Unconditional filter
155 filters = [lambda i: ((i["triage"]["action"] != "Ignore") and
156 (i["occurrences"][0]["checker"] not in _rule_exclusions))]
Fathi Boudra422bf772019-12-02 11:10:16 +0200157
Zelalemc9531f82020-08-04 15:37:08 -0500158 # Whether we need diffs only
159 if not self.show_all:
160 # Pick only issues that are not present in comparison snapshot
161 filters.append(lambda i: not i["presentInComparisonSnapshot"])
162
163 # Pick issue when all filters are true
164 filter_func = lambda i: all([f(i) for f in filters])
165
166 # Top-level is a group of issues, all sharing a common CID
167 for issue_group in filter(filter_func, report["issueInfo"]):
168 # Pick up individual occurrence of the CID
169 self.totals[_classify_checker(occurrence["checkerName"])] += 1
170 self.totals["total"] += 1
171 for occurrence in issue_group["occurrences"]:
172 yield _new_issue(issue_group["cid"], occurrence)
173
174 def iter_issues_v7(self, report):
175 # TODO: filter by triage and action
176 f = lambda i: i["checkerName"] not in _rule_exclusions
177 for issue_group in filter(f, report["issues"]):
178 self.totals[_classify_checker(issue_group["checkerName"])] += 1
179 self.totals["total"] += 1
180 for event in issue_group["events"]:
181 yield _new_issue_v7(
182 issue_group.get("cid"),
183 issue_group["checkerName"],
184 event
185 )
186
187 def _gen(self):
188 with open(self.path, encoding="utf-8") as fd:
189 report = json.load(fd)
190 if report.get("formatVersion", 0) >= 7:
191 return self.iter_issues_v7(report)
192 else:
193 return self.iter_issues_v1(report)
194
195 def __iter__(self):
196 if self.gen is None:
197 self.gen = self._gen()
198 yield from self.gen
Fathi Boudra422bf772019-12-02 11:10:16 +0200199
200# Format issue (returned from iter_issues()) as text.
201def format_issue(issue):
202 return ("{file}:{line}:[{checker}{cls}]<{cid}> {description}").format_map(
203 dict(issue, cls=_cls_string(issue)))
204
205
206# Format issue (returned from iter_issues()) as HTML table row.
207def format_issue_html(issue):
208 cls = _cls_string(issue)
209 cov_class = "cov-" + issue["classification"]
210
211 return """\
212<tr class="{cov_class}">
213 <td class="cov-file">{file}</td>
214 <td class="cov-line">{line}</td>
215 <td class="cov-checker">{checker}{cls}</td>
216 <td class="cov-cid">{cid}</td>
217 <td class="cov-description">{description}</td>
218</tr>""".format_map(dict(issue, cls=cls, cov_class=cov_class))
219
220
Zelalemc9531f82020-08-04 15:37:08 -0500221TOTALS_FORMAT = str.strip("""
222TotalDefects: {total}
223MandatoryDefects: {mandatory}
224RequiredDefects: {required}
225AdvisoryDefects: {advisory}
226""")
227
Fathi Boudra422bf772019-12-02 11:10:16 +0200228if __name__ == "__main__":
229 parser = argparse.ArgumentParser()
230
231 parser.add_argument("--all", default=False, dest="show_all",
232 action="store_const", const=True, help="List all issues")
233 parser.add_argument("--output",
234 help="File to output filtered defects to in JSON")
Zelalemc9531f82020-08-04 15:37:08 -0500235 parser.add_argument("--totals",
236 help="File to output total defects in flat text")
Fathi Boudra422bf772019-12-02 11:10:16 +0200237 parser.add_argument("json_report")
238
239 opts = parser.parse_args()
240
Zelalemc9531f82020-08-04 15:37:08 -0500241 issue_cls = Issues(opts.json_report, opts.show_all)
Fathi Boudra422bf772019-12-02 11:10:16 +0200242 issues = []
Zelalemc9531f82020-08-04 15:37:08 -0500243 for issue in sorted(issue_cls, key=lambda i: make_key(i)):
Fathi Boudra422bf772019-12-02 11:10:16 +0200244 print(format_issue(issue))
245 issues.append(issue)
246
247 if opts.output:
248 # Dump selected issues
249 with open(opts.output, "wt") as fd:
250 fd.write(json.dumps(issues))
251
Zelalemc9531f82020-08-04 15:37:08 -0500252 if opts.totals:
253 with open(opts.totals, "wt") as fd:
254 fd.write(TOTALS_FORMAT.format_map(issue_cls.totals))
255
Fathi Boudra422bf772019-12-02 11:10:16 +0200256 sys.exit(int(len(issues) > 0))