summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>2021-12-18 12:01:15 +0100
committerGitHub <noreply@github.com>2021-12-18 12:01:15 +0100
commitbfeca4ccb520a1057dc74bac84c5fc874b06380d (patch)
treef0299e839b154801ee410ebcbd516b90b850c560
parentbb9cb4b4428d2e5769737e9ca23e46e85614412a (diff)
downloadpylint-git-bfeca4ccb520a1057dc74bac84c5fc874b06380d.tar.gz
Check if decorator returns use keyword (``unexpected-keyword-arg``) (#5547)
* Improve coverage * Remove unnecessary declaration * Change spacing
-rw-r--r--ChangeLog4
-rw-r--r--doc/whatsnew/2.13.rst4
-rw-r--r--pylint/checkers/typecheck.py44
-rw-r--r--tests/functional/u/unexpected_keyword_arg.py118
-rw-r--r--tests/functional/u/unexpected_keyword_arg.txt4
5 files changed, 174 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index 08b017a63..5f9508643 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -107,6 +107,10 @@ Release date: TBA
* The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests
without these will trigger a ``DeprecationWarning``.
+* Fixed false positive ``unexpected-keyword-arg`` for decorators.
+
+ Closes #258
+
* ``missing-raises-doc`` will now check the class hierarchy of the raised exceptions
.. code-block:: python
diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst
index c5e33bb64..357f78b59 100644
--- a/doc/whatsnew/2.13.rst
+++ b/doc/whatsnew/2.13.rst
@@ -101,6 +101,10 @@ Other Changes
* The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests
without these will trigger a ``DeprecationWarning``.
+* Fixed false positive ``unexpected-keyword-arg`` for decorators.
+
+ Closes #258
+
* ``missing-raises-doc`` will now check the class hierarchy of the raised exceptions
.. code-block:: python
diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py
index 9a8d8cbbe..6885e8cbf 100644
--- a/pylint/checkers/typecheck.py
+++ b/pylint/checkers/typecheck.py
@@ -1457,6 +1457,10 @@ accessed. Python regular expressions are accepted.",
elif called.args.kwarg is not None:
# The keyword argument gets assigned to the **kwargs parameter.
pass
+ elif isinstance(
+ called, nodes.FunctionDef
+ ) and self._keyword_argument_is_in_all_decorator_returns(called, keyword):
+ pass
elif not overload_function:
# Unexpected keyword argument.
self.add_message(
@@ -1496,6 +1500,46 @@ accessed. Python regular expressions are accepted.",
):
self.add_message("missing-kwoa", node=node, args=(name, callable_name))
+ @staticmethod
+ def _keyword_argument_is_in_all_decorator_returns(
+ func: nodes.FunctionDef, keyword: str
+ ) -> bool:
+ """Check if the keyword argument exists in all signatures of the
+ return values of all decorators of the function.
+ """
+ if not func.decorators:
+ return False
+
+ for decorator in func.decorators.nodes:
+ inferred = safe_infer(decorator)
+
+ # If we can't infer the decorator we assume it satisfies consumes
+ # the keyword, so we don't raise false positives
+ if not inferred:
+ return True
+
+ # We only check arguments of function decorators
+ if not isinstance(inferred, nodes.FunctionDef):
+ return False
+
+ for return_value in inferred.infer_call_result():
+ # infer_call_result() returns nodes.Const.None for None return values
+ # so this also catches non-returning decorators
+ if not isinstance(return_value, nodes.FunctionDef):
+ return False
+
+ # If the return value uses a kwarg the keyword will be consumed
+ if return_value.args.kwarg:
+ continue
+
+ # Check if the keyword is another type of argument
+ if return_value.args.is_argument(keyword):
+ continue
+
+ return False
+
+ return True
+
def _check_invalid_sequence_index(self, subscript: nodes.Subscript):
# Look for index operations where the parent is a sequence type.
# If the types can be determined, only allow indices to be int,
diff --git a/tests/functional/u/unexpected_keyword_arg.py b/tests/functional/u/unexpected_keyword_arg.py
new file mode 100644
index 000000000..e7b648899
--- /dev/null
+++ b/tests/functional/u/unexpected_keyword_arg.py
@@ -0,0 +1,118 @@
+"""Tests for unexpected-keyword-arg"""
+# pylint: disable=undefined-variable, too-few-public-methods, missing-function-docstring, missing-class-docstring
+
+
+def non_param_decorator(func):
+ """Decorator without a parameter"""
+
+ def new_func():
+ func()
+
+ return new_func
+
+
+def param_decorator(func):
+ """Decorator with a parameter"""
+
+ def new_func(internal_arg=3):
+ func(junk=internal_arg)
+
+ return new_func
+
+
+def kwargs_decorator(func):
+ """Decorator with kwargs.
+ The if ... else makes the double decoration with param_decorator valid.
+ """
+
+ def new_func(**kwargs):
+ if "internal_arg" in kwargs:
+ func(junk=kwargs["internal_arg"])
+ else:
+ func(junk=kwargs["junk"])
+
+ return new_func
+
+
+@non_param_decorator
+def do_something(junk=None):
+ """A decorated function. This should not be passed a keyword argument"""
+ print(junk)
+
+
+do_something(internal_arg=2) # [unexpected-keyword-arg]
+
+
+@param_decorator
+def do_something_decorated(junk=None):
+ """A decorated function. This should be passed a keyword argument"""
+ print(junk)
+
+
+do_something_decorated(internal_arg=2)
+
+
+@kwargs_decorator
+def do_something_decorated_too(junk=None):
+ """A decorated function. This should be passed a keyword argument"""
+ print(junk)
+
+
+do_something_decorated_too(internal_arg=2)
+
+
+@non_param_decorator
+@kwargs_decorator
+def do_something_double_decorated(junk=None):
+ """A decorated function. This should not be passed a keyword argument.
+ non_param_decorator will raise an exception if a keyword argument is passed.
+ """
+ print(junk)
+
+
+do_something_double_decorated(internal_arg=2) # [unexpected-keyword-arg]
+
+
+@param_decorator
+@kwargs_decorator
+def do_something_double_decorated_correct(junk=None):
+ """A decorated function. This should be passed a keyword argument"""
+ print(junk)
+
+
+do_something_double_decorated_correct(internal_arg=2)
+
+
+# Test that we don't crash on Class decoration
+class DecoratorClass:
+ pass
+
+
+@DecoratorClass
+def crash_test():
+ pass
+
+
+crash_test(internal_arg=2) # [unexpected-keyword-arg]
+
+
+# Test that we don't emit a false positive for uninferable decorators
+@unknown_decorator
+def crash_test_two():
+ pass
+
+
+crash_test_two(internal_arg=2)
+
+
+# Test that we don't crash on decorators that don't return anything
+def no_return_decorator(func):
+ print(func)
+
+
+@no_return_decorator
+def test_no_return():
+ pass
+
+
+test_no_return(internal_arg=2) # [unexpected-keyword-arg]
diff --git a/tests/functional/u/unexpected_keyword_arg.txt b/tests/functional/u/unexpected_keyword_arg.txt
new file mode 100644
index 000000000..3cc968e88
--- /dev/null
+++ b/tests/functional/u/unexpected_keyword_arg.txt
@@ -0,0 +1,4 @@
+unexpected-keyword-arg:43:0:43:28::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
+unexpected-keyword-arg:73:0:73:45::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
+unexpected-keyword-arg:96:0:96:26::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
+unexpected-keyword-arg:118:0:118:30::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED