From d70e6d49959fcef0a447ecb57e8d31c68a99e912 Mon Sep 17 00:00:00 2001 From: James Lingard Date: Wed, 25 Nov 2009 09:52:56 +0100 Subject: Add a checker verifying that the arguments passed to a function call match the function's formal parameters --- checkers/typecheck.py | 149 ++++++++++++++++++++++++++++++++- test/input/func_arguments.py | 35 ++++++++ test/input/func_format.py | 2 +- test/input/func_method_missing_self.py | 1 - test/messages/func_arguments.txt | 13 +++ test/regrtest_data/numarray_import.py | 2 +- 6 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 test/input/func_arguments.py create mode 100644 test/messages/func_arguments.txt diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 2872a173d..ab0484323 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -43,6 +43,22 @@ MSGS = { 'W1111': ('Assigning to function call which only returns None', 'Used when an assignment is done on a function call but the \ inferred function returns nothing but None.'), + + 'E1120': ('No value passed for parameter %s in function call', + 'Used when a function call passes too few arguments.'), + 'E1121': ('Too many positional arguments for function call', + 'Used when a function call passes too many positional \ + arguments.'), + 'E1122': ('Duplicate keyword argument %r in function call', + 'Used when a function call passes the same keyword argument \ + multiple times.'), + 'E1123': ('Passing unexpected keyword argument %r in function call', + 'Used when a function call passes a keyword argument that \ + doesn\'t correspond to one of the function\'s parameter names.'), + 'E1124': ('Multiple values passed for parameter %r in function call', + 'Used when a function call would result in assigning multiple \ + values to a function parameter, one value from a positional \ + argument and one from a keyword argument.'), } class TypeChecker(BaseChecker): @@ -202,13 +218,144 @@ accessed.'} self.add_message('W1111', node=node) def visit_callfunc(self, node): - """check that called method are infered to callable objects + """check that called functions/methods are inferred to callable objects, + and that the arguments passed to the function match the parameters in + the inferred function's definition """ + + # Build the set of keyword arguments, checking for duplicate keywords, + # and count the positional arguments. + keyword_args = set() + num_positional_args = 0 + for arg in node.args: + if isinstance(arg, astng.Keyword): + keyword = arg.arg + if keyword in keyword_args: + self.add_message('E1122', node=node, args=keyword) + keyword_args.add(keyword) + else: + num_positional_args += 1 + called = safe_infer(node.func) # only function, generator and object defining __call__ are allowed if called is not None and not called.callable(): self.add_message('E1102', node=node, args=node.func.as_string()) + # Note that BoundMethod is a subclass of UnboundMethod (huh?), so must + # come first in this 'if..else'. + if isinstance(called, astng.BoundMethod): + # Bound methods have an extra implicit 'self' argument. + num_positional_args += 1 + elif isinstance(called, astng.UnboundMethod): + if called.decorators is not None: + for d in called.decorators.nodes: + if isinstance(d, astng.Name) and (d.name == 'classmethod'): + # Class methods have an extra implicit 'cls' argument. + num_positional_args += 1 + break + elif (isinstance(called, astng.Function) or + isinstance(called, astng.Lambda)): + pass + else: + return + + if called.args.args is None: + # Built-in functions have no argument information. + return + + if len( called.argnames() ) != len( set( called.argnames() ) ): + # Duplicate parameter name (see E9801). We can't really make sense + # of the function call in this case, so just return. + return + + # Analyze the list of formal parameters. + num_mandatory_parameters = len(called.args.args) - len(called.args.defaults) + parameters = [] + parameter_name_to_index = {} + for i, arg in enumerate(called.args.args): + if isinstance(arg, astng.Tuple): + name = None + # Don't store any parameter names within the tuple, since those + # are not assignable from keyword arguments. + else: + if isinstance(arg, astng.Keyword): + name = arg.arg + else: + assert isinstance(arg, astng.AssName) + # This occurs with: + # def f( (a), (b) ): pass + name = arg.name + parameter_name_to_index[name] = i + if i >= num_mandatory_parameters: + defval = called.args.defaults[i - num_mandatory_parameters] + else: + defval = None + parameters.append([(name, defval), False]) + + # Match the supplied arguments against the function parameters. + + # 1. Match the positional arguments. + for i in range(num_positional_args): + if i < len(parameters): + parameters[i][1] = True + elif called.args.vararg is not None: + # The remaining positional arguments get assigned to the *args + # parameter. + break + else: + # Too many positional arguments. + self.add_message('E1121', node=node) + break + + # 2. Match the keyword arguments. + for keyword in keyword_args: + if keyword in parameter_name_to_index: + i = parameter_name_to_index[keyword] + if parameters[i][1]: + # Duplicate definition of function parameter. + self.add_message('E1124', node=node, args=keyword) + else: + parameters[i][1] = True + elif called.args.kwarg is not None: + # The keyword argument gets assigned to the **kwargs parameter. + pass + else: + # Unexpected keyword argument. + self.add_message('E1123', node=node, args=keyword) + + # 3. Match the *args, if any. Note that Python actually processes + # *args _before_ any keyword arguments, but we wait until after + # looking at the keyword arguments so as to make a more conservative + # guess at how many values are in the *args sequence. + if node.starargs is not None: + for i in range(num_positional_args, len(parameters)): + [(name, defval), assigned] = parameters[i] + # Assume that *args provides just enough values for all + # non-default parameters after the last parameter assigned by + # the positional arguments but before the first parameter + # assigned by the keyword arguments. This is the best we can + # get without generating any false positives. + if (defval is not None) or assigned: + break + parameters[i][1] = True + + # 4. Match the **kwargs, if any. + if node.kwargs is not None: + for i, [(name, defval), assigned] in enumerate(parameters): + # Assume that *kwargs provides values for all remaining + # unassigned named parameters. + if name is not None: + parameters[i][1] = True + else: + # **kwargs can't assign to tuples. + pass + + # Check that any parameters without a default have been assigned + # values. + for [(name, defval), assigned] in parameters: + if (defval is None) and not assigned: + display_name = repr(name) if (name is not None) else '' + self.add_message('E1120', node=node, args=display_name) def register(linter): """required method to auto register this checker """ diff --git a/test/input/func_arguments.py b/test/input/func_arguments.py new file mode 100644 index 000000000..ce3cc5425 --- /dev/null +++ b/test/input/func_arguments.py @@ -0,0 +1,35 @@ +"""Test function argument checker""" +__revision__ = '' + +def function_1_arg(first_argument): + """one argument function""" + return first_argument + +def function_3_args(first_argument, second_argument, third_argument): + """three arguments function""" + return first_argument, second_argument, third_argument + +def function_default_arg(one=1, two=2): + """fonction with default value""" + return two, one + + +function_1_arg(420) +function_1_arg() +function_1_arg(1337, 347) + +function_3_args(420, 789) +function_3_args() +function_3_args(1337, 347, 456) +function_3_args('bab', 'bebe', None, 5.6) + +function_default_arg(1, two=5) +function_default_arg(two=5) +function_default_arg(two=5, two=7) +function_default_arg(two=5, one=7, one='bob') + +function_1_arg(bob=4) +function_default_arg(1, 4, coin="hello") + +function_default_arg(1, one=5) + diff --git a/test/input/func_format.py b/test/input/func_format.py index 1570f30d0..6cfbf6869 100644 --- a/test/input/func_format.py +++ b/test/input/func_format.py @@ -58,7 +58,7 @@ yo+=4 """ func('''Hello -''') +''', 0) assert boo <= 10, "Note is %.2f. Either you cheated, or pylint's \ broken!" % boo diff --git a/test/input/func_method_missing_self.py b/test/input/func_method_missing_self.py index 5bdbc6940..e553c418f 100644 --- a/test/input/func_method_missing_self.py +++ b/test/input/func_method_missing_self.py @@ -21,5 +21,4 @@ class MyClass: if __name__ == '__main__': OBJ = MyClass() - OBJ.met() diff --git a/test/messages/func_arguments.txt b/test/messages/func_arguments.txt new file mode 100644 index 000000000..2596f4ab8 --- /dev/null +++ b/test/messages/func_arguments.txt @@ -0,0 +1,13 @@ +E: 18: No value passed for parameter 'first_argument' in function call +E: 19: Too many positional arguments for function call +E: 21: No value passed for parameter 'third_argument' in function call +E: 22: No value passed for parameter 'first_argument' in function call +E: 22: No value passed for parameter 'second_argument' in function call +E: 22: No value passed for parameter 'third_argument' in function call +E: 24: Too many positional arguments for function call +E: 28: Duplicate keyword argument 'two' in function call +E: 29: Duplicate keyword argument 'one' in function call +E: 31: No value passed for parameter 'first_argument' in function call +E: 31: Passing unexpected keyword argument 'bob' in function call +E: 32: Passing unexpected keyword argument 'coin' in function call +E: 34: Multiple values passed for parameter 'one' in function call diff --git a/test/regrtest_data/numarray_import.py b/test/regrtest_data/numarray_import.py index 3f0be8b7b..e89757f19 100644 --- a/test/regrtest_data/numarray_import.py +++ b/test/regrtest_data/numarray_import.py @@ -4,4 +4,4 @@ __revision__ = 1 from numarray import zeros -zeros() +zeros(shape=(4, 5)) -- cgit v1.2.1