Here's a patch that adds this functionality to the 'typecheck' checker, and
uses the underscore naming convention (not CamelCase).  It also contains a
couple of fixes for bugs in the version that I originally posted.

James.

======================

--- typecheck.py  2009-11-23 14:39:03.000000000 -0800
+++ typecheck.py        2009-11-23 15:10:08.000000000 -0800
@@ -43,6 +43,22 @@
     'W1111': ('Assigning to function call which only returns None',
               'Used when an assigment is done on a function call but the \
               infered 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 @@
                 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
'<tuple>'
+                self.add_message('E1120', node=node, args=display_name)

 def register(linter):
     """required method to auto register this checker """
_______________________________________________
Python-Projects mailing list
Python-Projects@lists.logilab.org
http://lists.logilab.org/mailman/listinfo/python-projects

Reply via email to