Completed
Pull Request — master (#2152)
by Mischa
01:47
created

LinterBase.process_output_regex()   B

Complexity

Conditions 4

Size

Total Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
c 2
b 0
f 0
dl 0
loc 51
rs 8.8981

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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