| 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 |  | 
| Jimmy Brisson | d691938 | 2020-08-05 13:26:17 -0500 | [diff] [blame] | 16 | # 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. | 
|  | 21 | IGNORED_DIRS = [ | 
|  | 22 | "lib/libfdt", | 
|  | 23 | "include/lib/libfdt", | 
|  | 24 | "lib/compiler-rt", | 
|  | 25 | "lib/zlib", | 
|  | 26 | "include/lib/zlib", | 
|  | 27 | ] | 
|  | 28 |  | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 29 | _rule_exclusions = [ | 
| Zelalem | 219df41 | 2020-05-17 19:21:20 -0500 | [diff] [blame] | 30 | "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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 35 | "MISRA C-2012 Rule 8.6", | 
| Zelalem | 219df41 | 2020-05-17 19:21:20 -0500 | [diff] [blame] | 36 | "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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 49 | ] | 
|  | 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 |  | 
|  | 99 | def _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. | 
|  | 112 | def _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 |  | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 125 | def _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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 135 |  | 
|  | 136 | def _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. | 
|  | 145 | def make_key(i): | 
|  | 146 | return (i["file"] + str(i["line"]).zfill(5) + i["checker"] + | 
|  | 147 | str(i["cid"]).zfill(5)) | 
|  | 148 |  | 
|  | 149 |  | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 150 |  | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 151 |  | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 152 | class Issues(object): | 
|  | 153 | """An iterator over issue events that collects a summary | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 154 |  | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 155 | 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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 165 |  | 
| Jimmy Brisson | d691938 | 2020-08-05 13:26:17 -0500 | [diff] [blame] | 166 | 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: | 
| Jimmy Brisson | 540f214 | 2020-10-29 12:47:15 -0500 | [diff] [blame] | 173 | if group["occurrences"][0]["file"].lstrip("/").startswith(skip_dir): | 
| Jimmy Brisson | d691938 | 2020-08-05 13:26:17 -0500 | [diff] [blame] | 174 | return False | 
|  | 175 | # unless we're showing all groups, remove the groups that are in both | 
|  | 176 | # golden and branch | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 177 | if not self.show_all: | 
| Jimmy Brisson | d691938 | 2020-08-05 13:26:17 -0500 | [diff] [blame] | 178 | return not group["presentInComparisonSnapshot"] | 
|  | 179 | return True | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 180 |  | 
| Jimmy Brisson | d691938 | 2020-08-05 13:26:17 -0500 | [diff] [blame] | 181 | def iter_issues_v1(self, report): | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 182 | # Top-level is a group of issues, all sharing a common CID | 
| Jimmy Brisson | d691938 | 2020-08-05 13:26:17 -0500 | [diff] [blame] | 183 | for issue_group in filter(self.filter_groups_v1, report["issueInfo"]): | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 184 | # Pick up individual occurrence of the CID | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 185 | self.totals["total"] += 1 | 
|  | 186 | for occurrence in issue_group["occurrences"]: | 
| Mark Dykes | 30138ad | 2021-11-09 16:48:17 -0600 | [diff] [blame] | 187 | self.totals[_classify_checker(occurrence["checker"])] += 1 | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 188 | yield _new_issue(issue_group["cid"], occurrence) | 
|  | 189 |  | 
| Jimmy Brisson | d691938 | 2020-08-05 13:26:17 -0500 | [diff] [blame] | 190 | 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 |  | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 199 | def iter_issues_v7(self, report): | 
|  | 200 | # TODO: filter by triage and action | 
| Jimmy Brisson | d691938 | 2020-08-05 13:26:17 -0500 | [diff] [blame] | 201 | for issue_group in filter(self.filter_groups_v7, report["issues"]): | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 202 | 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 Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 223 |  | 
|  | 224 | # Format issue (returned from iter_issues()) as text. | 
|  | 225 | def 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. | 
|  | 231 | def 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 |  | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 245 | TOTALS_FORMAT = str.strip(""" | 
|  | 246 | TotalDefects:     {total} | 
|  | 247 | MandatoryDefects: {mandatory} | 
|  | 248 | RequiredDefects:  {required} | 
|  | 249 | AdvisoryDefects:  {advisory} | 
|  | 250 | """) | 
|  | 251 |  | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 252 | if __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") | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 259 | parser.add_argument("--totals", | 
|  | 260 | help="File to output total defects in flat text") | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 261 | parser.add_argument("json_report") | 
|  | 262 |  | 
|  | 263 | opts = parser.parse_args() | 
|  | 264 |  | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 265 | issue_cls = Issues(opts.json_report, opts.show_all) | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 266 | issues = [] | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 267 | for issue in sorted(issue_cls, key=lambda i: make_key(i)): | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 268 | 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 |  | 
| Zelalem | c9531f8 | 2020-08-04 15:37:08 -0500 | [diff] [blame] | 276 | if opts.totals: | 
|  | 277 | with open(opts.totals, "wt") as fd: | 
|  | 278 | fd.write(TOTALS_FORMAT.format_map(issue_cls.totals)) | 
|  | 279 |  | 
| Fathi Boudra | 422bf77 | 2019-12-02 11:10:16 +0200 | [diff] [blame] | 280 | sys.exit(int(len(issues) > 0)) |