Skip to content

Commit

Permalink
Treat AsyncFunctionDef just like FunctionDef nodes
Browse files Browse the repository at this point in the history
When support for Python 3.5 was added, AsyncFunctionDef
wasn't handled properly in terms of FunctionDef, which meant
that most of the checks which involved a function were never
called for AsyncFunctionDef. This led to spurious false positives
which occurred when AsyncFunctionDef were analyzed.
Closes #767
  • Loading branch information
PCManticore committed Jan 9, 2016
1 parent d2a7144 commit e00abd9
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 3 deletions.
6 changes: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ ChangeLog for Pylint

--

* Treat AsyncFunctionDef just like FunctionDef nodes,
by implementing visit_asyncfunctiondef in terms of
visit_functiondef.

Closes issue #767.

* Take in account kwonlyargs when verifying that arguments
are defined with the check_docs extension.

Expand Down
10 changes: 9 additions & 1 deletion pylint/checkers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ def visit_functiondef(self, node):
else:
args.add(name)

visit_asyncfunctiondef = visit_functiondef

def _check_nonlocal_and_global(self, node):
"""Check that a name is both nonlocal and global."""
def same_scope(current):
Expand Down Expand Up @@ -777,7 +779,7 @@ def visit_expr(self, node):
# * a yield (which are wrapped by a discard node in _ast XXX)
# warn W0106 if we have any underlying function call (we can't predict
# side effects), else pointless-statement
if (isinstance(expr, (astroid.Yield, astroid.Call)) or
if (isinstance(expr, (astroid.Yield, astroid.Await, astroid.Call)) or
(isinstance(node.parent, astroid.TryExcept) and
node.parent.body == [node])):
return
Expand Down Expand Up @@ -871,6 +873,8 @@ def visit_functiondef(self, node):
self.stats[node.is_method() and 'method' or 'function'] += 1
self._check_dangerous_default(node)

visit_asyncfunctiondef = visit_functiondef

def _check_dangerous_default(self, node):
# check for dangerous default values as arguments
is_iterable = lambda n: isinstance(n, (astroid.List,
Expand Down Expand Up @@ -1238,6 +1242,8 @@ def visit_functiondef(self, node):
if args is not None:
self._recursive_check_names(args, node)

visit_asyncfunctiondef = visit_functiondef

@check_messages('blacklisted-name', 'invalid-name')
def visit_global(self, node):
for name in node.names:
Expand Down Expand Up @@ -1389,6 +1395,8 @@ def visit_functiondef(self, node):
else:
self._check_docstring(ftype, node)

visit_asyncfunctiondef = visit_functiondef

def _check_docstring(self, node_type, node, report_missing=True,
confidence=HIGH):
"""check the node has a non empty docstring"""
Expand Down
4 changes: 4 additions & 0 deletions pylint/checkers/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ def visit_functiondef(self, node):
except astroid.NotFoundError:
pass

visit_asyncfunctiondef = visit_functiondef

def _check_slots(self, node):
if '__slots__' not in node.locals:
return
Expand Down Expand Up @@ -1015,6 +1017,8 @@ def visit_functiondef(self, node):
if node.name in PYMETHODS:
self._check_unexpected_method_signature(node)

visit_asyncfunctiondef = visit_functiondef

def _check_unexpected_method_signature(self, node):
expected_params = SPECIAL_METHODS_PARAMS[node.name]

Expand Down
4 changes: 4 additions & 0 deletions pylint/checkers/design_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ def visit_functiondef(self, node):
# init statements counter
self._stmts = 1

visit_asyncfunctiondef = visit_functiondef

@check_messages('too-many-return-statements', 'too-many-branches',
'too-many-arguments', 'too-many-locals',
'too-many-statements')
Expand All @@ -274,6 +276,8 @@ def leave_functiondef(self, node):
self.add_message('too-many-statements', node=node,
args=(self._stmts, self.config.max_statements))

leave_asyncfunctiondef = leave_functiondef

def visit_return(self, _):
"""count number of returns"""
if not self._returns:
Expand Down
2 changes: 2 additions & 0 deletions pylint/checkers/newstyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ def visit_functiondef(self, node):
if name is not None:
self.add_message('bad-super-call', node=call, args=(name, ))

visit_asyncfunctiondef = visit_functiondef


def register(linter):
"""required method to auto register this checker """
Expand Down
2 changes: 2 additions & 0 deletions pylint/checkers/spelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ def visit_functiondef(self, node):
return
self._check_docstring(node)

visit_asyncfunctiondef = visit_functiondef

def _check_docstring(self, node):
"""check the node has any spelling errors"""
docstring = node.doc
Expand Down
5 changes: 4 additions & 1 deletion pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ def visit_functiondef(self, node):
elif is_builtin(name):
# do not print Redefining builtin for additional builtins
self.add_message('redefined-builtin', args=name, node=stmt)

def leave_functiondef(self, node):
"""leave function: check function's locals are consumed"""
not_consumed = self._to_consume.pop()[0]
Expand Down Expand Up @@ -621,6 +621,9 @@ def leave_functiondef(self, node):
continue
self.add_message('unused-variable', args=name, node=stmt)

visit_asyncfunctiondef = visit_functiondef
leave_asyncfunctiondef = leave_functiondef

@check_messages('global-variable-undefined', 'global-variable-not-assigned',
'global-statement', 'global-at-module-level',
'redefined-builtin')
Expand Down
65 changes: 65 additions & 0 deletions pylint/test/functional/async_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Check that Python 3.5's async functions are properly analyzed by Pylint."""
# pylint: disable=missing-docstring,invalid-name,too-few-public-methods
# pylint: disable=using-constant-test

async def next(): # [redefined-builtin]
pass

async def some_function(arg1, arg2): # [unused-argument]
await arg1


class OtherClass(object):

@staticmethod
def test():
return 42


class Class(object):

async def some_method(self):
super(OtherClass, self).test() # [bad-super-call]


# +1: [too-many-arguments,too-many-return-statements, too-many-branches]
async def complex_function(this, function, has, more, arguments, than,
one, _, should, have):
if 1:
return this
elif 1:
return function
elif 1:
return has
elif 1:
return more
elif 1:
return arguments
elif 1:
return than
try:
return one
finally:
pass
if 2:
return should
while True:
pass
if 1:
return have
elif 2:
return function
elif 3:
pass


# +1: [duplicate-argument-name,dangerous-default-value]
async def func(a, a, b=[]):
return a, b


# +1: [empty-docstring, blacklisted-name]
async def foo():
""


2 changes: 2 additions & 0 deletions pylint/test/functional/async_functions.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.5
10 changes: 10 additions & 0 deletions pylint/test/functional/async_functions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
redefined-builtin:5:next:"Redefining built-in 'next'"
unused-argument:8:some_function:"Unused argument 'arg2'"
bad-super-call:22:Class.some_method:"Bad first argument 'OtherClass' given to super()"
too-many-arguments:26:complex_function:Too many arguments (10/5)
too-many-branches:26:complex_function:Too many branches (13/12)
too-many-return-statements:26:complex_function:Too many return statements (10/6)
dangerous-default-value:57:func:Dangerous default value [] as argument
duplicate-argument-name:57:func:Duplicate argument name a in function definition
blacklisted-name:62:foo:Black listed name "foo"
empty-docstring:62:foo:Empty function docstring
2 changes: 1 addition & 1 deletion pylint/test/functional/yield_inside_async_function.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Test that `yield` or `yield from` can't be used inside an async function."""
# pylint: disable=missing-docstring
# pylint: disable=missing-docstring, unused-variable

async def good_coro():
def _inner():
Expand Down

0 comments on commit e00abd9

Please sign in to comment.