Completed
Pull Request — master (#1404)
by Abdeali
01:43
created

coalib.bearlib.abstractions.Lint.__print_errors()   A

Complexity

Conditions 3

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 3
rs 10
1
import shutil
2
import subprocess
3
import tempfile
4
import re
5
import sys
6
7
from coalib.misc.Shell import escape_path_argument
8
from coalib.results.Diff import Diff
9
from coalib.results.Result import Result
10
from coalib.results.RESULT_SEVERITY import RESULT_SEVERITY
11
from coalib.bears.Bear import Bear
12
13
14
def is_binary_present(cls):
15
    """
16
    Checks whether the needed binary is present.
17
18
    The function is intended be used with classes
19
    having an executable member which will be checked.
20
21
    :return: True if binary is present, or is not required.
22
             not True otherwise, with a string containing a
23
             detailed description of what's missing.
24
    """
25
    try:
26
        if cls.executable is None:
27
            return True
28
        if shutil.which(cls.executable) is None:
29
            return repr(cls.executable) + " is not installed."
30
        else:
31
            return True
32
    except AttributeError:
33
        # Happens when `executable` does not exist in `cls`.
34
        return True
35
36
37
class Lint(Bear):
38
    """
39
    :param executable:      The executable to run the linter.
40
    :param arguments:       The arguments to supply to the linter, such
41
                            that the file name to be analyzed can be
42
                            appended to the end.
43
    :param output_regex:    The regex which will match the output of the linter
44
                            to get results. This regex should give out the
45
                            following variables:
46
                             line - The line where the issue starts.
47
                             column - The column where the issue starts.
48
                             end_line - The line where the issue ends.
49
                             end_column - The column where the issue ends.
50
                             severity - The severity of the issue.
51
                             message - The message of the result.
52
                            This is not used if `gives_corrected` is set.
53
    :param diff_severity:   The severity to use for all results if
54
                            `gives_corrected` is set.
55
    :param diff_message:    The message to use for all results if
56
                            `gives_corrected` is set.
57
    :param use_stderr:      Uses stderr as the output stream is it's True.
58
    :param use_stdin:       Sends file as stdin instead of giving the file name.
59
    :param gives_corrected: True if the executable gives the corrected file
60
                            or just the issues.
61
    :param severity_map:    A dict where the keys are the possible severity
62
                            values the Linter gives out and the values are the
63
                            severity of the coala Result to set it to. If it is
64
                            not a dict, it is ignored.
65
    """
66
    check_prerequisites = classmethod(is_binary_present)
67
    executable = None
68
    arguments = ""
69
    output_regex = re.compile(r'(?P<line>\d+)\.(?P<column>\d+)\|'
70
                              r'(?P<severity>\d+): (?P<message>.*)')
71
    diff_message = 'No result message was set'
72
    diff_severity = RESULT_SEVERITY.NORMAL
73
    use_stderr = False
74
    use_stdin = False
75
    gives_corrected = False
76
    severity_map = None
77
78
    def lint(self, filename=None, file=None):
79
        """
80
        Takes a file and lints it using the linter variables defined apriori.
81
82
        :param filename:  The name of the file to execute.
83
        :param file:      The contents of the file as a list of strings.
84
        """
85
        if self.use_stdin:
86
            assert file is not None
87
        else:
88
            assert filename is not None
89
90
        self._create_std()
91
        command = self._create_command(filename)
92
93
        if self.use_stdin:
94
            self._write_stdin(file)
95
        process = subprocess.Popen(command,
96
                                   shell=True,
97
                                   stdin=self.stdin_file,
98
                                   stdout=self.stdout_file,
99
                                   stderr=self.stderr_file,
100
                                   universal_newlines=True)
101
        process.wait()
102
        stdout_output = self._read_stdout()
103
        stderr_output = self._read_stderr()
104
        results = stderr_output if self.use_stderr else stdout_output
105
        results = self.process_output(results, filename, file)
106
        if not self.use_stderr:
107
            self.__print_errors(stderr_output)
108
        self._close_std()
109
110
        return results
111
112
    def process_output(self, output, filename, file):
113
        if self.gives_corrected:
114
            return self._process_corrected(output, filename, file)
115
        else:
116
            return self._process_issues(output, filename, file)
117
118
    def _process_corrected(self, output, filename, file):
119
        for diff in self.__yield_diffs(file, output):
120
            yield Result(self,
121
                         self.diff_message,
122
                         affected_code=(diff.range(filename),),
123
                         diffs={filename: diff},
124
                         severity=self.diff_severity)
125
126
    def _process_issues(self, output, filename, file):
127
        regex = self.output_regex
128
        if isinstance(regex, str):
129
            regex = regex % {"file_name": filename}
130
131
        # Note: We join `output` because the regex may want to capture
132
        #       multiple lines also.
133
        for match in re.finditer(regex, "\n".join(output)):
134
            yield self.match_to_result(match, filename)
135
136
    def _get_groupdict(self, match):
137
        groups = match.groupdict()
138
        if (
139
                isinstance(self.severity_map, dict) and
140
                "severity" in groups and
141
                groups["severity"] in self.severity_map):
142
            groups["severity"] = self.severity_map[groups["severity"]]
143
        return groups
144
145
    def _create_command(self, filename):
146
        command = self.executable + ' ' + self.arguments
147
        if not self.use_stdin:
148
            command += ' ' + escape_path_argument(filename)
149
        return command
150
151
    def _create_std(self):
152
        self.stdin_file = tempfile.TemporaryFile()
153
        self.stdout_file = tempfile.TemporaryFile()
154
        self.stderr_file = tempfile.TemporaryFile()
155
156
    def _close_std(self):
157
        if not self.stdin_file.closed:
158
            self.stdin_file.close()
159
        if not self.stdout_file.closed:
160
            self.stdout_file.close()
161
        if not self.stderr_file.closed:
162
            self.stderr_file.close()
163
164
    def _read_stdout(self):
165
        self.stdout_file.seek(0)
166
        return [line.decode(sys.stdout.encoding, errors="replace")
167
                for line in self.stdout_file.readlines()]
168
169
    def _read_stderr(self):
170
        self.stderr_file.seek(0)
171
        return [line.decode(sys.stderr.encoding, errors="replace")
172
                for line in self.stderr_file.readlines()]
173
174
    def _write_stdin(self, file):
175
        self.stdin_file.write(bytes("".join(file), 'UTF-8'))
176
        self.stdin_file.seek(0)
177
178
    def __print_errors(self, errors):
179
        for line in filter(lambda error: bool(error.strip()), errors):
180
            self.warn(line)
181
182
    @staticmethod
183
    def __yield_diffs(file, new_file):
184
        if new_file != file:
185
            wholediff = Diff.from_string_arrays(file, new_file)
186
187
            for diff in wholediff.split_diff():
188
                yield diff
189
190
    def match_to_result(self, match, filename):
191
        """
192
        Converts a regex match's groups into a result.
193
194
        :param match:    The match got from regex parsing.
195
        :param filename: The name of the file from which this match is got.
196
        """
197
        groups = self._get_groupdict(match)
198
199
        # Pre process the groups
200
        for variable in ("line", "column", "end_line", "end_column"):
201
            if variable in groups and groups[variable]:
202
                groups[variable] = int(groups[variable])
203
204
        return Result.from_values(
205
            origin=groups.get("origin", self),
206
            message=groups.get("message", ""),
207
            file=filename,
208
            severity=int(groups.get("severity", RESULT_SEVERITY.NORMAL)),
209
            line=groups.get("line", None),
210
            column=groups.get("column", None),
211
            end_line=groups.get("end_line", None),
212
            end_column=groups.get("end_column", None))
213