Completed
Pull Request — master (#1566)
by Abdeali
01:25
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 os
2
import re
3
import shutil
4
import tempfile
5
6
from coalib.bears.Bear import Bear
7
from coalib.misc.Shell import escape_path_argument, run_shell_command
8
from coalib.results.Diff import Diff
9
from coalib.results.Result import Result
10
from coalib.results.RESULT_SEVERITY import RESULT_SEVERITY
11
12
13
def is_binary_present(cls):
14
    """
15
    Checks whether the needed binary is present.
16
17
    The function is intended be used with classes
18
    having an executable member which will be checked.
19
20
    :return: True if binary is present, or is not required.
21
             not True otherwise, with a string containing a
22
             detailed description of what's missing.
23
    """
24
    try:
25
        if cls.executable is None:
26
            return True
27
        if shutil.which(cls.executable) is None:
28
            return repr(cls.executable) + " is not installed."
29
        else:
30
            return True
31
    except AttributeError:
32
        # Happens when `executable` does not exist in `cls`.
33
        return True
34
35
36
class Lint(Bear):
37
    """
38
    Deals with the creation of linting bears.
39
40
    For the tutorial see:
41
    http://coala.readthedocs.org/en/latest/Users/Tutorials/Linter_Bears.html
42
43
    :param executable:      The executable to run the linter.
44
    :param arguments:       The arguments to supply to the linter, such
45
                            that the file name to be analyzed can be
46
                            appended to the end. Note that we use .format()
47
                            on the arguments - so, `{abc}` needs to be given
48
                            as `{{abc}}`.
49
                            Currently, the following will be replaced:
50
                            {filename}    - the filename passed to lint()
51
                            {config_file} - The config file created using
52
                                            config_file()
53
    :param output_regex:    The regex which will match the output of the linter
54
                            to get results. This regex should give out the
55
                            following variables:
56
                             line - The line where the issue starts.
57
                             column - The column where the issue starts.
58
                             end_line - The line where the issue ends.
59
                             end_column - The column where the issue ends.
60
                             severity - The severity of the issue.
61
                             message - The message of the result.
62
                             origin - The origin of the issue.
63
                            This is not used if `gives_corrected` is set.
64
    :param diff_severity:   The severity to use for all results if
65
                            `gives_corrected` is set.
66
    :param diff_message:    The message to use for all results if
67
                            `gives_corrected` is set.
68
    :param use_stderr:      Uses stderr as the output stream is it's True.
69
    :param use_stdin:       Sends file as stdin instead of giving the file name.
70
    :param gives_corrected: True if the executable gives the corrected file
71
                            or just the issues.
72
    :param severity_map:    A dict where the keys are the possible severity
73
                            values the Linter gives out and the values are the
74
                            severity of the coala Result to set it to. If it is
75
                            not a dict, it is ignored.
76
    """
77
    check_prerequisites = classmethod(is_binary_present)
78
    executable = None
79
    arguments = ""
80
    output_regex = re.compile(r'(?P<line>\d+)\.(?P<column>\d+)\|'
81
                              r'(?P<severity>\d+): (?P<message>.*)')
82
    diff_message = 'No result message was set'
83
    diff_severity = RESULT_SEVERITY.NORMAL
84
    use_stderr = False
85
    use_stdin = False
86
    gives_corrected = False
87
    severity_map = None
88
89
    def lint(self, filename=None, file=None):
90
        """
91
        Takes a file and lints it using the linter variables defined apriori.
92
93
        :param filename:  The name of the file to execute.
94
        :param file:      The contents of the file as a list of strings.
95
        """
96
        assert ((self.use_stdin and file is not None) or
97
                (not self.use_stdin and filename is not None))
98
99
        config_file = self.generate_config_file()
100
        command = self._create_command(filename=filename,
101
                                       config_file=config_file)
102
103
        stdin_input = "".join(file) if self.use_stdin else None
104
        stdout_output, stderr_output = run_shell_command(command,
105
                                                         stdin=stdin_input)
106
        stdout_output = tuple(stdout_output.splitlines(keepends=True))
107
        stderr_output = tuple(stderr_output.splitlines(keepends=True))
108
        results_output = stderr_output if self.use_stderr else stdout_output
109
        results = self.process_output(results_output, filename, file)
110
        if not self.use_stderr:
111
            self._print_errors(stderr_output)
112
113
        if config_file:
114
            os.remove(config_file)
115
116
        return results
117
118
    def process_output(self, output, filename, file):
119
        if self.gives_corrected:
120
            return self._process_corrected(output, filename, file)
121
        else:
122
            return self._process_issues(output, filename)
123
124
    def _process_corrected(self, output, filename, file):
125
        for diff in self.__yield_diffs(file, output):
126
            yield Result(self,
127
                         self.diff_message,
128
                         affected_code=(diff.range(filename),),
129
                         diffs={filename: diff},
130
                         severity=self.diff_severity)
131
132
    def _process_issues(self, output, filename):
133
        regex = self.output_regex
134
        if isinstance(regex, str):
135
            regex = regex % {"file_name": filename}
136
137
        # Note: We join `output` because the regex may want to capture
138
        #       multiple lines also.
139
        for match in re.finditer(regex, "".join(output)):
140
            yield self.match_to_result(match, filename)
141
142
    def _get_groupdict(self, match):
143
        groups = match.groupdict()
144
        if (
145
                isinstance(self.severity_map, dict) and
146
                "severity" in groups and
147
                groups["severity"] in self.severity_map):
148
            groups["severity"] = self.severity_map[groups["severity"]]
149
        return groups
150
151
    def _create_command(self, **kwargs):
152
        command = self.executable + ' ' + self.arguments
153
        for key in ("filename", "config_file"):
154
            kwargs[key] = escape_path_argument(kwargs.get(key, "") or "")
155
        return command.format(**kwargs)
156
157
    def _print_errors(self, errors):
158
        for line in filter(lambda error: bool(error.strip()), errors):
159
            self.warn(line)
160
161
    @staticmethod
162
    def __yield_diffs(file, new_file):
163
        if tuple(new_file) != tuple(file):
164
            wholediff = Diff.from_string_arrays(file, new_file)
165
166
            for diff in wholediff.split_diff():
167
                yield diff
168
169
    def match_to_result(self, match, filename):
170
        """
171
        Converts a regex match's groups into a result.
172
173
        :param match:    The match got from regex parsing.
174
        :param filename: The name of the file from which this match is got.
175
        """
176
        groups = self._get_groupdict(match)
177
178
        # Pre process the groups
179
        for variable in ("line", "column", "end_line", "end_column"):
180
            if variable in groups and groups[variable]:
181
                groups[variable] = int(groups[variable])
182
183
        if "origin" in groups:
184
            groups['origin'] = "{} ({})".format(str(self.__class__.__name__),
185
                                                str(groups["origin"]))
186
187
        return Result.from_values(
188
            origin=groups.get("origin", self),
189
            message=groups.get("message", ""),
190
            file=filename,
191
            severity=int(groups.get("severity", RESULT_SEVERITY.NORMAL)),
192
            line=groups.get("line", None),
193
            column=groups.get("column", None),
194
            end_line=groups.get("end_line", None),
195
            end_column=groups.get("end_column", None))
196
197
    def generate_config_file(self):
198
        """
199
        Generates a temporary config file.
200
        Note: The user of the function is responsible for deleting the
201
        tempfile when done with it.
202
203
        :return: The file name of the tempfile created.
204
        """
205
        config_lines = self.config_file()
206
        config_file = ""
207
        if config_lines is not None:
208
            for i, line in enumerate(config_lines):
209
                config_lines[i] = line if line.endswith("\n") else line + "\n"
210
            config_fd, config_file = tempfile.mkstemp()
211
            os.close(config_fd)
212
            with open(config_file, 'w') as conf_file:
213
                conf_file.writelines(config_lines)
214
        return config_file
215
216
    @staticmethod
217
    def config_file():
218
        """
219
        Returns a configuation file from the section given to the bear.
220
        The section is available in `self.section`. To add the config
221
        file's name generated by this function to the arguments,
222
        use `{config_file}`.
223
224
        :return: A list of lines of the config file to be used or None.
225
        """
226
        return None
227