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

coalib.bearlib.abstractions.Lint   B

Complexity

Total Complexity 36

Size/Duplication

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

11 Methods

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