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