Completed
Pull Request — master (#1522)
by Abdeali
04:57
created

coalib.bearlib.abstractions.Lint   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 181
Duplicated Lines 0 %
Metric Value
dl 0
loc 181
rs 8.8
wmc 36

11 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 lint() 0 27 6
A process_output() 0 5 2
A _get_groupdict() 0 8 4
A _process_issues() 0 9 3
A _create_command() 0 5 2
A __yield_diffs() 0 7 3
B generate_config_file() 0 18 5
A config_file() 0 11 1
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
    :param executable:      The executable to run the linter.
39
    :param arguments:       The arguments to supply to the linter, such
40
                            that the file name to be analyzed can be
41
                            appended to the end. Note that we use .format()
42
                            on the arguments - so, `{abc}` needs to be given
43
                            as `{{abc}}`.
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
        config_file = self.generate_config_file()
91
        command = self._create_command(filename, config_file=config_file)
92
93
        stdin_input = "".join(file) if self.use_stdin else None
94
        stdout_output, stderr_output = run_shell_command(command,
95
                                                         stdin=stdin_input)
96
        stdout_output = tuple(stdout_output.splitlines(keepends=True))
97
        stderr_output = tuple(stderr_output.splitlines(keepends=True))
98
        results_output = stderr_output if self.use_stderr else stdout_output
99
        results = self.process_output(results_output, filename, file)
100
        if not self.use_stderr:
101
            self.__print_errors(stderr_output)
102
103
        if config_file:
104
            os.remove(config_file)
105
106
        return results
107
108
    def process_output(self, output, filename, file):
109
        if self.gives_corrected:
110
            return self._process_corrected(output, filename, file)
111
        else:
112
            return self._process_issues(output, filename)
113
114
    def _process_corrected(self, output, filename, file):
115
        for diff in self.__yield_diffs(file, output):
116
            yield Result(self,
117
                         self.diff_message,
118
                         affected_code=(diff.range(filename),),
119
                         diffs={filename: diff},
120
                         severity=self.diff_severity)
121
122
    def _process_issues(self, output, filename):
123
        regex = self.output_regex
124
        if isinstance(regex, str):
125
            regex = regex % {"file_name": filename}
126
127
        # Note: We join `output` because the regex may want to capture
128
        #       multiple lines also.
129
        for match in re.finditer(regex, "".join(output)):
130
            yield self.match_to_result(match, filename)
131
132
    def _get_groupdict(self, match):
133
        groups = match.groupdict()
134
        if (
135
                isinstance(self.severity_map, dict) and
136
                "severity" in groups and
137
                groups["severity"] in self.severity_map):
138
            groups["severity"] = self.severity_map[groups["severity"]]
139
        return groups
140
141
    def _create_command(self, filename, **kwargs):
142
        command = self.executable + ' ' + self.arguments
143
        if not self.use_stdin:
144
            command += ' ' + escape_path_argument(filename)
145
        return command.format(**kwargs)
146
147
    def __print_errors(self, errors):
148
        for line in filter(lambda error: bool(error.strip()), errors):
149
            self.warn(line)
150
151
    @staticmethod
152
    def __yield_diffs(file, new_file):
153
        if tuple(new_file) != tuple(file):
154
            wholediff = Diff.from_string_arrays(file, new_file)
155
156
            for diff in wholediff.split_diff():
157
                yield diff
158
159
    def match_to_result(self, match, filename):
160
        """
161
        Converts a regex match's groups into a result.
162
163
        :param match:    The match got from regex parsing.
164
        :param filename: The name of the file from which this match is got.
165
        """
166
        groups = self._get_groupdict(match)
167
168
        # Pre process the groups
169
        for variable in ("line", "column", "end_line", "end_column"):
170
            if variable in groups and groups[variable]:
171
                groups[variable] = int(groups[variable])
172
173
        if "origin" in groups:
174
            groups['origin'] = "{} ({})".format(str(self.__class__.__name__),
175
                                                str(groups["origin"]))
176
177
        return Result.from_values(
178
            origin=groups.get("origin", self),
179
            message=groups.get("message", ""),
180
            file=filename,
181
            severity=int(groups.get("severity", RESULT_SEVERITY.NORMAL)),
182
            line=groups.get("line", None),
183
            column=groups.get("column", None),
184
            end_line=groups.get("end_line", None),
185
            end_column=groups.get("end_column", None))
186
187
    def generate_config_file(self):
188
        """
189
        Generates a temporary config file.
190
        Note: The user of the function is responsible for deleting the
191
        tempfile when done with it.
192
193
        :return: The file name of the tempfile created.
194
        """
195
        config_lines = self.config_file()
196
        config_file = ""
197
        if config_lines is not None:
198
            for i, line in enumerate(config_lines):
199
                config_lines[i] = line if line.endswith("\n") else line + "\n"
200
            config_fd, config_file = tempfile.mkstemp()
201
            os.close(config_fd)
202
            with open(config_file, 'w') as conf_file:
203
                conf_file.writelines(config_lines)
204
        return config_file
205
206
    @staticmethod
207
    def config_file():
208
        """
209
        Returns a configuation file from the section given to the bear.
210
        The section is available in `self.section`. To add the config
211
        file's name generated by this function to the arguments,
212
        use `{config_file}`.
213
214
        :return: A list of lines of the config file to be used or None.
215
        """
216
        return None
217