From 1669bc3868b198a1e97fae4fa60e0d6cfaed53d9 Mon Sep 17 00:00:00 2001 From: Ashley Whetter Date: Sun, 26 May 2019 00:29:11 -0700 Subject: Fixed false positives for function stubs (#2927) Close #1581 --- ChangeLog | 4 ++ pylint/checkers/base.py | 13 +++++- pylint/checkers/utils.py | 12 ++++++ pylint/checkers/variables.py | 79 ++++++++++++++++++++--------------- pylint/test/functional/typing_use.py | 51 ++++++++++++++++++++++ pylint/test/functional/typing_use.rc | 2 + pylint/test/functional/typing_use.txt | 1 + 7 files changed, 127 insertions(+), 35 deletions(-) create mode 100644 pylint/test/functional/typing_use.py create mode 100644 pylint/test/functional/typing_use.rc create mode 100644 pylint/test/functional/typing_use.txt diff --git a/ChangeLog b/ChangeLog index 9ad2a79b6..5afba3ebc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -147,6 +147,10 @@ Release date: TBA Close #202 +* Fixed `unused-argument` and `function-redefined` getting raised for + functions decorated with `typing.overload`. + + Close #1581 What's New in Pylint 2.3.0? =========================== diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index e9aecb287..76ef09608 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -836,7 +836,15 @@ class BasicErrorChecker(_BasicChecker): def _check_redefinition(self, redeftype, node): """check for redefinition of a function / method / class name""" parent_frame = node.parent.frame() - defined_self = parent_frame[node.name] + # Ignore function stubs created for type information + defined_self = next( + ( + local + for local in parent_frame.locals[node.name] + if not utils.is_overload_stub(local) + ), + node, + ) if defined_self is not node and not astroid.are_exclusive(node, defined_self): # Additional checks for methods which are not considered @@ -847,6 +855,9 @@ class BasicErrorChecker(_BasicChecker): ): return + if utils.is_overload_stub(node): + return + dummy_variables_rgx = lint_utils.get_global_option( self, "dummy-variables-rgx", default=None ) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 2f7402892..826181845 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1225,3 +1225,15 @@ def is_subclass_of(child: astroid.ClassDef, parent: astroid.ClassDef) -> bool: except _NonDeducibleTypeHierarchy: continue return False + + +@lru_cache(maxsize=1024) +def is_overload_stub(node: astroid.node_classes.NodeNG) -> bool: + """Check if a node if is a function stub decorated with typing.overload. + + :param node: Node to check. + :returns: True if node is an overload function stub. False otherwise. + """ + return isinstance(node, astroid.FunctionDef) and decorated_with( + node, ["typing.overload"] + ) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 4ae13cadf..26cb94555 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -968,6 +968,45 @@ class VariablesChecker(BaseChecker): regex = authorized_rgx return regex and regex.match(name) + def _check_unused_arguments(self, name, node, stmt, argnames): + is_method = node.is_method() + klass = node.parent.frame() + if is_method and isinstance(klass, astroid.ClassDef): + confidence = ( + INFERENCE if utils.has_known_bases(klass) else INFERENCE_FAILURE + ) + else: + confidence = HIGH + + if is_method: + # Don't warn for the first argument of a (non static) method + if node.type != "staticmethod" and name == argnames[0]: + return + # Don't warn for argument of an overridden method + overridden = overridden_method(klass, node.name) + if overridden is not None and name in overridden.argnames(): + return + if node.name in utils.PYMETHODS and node.name not in ( + "__init__", + "__new__", + ): + return + # Don't check callback arguments + if any( + node.name.startswith(cb) or node.name.endswith(cb) + for cb in self.config.callbacks + ): + return + # Don't check arguments of singledispatch.register function. + if utils.is_registered_in_singledispatch_function(node): + return + + # Don't check function stubs created only for type information + if utils.is_overload_stub(node): + return + + self.add_message("unused-argument", args=name, node=stmt, confidence=confidence) + def _check_is_unused(self, name, node, stmt, global_names, nonlocal_names): # pylint: disable=too-many-branches # Ignore some special names specified by user configuration. @@ -991,42 +1030,9 @@ class VariablesChecker(BaseChecker): argnames = list( itertools.chain(node.argnames(), [arg.name for arg in node.args.kwonlyargs]) ) - is_method = node.is_method() - klass = node.parent.frame() - if is_method and isinstance(klass, astroid.ClassDef): - confidence = ( - INFERENCE if utils.has_known_bases(klass) else INFERENCE_FAILURE - ) - else: - confidence = HIGH - # Care about functions with unknown argument (builtins) if name in argnames: - if is_method: - # Don't warn for the first argument of a (non static) method - if node.type != "staticmethod" and name == argnames[0]: - return - # Don't warn for argument of an overridden method - overridden = overridden_method(klass, node.name) - if overridden is not None and name in overridden.argnames(): - return - if node.name in utils.PYMETHODS and node.name not in ( - "__init__", - "__new__", - ): - return - # Don't check callback arguments - if any( - node.name.startswith(cb) or node.name.endswith(cb) - for cb in self.config.callbacks - ): - return - # Don't check arguments of singledispatch.register function. - if utils.is_registered_in_singledispatch_function(node): - return - self.add_message( - "unused-argument", args=name, node=stmt, confidence=confidence - ) + self._check_unused_arguments(name, node, stmt, argnames) else: if stmt.parent and isinstance( stmt.parent, (astroid.Assign, astroid.AnnAssign) @@ -1069,6 +1075,11 @@ class VariablesChecker(BaseChecker): self.add_message("unused-import", args=msg, node=stmt) return message_name = "unused-variable" + + # Don't check function stubs created only for type information + if utils.is_overload_stub(node): + return + self.add_message(message_name, args=name, node=stmt) def leave_functiondef(self, node): diff --git a/pylint/test/functional/typing_use.py b/pylint/test/functional/typing_use.py new file mode 100644 index 000000000..3e2c48ba0 --- /dev/null +++ b/pylint/test/functional/typing_use.py @@ -0,0 +1,51 @@ +# pylint: disable=missing-docstring + +import typing + + +@typing.overload +def double_with_docstring(arg: str) -> str: + """Return arg, concatenated with itself.""" + + +@typing.overload +def double_with_docstring(arg: int) -> int: + """Return twice arg.""" + + +def double_with_docstring(arg): + """Return 2 * arg.""" + return 2 * arg + + +def double_with_docstring(arg): # [function-redefined] + """Redefined function implementation""" + return 2 * arg + + +@typing.overload +def double_with_ellipsis(arg: str) -> str: + ... + + +@typing.overload +def double_with_ellipsis(arg: int) -> int: + ... + + +def double_with_ellipsis(arg): + return 2 * arg + + +@typing.overload +def double_with_pass(arg: str) -> str: + pass + + +@typing.overload +def double_with_pass(arg: int) -> int: + pass + + +def double_with_pass(arg): + return 2 * arg diff --git a/pylint/test/functional/typing_use.rc b/pylint/test/functional/typing_use.rc new file mode 100644 index 000000000..0ba2b6333 --- /dev/null +++ b/pylint/test/functional/typing_use.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.6 diff --git a/pylint/test/functional/typing_use.txt b/pylint/test/functional/typing_use.txt new file mode 100644 index 000000000..1d0233c98 --- /dev/null +++ b/pylint/test/functional/typing_use.txt @@ -0,0 +1 @@ +function-redefined:21:double_with_docstring:"function already defined line 16" -- cgit v1.2.1