Completed
Pull Request — master (#2191)
by Mischa
01:54
created

_prepare_options()   F

Complexity

Conditions 22

Size

Total Lines 95

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 22
c 1
b 0
f 0
dl 0
loc 95
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like _prepare_options() 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
from contextlib import contextmanager
2
from functools import partial
3
import inspect
4
from itertools import chain, compress
5
import re
6
import shutil
7
from subprocess import check_call, CalledProcessError, DEVNULL
8
from types import MappingProxyType
9
10
from coalib.bears.LocalBear import LocalBear
11
from coalib.misc.ContextManagers import make_temp
12
from coala_decorators.decorators import assert_right_type, enforce_signature
13
from coalib.misc.Future import partialmethod
14
from coalib.misc.Shell import run_shell_command
15
from coalib.results.Diff import Diff
16
from coalib.results.Result import Result
17
from coalib.results.RESULT_SEVERITY import RESULT_SEVERITY
18
from coalib.settings.FunctionMetadata import FunctionMetadata
19
20
21
def _prepare_options(options):
22
    """
23
    Prepares options for ``linter`` for a given options dict in-place.
24
25
    :param options:
26
        The options dict that contains user/developer inputs.
27
    """
28
    allowed_options = {"executable",
29
                       "output_format",
30
                       "use_stdin",
31
                       "use_stdout",
32
                       "use_stderr",
33
                       "config_suffix",
34
                       "executable_check_fail_info",
35
                       "prerequisite_check_command"}
36
37
    if not options["use_stdout"] and not options["use_stderr"]:
38
        raise ValueError("No output streams provided at all.")
39
40
    if options["output_format"] == "corrected":
41
        if (
42
                "diff_severity" in options and
43
                options["diff_severity"] not in RESULT_SEVERITY.reverse):
44
            raise TypeError("Invalid value for `diff_severity`: " +
45
                            repr(options["diff_severity"]))
46
47
        if "result_message" in options:
48
            assert_right_type(options["result_message"], str, "result_message")
49
50
        if "diff_distance" in options:
51
            assert_right_type(options["diff_distance"], int, "diff_distance")
52
53
        allowed_options |= {"diff_severity", "result_message", "diff_distance"}
54
    elif options["output_format"] == "regex":
55
        if "output_regex" not in options:
56
            raise ValueError("`output_regex` needed when specified "
57
                             "output-format 'regex'.")
58
59
        options["output_regex"] = re.compile(options["output_regex"])
60
61
        # Don't setup severity_map if one is provided by user or if it's not
62
        # used inside the output_regex. If one is manually provided but not
63
        # used in the output_regex, throw an exception.
64
        if "severity_map" in options:
65
            if "severity" not in options["output_regex"].groupindex:
66
                raise ValueError("Provided `severity_map` but named group "
67
                                 "`severity` is not used in `output_regex`.")
68
            assert_right_type(options["severity_map"], dict, "severity_map")
69
70
            for key, value in options["severity_map"].items():
71
                assert_right_type(key, str, "severity_map key")
72
73
                try:
74
                    assert_right_type(value, int, "<severity_map dict-value>")
75
                except TypeError:
76
                    raise TypeError(
77
                        "The value {!r} for key {!r} inside given "
78
                        "severity-map is no valid severity value.".format(
79
                            value, key))
80
81
                if value not in RESULT_SEVERITY.reverse:
82
                    raise TypeError(
83
                        "Invalid severity value {!r} for key {!r} inside "
84
                        "given severity-map.".format(value, key))
85
86
            # Auto-convert keys to lower-case. This creates automatically a new
87
            # dict which prevents runtime-modifications.
88
            options["severity_map"] = {
89
                key.lower(): value
90
                for key, value in options["severity_map"].items()}
91
92
        if "result_message" in options:
93
            assert_right_type(options["result_message"], str, "result_message")
94
95
        allowed_options |= {"output_regex", "severity_map", "result_message"}
96
    elif options["output_format"] is not None:
97
        raise ValueError("Invalid `output_format` specified.")
98
99
    if options["prerequisite_check_command"]:
100
        if "prerequisite_check_fail_message" in options:
101
            assert_right_type(options["prerequisite_check_fail_message"],
102
                              str,
103
                              "prerequisite_check_fail_message")
104
        else:
105
            options["prerequisite_check_fail_message"] = (
106
                "Prerequisite check failed.")
107
108
        allowed_options.add("prerequisite_check_fail_message")
109
110
    # Check for illegal superfluous options.
111
    superfluous_options = options.keys() - allowed_options
112
    if superfluous_options:
113
        raise ValueError(
114
            "Invalid keyword arguments provided: " +
115
            ", ".join(repr(s) for s in sorted(superfluous_options)))
116
117
118
def _create_linter(klass, options):
119
    class LinterMeta(type):
120
121
        def __repr__(cls):
122
            return "<{} linter class (wrapping {!r})>".format(
123
                cls.__name__, options["executable"])
124
125
    class LinterBase(LocalBear, metaclass=LinterMeta):
126
127
        @staticmethod
128
        def generate_config(filename, file):
129
            """
130
            Generates the content of a config-file the linter-tool might need.
131
132
            The contents generated from this function are written to a
133
            temporary file and the path is provided inside
134
            ``create_arguments()``.
135
136
            By default no configuration is generated.
137
138
            You can provide additional keyword arguments and defaults. These
139
            will be interpreted as required settings that need to be provided
140
            through a coafile-section.
141
142
            :param filename:
143
                The name of the file currently processed.
144
            :param file:
145
                The contents of the file currently processed.
146
            :return:
147
                The config-file-contents as a string or ``None``.
148
            """
149
            return None
150
151
        @staticmethod
152
        def create_arguments(filename, file, config_file):
153
            """
154
            Creates the arguments for the linter.
155
156
            You can provide additional keyword arguments and defaults. These
157
            will be interpreted as required settings that need to be provided
158
            through a coafile-section.
159
160
            :param filename:
161
                The name of the file the linter-tool shall process.
162
            :param file:
163
                The contents of the file.
164
            :param config_file:
165
                The path of the config-file if used. ``None`` if unused.
166
            :return:
167
                A sequence of arguments to feed the linter-tool with.
168
            """
169
            raise NotImplementedError
170
171
        @staticmethod
172
        def get_executable():
173
            """
174
            Returns the executable of this class.
175
176
            :return:
177
                The executable name.
178
            """
179
            return options["executable"]
180
181
        @classmethod
182
        def check_prerequisites(cls):
183
            """
184
            Checks whether the linter-tool the bear uses is operational.
185
186
            :return:
187
                True if operational, otherwise a string containing more info.
188
            """
189
            if shutil.which(cls.get_executable()) is None:
190
                return (repr(cls.get_executable()) + " is not installed." +
191
                        (" " + options["executable_check_fail_info"]
192
                         if options["executable_check_fail_info"] else
193
                         ""))
194
            else:
195
                if options["prerequisite_check_command"]:
196
                    try:
197
                        check_call(options["prerequisite_check_command"],
198
                                   stdout=DEVNULL,
199
                                   stderr=DEVNULL)
200
                        return True
201
                    except (OSError, CalledProcessError):
202
                        return options["prerequisite_check_fail_message"]
203
                return True
204
205
        @classmethod
206
        def _get_create_arguments_metadata(cls):
207
            return FunctionMetadata.from_function(
208
                cls.create_arguments,
209
                omit={"self", "filename", "file", "config_file"})
210
211
        @classmethod
212
        def _get_generate_config_metadata(cls):
213
            return FunctionMetadata.from_function(
214
                cls.generate_config,
215
                omit={"filename", "file"})
216
217
        @classmethod
218
        def _get_process_output_metadata(cls):
219
            metadata = FunctionMetadata.from_function(cls.process_output)
220
221
            if options["output_format"] is None:
222
                omitted = {"self", "output", "filename", "file"}
223
            else:
224
                # If a specific output format is provided, function signatures
225
                # from process_output functions should not appear in the help.
226
                omitted = set(chain(metadata.non_optional_params,
227
                                    metadata.optional_params))
228
229
            metadata.omit = omitted
230
            return metadata
231
232
        @classmethod
233
        def get_metadata(cls):
234
            merged_metadata = FunctionMetadata.merge(
235
                cls._get_process_output_metadata(),
236
                cls._get_generate_config_metadata(),
237
                cls._get_create_arguments_metadata())
238
            merged_metadata.desc = (
239
                "{}\n\nThis bear uses the {!r} tool.".format(
240
                    inspect.getdoc(cls), cls.get_executable()))
241
242
            return merged_metadata
243
244
        def _convert_output_regex_match_to_result(self,
245
                                                  match,
246
                                                  filename,
247
                                                  severity_map,
248
                                                  result_message):
249
            """
250
            Converts the matched named-groups of ``output_regex`` to an actual
251
            ``Result``.
252
253
            :param match:
254
                The regex match object.
255
            :param filename:
256
                The name of the file this match belongs to.
257
            :param severity_map:
258
                The dict to use to map the severity-match to an actual
259
                ``RESULT_SEVERITY``.
260
            :param result_message:
261
                The static message to use for results instead of grabbing it
262
                from the executable output via the ``message`` named regex
263
                group.
264
            """
265
            # Pre process the groups
266
            groups = match.groupdict()
267
268
            if 'severity' in groups:
269
                try:
270
                    groups["severity"] = severity_map[
271
                        groups["severity"].lower()]
272
                except KeyError:
273
                    self.warn(
274
                        repr(groups["severity"]) + " not found in "
275
                        "severity-map. Assuming `RESULT_SEVERITY.NORMAL`.")
276
                    groups["severity"] = RESULT_SEVERITY.NORMAL
277
            else:
278
                groups['severity'] = RESULT_SEVERITY.NORMAL
279
280
            for variable in ("line", "column", "end_line", "end_column"):
281
                groups[variable] = (None
282
                                    if groups.get(variable, None) is None else
283
                                    int(groups[variable]))
284
285
            if "origin" in groups:
286
                groups["origin"] = "{} ({})".format(klass.__name__,
287
                                                    groups["origin"].strip())
288
289
            # Construct the result.
290
            return Result.from_values(
291
                origin=groups.get("origin", self),
292
                message=(groups.get("message", "").strip()
293
                         if result_message is None else result_message),
294
                file=filename,
295
                severity=groups["severity"],
296
                line=groups["line"],
297
                column=groups["column"],
298
                end_line=groups["end_line"],
299
                end_column=groups["end_column"],
300
                additional_info=groups.get("additional_info", "").strip())
301
302
        def process_output_corrected(self,
303
                                     output,
304
                                     filename,
305
                                     file,
306
                                     diff_severity=RESULT_SEVERITY.NORMAL,
307
                                     result_message="Inconsistency found.",
308
                                     diff_distance=1):
309
            """
310
            Processes the executable's output as a corrected file.
311
312
            :param output:
313
                The output of the program. This can be either a single
314
                string or a sequence of strings.
315
            :param filename:
316
                The filename of the file currently being corrected.
317
            :param file:
318
                The contents of the file currently being corrected.
319
            :param diff_severity:
320
                The severity to use for generating results.
321
            :param result_message:
322
                The message to use for generating results.
323
            :param diff_distance:
324
                Number of unchanged lines that are allowed in between two
325
                changed lines so they get yielded as one diff. If a negative
326
                distance is given, every change will be yielded as an own diff,
327
                even if they are right beneath each other.
328
            :return:
329
                An iterator returning results containing patches for the
330
                file to correct.
331
            """
332
            if isinstance(output, str):
333
                output = (output,)
334
335
            for string in output:
336
                for diff in Diff.from_string_arrays(
337
                        file,
338
                        string.splitlines(keepends=True)).split_diff(
339
                            distance=diff_distance):
340
                    yield Result(self,
341
                                 result_message,
342
                                 affected_code=diff.affected_code(filename),
343
                                 diffs={filename: diff},
344
                                 severity=diff_severity)
345
346
        def process_output_regex(
347
                self, output, filename, file, output_regex,
348
                severity_map=MappingProxyType({
349
                    "critical": RESULT_SEVERITY.MAJOR,
350
                    "c": RESULT_SEVERITY.MAJOR,
351
                    "fatal": RESULT_SEVERITY.MAJOR,
352
                    "fail": RESULT_SEVERITY.MAJOR,
353
                    "f": RESULT_SEVERITY.MAJOR,
354
                    "error": RESULT_SEVERITY.MAJOR,
355
                    "err": RESULT_SEVERITY.MAJOR,
356
                    "e": RESULT_SEVERITY.MAJOR,
357
                    "warning": RESULT_SEVERITY.NORMAL,
358
                    "warn": RESULT_SEVERITY.NORMAL,
359
                    "w": RESULT_SEVERITY.NORMAL,
360
                    "information": RESULT_SEVERITY.INFO,
361
                    "info": RESULT_SEVERITY.INFO,
362
                    "i", RESULT_SEVERITY.INFO,
363
                    "suggestion", RESULT_SEVERITY.INFO}),
364
                result_message=None):
365
            """
366
            Processes the executable's output using a regex.
367
368
            :param output:
369
                The output of the program. This can be either a single
370
                string or a sequence of strings.
371
            :param filename:
372
                The filename of the file currently being corrected.
373
            :param file:
374
                The contents of the file currently being corrected.
375
            :param output_regex:
376
                The regex to parse the output with. It should use as many
377
                of the following named groups (via ``(?P<name>...)``) to
378
                provide a good result:
379
380
                - line - The line where the issue starts.
381
                - column - The column where the issue starts.
382
                - end_line - The line where the issue ends.
383
                - end_column - The column where the issue ends.
384
                - severity - The severity of the issue.
385
                - message - The message of the result.
386
                - origin - The origin of the issue.
387
                - additional_info - Additional info provided by the issue.
388
389
                The groups ``line``, ``column``, ``end_line`` and
390
                ``end_column`` don't have to match numbers only, they can
391
                also match nothing, the generated ``Result`` is filled
392
                automatically with ``None`` then for the appropriate
393
                properties.
394
            :param severity_map:
395
                A dict used to map a severity string (captured from the
396
                ``output_regex`` with the named group ``severity``) to an
397
                actual ``coalib.results.RESULT_SEVERITY`` for a result.
398
            :param result_message:
399
                The static message to use for results instead of grabbing it
400
                from the executable output via the ``message`` named regex
401
                group.
402
            :return:
403
                An iterator returning results.
404
            """
405
            if isinstance(output, str):
406
                output = (output,)
407
408
            for string in output:
409
                for match in re.finditer(output_regex, string):
410
                    yield self._convert_output_regex_match_to_result(
411
                        match, filename, severity_map=severity_map,
412
                        result_message=result_message)
413
414
        if options["output_format"] is None:
415
            # Check if user supplied a `process_output` override.
416
            if not callable(getattr(klass, "process_output", None)):
417
                raise ValueError("`process_output` not provided by given "
418
                                 "class {!r}.".format(klass.__name__))
419
                # No need to assign to `process_output` here, the class mixing
420
                # below automatically does that.
421
        else:
422
            # Prevent people from accidentally defining `process_output`
423
            # manually, as this would implicitly override the internally
424
            # set-up `process_output`.
425
            if hasattr(klass, "process_output"):
426
                raise ValueError("Found `process_output` already defined "
427
                                 "by class {!r}, but {!r} output-format is "
428
                                 "specified.".format(klass.__name__,
429
                                                     options["output_format"]))
430
431
            if options["output_format"] == "corrected":
432
                process_output_args = {
433
                    key: options[key]
434
                    for key in ("result_message", "diff_severity",
435
                                "diff_distance")
436
                    if key in options}
437
438
                process_output = partialmethod(
439
                    process_output_corrected, **process_output_args)
440
441
            else:
442
                assert options["output_format"] == "regex"
443
444
                process_output_args = {
445
                    key: options[key]
446
                    for key in ("output_regex", "severity_map",
447
                                "result_message")
448
                    if key in options}
449
450
                process_output = partialmethod(
451
                    process_output_regex, **process_output_args)
452
453
        @classmethod
454
        @contextmanager
455
        def _create_config(cls, filename, file, **kwargs):
456
            """
457
            Provides a context-manager that creates the config file if the
458
            user provides one and cleans it up when done with linting.
459
460
            :param filename:
461
                The filename of the file.
462
            :param file:
463
                The file contents.
464
            :param kwargs:
465
                Section settings passed from ``run()``.
466
            :return:
467
                A context-manager handling the config-file.
468
            """
469
            content = cls.generate_config(filename, file, **kwargs)
470
            if content is None:
471
                yield None
472
            else:
473
                with make_temp(
474
                        suffix=options["config_suffix"]) as config_file:
475
                    with open(config_file, mode="w") as fl:
476
                        fl.write(content)
477
                    yield config_file
478
479
        def run(self, filename, file, **kwargs):
480
            # Get the **kwargs params to forward to `generate_config()`
481
            # (from `_create_config()`).
482
            generate_config_kwargs = FunctionMetadata.filter_parameters(
483
                self._get_generate_config_metadata(), kwargs)
484
485
            with self._create_config(
486
                    filename,
487
                    file,
488
                    **generate_config_kwargs) as config_file:
489
                # And now retrieve the **kwargs for `create_arguments()`.
490
                create_arguments_kwargs = (
491
                    FunctionMetadata.filter_parameters(
492
                        self._get_create_arguments_metadata(), kwargs))
493
494
                args = self.create_arguments(filename, file, config_file,
495
                                             **create_arguments_kwargs)
496
497
                try:
498
                    args = tuple(args)
499
                except TypeError:
500
                    self.err("The given arguments "
501
                             "{!r} are not iterable.".format(args))
502
                    return
503
504
                arguments = (self.get_executable(),) + args
505
                self.debug("Running '{}'".format(' '.join(arguments)))
506
507
                output = run_shell_command(
508
                    arguments,
509
                    stdin="".join(file) if options["use_stdin"] else None)
510
511
                output = tuple(compress(
512
                    output,
513
                    (options["use_stdout"], options["use_stderr"])))
514
                if len(output) == 1:
515
                    output = output[0]
516
517
                process_output_kwargs = FunctionMetadata.filter_parameters(
518
                    self._get_process_output_metadata(), kwargs)
519
                return self.process_output(output, filename, file,
520
                                           **process_output_kwargs)
521
522
        def __repr__(self):
523
            return "<{} linter object (wrapping {!r}) at {}>".format(
524
                type(self).__name__, self.get_executable(), hex(id(self)))
525
526
    # Mixin the linter into the user-defined interface, otherwise
527
    # `create_arguments` and other methods would be overridden by the
528
    # default version.
529
    result_klass = type(klass.__name__, (klass, LinterBase), {})
530
    result_klass.__doc__ = klass.__doc__ if klass.__doc__ else ""
531
    return result_klass
532
533
534
@enforce_signature
535
def linter(executable: str,
536
           use_stdin: bool=False,
537
           use_stdout: bool=True,
538
           use_stderr: bool=False,
539
           config_suffix: str="",
540
           executable_check_fail_info: str="",
541
           prerequisite_check_command: tuple=(),
542
           output_format: (str, None)=None,
543
           **options):
544
    """
545
    Decorator that creates a ``LocalBear`` that is able to process results from
546
    an external linter tool.
547
548
    The main functionality is achieved through the ``create_arguments()``
549
    function that constructs the command-line-arguments that get parsed to your
550
    executable.
551
552
    >>> @linter("xlint", output_format="regex", output_regex="...")
553
    ... class XLintBear:
554
    ...     @staticmethod
555
    ...     def create_arguments(filename, file, config_file):
556
    ...         return "--lint", filename
557
558
    Requiring settings is possible like in ``Bear.run()`` with supplying
559
    additional keyword arguments (and if needed with defaults).
560
561
    >>> @linter("xlint", output_format="regex", output_regex="...")
562
    ... class XLintBear:
563
    ...     @staticmethod
564
    ...     def create_arguments(filename,
565
    ...                          file,
566
    ...                          config_file,
567
    ...                          lintmode: str,
568
    ...                          enable_aggressive_lints: bool=False):
569
    ...         arguments = ("--lint", filename, "--mode=" + lintmode)
570
    ...         if enable_aggressive_lints:
571
    ...             arguments += ("--aggressive",)
572
    ...         return arguments
573
574
    Sometimes your tool requires an actual file that contains configuration.
575
    ``linter`` allows you to just define the contents the configuration shall
576
    contain via ``generate_config()`` and handles everything else for you.
577
578
    >>> @linter("xlint", output_format="regex", output_regex="...")
579
    ... class XLintBear:
580
    ...     @staticmethod
581
    ...     def generate_config(filename,
582
    ...                         file,
583
    ...                         lintmode,
584
    ...                         enable_aggressive_lints):
585
    ...         modestring = ("aggressive"
586
    ...                       if enable_aggressive_lints else
587
    ...                       "non-aggressive")
588
    ...         contents = ("<xlint>",
589
    ...                     "    <mode>" + lintmode + "</mode>",
590
    ...                     "    <aggressive>" + modestring + "</aggressive>",
591
    ...                     "</xlint>")
592
    ...         return "\\n".join(contents)
593
    ...
594
    ...     @staticmethod
595
    ...     def create_arguments(filename,
596
    ...                          file,
597
    ...                          config_file):
598
    ...         return "--lint", filename, "--config", config_file
599
600
    As you can see you don't need to copy additional keyword-arguments you
601
    introduced from ``create_arguments()`` to ``generate_config()`` and
602
    vice-versa. ``linter`` takes care of forwarding the right arguments to the
603
    right place, so you are able to avoid signature duplication.
604
605
    If you override ``process_output``, you have the same feature like above
606
    (auto-forwarding of the right arguments defined in your function
607
    signature).
608
609
    Note when overriding ``process_output``: Providing a single output stream
610
    (via ``use_stdout`` or ``use_stderr``) puts the according string attained
611
    from the stream into parameter ``output``, providing both output streams
612
    inputs a tuple with ``(stdout, stderr)``. Providing ``use_stdout=False``
613
    and ``use_stderr=False`` raises a ``ValueError``. By default ``use_stdout``
614
    is ``True`` and ``use_stderr`` is ``False``.
615
616
    Documentation:
617
    Bear description shall be provided at class level.
618
    If you document your additional parameters inside ``create_arguments``,
619
    ``generate_config`` and ``process_output``, beware that conflicting
620
    documentation between them may be overridden. Document duplicated
621
    parameters inside ``create_arguments`` first, then in ``generate_config``
622
    and after that inside ``process_output``.
623
624
    For the tutorial see:
625
    http://coala.readthedocs.org/en/latest/Users/Tutorials/Linter_Bears.html
626
627
    :param executable:
628
        The linter tool.
629
    :param use_stdin:
630
        Whether the input file is sent via stdin instead of passing it over the
631
        command-line-interface.
632
    :param use_stdout:
633
        Whether to use the stdout output stream.
634
    :param use_stderr:
635
        Whether to use the stderr output stream.
636
    :param config_suffix:
637
        The suffix-string to append to the filename of the configuration file
638
        created when ``generate_config`` is supplied. Useful if your executable
639
        expects getting a specific file-type with specific file-ending for the
640
        configuration file.
641
    :param executable_check_fail_info:
642
        Information that is provided together with the fail message from the
643
        normal executable check. By default no additional info is printed.
644
    :param prerequisite_check_command:
645
        A custom command to check for when ``check_prerequisites`` gets
646
        invoked (via ``subprocess.check_call()``). Must be an ``Iterable``.
647
    :param prerequisite_check_fail_message:
648
        A custom message that gets displayed when ``check_prerequisites``
649
        fails while invoking ``prerequisite_check_command``. Can only be
650
        provided together with ``prerequisite_check_command``.
651
    :param output_format:
652
        The output format of the underlying executable. Valid values are
653
654
        - ``None``: Define your own format by overriding ``process_output``.
655
          Overriding ``process_output`` is then mandatory, not specifying it
656
          raises a ``ValueError``.
657
        - ``'regex'``: Parse output using a regex. See parameter
658
          ``output_regex``.
659
        - ``'corrected'``: The output is the corrected of the given file. Diffs
660
          are then generated to supply patches for results.
661
662
        Passing something else raises a ``ValueError``.
663
    :param output_regex:
664
        The regex expression as a string that is used to parse the output
665
        generated by the underlying executable. It should use as many of the
666
        following named groups (via ``(?P<name>...)``) to provide a good
667
        result:
668
669
        - line - The line where the issue starts.
670
        - column - The column where the issue starts.
671
        - end_line - The line where the issue ends.
672
        - end_column - The column where the issue ends.
673
        - severity - The severity of the issue.
674
        - message - The message of the result.
675
        - origin - The origin of the issue.
676
        - additional_info - Additional info provided by the issue.
677
678
        The groups ``line``, ``column``, ``end_line`` and ``end_column`` don't
679
        have to match numbers only, they can also match nothing, the generated
680
        ``Result`` is filled automatically with ``None`` then for the
681
        appropriate properties.
682
683
        Needs to be provided if ``output_format`` is ``'regex'``.
684
    :param severity_map:
685
        A dict used to map a severity string (captured from the
686
        ``output_regex`` with the named group ``severity``) to an actual
687
        ``coalib.results.RESULT_SEVERITY`` for a result. Severity strings are
688
        mapped **case-insensitive**!
689
690
        - ``RESULT_SEVERITY.MAJOR``: Mapped by ``error``.
691
        - ``RESULT_SEVERITY.NORMAL``: Mapped by ``warning`` or ``warn``.
692
        - ``RESULT_SEVERITY.MINOR``: Mapped by ``info``.
693
694
        A ``ValueError`` is raised when the named group ``severity`` is not
695
        used inside ``output_regex`` and this parameter is given.
696
    :param diff_severity:
697
        The severity to use for all results if ``output_format`` is
698
        ``'corrected'``. By default this value is
699
        ``coalib.results.RESULT_SEVERITY.NORMAL``. The given value needs to be
700
        defined inside ``coalib.results.RESULT_SEVERITY``.
701
    :param result_message:
702
        The message-string to use for all results. Can be used only together
703
        with ``corrected`` or ``regex`` output format. When using
704
        ``corrected``, the default value is ``"Inconsistency found."``, while
705
        for ``regex`` this static message is disabled and the message matched
706
        by ``output_regex`` is used instead.
707
    :param diff_distance:
708
        Number of unchanged lines that are allowed in between two changed lines
709
        so they get yielded as one diff if ``corrected`` output-format is
710
        given. If a negative distance is given, every change will be yielded as
711
        an own diff, even if they are right beneath each other. By default this
712
        value is ``1``.
713
    :raises ValueError:
714
        Raised when invalid options are supplied.
715
    :raises TypeError:
716
        Raised when incompatible types are supplied.
717
        See parameter documentations for allowed types.
718
    :return:
719
        A ``LocalBear`` derivation that lints code using an external tool.
720
    """
721
    options["executable"] = executable
722
    options["output_format"] = output_format
723
    options["use_stdin"] = use_stdin
724
    options["use_stdout"] = use_stdout
725
    options["use_stderr"] = use_stderr
726
    options["config_suffix"] = config_suffix
727
    options["executable_check_fail_info"] = executable_check_fail_info
728
    options["prerequisite_check_command"] = prerequisite_check_command
729
730
    _prepare_options(options)
731
732
    return partial(_create_linter, options=options)
733