Total Complexity | 43 |
Total Lines | 321 |
Duplicated Lines | 0 % |
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 |
||
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 |