1# -*- coding: utf-8 -*-
2# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
3# See https://llvm.org/LICENSE.txt for license information.
4# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5""" This module is responsible for the Clang executable.
6
7Since Clang command line interface is so rich, but this project is using only
8a subset of that, it makes sense to create a function specific wrapper. """
9
10import subprocess
11import re
12from libscanbuild import run_command
13from libscanbuild.shell import decode
14
15__all__ = [
16    "get_version",
17    "get_arguments",
18    "get_checkers",
19    "is_ctu_capable",
20    "get_triple_arch",
21]
22
23# regex for activated checker
24ACTIVE_CHECKER_PATTERN = re.compile(r"^-analyzer-checker=(.*)$")
25
26
27class ClangErrorException(Exception):
28    def __init__(self, error):
29        self.error = error
30
31
32def get_version(clang):
33    """Returns the compiler version as string.
34
35    :param clang:   the compiler we are using
36    :return:        the version string printed to stderr"""
37
38    output = run_command([clang, "-v"])
39    # the relevant version info is in the first line
40    return output[0]
41
42
43def get_arguments(command, cwd):
44    """Capture Clang invocation.
45
46    :param command: the compilation command
47    :param cwd:     the current working directory
48    :return:        the detailed front-end invocation command"""
49
50    cmd = command[:]
51    cmd.insert(1, "-###")
52    cmd.append("-fno-color-diagnostics")
53
54    output = run_command(cmd, cwd=cwd)
55    # The relevant information is in the last line of the output.
56    # Don't check if finding last line fails, would throw exception anyway.
57    last_line = output[-1]
58    if re.search(r"clang(.*): error:", last_line):
59        raise ClangErrorException(last_line)
60    return decode(last_line)
61
62
63def get_active_checkers(clang, plugins):
64    """Get the active checker list.
65
66    :param clang:   the compiler we are using
67    :param plugins: list of plugins which was requested by the user
68    :return:        list of checker names which are active
69
70    To get the default checkers we execute Clang to print how this
71    compilation would be called. And take out the enabled checker from the
72    arguments. For input file we specify stdin and pass only language
73    information."""
74
75    def get_active_checkers_for(language):
76        """Returns a list of active checkers for the given language."""
77
78        load_args = [
79            arg for plugin in plugins for arg in ["-Xclang", "-load", "-Xclang", plugin]
80        ]
81        cmd = [clang, "--analyze"] + load_args + ["-x", language, "-"]
82        return [
83            ACTIVE_CHECKER_PATTERN.match(arg).group(1)
84            for arg in get_arguments(cmd, ".")
85            if ACTIVE_CHECKER_PATTERN.match(arg)
86        ]
87
88    result = set()
89    for language in ["c", "c++", "objective-c", "objective-c++"]:
90        result.update(get_active_checkers_for(language))
91    return frozenset(result)
92
93
94def is_active(checkers):
95    """Returns a method, which classifies the checker active or not,
96    based on the received checker name list."""
97
98    def predicate(checker):
99        """Returns True if the given checker is active."""
100
101        return any(pattern.match(checker) for pattern in predicate.patterns)
102
103    predicate.patterns = [re.compile(r"^" + a + r"(\.|$)") for a in checkers]
104    return predicate
105
106
107def parse_checkers(stream):
108    """Parse clang -analyzer-checker-help output.
109
110    Below the line 'CHECKERS:' are there the name description pairs.
111    Many of them are in one line, but some long named checker has the
112    name and the description in separate lines.
113
114    The checker name is always prefixed with two space character. The
115    name contains no whitespaces. Then followed by newline (if it's
116    too long) or other space characters comes the description of the
117    checker. The description ends with a newline character.
118
119    :param stream:  list of lines to parse
120    :return:        generator of tuples
121
122    (<checker name>, <checker description>)"""
123
124    lines = iter(stream)
125    # find checkers header
126    for line in lines:
127        if re.match(r"^CHECKERS:", line):
128            break
129    # find entries
130    state = None
131    for line in lines:
132        if state and not re.match(r"^\s\s\S", line):
133            yield (state, line.strip())
134            state = None
135        elif re.match(r"^\s\s\S+$", line.rstrip()):
136            state = line.strip()
137        else:
138            pattern = re.compile(r"^\s\s(?P<key>\S*)\s*(?P<value>.*)")
139            match = pattern.match(line.rstrip())
140            if match:
141                current = match.groupdict()
142                yield (current["key"], current["value"])
143
144
145def get_checkers(clang, plugins):
146    """Get all the available checkers from default and from the plugins.
147
148    :param clang:   the compiler we are using
149    :param plugins: list of plugins which was requested by the user
150    :return:        a dictionary of all available checkers and its status
151
152    {<checker name>: (<checker description>, <is active by default>)}"""
153
154    load = [elem for plugin in plugins for elem in ["-load", plugin]]
155    cmd = [clang, "-cc1"] + load + ["-analyzer-checker-help"]
156
157    lines = run_command(cmd)
158
159    is_active_checker = is_active(get_active_checkers(clang, plugins))
160
161    checkers = {
162        name: (description, is_active_checker(name))
163        for name, description in parse_checkers(lines)
164    }
165    if not checkers:
166        raise Exception("Could not query Clang for available checkers.")
167
168    return checkers
169
170
171def is_ctu_capable(extdef_map_cmd):
172    """Detects if the current (or given) clang and external definition mapping
173    executables are CTU compatible."""
174
175    try:
176        run_command([extdef_map_cmd, "-version"])
177    except (OSError, subprocess.CalledProcessError):
178        return False
179    return True
180
181
182def get_triple_arch(command, cwd):
183    """Returns the architecture part of the target triple for the given
184    compilation command."""
185
186    cmd = get_arguments(command, cwd)
187    try:
188        separator = cmd.index("-triple")
189        return cmd[separator + 1]
190    except (IndexError, ValueError):
191        return ""
192