Completed
Pull Request — master (#2202)
by Mischa
05:01 queued 03:15
created

LinterBase.__init__()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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