Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 3 | # Copyright (c) 2019-2020, Arm Limited. All rights reserved. |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 4 | # |
| 5 | # SPDX-License-Identifier: BSD-3-Clause |
| 6 | # |
| 7 | |
| 8 | import argparse |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 9 | import collections |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 10 | import json |
| 11 | import re |
| 12 | import shutil |
| 13 | import sys |
| 14 | |
| 15 | |
| 16 | _rule_exclusions = [ |
Zelalem | 219df41 | 2020-05-17 19:21:20 -0500 | [diff] [blame] | 17 | "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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 22 | "MISRA C-2012 Rule 8.6", |
Zelalem | 219df41 | 2020-05-17 19:21:20 -0500 | [diff] [blame] | 23 | "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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 36 | ] |
| 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 | |
| 86 | def _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. |
| 99 | def _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 | |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 112 | def _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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 122 | |
| 123 | def _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. |
| 132 | def make_key(i): |
| 133 | return (i["file"] + str(i["line"]).zfill(5) + i["checker"] + |
| 134 | str(i["cid"]).zfill(5)) |
| 135 | |
| 136 | |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 137 | |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 138 | |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 139 | class Issues(object): |
| 140 | """An iterator over issue events that collects a summary |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 141 | |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 142 | 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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 152 | |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 153 | 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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 157 | |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 158 | # 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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 199 | |
| 200 | # Format issue (returned from iter_issues()) as text. |
| 201 | def 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. |
| 207 | def 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 | |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 221 | TOTALS_FORMAT = str.strip(""" |
| 222 | TotalDefects: {total} |
| 223 | MandatoryDefects: {mandatory} |
| 224 | RequiredDefects: {required} |
| 225 | AdvisoryDefects: {advisory} |
| 226 | """) |
| 227 | |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 228 | if __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") |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 235 | parser.add_argument("--totals", |
| 236 | help="File to output total defects in flat text") |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 237 | parser.add_argument("json_report") |
| 238 | |
| 239 | opts = parser.parse_args() |
| 240 | |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 241 | issue_cls = Issues(opts.json_report, opts.show_all) |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 242 | issues = [] |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 243 | for issue in sorted(issue_cls, key=lambda i: make_key(i)): |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 244 | 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 | |
Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 252 | if opts.totals: |
| 253 | with open(opts.totals, "wt") as fd: |
| 254 | fd.write(TOTALS_FORMAT.format_map(issue_cls.totals)) |
| 255 | |
Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 256 | sys.exit(int(len(issues) > 0)) |