Failed Conditions
Pull Request — master (#1522)
by Abdeali
01:38
created

generate_config_file()   B

Complexity

Conditions 5

Size

Total Lines 11

Duplication

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

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
223
        """
224
        Returns a configuation file from the section given to the bear.
225
        The section is available in `self.section`. To add the config
226
        file's name generated by this function to the arguments,
227
        use `{config_file}`.
228
229
        :return: A list of lines of the config file to be used.
230
        """
231
        return []
232