Completed
Pull Request — master (#1941)
by Abdeali
01:40
created

check_prerequisites()   A

Complexity

Conditions 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 19
rs 9.4285
1
import os
2
import re
3
import shutil
4
from subprocess import check_call, CalledProcessError, DEVNULL
5
import tempfile
6
7
from coalib.bears.Bear import Bear
8
from coalib.misc.Decorators import enforce_signature
9
from coalib.misc.Shell import escape_path_argument, run_shell_command
10
from coalib.results.Diff import Diff
11
from coalib.results.Result import Result
12
from coalib.results.RESULT_SEVERITY import RESULT_SEVERITY
13
14
15
class Lint(Bear):
16
    """
17
    Deals with the creation of linting bears.
18
19
    For the tutorial see:
20
    http://coala.readthedocs.org/en/latest/Users/Tutorials/Linter_Bears.html
21
22
    :param executable:                  The executable to run the linter.
23
    :param prerequisite_command:        The command to run as a prerequisite
24
                                        and is of type ``list``.
25
    :param prerequisites_fail_msg:      The message to be displayed if the
26
                                        prerequisite fails.
27
    :param arguments:                   The arguments to supply to the linter,
28
                                        such that the file name to be analyzed
29
                                        can be appended to the end. Note that
30
                                        we use ``.format()`` on the arguments -
31
                                        so, ``{abc}`` needs to be given as
32
                                        ``{{abc}}``. Currently, the following
33
                                        will be replaced:
34
35
                                         - ``{filename}`` - The filename passed
36
                                           to ``lint()``
37
                                         - ``{config_file}`` - The config file
38
                                           created using ``config_file()``
39
40
    :param output_regex:    The regex which will match the output of the linter
41
                            to get results. This is not used if
42
                            ``gives_corrected`` is set. This regex should give
43
                            out the following variables:
44
45
                             - line - The line where the issue starts.
46
                             - column - The column where the issue starts.
47
                             - end_line - The line where the issue ends.
48
                             - end_column - The column where the issue ends.
49
                             - severity - The severity of the issue.
50
                             - message - The message of the result.
51
                             - origin - The origin of the issue.
52
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
    executable = None
67
    prerequisite_command = None
68
    prerequisite_fail_msg = 'Unknown failure.'
69
    arguments = ""
70
    output_regex = re.compile(r'(?P<line>\d+)\.(?P<column>\d+)\|'
71
                              r'(?P<severity>\d+): (?P<message>.*)')
72
    diff_message = 'No result message was set'
73
    diff_severity = RESULT_SEVERITY.NORMAL
74
    use_stderr = False
75
    use_stdin = False
76
    gives_corrected = False
77
    severity_map = None
78
79
    def lint(self, filename=None, file=None):
80
        """
81
        Takes a file and lints it using the linter variables defined apriori.
82
83
        :param filename:  The name of the file to execute.
84
        :param file:      The contents of the file as a list of strings.
85
        """
86
        assert ((self.use_stdin and file is not None) or
87
                (not self.use_stdin and filename is not None))
88
89
        config_file = self.generate_config_file()
90
        self.command = self._create_command(filename=filename,
91
                                            config_file=config_file)
92
93
        stdin_input = "".join(file) if self.use_stdin else None
94
        stdout_output, stderr_output = run_shell_command(self.command,
95
                                                         stdin=stdin_input)
96
        self.stdout_output = tuple(stdout_output.splitlines(keepends=True))
97
        self.stderr_output = tuple(stderr_output.splitlines(keepends=True))
98
        results_output = (self.stderr_output if self.use_stderr
99
                          else self.stdout_output)
100
        results = self.process_output(results_output, filename, file)
101
        if not self.use_stderr:
102
            self._print_errors(self.stderr_output)
103
104
        if config_file:
105
            os.remove(config_file)
106
107
        return results
108
109
    def process_output(self, output, filename, file):
110
        """
111
        Take the output (from stdout or stderr) and use it to create Results.
112
        If the class variable ``gives_corrected`` is set to True, the
113
        ``_process_corrected()`` is called. If it is False,
114
        ``_process_issues()`` is called.
115
116
        :param output:   The output to be used to obtain Results from. The
117
                         output is either stdout or stderr depending on the
118
                         class variable ``use_stderr``.
119
        :param filename: The name of the file whose output is being processed.
120
        :param file:     The contents of the file whose output is being
121
                         processed.
122
        :return:         Generator which gives Results produced based on this
123
                         output.
124
        """
125
        if self.gives_corrected:
126
            return self._process_corrected(output, filename, file)
127
        else:
128
            return self._process_issues(output, filename)
129
130
    def _process_corrected(self, output, filename, file):
131
        """
132
        Process the output and use it to create Results by creating diffs.
133
        The diffs are created by comparing the output and the original file.
134
135
        :param output:   The corrected file contents.
136
        :param filename: The name of the file.
137
        :param file:     The original contents of the file.
138
        :return:         Generator which gives Results produced based on the
139
                         diffs created by comparing the original and corrected
140
                         contents.
141
        """
142
        for diff in self.__yield_diffs(file, output):
143
            yield Result(self,
144
                         self.diff_message,
145
                         affected_code=(diff.range(filename),),
146
                         diffs={filename: diff},
147
                         severity=self.diff_severity)
148
149
    def _process_issues(self, output, filename):
150
        """
151
        Process the output using the regex provided in ``output_regex`` and
152
        use it to create Results by using named captured groups from the regex.
153
154
        :param output:   The output to be parsed by regex.
155
        :param filename: The name of the file.
156
        :param file:     The original contents of the file.
157
        :return:         Generator which gives Results produced based on regex
158
                         matches using the ``output_regex`` provided and the
159
                         ``output`` parameter.
160
        """
161
        regex = self.output_regex
162
        if isinstance(regex, str):
163
            regex = regex % {"file_name": filename}
164
165
        # Note: We join ``output`` because the regex may want to capture
166
        #       multiple lines also.
167
        for match in re.finditer(regex, "".join(output)):
168
            yield self.match_to_result(match, filename)
169
170
    def _get_groupdict(self, match):
171
        """
172
        Convert a regex match's groups into a dictionary with data to be used
173
        to create a Result. This is used internally in ``match_to_result``.
174
175
        :param match:    The match got from regex parsing.
176
        :param filename: The name of the file from which this match is got.
177
        :return:         The dictionary containing the information:
178
                         - line - The line where the result starts.
179
                         - column - The column where the result starts.
180
                         - end_line - The line where the result ends.
181
                         - end_column - The column where the result ends.
182
                         - severity - The severity of the result.
183
                         - message - The message of the result.
184
                         - origin - The origin of the result.
185
        """
186
        groups = match.groupdict()
187
        if (
188
                isinstance(self.severity_map, dict) and
189
                "severity" in groups and
190
                groups["severity"] in self.severity_map):
191
            groups["severity"] = self.severity_map[groups["severity"]]
192
        return groups
193
194
    def _create_command(self, **kwargs):
195
        command = self.executable + ' ' + self.arguments
196
        for key in ("filename", "config_file"):
197
            kwargs[key] = escape_path_argument(kwargs.get(key, "") or "")
198
        return command.format(**kwargs)
199
200
    def _print_errors(self, errors):
201
        for line in filter(lambda error: bool(error.strip()), errors):
202
            self.warn(line)
203
204
    @staticmethod
205
    def __yield_diffs(file, new_file):
206
        if tuple(new_file) != tuple(file):
207
            wholediff = Diff.from_string_arrays(file, new_file)
208
209
            for diff in wholediff.split_diff():
210
                yield diff
211
212
    def match_to_result(self, match, filename):
213
        """
214
        Convert a regex match's groups into a coala Result object.
215
216
        :param match:    The match got from regex parsing.
217
        :param filename: The name of the file from which this match is got.
218
        :return:         The Result object.
219
        """
220
        groups = self._get_groupdict(match)
221
222
        # Pre process the groups
223
        for variable in ("line", "column", "end_line", "end_column"):
224
            if variable in groups and groups[variable]:
225
                groups[variable] = int(groups[variable])
226
227
        if "origin" in groups:
228
            groups['origin'] = "{} ({})".format(str(self.__class__.__name__),
229
                                                str(groups["origin"]))
230
231
        return Result.from_values(
232
            origin=groups.get("origin", self),
233
            message=groups.get("message", ""),
234
            file=filename,
235
            severity=int(groups.get("severity", RESULT_SEVERITY.NORMAL)),
236
            line=groups.get("line", None),
237
            column=groups.get("column", None),
238
            end_line=groups.get("end_line", None),
239
            end_column=groups.get("end_column", None))
240
241
    @classmethod
242
    def check_prerequisites(cls):
243
        """
244
        Checks for prerequisites required by the Linter Bear.
245
246
        It uses the class variables:
247
        -  ``executable`` - Checks that it is available in the PATH using
248
        ``shutil.which``.
249
        -  ``prerequisite_command`` - Checks that when this command is run,
250
        the exitcode is 0. If it is not zero, ``prerequisite_fail_msg``
251
        is gives as the failure message.
252
253
        If either of them is set to ``None`` that check is ignored.
254
255
        :return: True is all checks are valid, else False.
256
        """
257
        return cls._check_command(executable=cls.executable,
258
                                  command=cls.prerequisite_command,
259
                                  fail_msg=cls.prerequisite_fail_msg)
260
261
    @classmethod
262
    @enforce_signature
263
    def _check_command(cls, executable, command: (list, tuple, None), fail_msg):
264
        """
265
        Checks whether the required executable is found and the
266
        required command succesfully executes.
267
268
        The function is intended be used with classes having an
269
        executable, prerequisite_command and prerequisite_fail_msg.
270
271
        :param executable:   The executable to check for.
272
        :param command:      The command to check as a prerequisite.
273
        :param fail_msg:     The fail message to display when the
274
                             command doesn't return an exitcode of zero.
275
276
        :return: True if command successfully executes, or is not required.
277
                 not True otherwise, with a string containing a
278
                 detailed description of the error.
279
        """
280
        if cls._check_executable(executable):
281
            if command is None:
282
                return True  # when there are no prerequisites
283
            try:
284
                check_call(command, stdout=DEVNULL, stderr=DEVNULL)
285
                return True
286
            except (OSError, CalledProcessError):
287
                return fail_msg
288
        else:
289
            return repr(executable) + " is not installed."
290
291
    @staticmethod
292
    def _check_executable(executable):
293
        """
294
        Checks whether the needed executable is present in the system.
295
296
        :param executable: The executable to check for.
297
298
        :return: True if binary is present, or is not required.
299
                 not True otherwise, with a string containing a
300
                 detailed description of what's missing.
301
        """
302
        if executable is None:
303
            return True
304
        return shutil.which(executable) is not None
305
306
    def generate_config_file(self):
307
        """
308
        Generates a temporary config file.
309
        Note: The user of the function is responsible for deleting the
310
        tempfile when done with it.
311
312
        :return: The file name of the tempfile created.
313
        """
314
        config_lines = self.config_file()
315
        config_file = ""
316
        if config_lines is not None:
317
            for i, line in enumerate(config_lines):
318
                config_lines[i] = line if line.endswith("\n") else line + "\n"
319
            config_fd, config_file = tempfile.mkstemp()
320
            os.close(config_fd)
321
            with open(config_file, 'w') as conf_file:
322
                conf_file.writelines(config_lines)
323
        return config_file
324
325
    @staticmethod
326
    def config_file():
327
        """
328
        Returns a configuation file from the section given to the bear.
329
        The section is available in ``self.section``. To add the config
330
        file's name generated by this function to the arguments,
331
        use ``{config_file}``.
332
333
        :return: A list of lines of the config file to be used or None.
334
        """
335
        return None
336