Completed
Pull Request — master (#2159)
by Mischa
01:58
created

LinterBase.process_output_regex()   A

Complexity

Conditions 4

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
c 2
b 0
f 0
dl 0
loc 57
rs 9.0309

How to fix   Long Method   

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:

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 coalib.misc.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):
0 ignored issues
show
Unused Code introduced by
This abstract class does not seem to be used anywhere.
Loading history...
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
                    "error": RESULT_SEVERITY.MAJOR,
350
                    "warning": RESULT_SEVERITY.NORMAL,
351
                    "warn": RESULT_SEVERITY.NORMAL,
352
                    "information": RESULT_SEVERITY.INFO,
353
                    "info": RESULT_SEVERITY.INFO}),
354
                result_message=None):
355
            """
356
            Processes the executable's output using a regex.
357
358
            :param output:
359
                The output of the program. This can be either a single
360
                string or a sequence of strings.
361
            :param filename:
362
                The filename of the file currently being corrected.
363
            :param file:
364
                The contents of the file currently being corrected.
365
            :param output_regex:
366
                The regex to parse the output with. It should use as many
367
                of the following named groups (via ``(?P<name>...)``) to
368
                provide a good result:
369
370
                - line - The line where the issue starts.
371
                - column - The column where the issue starts.
372
                - end_line - The line where the issue ends.
373
                - end_column - The column where the issue ends.
374
                - severity - The severity of the issue.
375
                - message - The message of the result.
376
                - origin - The origin of the issue.
377
                - additional_info - Additional info provided by the issue.
378
379
                The groups ``line``, ``column``, ``end_line`` and
380
                ``end_column`` don't have to match numbers only, they can
381
                also match nothing, the generated ``Result`` is filled
382
                automatically with ``None`` then for the appropriate
383
                properties.
384
            :param severity_map:
385
                A dict used to map a severity string (captured from the
386
                ``output_regex`` with the named group ``severity``) to an
387
                actual ``coalib.results.RESULT_SEVERITY`` for a result.
388
            :param result_message:
389
                The static message to use for results instead of grabbing it
390
                from the executable output via the ``message`` named regex
391
                group.
392
            :return:
393
                An iterator returning results.
394
            """
395
            if isinstance(output, str):
396
                output = (output,)
397
398
            for string in output:
399
                for match in re.finditer(output_regex, string):
400
                    yield self._convert_output_regex_match_to_result(
401
                        match, filename, severity_map=severity_map,
402
                        result_message=result_message)
403
404
        if options["output_format"] is None:
405
            # Check if user supplied a `process_output` override.
406
            if not callable(getattr(klass, "process_output", None)):
407
                raise ValueError("`process_output` not provided by given "
408
                                 "class {!r}.".format(klass.__name__))
409
                # No need to assign to `process_output` here, the class mixing
410
                # below automatically does that.
411
        else:
412
            # Prevent people from accidentally defining `process_output`
413
            # manually, as this would implicitly override the internally
414
            # set-up `process_output`.
415
            if hasattr(klass, "process_output"):
416
                raise ValueError("Found `process_output` already defined "
417
                                 "by class {!r}, but {!r} output-format is "
418
                                 "specified.".format(klass.__name__,
419
                                                     options["output_format"]))
420
421
            if options["output_format"] == "corrected":
422
                process_output_args = {
423
                    key: options[key]
424
                    for key in ("result_message", "diff_severity",
425
                                "diff_distance")
426
                    if key in options}
427
428
                process_output = partialmethod(
429
                    process_output_corrected, **process_output_args)
430
431
            else:
432
                assert options["output_format"] == "regex"
433
434
                process_output_args = {
435
                    key: options[key]
436
                    for key in ("output_regex", "severity_map",
437
                                "result_message")
438
                    if key in options}
439
440
                process_output = partialmethod(
441
                    process_output_regex, **process_output_args)
442
443
        @classmethod
444
        @contextmanager
445
        def _create_config(cls, filename, file, **kwargs):
446
            """
447
            Provides a context-manager that creates the config file if the
448
            user provides one and cleans it up when done with linting.
449
450
            :param filename:
451
                The filename of the file.
452
            :param file:
453
                The file contents.
454
            :param kwargs:
455
                Section settings passed from ``run()``.
456
            :return:
457
                A context-manager handling the config-file.
458
            """
459
            content = cls.generate_config(filename, file, **kwargs)
460
            if content is None:
461
                yield None
462
            else:
463
                with make_temp(
464
                        suffix=options["config_suffix"]) as config_file:
465
                    with open(config_file, mode="w") as fl:
466
                        fl.write(content)
467
                    yield config_file
468
469
        def run(self, filename, file, **kwargs):
470
            # Get the **kwargs params to forward to `generate_config()`
471
            # (from `_create_config()`).
472
            generate_config_kwargs = FunctionMetadata.filter_parameters(
473
                self._get_generate_config_metadata(), kwargs)
474
475
            with self._create_config(
476
                    filename,
477
                    file,
478
                    **generate_config_kwargs) as config_file:
479
                # And now retrieve the **kwargs for `create_arguments()`.
480
                create_arguments_kwargs = (
481
                    FunctionMetadata.filter_parameters(
482
                        self._get_create_arguments_metadata(), kwargs))
483
484
                args = self.create_arguments(filename, file, config_file,
485
                                             **create_arguments_kwargs)
486
487
                try:
488
                    args = tuple(args)
489
                except TypeError:
490
                    self.err("The given arguments "
491
                             "{!r} are not iterable.".format(args))
492
                    return
493
494
                arguments = (self.get_executable(),) + args
495
                self.debug("Running '{}'".format(' '.join(arguments)))
496
497
                output = run_shell_command(
498
                    arguments,
499
                    stdin="".join(file) if options["use_stdin"] else None)
500
501
                output = tuple(compress(
502
                    output,
503
                    (options["use_stdout"], options["use_stderr"])))
504
                if len(output) == 1:
505
                    output = output[0]
506
507
                process_output_kwargs = FunctionMetadata.filter_parameters(
508
                    self._get_process_output_metadata(), kwargs)
509
                return self.process_output(output, filename, file,
510
                                           **process_output_kwargs)
511
512
        def __repr__(self):
513
            return "<{} linter object (wrapping {!r}) at {}>".format(
514
                type(self).__name__, self.get_executable(), hex(id(self)))
515
516
    # Mixin the linter into the user-defined interface, otherwise
517
    # `create_arguments` and other methods would be overridden by the
518
    # default version.
519
    result_klass = type(klass.__name__, (klass, LinterBase), {})
520
    result_klass.__doc__ = klass.__doc__ if klass.__doc__ else ""
521
    return result_klass
522
523
524
@enforce_signature
525
def linter(executable: str,
526
           use_stdin: bool=False,
527
           use_stdout: bool=True,
528
           use_stderr: bool=False,
529
           config_suffix: str="",
530
           executable_check_fail_info: str="",
531
           prerequisite_check_command: tuple=(),
532
           output_format: (str, None)=None,
533
           **options):
534
    """
535
    Decorator that creates a ``LocalBear`` that is able to process results from
536
    an external linter tool.
537
538
    The main functionality is achieved through the ``create_arguments()``
539
    function that constructs the command-line-arguments that get parsed to your
540
    executable.
541
542
    >>> @linter("xlint", output_format="regex", output_regex="...")
543
    ... class XLintBear:
544
    ...     @staticmethod
545
    ...     def create_arguments(filename, file, config_file):
546
    ...         return "--lint", filename
547
548
    Requiring settings is possible like in ``Bear.run()`` with supplying
549
    additional keyword arguments (and if needed with defaults).
550
551
    >>> @linter("xlint", output_format="regex", output_regex="...")
552
    ... class XLintBear:
553
    ...     @staticmethod
554
    ...     def create_arguments(filename,
555
    ...                          file,
556
    ...                          config_file,
557
    ...                          lintmode: str,
558
    ...                          enable_aggressive_lints: bool=False):
559
    ...         arguments = ("--lint", filename, "--mode=" + lintmode)
560
    ...         if enable_aggressive_lints:
561
    ...             arguments += ("--aggressive",)
562
    ...         return arguments
563
564
    Sometimes your tool requires an actual file that contains configuration.
565
    ``linter`` allows you to just define the contents the configuration shall
566
    contain via ``generate_config()`` and handles everything else for you.
567
568
    >>> @linter("xlint", output_format="regex", output_regex="...")
569
    ... class XLintBear:
570
    ...     @staticmethod
571
    ...     def generate_config(filename,
572
    ...                         file,
573
    ...                         lintmode,
574
    ...                         enable_aggressive_lints):
575
    ...         modestring = ("aggressive"
576
    ...                       if enable_aggressive_lints else
577
    ...                       "non-aggressive")
578
    ...         contents = ("<xlint>",
579
    ...                     "    <mode>" + lintmode + "</mode>",
580
    ...                     "    <aggressive>" + modestring + "</aggressive>",
581
    ...                     "</xlint>")
582
    ...         return "\\n".join(contents)
583
    ...
584
    ...     @staticmethod
585
    ...     def create_arguments(filename,
586
    ...                          file,
587
    ...                          config_file):
588
    ...         return "--lint", filename, "--config", config_file
589
590
    As you can see you don't need to copy additional keyword-arguments you
591
    introduced from ``create_arguments()`` to ``generate_config()`` and
592
    vice-versa. ``linter`` takes care of forwarding the right arguments to the
593
    right place, so you are able to avoid signature duplication.
594
595
    If you override ``process_output``, you have the same feature like above
596
    (auto-forwarding of the right arguments defined in your function
597
    signature).
598
599
    Note when overriding ``process_output``: Providing a single output stream
600
    (via ``use_stdout`` or ``use_stderr``) puts the according string attained
601
    from the stream into parameter ``output``, providing both output streams
602
    inputs a tuple with ``(stdout, stderr)``. Providing ``use_stdout=False``
603
    and ``use_stderr=False`` raises a ``ValueError``. By default ``use_stdout``
604
    is ``True`` and ``use_stderr`` is ``False``.
605
606
    Documentation:
607
    Bear description shall be provided at class level.
608
    If you document your additional parameters inside ``create_arguments``,
609
    ``generate_config`` and ``process_output``, beware that conflicting
610
    documentation between them may be overridden. Document duplicated
611
    parameters inside ``create_arguments`` first, then in ``generate_config``
612
    and after that inside ``process_output``.
613
614
    For the tutorial see:
615
    http://coala.readthedocs.org/en/latest/Users/Tutorials/Linter_Bears.html
616
617
    :param executable:
618
        The linter tool.
619
    :param use_stdin:
620
        Whether the input file is sent via stdin instead of passing it over the
621
        command-line-interface.
622
    :param use_stdout:
623
        Whether to use the stdout output stream.
624
    :param use_stderr:
625
        Whether to use the stderr output stream.
626
    :param config_suffix:
627
        The suffix-string to append to the filename of the configuration file
628
        created when ``generate_config`` is supplied. Useful if your executable
629
        expects getting a specific file-type with specific file-ending for the
630
        configuration file.
631
    :param executable_check_fail_info:
632
        Information that is provided together with the fail message from the
633
        normal executable check. By default no additional info is printed.
634
    :param prerequisite_check_command:
635
        A custom command to check for when ``check_prerequisites`` gets
636
        invoked (via ``subprocess.check_call()``). Must be an ``Iterable``.
637
    :param prerequisite_check_fail_message:
638
        A custom message that gets displayed when ``check_prerequisites``
639
        fails while invoking ``prerequisite_check_command``. Can only be
640
        provided together with ``prerequisite_check_command``.
641
    :param output_format:
642
        The output format of the underlying executable. Valid values are
643
644
        - ``None``: Define your own format by overriding ``process_output``.
645
          Overriding ``process_output`` is then mandatory, not specifying it
646
          raises a ``ValueError``.
647
        - ``'regex'``: Parse output using a regex. See parameter
648
          ``output_regex``.
649
        - ``'corrected'``: The output is the corrected of the given file. Diffs
650
          are then generated to supply patches for results.
651
652
        Passing something else raises a ``ValueError``.
653
    :param output_regex:
654
        The regex expression as a string that is used to parse the output
655
        generated by the underlying executable. It should use as many of the
656
        following named groups (via ``(?P<name>...)``) to provide a good
657
        result:
658
659
        - line - The line where the issue starts.
660
        - column - The column where the issue starts.
661
        - end_line - The line where the issue ends.
662
        - end_column - The column where the issue ends.
663
        - severity - The severity of the issue.
664
        - message - The message of the result.
665
        - origin - The origin of the issue.
666
        - additional_info - Additional info provided by the issue.
667
668
        The groups ``line``, ``column``, ``end_line`` and ``end_column`` don't
669
        have to match numbers only, they can also match nothing, the generated
670
        ``Result`` is filled automatically with ``None`` then for the
671
        appropriate properties.
672
673
        Needs to be provided if ``output_format`` is ``'regex'``.
674
    :param severity_map:
675
        A dict used to map a severity string (captured from the
676
        ``output_regex`` with the named group ``severity``) to an actual
677
        ``coalib.results.RESULT_SEVERITY`` for a result. Severity strings are
678
        mapped **case-insensitive**!
679
680
        - ``RESULT_SEVERITY.MAJOR``: Mapped by ``error``.
681
        - ``RESULT_SEVERITY.NORMAL``: Mapped by ``warning`` or ``warn``.
682
        - ``RESULT_SEVERITY.MINOR``: Mapped by ``info``.
683
684
        A ``ValueError`` is raised when the named group ``severity`` is not
685
        used inside ``output_regex`` and this parameter is given.
686
    :param diff_severity:
687
        The severity to use for all results if ``output_format`` is
688
        ``'corrected'``. By default this value is
689
        ``coalib.results.RESULT_SEVERITY.NORMAL``. The given value needs to be
690
        defined inside ``coalib.results.RESULT_SEVERITY``.
691
    :param result_message:
692
        The message-string to use for all results. Can be used only together
693
        with ``corrected`` or ``regex`` output format. When using
694
        ``corrected``, the default value is ``"Inconsistency found."``, while
695
        for ``regex`` this static message is disabled and the message matched
696
        by ``output_regex`` is used instead.
697
    :param diff_distance:
698
        Number of unchanged lines that are allowed in between two changed lines
699
        so they get yielded as one diff if ``corrected`` output-format is
700
        given. If a negative distance is given, every change will be yielded as
701
        an own diff, even if they are right beneath each other. By default this
702
        value is ``1``.
703
    :raises ValueError:
704
        Raised when invalid options are supplied.
705
    :raises TypeError:
706
        Raised when incompatible types are supplied.
707
        See parameter documentations for allowed types.
708
    :return:
709
        A ``LocalBear`` derivation that lints code using an external tool.
710
    """
711
    options["executable"] = executable
712
    options["output_format"] = output_format
713
    options["use_stdin"] = use_stdin
714
    options["use_stdout"] = use_stdout
715
    options["use_stderr"] = use_stderr
716
    options["config_suffix"] = config_suffix
717
    options["executable_check_fail_info"] = executable_check_fail_info
718
    options["prerequisite_check_command"] = prerequisite_check_command
719
720
    _prepare_options(options)
721
722
    return partial(_create_linter, options=options)
723