Below is a new pylint checker that checks that the arguments passed to a function call match the function's formal parameters. For example, on the following file:
def f( a, b=1 ): print a, b f() f( 1, 2, 3 ) f( a=1, a=2 ) f( 1, c=3 ) f( 1, a=2 ) the checker produces the following output: ************* Module xyz E9700: 3: No value passed for parameter 'a' in function call E9701: 4: Two many positional arguments for function call E9702: 5: Duplicate keyword argument 'a' in function call E9703: 6: Passing unexpected keyword argument 'c' in function call E9704: 7: Multiple values passed for parameter 'a' in function call It handles function definitions that use *args and **kwargs. It handles function calls that pass *args and/or **kwargs somewhat conservatively, only warning if there are no possible values of the args and kwargs that will make the call succeed -- it doesn't attempt to do any inference on the args or kwargs. The checker also checks calls to bound and unbound methods (including static and class methods) and lambda functions. I've tested this on a large body of code, and it's successfully found several errors. The only false positives I'm aware of are in the following case: class Foo( object ): def f( self ): pass g = f The checker will complain that a call to "Foo().g()" is missing the "self" parameter, since inference claims that Foo().g is a function definition, not a bound method. As before, let me know if you're interested in incorporating this into pylint. If so, I'd be happy to work on some unit test cases, and I'd love to hear any feedback. Thanks, James. ==================================== # Copyright (c) 2009 Arista Networks, Inc. # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """Checker for function arguments. """ from logilab import astng from pylint.interfaces import IASTNGChecker from pylint.checkers import BaseChecker from pylint.checkers.utils import safe_infer MSGS = { 'E9700': ("No value passed for parameter %s in function call", "Used when a function call passes too few arguments."), 'E9701': ("Two many positional arguments for function call", "Used when a function call passes too many positional \ arguments."), 'E9702': ("Duplicate keyword argument %r in function call", "Used when a function call passes the same keyword argument \ multiple times."), 'E9703': ("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."), 'E9704': ("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 ArgumentChecker(BaseChecker): """Checks that the arguments passed to a function or method call match the parameters in the function's definition. """ __implements__ = (IASTNGChecker,) name = 'arguments' msgs = MSGS def visit_callfunc(self, node): # Build the set of keyword arguments, checking for duplicate keywords, # and count the positional arguments. keywordArgs = set() numPositionalArgs = 0 for arg in node.args: if isinstance(arg, astng.Keyword): keyword = arg.arg if keyword in keywordArgs: self.add_message('E9702', node=node, args=keyword) keywordArgs.add(keyword) else: numPositionalArgs += 1 func = safe_infer(node.func) # Note that BoundMethod is a subclass of UnboundMethod (huh?), so must # come first in this 'if..else'. if isinstance(func, astng.BoundMethod): # Bound methods have an extra implicit 'self' argument. numPositionalArgs += 1 elif isinstance(func, astng.UnboundMethod): if func.decorators is not None: for d in func.decorators.nodes: if d.name == 'classmethod': # Class methods have an extra implicit 'cls' argument. numPositionalArgs += 1 break elif isinstance(func, astng.Function) or isinstance(func, astng.Lambda): pass else: return if func.args.args is None: # Built-in functions have no argument information. return if len( func.argnames() ) != len( set( func.argnames() ) ): # Duplicate parameter name. We can't really make sense # of the function call in this case, so just return. return # Analyze the list of formal parameters. numMandatoryParameters = len(func.args.args) - len(func.args.defaults) parameters = [] parameterNameToIndex = {} for i, arg in enumerate(func.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 parameterNameToIndex[name] = i if i >= numMandatoryParameters: defval = func.args.defaults[i - numMandatoryParameters] 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(numPositionalArgs): if i < len(parameters): parameters[i][1] = True elif func.args.vararg is not None: # The remaining positional arguments get assigned to the *args # parameter. break else: # Too many positional arguments. self.add_message('E9701', node=node) break # 2. Match the keyword arguments. for keyword in keywordArgs: if keyword in parameterNameToIndex: i = parameterNameToIndex[keyword] if parameters[i][1]: # Duplicate definition of function parameter. self.add_message('E9704', node=node, args=keyword) else: parameters[i][1] = True elif func.args.kwarg is not None: # The keyword argument gets assigned to the **kwargs parameter. pass else: # Unexpected keyword argument. self.add_message('E9703', 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(numPositionalArgs, 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: displayName = repr(name) if (name is not None) else '<tuple>' self.add_message('E9700', node=node, args=displayName) def register(linter): """required method to auto register this checker """ linter.register_checker(ArgumentChecker(linter))
_______________________________________________ Python-Projects mailing list Python-Projects@lists.logilab.org http://lists.logilab.org/mailman/listinfo/python-projects