blob: a458da5ca45ea69ae8b3f3660c955bfc972ef391 [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
Jimmy Brissond6919382020-08-05 13:26:17 -050016# Ignore some directories, as they're imported from other projects. Their
17# code style is not necessarily the same as ours, and enforcing our rules
18# on their code will likely lead to many false positives. They're false in
19# the sense that they may not be violations of the origin project's code
20# style, when they're violations of our code style.
21IGNORED_DIRS = [
22 "lib/libfdt",
23 "include/lib/libfdt",
24 "lib/compiler-rt",
25 "lib/zlib",
26 "include/lib/zlib",
27]
28
Fathi Boudra422bf772019-12-02 11:10:16 +020029_rule_exclusions = [
Zelalem219df412020-05-17 19:21:20 -050030 "MISRA C-2012 Rule 2.4",
31 "MISRA C-2012 Rule 2.5",
32 "MISRA C-2012 Rule 2.7",
33 "MISRA C-2012 Rule 5.1",
34 "MISRA C-2012 Rule 5.8",
Fathi Boudra422bf772019-12-02 11:10:16 +020035 "MISRA C-2012 Rule 8.6",
Zelalem219df412020-05-17 19:21:20 -050036 "MISRA C-2012 Rule 8.7",
37 "MISRA C-2012 Rule 11.4",
38 "MISRA C-2012 Rule 11.5",
39 "MISRA C-2012 Rule 15.1",
40 "MISRA C-2012 Rule 15.5",
41 "MISRA C-2012 Rule 15.6",
42 "MISRA C-2012 Rule 16.1",
43 "MISRA C-2012 Rule 16.3",
44 "MISRA C-2012 Rule 17.1",
45 "MISRA C-2012 Rule 21.6",
46 "MISRA C-2012 Directive 4.6",
47 "MISRA C-2012 Directive 4.8",
48 "MISRA C-2012 Directive 4.9"
Fathi Boudra422bf772019-12-02 11:10:16 +020049]
50
51# The following classification of rules and directives include 'MISRA C:2012
52# Amendment 1'
53
54# Directives
55_dir_required = set(["1.1", "2.1", "3.1", "4.1", "4.3", "4.7", "4.10", "4.11",
56 "4.12", "4.14"])
57
58_dir_advisory = set(["4.2", "4.4", "4.5", "4.6", "4.8", "4.9", "4.13"])
59
60# Rules
61_rule_mandatory = set(["9.1", "9.2", "9.3", "12.5", "13.6", "17.3", "17.4",
62 "17.6", "19.1", "21.13", "21.17", "21.18", "21.19", "21.20", "22.2", "22.5",
63 "22.6"])
64
65_rule_required = set(["1.1", "1.3", "2.1", "2.2", "3.1", "3.2", "4.1", "5.1",
66 "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "6.1", "6.2", "7.1", "7.2",
67 "7.3", "7.4", "8.1", "8.2", "8.3", "8.4", "8.5", "8.6", "8.7", "8.8",
68 "8.10", "8.12", "8.14", "9.2", "9.3", "9.4", "9.5", "10.1", "10.2", "10.3",
69 "10.4", "10.6", "10.7", "10.8", "11.1", "11.2", "11.3", "11.6", "11.7",
70 "11.8", "11.9", "12.2", "13.1", "13.2", "13.5", "14.1", "14.2", "14.3",
71 "14.4", "15.2", "15.3", "15.6", "15.7", "16.1", "16.2", "16.3", "16.4",
72 "16.5", "16.6", "16.7", "17.1", "17.2", "17.7", "18.1", "18.2", "18.3",
73 "18.6", "18.7", "18.8", "20.3", "20.4", "20.6", "20.7", "20.8", "20.9",
74 "20.11", "20.12", "20.13", "20.14", "21.1", "21.2", "21.3", "21.4", "21.5",
75 "21.6", "21.7", "21.8", "21.9", "21.10", "21.11", "21.14", "21.15", "21.16",
76 "22.1", "22.3", "22.4", "22.7", "22.8", "22.9", "22.10"])
77
78_rule_advisory = set(["1.2", "2.3", "2.4", "2.5", "2.6", "2.7", "4.2", "5.9",
79 "8.9", "8.11", "8.13", "10.5", "11.4", "11.5", "12.1", "12.3", "12.4",
80 "13.3", "13.4", "15.1", "15.4", "15.5", "17.5", "17.8", "18.4", "18.5",
81 "19.2", "20.1", "20.2", "20.5", "20.10", "21.12"])
82
83
84_checker_lookup = {
85 "Directive": {
86 "required": _dir_required,
87 "advisory": _dir_advisory
88 },
89 "Rule": {
90 "mandatory": _rule_mandatory,
91 "required": _rule_required,
92 "advisory": _rule_advisory
93 }
94 }
95
96_checker_re = re.compile(r"""(?P<kind>\w+) (?P<number>[\d\.]+)$""")
97
98
99def _classify_checker(checker):
100 match = _checker_re.search(checker)
101 if match:
102 kind, number = match.group("kind"), match.group("number")
103 for classification, class_set in _checker_lookup[kind].items():
104 if number in class_set:
105 return classification
106
107 return "unknown"
108
109
110# Return a copy of the original issue description. Update file path to strip
111# heading '/', and also insert CID.
112def _new_issue(cid, orig_issue):
113 checker = orig_issue["checker"]
114 classification = _classify_checker(checker)
115
116 return {
117 "cid": cid,
118 "file": orig_issue["file"].lstrip("/"),
119 "line": orig_issue["mainEventLineNumber"],
120 "checker": checker,
121 "classification": classification,
122 "description": orig_issue["mainEventDescription"]
123 }
124
Zelalemc9531f82020-08-04 15:37:08 -0500125def _new_issue_v7(cid, checker, issue):
126 return {
127 "cid": cid,
128 "file": issue["strippedFilePathname"],
129 "line": issue["lineNumber"],
130 "checker": checker,
131 "classification": _classify_checker(checker),
132 "description": issue["eventDescription"],
133 }
134
Fathi Boudra422bf772019-12-02 11:10:16 +0200135
136def _cls_string(issue):
137 cls = issue["classification"]
138
139 return " (" + cls + ")" if cls != "unknown" else ""
140
141
142# Given an issue, make a string formed of file name, line number, checker, and
143# the CID. This could be used as a dictionary key to identify unique defects
144# across the scan. Convert inegers to zero-padded strings for proper sorting.
145def make_key(i):
146 return (i["file"] + str(i["line"]).zfill(5) + i["checker"] +
147 str(i["cid"]).zfill(5))
148
149
Fathi Boudra422bf772019-12-02 11:10:16 +0200150
Fathi Boudra422bf772019-12-02 11:10:16 +0200151
Zelalemc9531f82020-08-04 15:37:08 -0500152class Issues(object):
153 """An iterator over issue events that collects a summary
Fathi Boudra422bf772019-12-02 11:10:16 +0200154
Zelalemc9531f82020-08-04 15:37:08 -0500155 After using this object as an iterator, the totals member will contain a
156 dict that maps defect types to their totals, and a "total" key with the
157 total number of defects in this scan.
158 """
159 def __init__(self, path, show_all):
160 self.path = path
161 self.show_all = show_all
162 self.iterated = False
163 self.totals = collections.defaultdict(int)
164 self.gen = None
Fathi Boudra422bf772019-12-02 11:10:16 +0200165
Jimmy Brissond6919382020-08-05 13:26:17 -0500166 def filter_groups_v1(self, group):
167 """Decide if we should keep an issue group from a v1-6 format dict"""
168 if group["triage"]["action"] == "Ignore":
169 return False
170 if group["occurrences"][0]["checker"] in _rule_exclusions:
171 return False
172 for skip_dir in IGNORED_DIRS:
173 if group["file"].lstrip("/").startswith(skip_dir):
174 return False
175 # unless we're showing all groups, remove the groups that are in both
176 # golden and branch
Zelalemc9531f82020-08-04 15:37:08 -0500177 if not self.show_all:
Jimmy Brissond6919382020-08-05 13:26:17 -0500178 return not group["presentInComparisonSnapshot"]
179 return True
Zelalemc9531f82020-08-04 15:37:08 -0500180
Jimmy Brissond6919382020-08-05 13:26:17 -0500181 def iter_issues_v1(self, report):
Zelalemc9531f82020-08-04 15:37:08 -0500182 # Top-level is a group of issues, all sharing a common CID
Jimmy Brissond6919382020-08-05 13:26:17 -0500183 for issue_group in filter(self.filter_groups_v1, report["issueInfo"]):
Zelalemc9531f82020-08-04 15:37:08 -0500184 # Pick up individual occurrence of the CID
185 self.totals[_classify_checker(occurrence["checkerName"])] += 1
186 self.totals["total"] += 1
187 for occurrence in issue_group["occurrences"]:
188 yield _new_issue(issue_group["cid"], occurrence)
189
Jimmy Brissond6919382020-08-05 13:26:17 -0500190 def filter_groups_v7(self, group):
191 """Decide if we should keep an issue group from a v7 format dict"""
192 if group.get("checker_name") in _rule_exclusions:
193 return False
194 for skip_dir in IGNORED_DIRS:
195 if group["strippedMainEventFilePathname"].startswith(skip_dir):
196 return False
197 return True
198
Zelalemc9531f82020-08-04 15:37:08 -0500199 def iter_issues_v7(self, report):
200 # TODO: filter by triage and action
Jimmy Brissond6919382020-08-05 13:26:17 -0500201 for issue_group in filter(self.filter_groups_v7, report["issues"]):
Zelalemc9531f82020-08-04 15:37:08 -0500202 self.totals[_classify_checker(issue_group["checkerName"])] += 1
203 self.totals["total"] += 1
204 for event in issue_group["events"]:
205 yield _new_issue_v7(
206 issue_group.get("cid"),
207 issue_group["checkerName"],
208 event
209 )
210
211 def _gen(self):
212 with open(self.path, encoding="utf-8") as fd:
213 report = json.load(fd)
214 if report.get("formatVersion", 0) >= 7:
215 return self.iter_issues_v7(report)
216 else:
217 return self.iter_issues_v1(report)
218
219 def __iter__(self):
220 if self.gen is None:
221 self.gen = self._gen()
222 yield from self.gen
Fathi Boudra422bf772019-12-02 11:10:16 +0200223
224# Format issue (returned from iter_issues()) as text.
225def format_issue(issue):
226 return ("{file}:{line}:[{checker}{cls}]<{cid}> {description}").format_map(
227 dict(issue, cls=_cls_string(issue)))
228
229
230# Format issue (returned from iter_issues()) as HTML table row.
231def format_issue_html(issue):
232 cls = _cls_string(issue)
233 cov_class = "cov-" + issue["classification"]
234
235 return """\
236<tr class="{cov_class}">
237 <td class="cov-file">{file}</td>
238 <td class="cov-line">{line}</td>
239 <td class="cov-checker">{checker}{cls}</td>
240 <td class="cov-cid">{cid}</td>
241 <td class="cov-description">{description}</td>
242</tr>""".format_map(dict(issue, cls=cls, cov_class=cov_class))
243
244
Zelalemc9531f82020-08-04 15:37:08 -0500245TOTALS_FORMAT = str.strip("""
246TotalDefects: {total}
247MandatoryDefects: {mandatory}
248RequiredDefects: {required}
249AdvisoryDefects: {advisory}
250""")
251
Fathi Boudra422bf772019-12-02 11:10:16 +0200252if __name__ == "__main__":
253 parser = argparse.ArgumentParser()
254
255 parser.add_argument("--all", default=False, dest="show_all",
256 action="store_const", const=True, help="List all issues")
257 parser.add_argument("--output",
258 help="File to output filtered defects to in JSON")
Zelalemc9531f82020-08-04 15:37:08 -0500259 parser.add_argument("--totals",
260 help="File to output total defects in flat text")
Fathi Boudra422bf772019-12-02 11:10:16 +0200261 parser.add_argument("json_report")
262
263 opts = parser.parse_args()
264
Zelalemc9531f82020-08-04 15:37:08 -0500265 issue_cls = Issues(opts.json_report, opts.show_all)
Fathi Boudra422bf772019-12-02 11:10:16 +0200266 issues = []
Zelalemc9531f82020-08-04 15:37:08 -0500267 for issue in sorted(issue_cls, key=lambda i: make_key(i)):
Fathi Boudra422bf772019-12-02 11:10:16 +0200268 print(format_issue(issue))
269 issues.append(issue)
270
271 if opts.output:
272 # Dump selected issues
273 with open(opts.output, "wt") as fd:
274 fd.write(json.dumps(issues))
275
Zelalemc9531f82020-08-04 15:37:08 -0500276 if opts.totals:
277 with open(opts.totals, "wt") as fd:
278 fd.write(TOTALS_FORMAT.format_map(issue_cls.totals))
279
Fathi Boudra422bf772019-12-02 11:10:16 +0200280 sys.exit(int(len(issues) > 0))