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

LinterBase.create_arguments()   A

Complexity

Conditions 1

Size

Total Lines 19

Duplication

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

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
679
            pass
680
681
        return Linter
682
683
    return create_linter
684