Skip to content

Commit

Permalink
Fixed false positives for function stubs
Browse files Browse the repository at this point in the history
Close #1581
  • Loading branch information
AWhetter committed May 25, 2019
1 parent a9cb1c7 commit 6167f2d
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 35 deletions.
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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?
===========================
Expand Down
13 changes: 12 additions & 1 deletion pylint/checkers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,15 @@ def _check_in_loop(self, node, node_name):
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
Expand All @@ -847,6 +855,9 @@ def _check_redefinition(self, redeftype, node):
):
return

if utils.is_overload_stub(node):
return

dummy_variables_rgx = lint_utils.get_global_option(
self, "dummy-variables-rgx", default=None
)
Expand Down
12 changes: 12 additions & 0 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
)
79 changes: 45 additions & 34 deletions pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,45 @@ def _is_name_ignored(self, stmt, name):
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.
Expand All @@ -991,42 +1030,9 @@ def _check_is_unused(self, name, node, stmt, global_names, nonlocal_names):
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)
Expand Down Expand Up @@ -1069,6 +1075,11 @@ def _check_is_unused(self, name, node, stmt, global_names, nonlocal_names):
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):
Expand Down
51 changes: 51 additions & 0 deletions pylint/test/functional/typing_use.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions pylint/test/functional/typing_use.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.6
1 change: 1 addition & 0 deletions pylint/test/functional/typing_use.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
function-redefined:21:double_with_docstring:"function already defined line 16"

0 comments on commit 6167f2d

Please sign in to comment.