1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2017, Data61
5# Commonwealth Scientific and Industrial Research Organisation (CSIRO)
6# ABN 41 687 119 230.
7#
8# This software may be distributed and modified according to the terms of
9# the BSD 2-Clause license. Note that NO WARRANTY is provided.
10# See "LICENSE_BSD2.txt" for details.
11#
12# @TAG(DATA61_BSD)
13#
14
15'''
16A Goanna wrapper to aid in suppressing false positives.
17
18The Goanna static analysis tool is an excellent way of finding bugs in C code,
19but it has no systematic way of suppressing false positives in the command line
20tool. This wrapper script provides support for C code annotations to suppress
21particular classes of warnings in order to achieve this. The C code annotations
22are expected to be formulated as multiline-style comments containing
23"goanna: suppress=" followed by a comma-separated list of warnings you want to
24suppress for that line. For example, if goannacc emitted the following
25warnings:
26
27    foo.c:10: warning: Goanna [ARR-inv-index] Severity-High, ...
28    foo.c:10: warning: Goanna [MEM-leak-alias] Severity-Medium, ...
29
30and you had examined your source code and confirmed that these are false
31positives, you could suppress them with the following comment on line 10:
32
33    return a[x]; /* goanna: suppress=ARR-inv-index,MEM-leak-alias */
34'''
35
36import re, subprocess, sys
37
38def execute(args, stdout):
39    p = subprocess.Popen(args, stdout=stdout, stderr=subprocess.PIPE,
40        universal_newlines=True)
41    _, stderr = p.communicate()
42    return p.returncode, stderr
43
44def main(argv, out, err):
45
46    # Run Goanna.
47    try:
48        ret, stderr = execute(['goannacc'] + argv[1:], out)
49    except OSError:
50        err.write('goannacc not found\n')
51        return -1
52
53    if ret != 0:
54        # Compilation failed. Don't bother trying to suppress warnings.
55        err.write(stderr)
56        return ret
57
58    # A regex that matches lines of output from Goanna that represent warnings.
59    # See section 10.7 of the Goanna user guide.
60    warning_line = re.compile(r'(?P<relfile>[^\s]+):(?P<lineno>\d+):'
61        r'\s*warning:\s*Goanna\[(?P<checkname>[^\]]+)\]\s*Severity-'
62        r'(?P<severity>\w+),\s*(?P<message>[^\.]*)\.\s*(?P<rules>.*)$')
63
64    # A special formatted comment, instructing us to suppress certain warnings.
65    suppression_mark = re.compile(
66        r'/\*\s*goanna:\s*suppress\s*=\s*(?P<checks>.*?)\s*\*/')
67
68    for line in stderr.split('\n')[:-1]:
69
70        m = warning_line.match(line)
71
72        if m is not None:
73            # This line is a warning.
74
75            relfile = m.group('relfile')
76            lineno = int(m.group('lineno'))
77            checkname = m.group('checkname')
78
79            # Find the source line that triggered this warning.
80            # XXX: Given we may be repeatedly opening and searching the same
81            # file, it might make sense to retain open file handles in a cache,
82            # but for now just close each file after dealing with the current
83            # line.
84            source_line = None
85            try:
86                with open(relfile, 'rt') as f:
87                    for index, l in enumerate(f):
88                        if index + 1 == lineno:
89                            source_line = l
90                            break
91            except IOError:
92                # Source file not found.
93                pass
94
95            if source_line is not None:
96
97                # Extract the (possible) suppression marker from this line.
98                s = suppression_mark.search(source_line)
99                if s is not None:
100                    # This line contains a suppression marker.
101                    checks = s.group('checks').split(',')
102                    if checkname in checks:
103                        # The marker matches this warning; suppress.
104                        continue
105
106        # If we reached here, either the output line was not a warning, the
107        # source file was not found, the line number was invalid or there was no
108        # matching suppression marker. Therefore, don't suppress.
109        err.write('%s\n' % line)
110
111    return 0
112
113if __name__ == '__main__':
114    sys.exit(main(sys.argv, sys.stdout, sys.stderr))
115