1#!/usr/bin/env python3
2
3import os, re, shutil
4from . import settings, utils, cppcheck_report_utils, exclusion_file_list
5from .exclusion_file_list import (ExclusionFileListError,
6                                  cppcheck_exclusion_file_list)
7
8class GetMakeVarsPhaseError(Exception):
9    pass
10
11class CppcheckDepsPhaseError(Exception):
12    pass
13
14class CppcheckReportPhaseError(Exception):
15    pass
16
17CPPCHECK_BUILD_DIR = "build-dir-cppcheck"
18CPPCHECK_HTMLREPORT_OUTDIR = "cppcheck-htmlreport"
19CPPCHECK_REPORT_OUTDIR = "cppcheck-report"
20cppcheck_extra_make_args = ""
21xen_cc = ""
22
23def get_make_vars():
24    global xen_cc
25    invoke_make = utils.invoke_command(
26            "make -C {} {} export-variable-CC"
27                .format(settings.xen_dir, settings.make_forward_args),
28            True, GetMakeVarsPhaseError,
29            "Error occured retrieving make vars:\n{}"
30        )
31
32    cc_var_regex = re.search('^CC=(.*)$', invoke_make, flags=re.M)
33    if cc_var_regex:
34        xen_cc = cc_var_regex.group(1)
35
36    if xen_cc == "":
37        raise GetMakeVarsPhaseError("CC variable not found in Xen make output")
38
39
40def __generate_suppression_list(out_file):
41    # The following lambda function will return a file if it contains lines with
42    # a comment containing "cppcheck-suppress[*]" on a single line.
43    grep_action = lambda x: utils.grep(x,
44                    r'^.*/\* cppcheck-suppress\[(?P<id>.*)\] \*/$')
45    # Look for a list of .h files that matches the condition above
46    headers = utils.recursive_find_file(settings.xen_dir, r'.*\.h$',
47                                        grep_action)
48
49    try:
50        with open(out_file, "wt") as supplist_file:
51            # Add this rule to skip every finding in the autogenerated
52            # header for cppcheck
53            supplist_file.write("*:*generated/compiler-def.h\n")
54
55            try:
56                exclusion_file = \
57                    "{}/docs/misra/exclude-list.json".format(settings.repo_dir)
58                exclusion_list = cppcheck_exclusion_file_list(exclusion_file)
59            except ExclusionFileListError as e:
60                raise CppcheckDepsPhaseError(
61                    "Issue with reading file {}: {}".format(exclusion_file, e)
62                )
63
64            # Append excluded files to the suppression list, using * as error id
65            # to suppress every findings on that file
66            for path in exclusion_list:
67                supplist_file.write("*:{}\n".format(path))
68
69            for entry in headers:
70                filename = entry["file"]
71                try:
72                    with open(filename, "rt") as infile:
73                        header_content = infile.readlines()
74                except OSError as e:
75                    raise CppcheckDepsPhaseError(
76                            "Issue with reading file {}: {}"
77                                .format(filename, e)
78                          )
79                header_lines_len = len(header_content)
80                # line_num in entry will be header_content[line_num-1], here we
81                # are going to search the first line after line_num that have
82                # anything different from comments or empty line, because the
83                # in-code comment suppression is related to that line then.
84                for line_num in entry["matches"]:
85                    cppcheck_violation_id = ""
86                    tmp_line = line_num
87                    # look up to which line is referring the comment at
88                    # line_num (which would be header_content[tmp_line-1])
89                    comment_section = False
90                    while tmp_line < header_lines_len:
91                        line = header_content[tmp_line]
92                        # Matches a line with just optional spaces/tabs and the
93                        # start of a comment '/*'
94                        comment_line_starts = re.match(r'^[ \t]*/\*.*$', line)
95                        # Matches a line with text and the end of a comment '*/'
96                        comment_line_stops = re.match(r'^.*\*/$', line)
97                        if (not comment_section) and comment_line_starts:
98                            comment_section = True
99                        if (len(line.strip()) != 0) and (not comment_section):
100                            cppcheck_violation_id = entry["matches"][line_num]['id']
101                            break
102                        if comment_section and comment_line_stops:
103                            comment_section = False
104                        tmp_line = tmp_line + 1
105
106                    if cppcheck_violation_id == "":
107                        raise CppcheckDepsPhaseError(
108                            "Error matching cppcheck comment in {} at line {}."
109                                .format(filename, line_num)
110                          )
111                    # Write [error id]:[filename]:[line]
112                    # tmp_line refers to the array index, so translated to the
113                    # file line (that begins with 1) it is tmp_line+1
114                    supplist_file.write(
115                            "{}:{}:{}\n".format(cppcheck_violation_id, filename,
116                                                (tmp_line + 1))
117                        )
118    except OSError as e:
119        raise CppcheckDepsPhaseError("Issue with writing file {}: {}"
120                                     .format(out_file, e))
121
122
123def generate_cppcheck_deps():
124    global cppcheck_extra_make_args
125
126    # Compile flags to pass to cppcheck:
127    # - include config.h as this is passed directly to the compiler.
128    # - define CPPCHECK as we use it to disable or enable some specific part of
129    #   the code to solve some cppcheck issues.
130    # - explicitely enable some cppcheck checks as we do not want to use "all"
131    #   which includes unusedFunction which gives wrong positives as we check
132    #   file per file.
133    # - Explicitly suppress warnings on compiler-def.h because cppcheck throws
134    #   an unmatchedSuppression due to the rule we put in suppression-list.txt
135    #   to skip every finding in the file.
136    # - Explicitly suppress findings for unusedStructMember that is not very
137    #   reliable and causes lots of false positives.
138    #
139    # Compiler defines are in compiler-def.h which is included in config.h
140    #
141    cppcheck_flags="""
142 --max-ctu-depth=10
143 --enable=style,information,missingInclude
144 --template=\'{{file}}({{line}},{{column}}):{{id}}:{{severity}}:{{message}}\'
145 --relative-paths={}
146 --inline-suppr
147 --suppressions-list={}/suppression-list.txt
148 --suppress='unmatchedSuppression:*'
149 --suppress='unusedStructMember:*'
150 --include={}/include/xen/config.h
151 -DCPPCHECK
152""".format(settings.repo_dir, settings.outdir, settings.xen_dir)
153
154    invoke_cppcheck = utils.invoke_command(
155            "{} --version".format(settings.cppcheck_binpath),
156            True, CppcheckDepsPhaseError,
157            "Error occured retrieving cppcheck version:\n{}\n\n{}"
158        )
159
160    version_regex = re.search(r'^Cppcheck (\d+)\.(\d+)(?:\.\d+)?$',
161                              invoke_cppcheck, flags=re.M)
162    # Currently, only cppcheck version >= 2.7 is supported, but version 2.8 is
163    # known to be broken, please refer to docs/misra/cppcheck.txt
164    if (not version_regex) or len(version_regex.groups()) < 2:
165        raise CppcheckDepsPhaseError(
166            "Can't find cppcheck version or version not identified: "
167            "{}".format(invoke_cppcheck)
168        )
169    version = (int(version_regex.group(1)), int(version_regex.group(2)))
170    if version < (2, 7) or version == (2, 8):
171        raise CppcheckDepsPhaseError(
172            "Cppcheck version < 2.7 or 2.8 are not supported"
173        )
174
175    # If misra option is selected, append misra addon and generate cppcheck
176    # files for misra analysis
177    if settings.cppcheck_misra:
178        cppcheck_flags = cppcheck_flags + " --addon=cppcheck-misra.json"
179
180        skip_rules_arg = ""
181        if settings.cppcheck_skip_rules != "":
182            skip_rules_arg = "-s {}".format(settings.cppcheck_skip_rules)
183
184        utils.invoke_command(
185            "{}/convert_misra_doc.py -i {}/docs/misra/rules.rst"
186            " -o {}/cppcheck-misra.txt -j {}/cppcheck-misra.json {}"
187                .format(settings.tools_dir, settings.repo_dir,
188                        settings.outdir, settings.outdir, skip_rules_arg),
189            False, CppcheckDepsPhaseError,
190            "An error occured when running:\n{}"
191        )
192
193    # Generate compiler macros
194    os.makedirs("{}/include/generated".format(settings.outdir), exist_ok=True)
195    utils.invoke_command(
196            "{} -dM -E -o \"{}/include/generated/compiler-def.h\" - < /dev/null"
197                .format(xen_cc, settings.outdir),
198            False, CppcheckDepsPhaseError,
199            "An error occured when running:\n{}"
200        )
201
202    # Generate cppcheck suppression list
203    __generate_suppression_list(
204        "{}/suppression-list.txt".format(settings.outdir))
205
206    # Generate cppcheck build folder
207    os.makedirs("{}/{}".format(settings.outdir, CPPCHECK_BUILD_DIR),
208                exist_ok=True)
209
210    cppcheck_cc_flags = """--compiler={} --cppcheck-cmd={} {}
211 --cppcheck-plat={}/cppcheck-plat --ignore-path=tools/
212 --ignore-path=arch/x86/efi/check.c --build-dir={}/{}
213""".format(xen_cc, settings.cppcheck_binpath, cppcheck_flags,
214           settings.tools_dir, settings.outdir, CPPCHECK_BUILD_DIR)
215
216    if settings.cppcheck_html:
217        cppcheck_cc_flags = cppcheck_cc_flags + " --cppcheck-html"
218
219    # Generate the extra make argument to pass the cppcheck-cc.sh wrapper as CC
220    cppcheck_extra_make_args = "CC=\"{}/cppcheck-cc.sh {} --\"".format(
221                                        settings.tools_dir,
222                                        cppcheck_cc_flags
223                                    ).replace("\n", "")
224
225
226def generate_cppcheck_report():
227    # Prepare text report
228    # Look for a list of .cppcheck.txt files, those are the txt report
229    # fragments
230    fragments = utils.recursive_find_file(settings.outdir, r'.*\.cppcheck.txt$')
231    text_report_dir = "{}/{}".format(settings.outdir,
232                                        CPPCHECK_REPORT_OUTDIR)
233    report_filename = "{}/xen-cppcheck.txt".format(text_report_dir)
234    os.makedirs(text_report_dir, exist_ok=True)
235    try:
236        cppcheck_report_utils.cppcheck_merge_txt_fragments(fragments,
237                                                           report_filename,
238                                                           [settings.repo_dir])
239    except cppcheck_report_utils.CppcheckTXTReportError as e:
240        raise CppcheckReportPhaseError(e)
241
242    # If HTML output is requested
243    if settings.cppcheck_html:
244        # Look for a list of .cppcheck.xml files, those are the XML report
245        # fragments
246        fragments = utils.recursive_find_file(settings.outdir,
247                                              r'.*\.cppcheck.xml$')
248        html_report_dir = "{}/{}".format(settings.outdir,
249                                         CPPCHECK_HTMLREPORT_OUTDIR)
250        xml_filename = "{}/xen-cppcheck.xml".format(html_report_dir)
251        os.makedirs(html_report_dir, exist_ok=True)
252        try:
253            cppcheck_report_utils.cppcheck_merge_xml_fragments(fragments,
254                                                               xml_filename,
255                                                               settings.repo_dir,
256                                                               settings.outdir)
257        except cppcheck_report_utils.CppcheckHTMLReportError as e:
258            raise CppcheckReportPhaseError(e)
259        # Call cppcheck-htmlreport utility to generate the HTML output
260        utils.invoke_command(
261            "{} --file={} --source-dir={} --report-dir={}/html --title=Xen"
262                .format(settings.cppcheck_htmlreport_binpath, xml_filename,
263                        settings.repo_dir, html_report_dir),
264            False, CppcheckReportPhaseError,
265            "Error occured generating Cppcheck HTML report:\n{}"
266        )
267        # Strip src and obj path from *.html files
268        html_files = utils.recursive_find_file(html_report_dir, r'.*\.html$')
269        try:
270            cppcheck_report_utils.cppcheck_strip_path_html(html_files,
271                                                           (settings.repo_dir,
272                                                            settings.outdir))
273        except cppcheck_report_utils.CppcheckHTMLReportError as e:
274            raise CppcheckReportPhaseError(e)
275
276
277def clean_analysis_artifacts():
278    clean_files = ("suppression-list.txt", "cppcheck-misra.txt",
279                   "cppcheck-misra.json")
280    cppcheck_build_dir = "{}/{}".format(settings.outdir, CPPCHECK_BUILD_DIR)
281    if os.path.isdir(cppcheck_build_dir):
282        shutil.rmtree(cppcheck_build_dir)
283    artifact_files = utils.recursive_find_file(settings.outdir,
284                                r'.*\.(?:c\.json|cppcheck\.txt|cppcheck\.xml)$')
285    for file in clean_files:
286        file = "{}/{}".format(settings.outdir, file)
287        if os.path.isfile(file):
288            artifact_files.append(file)
289    for delfile in artifact_files:
290        os.remove(delfile)
291
292
293def clean_reports():
294    text_report_dir = "{}/{}".format(settings.outdir,
295                                     CPPCHECK_REPORT_OUTDIR)
296    html_report_dir = "{}/{}".format(settings.outdir,
297                                     CPPCHECK_HTMLREPORT_OUTDIR)
298    if os.path.isdir(text_report_dir):
299        shutil.rmtree(text_report_dir)
300    if os.path.isdir(html_report_dir):
301        shutil.rmtree(html_report_dir)
302