Failed Conditions
Pull Request — master (#1990)
by Mischa
01:37
created

LinterMeta   A

Complexity

Total Complexity 1

Size/Duplication

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