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

LinterBase._create_config()   B

Complexity

Conditions 4

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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