Completed
Pull Request — master (#1522)
by Abdeali
01:36
created

coalib.bearlib.abstractions.Lint   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 197
Duplicated Lines 0 %
Metric Value
dl 0
loc 197
rs 8.2608
wmc 40

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __print_errors() 0 3 3
A _process_corrected() 0 7 2
B match_to_result() 0 27 5
B generate_config_file() 0 11 5
C lint() 0 39 7
A process_output() 0 5 2
A _get_groupdict() 0 8 4
A _write() 0 4 1
A _process_issues() 0 9 3
A _read() 0 5 2
A config_file() 0 11 1
A _create_command() 0 5 2
A __yield_diffs() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like coalib.bearlib.abstractions.Lint often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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. Note that we use .format()
44
                            on the arguments - so, `{abc}` needs to be given
45
                            as `{{abc}}`.
46
    :param output_regex:    The regex which will match the output of the linter
47
                            to get results. This regex should give out the
48
                            following variables:
49
                             line - The line where the issue starts.
50
                             column - The column where the issue starts.
51
                             end_line - The line where the issue ends.
52
                             end_column - The column where the issue ends.
53
                             severity - The severity of the issue.
54
                             message - The message of the result.
55
                             origin - The origin of the issue.
56
                            This is not used if `gives_corrected` is set.
57
    :param diff_severity:   The severity to use for all results if
58
                            `gives_corrected` is set.
59
    :param diff_message:    The message to use for all results if
60
                            `gives_corrected` is set.
61
    :param use_stderr:      Uses stderr as the output stream is it's True.
62
    :param use_stdin:       Sends file as stdin instead of giving the file name.
63
    :param gives_corrected: True if the executable gives the corrected file
64
                            or just the issues.
65
    :param severity_map:    A dict where the keys are the possible severity
66
                            values the Linter gives out and the values are the
67
                            severity of the coala Result to set it to. If it is
68
                            not a dict, it is ignored.
69
    """
70
    check_prerequisites = classmethod(is_binary_present)
71
    executable = None
72
    arguments = ""
73
    output_regex = re.compile(r'(?P<line>\d+)\.(?P<column>\d+)\|'
74
                              r'(?P<severity>\d+): (?P<message>.*)')
75
    diff_message = 'No result message was set'
76
    diff_severity = RESULT_SEVERITY.NORMAL
77
    use_stderr = False
78
    use_stdin = False
79
    gives_corrected = False
80
    severity_map = None
81
82
    def lint(self, filename=None, file=None):
83
        """
84
        Takes a file and lints it using the linter variables defined apriori.
85
86
        :param filename:  The name of the file to execute.
87
        :param file:      The contents of the file as a list of strings.
88
        """
89
        assert ((self.use_stdin and file is not None) or
90
                (not self.use_stdin and filename is not None))
91
92
        stdin_file = tempfile.TemporaryFile()
93
        stdout_file = tempfile.TemporaryFile()
94
        stderr_file = tempfile.TemporaryFile()
95
96
        config_file = self.generate_config_file()
97
        command = self._create_command(filename, config_file=config_file)
98
99
        if self.use_stdin:
100
            self._write(file, stdin_file, sys.stdin.encoding)
101
        process = subprocess.Popen(command,
102
                                   shell=True,
103
                                   stdin=stdin_file,
104
                                   stdout=stdout_file,
105
                                   stderr=stderr_file,
106
                                   universal_newlines=True)
107
        process.wait()
108
        stdout_output = self._read(stdout_file, sys.stdout.encoding)
109
        stderr_output = self._read(stderr_file, sys.stderr.encoding)
110
        results_output = stderr_output if self.use_stderr else stdout_output
111
        results = self.process_output(results_output, filename, file)
112
        for file in (stdin_file, stdout_file, stderr_file):
113
            file.close()
114
        if config_file:
115
            os.remove(config_file)
116
117
        if not self.use_stderr:
118
            self.__print_errors(stderr_output)
119
120
        return results
121
122
    def process_output(self, output, filename, file):
123
        if self.gives_corrected:
124
            return self._process_corrected(output, filename, file)
125
        else:
126
            return self._process_issues(output, filename)
127
128
    def _process_corrected(self, output, filename, file):
129
        for diff in self.__yield_diffs(file, output):
130
            yield Result(self,
131
                         self.diff_message,
132
                         affected_code=(diff.range(filename),),
133
                         diffs={filename: diff},
134
                         severity=self.diff_severity)
135
136
    def _process_issues(self, output, filename):
137
        regex = self.output_regex
138
        if isinstance(regex, str):
139
            regex = regex % {"file_name": filename}
140
141
        # Note: We join `output` because the regex may want to capture
142
        #       multiple lines also.
143
        for match in re.finditer(regex, "".join(output)):
144
            yield self.match_to_result(match, filename)
145
146
    def _get_groupdict(self, match):
147
        groups = match.groupdict()
148
        if (
149
                isinstance(self.severity_map, dict) and
150
                "severity" in groups and
151
                groups["severity"] in self.severity_map):
152
            groups["severity"] = self.severity_map[groups["severity"]]
153
        return groups
154
155
    def _create_command(self, filename, **kwargs):
156
        command = self.executable + ' ' + self.arguments
157
        if not self.use_stdin:
158
            command += ' ' + escape_path_argument(filename)
159
        return command.format(**kwargs)
160
161
    @staticmethod
162
    def _read(file, encoding):
163
        file.seek(0)
164
        return [line.decode(encoding or 'UTF-8', errors="replace")
165
                for line in file.readlines()]
166
167
    @staticmethod
168
    def _write(file_contents, file, encoding):
169
        file.write(bytes("".join(file_contents), encoding or 'UTF-8'))
170
        file.seek(0)
171
172
    def __print_errors(self, errors):
173
        for line in filter(lambda error: bool(error.strip()), errors):
174
            self.warn(line)
175
176
    @staticmethod
177
    def __yield_diffs(file, new_file):
178
        if new_file != file:
179
            wholediff = Diff.from_string_arrays(file, new_file)
180
181
            for diff in wholediff.split_diff():
182
                yield diff
183
184
    def match_to_result(self, match, filename):
185
        """
186
        Converts a regex match's groups into a result.
187
188
        :param match:    The match got from regex parsing.
189
        :param filename: The name of the file from which this match is got.
190
        """
191
        groups = self._get_groupdict(match)
192
193
        # Pre process the groups
194
        for variable in ("line", "column", "end_line", "end_column"):
195
            if variable in groups and groups[variable]:
196
                groups[variable] = int(groups[variable])
197
198
        if "origin" in groups:
199
            groups['origin'] = "{} ({})".format(str(self.__class__.__name__),
200
                                                str(groups["origin"]))
201
202
        return Result.from_values(
203
            origin=groups.get("origin", self),
204
            message=groups.get("message", ""),
205
            file=filename,
206
            severity=int(groups.get("severity", RESULT_SEVERITY.NORMAL)),
207
            line=groups.get("line", None),
208
            column=groups.get("column", None),
209
            end_line=groups.get("end_line", None),
210
            end_column=groups.get("end_column", None))
211
212
    def generate_config_file(self):
213
        config_lines = self.config_file()
214
        config_file = ""
215
        if config_lines:
216
            for i, line in enumerate(config_lines):
217
                config_lines[i] = line if line.endswith("\n") else line + "\n"
218
            config_fd, config_file = tempfile.mkstemp()
219
            os.close(config_fd)
220
            with open(config_file, 'w') as file:
221
                file.writelines(config_lines)
222
        return config_file
223
224
    @staticmethod
225
    def config_file():
226
        """
227
        Returns a configuation file from the section given to the bear.
228
        The section is available in `self.section`. To add the config
229
        file's name generated by this function to the arguments,
230
        use `{config_file}`.
231
232
        :return: A list of lines of the config file to be used.
233
        """
234
        return []
235