Completed
Pull Request — master (#1404)
by Abdeali
04:50 queued 03:14
created

_process_corrected()   A

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 7
rs 9.4285
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
        assert ((self.use_stdin and file is not None) or
86
                (not self.use_stdin and filename is not None))
87
88
        stdin_file = tempfile.TemporaryFile()
89
        stdout_file = tempfile.TemporaryFile()
90
        stderr_file = tempfile.TemporaryFile()
91
92
        command = self._create_command(filename)
93
94
        if self.use_stdin:
95
            self._write(file, stdin_file, sys.stdin.encoding)
96
        process = subprocess.Popen(command,
97
                                   shell=True,
98
                                   stdin=stdin_file,
99
                                   stdout=stdout_file,
100
                                   stderr=stderr_file,
101
                                   universal_newlines=True)
102
        process.wait()
103
        stdout_output = self._read(stdout_file, sys.stdout.encoding)
104
        stderr_output = self._read(stderr_file, sys.stderr.encoding)
105
        results_output = stderr_output if self.use_stderr else stdout_output
106
        results = self.process_output(results_output, filename, file)
107
        if not self.use_stderr:
108
            self.__print_errors(stderr_output)
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)
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):
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, "".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
    @staticmethod
152
    def _read(file, encoding):
153
        file.seek(0)
154
        return [line.decode(encoding or 'UTF-8', errors="replace")
155
                for line in file.readlines()]
156
157
    @staticmethod
158
    def _write(file_contents, file, encoding):
159
        file.write(bytes("".join(file_contents), encoding or 'UTF-8'))
160
        file.seek(0)
161
162
    def __print_errors(self, errors):
163
        for line in filter(lambda error: bool(error.strip()), errors):
164
            self.warn(line)
165
166
    @staticmethod
167
    def __yield_diffs(file, new_file):
168
        if new_file != file:
169
            wholediff = Diff.from_string_arrays(file, new_file)
170
171
            for diff in wholediff.split_diff():
172
                yield diff
173
174
    def match_to_result(self, match, filename):
175
        """
176
        Converts a regex match's groups into a result.
177
178
        :param match:    The match got from regex parsing.
179
        :param filename: The name of the file from which this match is got.
180
        """
181
        groups = self._get_groupdict(match)
182
183
        # Pre process the groups
184
        for variable in ("line", "column", "end_line", "end_column"):
185
            if variable in groups and groups[variable]:
186
                groups[variable] = int(groups[variable])
187
188
        return Result.from_values(
189
            origin=groups.get("origin", self),
190
            message=groups.get("message", ""),
191
            file=filename,
192
            severity=int(groups.get("severity", RESULT_SEVERITY.NORMAL)),
193
            line=groups.get("line", None),
194
            column=groups.get("column", None),
195
            end_line=groups.get("end_line", None),
196
            end_column=groups.get("end_column", None))
197