Completed
Pull Request — master (#2432)
by Zatreanu
01:47
created

LinterBase   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 397
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
dl 0
loc 397
rs 8.439
c 5
b 0
f 0
wmc 47

How to fix   Complexity   

Complex Class

Complex classes like LinterBase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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