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

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

Complexity

Conditions 3

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 5
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)
86
                or (not self.use_stdin and filename is not None))
87
88
        self._create_std()
89
        command = self._create_command(filename)
90
91
        if self.use_stdin:
92
            self._write_stdin(file)
93
        process = subprocess.Popen(command,
94
                                   shell=True,
95
                                   stdin=self.stdin_file,
96
                                   stdout=self.stdout_file,
97
                                   stderr=self.stderr_file,
98
                                   universal_newlines=True)
99
        process.wait()
100
        stdout_output = self._read(sys.sdout)
101
        stderr_output = self._read(sys.stderr)
102
        results_output = stderr_output if self.use_stderr else stdout_output
103
        results = self.process_output(results_output, filename, file)
104
        if not self.use_stderr:
105
            self.__print_errors(stderr_output)
106
        self._close_std()
107
108
        return results
109
110
    def process_output(self, output, filename, file):
111
        if self.gives_corrected:
112
            return self._process_corrected(output, filename, file)
113
        else:
114
            return self._process_issues(output, filename)
115
116
    def _process_corrected(self, output, filename, file):
117
        for diff in self.__yield_diffs(file, output):
118
            yield Result(self,
119
                         self.diff_message,
120
                         affected_code=(diff.range(filename),),
121
                         diffs={filename: diff},
122
                         severity=self.diff_severity)
123
124
    def _process_issues(self, output, filename):
125
        regex = self.output_regex
126
        if isinstance(regex, str):
127
            regex = regex % {"file_name": filename}
128
129
        # Note: We join `output` because the regex may want to capture
130
        #       multiple lines also.
131
        for match in re.finditer(regex, "".join(output)):
132
            yield self.match_to_result(match, filename)
133
134
    def _get_groupdict(self, match):
135
        groups = match.groupdict()
136
        if (
137
                isinstance(self.severity_map, dict) and
138
                "severity" in groups and
139
                groups["severity"] in self.severity_map):
140
            groups["severity"] = self.severity_map[groups["severity"]]
141
        return groups
142
143
    def _create_command(self, filename):
144
        command = self.executable + ' ' + self.arguments
145
        if not self.use_stdin:
146
            command += ' ' + escape_path_argument(filename)
147
        return command
148
149
    def _create_std(self):
150
        self.stdin_file = tempfile.TemporaryFile()
151
        self.stdout_file = tempfile.TemporaryFile()
152
        self.stderr_file = tempfile.TemporaryFile()
153
154
    def _close_std(self):
155
        self.stdin_file.close()
156
        self.stdout_file.close()
157
        self.stderr_file.close()
158
159
    def _read(self, std):
160
        file = self.stdout_file if std == sys.stdout else self.stderr_file
161
        file.seek(0)
162
        return (line.decode(std.encoding, errors="replace")
163
                for line in file.readlines())
164
165
    def _write_stdin(self, file):
166
        self.stdin_file.write(bytes("".join(file), 'UTF-8'))
167
        self.stdin_file.seek(0)
168
169
    def __print_errors(self, errors):
170
        for line in filter(lambda error: bool(error.strip()), errors):
171
            self.warn(line)
172
173
    @staticmethod
174
    def __yield_diffs(file, new_file):
175
        if new_file != file:
176
            wholediff = Diff.from_string_arrays(file, new_file)
177
178
            for diff in wholediff.split_diff():
179
                yield diff
180
181
    def match_to_result(self, match, filename):
182
        """
183
        Converts a regex match's groups into a result.
184
185
        :param match:    The match got from regex parsing.
186
        :param filename: The name of the file from which this match is got.
187
        """
188
        groups = self._get_groupdict(match)
189
190
        # Pre process the groups
191
        for variable in ("line", "column", "end_line", "end_column"):
192
            if variable in groups and groups[variable]:
193
                groups[variable] = int(groups[variable])
194
195
        return Result.from_values(
196
            origin=groups.get("origin", self),
197
            message=groups.get("message", ""),
198
            file=filename,
199
            severity=int(groups.get("severity", RESULT_SEVERITY.NORMAL)),
200
            line=groups.get("line", None),
201
            column=groups.get("column", None),
202
            end_line=groups.get("end_line", None),
203
            end_column=groups.get("end_column", None))
204