class ClassChecker(BaseChecker): """checks for : * methods without self as first argument * overridden methods signature * access only to existent members via self * attributes not defined in the __init__ method * unreachable code """ __implements__ = (IAstroidChecker, ) # configuration section name name = 'classes' # messages msgs = MSGS priority = -2 # configuration options options = ( ( 'ignore-iface-methods', # TODO(cpopa): remove this in Pylint 1.6. deprecated_option(opt_type="csv", help_msg="This is deprecated, because " "it is not used anymore.")), ('defining-attr-methods', { 'default': ('__init__', '__new__', 'setUp'), 'type': 'csv', 'metavar': '<method names>', 'help': 'List of method names used to declare (i.e. assign) \ instance attributes.' }), ('valid-classmethod-first-arg', { 'default': ('cls', ), 'type': 'csv', 'metavar': '<argument names>', 'help': 'List of valid names for the first argument in \ a class method.' }), ('valid-metaclass-classmethod-first-arg', { 'default': ('mcs', ), 'type': 'csv', 'metavar': '<argument names>', 'help': 'List of valid names for the first argument in \ a metaclass class method.' }), ( 'exclude-protected', { 'default': ( # namedtuple public API. '_asdict', '_fields', '_replace', '_source', '_make'), 'type': 'csv', 'metavar': '<protected access exclusions>', 'help': ('List of member names, which should be excluded ' 'from the protected access warning.') })) def __init__(self, linter=None): BaseChecker.__init__(self, linter) self._accessed = [] self._first_attrs = [] self._meth_could_be_func = None def visit_classdef(self, node): """init visit variable _accessed """ self._accessed.append(defaultdict(list)) self._check_bases_classes(node) # if not an exception or a metaclass if node.type == 'class' and has_known_bases(node): try: node.local_attr('__init__') except astroid.NotFoundError: self.add_message('no-init', args=node, node=node) self._check_slots(node) self._check_proper_bases(node) self._check_consistent_mro(node) def _check_consistent_mro(self, node): """Detect that a class has a consistent mro or duplicate bases.""" try: node.mro() except InconsistentMroError: self.add_message('inconsistent-mro', args=node.name, node=node) except DuplicateBasesError: self.add_message('duplicate-bases', args=node.name, node=node) except NotImplementedError: # Old style class, there's no mro so don't do anything. pass def _check_proper_bases(self, node): """ Detect that a class inherits something which is not a class or a type. """ for base in node.bases: ancestor = safe_infer(base) if ancestor in (astroid.YES, None): continue if (isinstance(ancestor, astroid.Instance) and ancestor.is_subtype_of('%s.type' % (BUILTINS, ))): continue if (not isinstance(ancestor, astroid.ClassDef) or _is_invalid_base_class(ancestor)): self.add_message('inherit-non-class', args=base.as_string(), node=node) def leave_classdef(self, cnode): """close a class node: check that instance attributes are defined in __init__ and check access to existent members """ # check access to existent members on non metaclass classes ignore_mixins = get_global_option(self, 'ignore-mixin-members', default=True) if ignore_mixins and cnode.name[-5:].lower() == 'mixin': # We are in a mixin class. No need to try to figure out if # something is missing, since it is most likely that it will # miss. return accessed = self._accessed.pop() if cnode.type != 'metaclass': self._check_accessed_members(cnode, accessed) # checks attributes are defined in an allowed method such as __init__ if not self.linter.is_message_enabled( 'attribute-defined-outside-init'): return defining_methods = self.config.defining_attr_methods current_module = cnode.root() for attr, nodes in six.iteritems(cnode.instance_attrs): # skip nodes which are not in the current module and it may screw up # the output, while it's not worth it nodes = [ n for n in nodes if not isinstance(n.statement(), (astroid.Delete, astroid.AugAssign)) and n.root() is current_module ] if not nodes: continue # error detected by typechecking # check if any method attr is defined in is a defining method if any(node.frame().name in defining_methods for node in nodes): continue # check attribute is defined in a parent's __init__ for parent in cnode.instance_attr_ancestors(attr): attr_defined = False # check if any parent method attr is defined in is a defining method for node in parent.instance_attrs[attr]: if node.frame().name in defining_methods: attr_defined = True if attr_defined: # we're done :) break else: # check attribute is defined as a class attribute try: cnode.local_attr(attr) except astroid.NotFoundError: for node in nodes: if node.frame().name not in defining_methods: # If the attribute was set by a callfunc in any # of the defining methods, then don't emit # the warning. if _called_in_methods(node.frame(), cnode, defining_methods): continue self.add_message('attribute-defined-outside-init', args=attr, node=node) def visit_functiondef(self, node): """check method arguments, overriding""" # ignore actual functions if not node.is_method(): return klass = node.parent.frame() self._meth_could_be_func = True # check first argument is self if this is actually a method self._check_first_arg_for_type(node, klass.type == 'metaclass') if node.name == '__init__': self._check_init(node) return # check signature if the method overloads inherited method for overridden in klass.local_attr_ancestors(node.name): # get astroid for the searched method try: meth_node = overridden[node.name] except KeyError: # we have found the method but it's not in the local # dictionary. # This may happen with astroid build from living objects continue if not isinstance(meth_node, astroid.FunctionDef): continue self._check_signature(node, meth_node, 'overridden', klass) break if node.decorators: for decorator in node.decorators.nodes: if isinstance(decorator, astroid.Attribute) and \ decorator.attrname in ('getter', 'setter', 'deleter'): # attribute affectation will call this method, not hiding it return if isinstance(decorator, astroid.Name) and decorator.name == 'property': # attribute affectation will either call a setter or raise # an attribute error, anyway not hiding the function return # check if the method is hidden by an attribute try: overridden = klass.instance_attr(node.name)[0] # XXX overridden_frame = overridden.frame() if (isinstance(overridden_frame, astroid.FunctionDef) and overridden_frame.type == 'method'): overridden_frame = overridden_frame.parent.frame() if (isinstance(overridden_frame, astroid.ClassDef) and klass.is_subtype_of(overridden_frame.qname())): args = (overridden.root().name, overridden.fromlineno) self.add_message('method-hidden', args=args, node=node) except astroid.NotFoundError: pass visit_asyncfunctiondef = visit_functiondef def _check_slots(self, node): if '__slots__' not in node.locals: return for slots in node.igetattr('__slots__'): # check if __slots__ is a valid type if slots is astroid.YES: continue if not is_iterable(slots) and not is_comprehension(slots): self.add_message('invalid-slots', node=node) continue if isinstance(slots, astroid.Const): # a string, ignore the following checks continue if not hasattr(slots, 'itered'): # we can't obtain the values, maybe a .deque? continue if isinstance(slots, astroid.Dict): values = [item[0] for item in slots.items] else: values = slots.itered() if values is astroid.YES: return for elt in values: try: self._check_slots_elt(elt) except astroid.InferenceError: continue def _check_slots_elt(self, elt): for infered in elt.infer(): if infered is astroid.YES: continue if (not isinstance(infered, astroid.Const) or not isinstance(infered.value, six.string_types)): self.add_message('invalid-slots-object', args=infered.as_string(), node=elt) continue if not infered.value: self.add_message('invalid-slots-object', args=infered.as_string(), node=elt) def leave_functiondef(self, node): """on method node, check if this method couldn't be a function ignore class, static and abstract methods, initializer, methods overridden from a parent class. """ if node.is_method(): if node.args.args is not None: self._first_attrs.pop() if not self.linter.is_message_enabled('no-self-use'): return class_node = node.parent.frame() if (self._meth_could_be_func and node.type == 'method' and node.name not in PYMETHODS and not (node.is_abstract() or overrides_a_method(class_node, node.name) or decorated_with_property(node) or (six.PY3 and _has_bare_super_call(node)))): self.add_message('no-self-use', node=node) def visit_attribute(self, node): """check if the getattr is an access to a class member if so, register it. Also check for access to protected class member from outside its class (but ignore __special__ methods) """ attrname = node.attrname # Check self if self.is_first_attr(node): self._accessed[-1][attrname].append(node) return if not self.linter.is_message_enabled('protected-access'): return self._check_protected_attribute_access(node) def visit_assignattr(self, node): if isinstance(node.assign_type(), astroid.AugAssign) and self.is_first_attr(node): self._accessed[-1][node.attrname].append(node) self._check_in_slots(node) def _check_in_slots(self, node): """ Check that the given assattr node is defined in the class slots. """ infered = safe_infer(node.expr) if infered and isinstance(infered, astroid.Instance): klass = infered._proxied if '__slots__' not in klass.locals or not klass.newstyle: return slots = klass.slots() if slots is None: return # If any ancestor doesn't use slots, the slots # defined for this class are superfluous. if any('__slots__' not in ancestor.locals and ancestor.name != 'object' for ancestor in klass.ancestors()): return if not any(slot.value == node.attrname for slot in slots): # If we have a '__dict__' in slots, then # assigning any name is valid. if not any(slot.value == '__dict__' for slot in slots): if _is_attribute_property(node.attrname, klass): # Properties circumvent the slots mechanism, # so we should not emit a warning for them. return if (node.attrname in klass.locals and _has_data_descriptor(klass, node.attrname)): # Descriptors circumvent the slots mechanism as well. return self.add_message('assigning-non-slot', args=(node.attrname, ), node=node) @check_messages('protected-access', 'no-classmethod-decorator', 'no-staticmethod-decorator') def visit_assign(self, assign_node): self._check_classmethod_declaration(assign_node) node = assign_node.targets[0] if not isinstance(node, astroid.AssignAttr): return if self.is_first_attr(node): return self._check_protected_attribute_access(node) def _check_classmethod_declaration(self, node): """Checks for uses of classmethod() or staticmethod() When a @classmethod or @staticmethod decorator should be used instead. A message will be emitted only if the assignment is at a class scope and only if the classmethod's argument belongs to the class where it is defined. `node` is an assign node. """ if not isinstance(node.value, astroid.Call): return # check the function called is "classmethod" or "staticmethod" func = node.value.func if (not isinstance(func, astroid.Name) or func.name not in ('classmethod', 'staticmethod')): return msg = ('no-classmethod-decorator' if func.name == 'classmethod' else 'no-staticmethod-decorator') # assignment must be at a class scope parent_class = node.scope() if not isinstance(parent_class, astroid.ClassDef): return # Check if the arg passed to classmethod is a class member classmeth_arg = node.value.args[0] if not isinstance(classmeth_arg, astroid.Name): return method_name = classmeth_arg.name if any(method_name == member.name for member in parent_class.mymethods()): self.add_message(msg, node=node.targets[0]) def _check_protected_attribute_access(self, node): '''Given an attribute access node (set or get), check if attribute access is legitimate. Call _check_first_attr with node before calling this method. Valid cases are: * self._attr in a method or cls._attr in a classmethod. Checked by _check_first_attr. * Klass._attr inside "Klass" class. * Klass2._attr inside "Klass" class when Klass2 is a base class of Klass. ''' attrname = node.attrname if (is_attr_protected(attrname) and attrname not in self.config.exclude_protected): klass = node_frame_class(node) # XXX infer to be more safe and less dirty ?? # in classes, check we are not getting a parent method # through the class object or through super callee = node.expr.as_string() # We are not in a class, no remaining valid case if klass is None: self.add_message('protected-access', node=node, args=attrname) return # If the expression begins with a call to super, that's ok. if isinstance(node.expr, astroid.Call) and \ isinstance(node.expr.func, astroid.Name) and \ node.expr.func.name == 'super': return # We are in a class, one remaining valid cases, Klass._attr inside # Klass if not (callee == klass.name or callee in klass.basenames): # Detect property assignments in the body of the class. # This is acceptable: # # class A: # b = property(lambda: self._b) stmt = node.parent.statement() if (isinstance(stmt, astroid.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], astroid.AssignName)): name = stmt.targets[0].name if _is_attribute_property(name, klass): return self.add_message('protected-access', node=node, args=attrname) def visit_name(self, node): """check if the name handle an access to a class member if so, register it """ if self._first_attrs and (node.name == self._first_attrs[-1] or not self._first_attrs[-1]): self._meth_could_be_func = False def _check_accessed_members(self, node, accessed): """check that accessed members are defined""" # XXX refactor, probably much simpler now that E0201 is in type checker excs = ('AttributeError', 'Exception', 'BaseException') for attr, nodes in six.iteritems(accessed): try: # is it a class attribute ? node.local_attr(attr) # yes, stop here continue except astroid.NotFoundError: pass # is it an instance attribute of a parent class ? try: next(node.instance_attr_ancestors(attr)) # yes, stop here continue except StopIteration: pass # is it an instance attribute ? try: defstmts = node.instance_attr(attr) except astroid.NotFoundError: pass else: # filter out augment assignment nodes defstmts = [stmt for stmt in defstmts if stmt not in nodes] if not defstmts: # only augment assignment for this node, no-member should be # triggered by the typecheck checker continue # filter defstmts to only pick the first one when there are # several assignments in the same scope scope = defstmts[0].scope() defstmts = [ stmt for i, stmt in enumerate(defstmts) if i == 0 or stmt.scope() is not scope ] # if there are still more than one, don't attempt to be smarter # than we can be if len(defstmts) == 1: defstmt = defstmts[0] # check that if the node is accessed in the same method as # it's defined, it's accessed after the initial assignment frame = defstmt.frame() lno = defstmt.fromlineno for _node in nodes: if _node.frame() is frame and _node.fromlineno < lno \ and not astroid.are_exclusive(_node.statement(), defstmt, excs): self.add_message('access-member-before-definition', node=_node, args=(attr, lno)) def _check_first_arg_for_type(self, node, metaclass=0): """check the name of first argument, expect: * 'self' for a regular method * 'cls' for a class method or a metaclass regular method (actually valid-classmethod-first-arg value) * 'mcs' for a metaclass class method (actually valid-metaclass-classmethod-first-arg) * not one of the above for a static method """ # don't care about functions with unknown argument (builtins) if node.args.args is None: return first_arg = node.args.args and node.argnames()[0] self._first_attrs.append(first_arg) first = self._first_attrs[-1] # static method if node.type == 'staticmethod': if (first_arg == 'self' or first_arg in self.config.valid_classmethod_first_arg or first_arg in self.config.valid_metaclass_classmethod_first_arg): self.add_message('bad-staticmethod-argument', args=first, node=node) return self._first_attrs[-1] = None # class / regular method with no args elif not node.args.args: self.add_message('no-method-argument', node=node) # metaclass elif metaclass: # metaclass __new__ or classmethod if node.type == 'classmethod': self._check_first_arg_config( first, self.config.valid_metaclass_classmethod_first_arg, node, 'bad-mcs-classmethod-argument', node.name) # metaclass regular method else: self._check_first_arg_config( first, self.config.valid_classmethod_first_arg, node, 'bad-mcs-method-argument', node.name) # regular class else: # class method if node.type == 'classmethod': self._check_first_arg_config( first, self.config.valid_classmethod_first_arg, node, 'bad-classmethod-argument', node.name) # regular method without self as argument elif first != 'self': self.add_message('no-self-argument', node=node) def _check_first_arg_config(self, first, config, node, message, method_name): if first not in config: if len(config) == 1: valid = repr(config[0]) else: valid = ', '.join(repr(v) for v in config[:-1]) valid = '%s or %r' % (valid, config[-1]) self.add_message(message, args=(method_name, valid), node=node) def _check_bases_classes(self, node): """check that the given class node implements abstract methods from base classes """ def is_abstract(method): return method.is_abstract(pass_is_abstract=False) # check if this class abstract if class_is_abstract(node): return methods = sorted( unimplemented_abstract_methods(node, is_abstract).items(), key=lambda item: item[0], ) for name, method in methods: owner = method.parent.frame() if owner is node: continue # owner is not this class, it must be a parent class # check that the ancestor's method is not abstract if name in node.locals: # it is redefined as an attribute or with a descriptor continue self.add_message('abstract-method', node=node, args=(name, owner.name)) def _check_init(self, node): """check that the __init__ method call super or ancestors'__init__ method """ if (not self.linter.is_message_enabled('super-init-not-called') and not self.linter.is_message_enabled('non-parent-init-called')): return klass_node = node.parent.frame() to_call = _ancestors_to_call(klass_node) not_called_yet = dict(to_call) for stmt in node.nodes_of_class(astroid.Call): expr = stmt.func if not isinstance(expr, astroid.Attribute) \ or expr.attrname != '__init__': continue # skip the test if using super if isinstance(expr.expr, astroid.Call) and \ isinstance(expr.expr.func, astroid.Name) and \ expr.expr.func.name == 'super': return try: for klass in expr.expr.infer(): if klass is astroid.YES: continue # The infered klass can be super(), which was # assigned to a variable and the `__init__` # was called later. # # base = super() # base.__init__(...) if (isinstance(klass, astroid.Instance) and isinstance(klass._proxied, astroid.ClassDef) and is_builtin_object(klass._proxied) and klass._proxied.name == 'super'): return elif isinstance(klass, objects.Super): return try: del not_called_yet[klass] except KeyError: if klass not in to_call: self.add_message('non-parent-init-called', node=expr, args=klass.name) except astroid.InferenceError: continue for klass, method in six.iteritems(not_called_yet): cls = node_frame_class(method) if klass.name == 'object' or (cls and cls.name == 'object'): continue self.add_message('super-init-not-called', args=klass.name, node=node) def _check_signature(self, method1, refmethod, class_type, cls): """check that the signature of the two given methods match """ if not (isinstance(method1, astroid.FunctionDef) and isinstance(refmethod, astroid.FunctionDef)): self.add_message('method-check-failed', args=(method1, refmethod), node=method1) return instance = cls.instanciate_class() method1 = function_to_method(method1, instance) refmethod = function_to_method(refmethod, instance) # Don't care about functions with unknown argument (builtins). if method1.args.args is None or refmethod.args.args is None: return # If we use *args, **kwargs, skip the below checks. if method1.args.vararg or method1.args.kwarg: return # Ignore private to class methods. if is_attr_private(method1.name): return # Ignore setters, they have an implicit extra argument, # which shouldn't be taken in consideration. if method1.decorators: for decorator in method1.decorators.nodes: if (isinstance(decorator, astroid.Attribute) and decorator.attrname == 'setter'): return method1_args = _get_method_args(method1) refmethod_args = _get_method_args(refmethod) if method1_args != refmethod_args: self.add_message('arguments-differ', args=(class_type, method1.name), node=method1) elif len(method1.args.defaults) < len(refmethod.args.defaults): self.add_message('signature-differs', args=(class_type, method1.name), node=method1) def is_first_attr(self, node): """Check that attribute lookup name use first attribute variable name (self for method, cls for classmethod and mcs for metaclass). """ return self._first_attrs and isinstance(node.expr, astroid.Name) and \ node.expr.name == self._first_attrs[-1]
class TypeChecker(BaseChecker): """try to find bugs in the code using type inference """ __implements__ = (IAstroidChecker, ) # configuration section name name = 'typecheck' # messages msgs = MSGS priority = -1 # configuration options options = ( ('ignore-mixin-members', { 'default': True, 'type': 'yn', 'metavar': '<y_or_n>', 'help': 'Tells whether missing members accessed in mixin \ class should be ignored. A mixin class is detected if its name ends with \ "mixin" (case insensitive).' }), ('ignored-modules', { 'default': (), 'type': 'csv', 'metavar': '<module names>', 'help': 'List of module names for which member attributes ' 'should not be checked (useful for modules/projects ' 'where namespaces are manipulated during runtime and ' 'thus existing member attributes cannot be ' 'deduced by static analysis. It supports qualified ' 'module names, as well as Unix pattern matching.' }), ('ignored-classes', { 'default': (), 'type': 'csv', 'metavar': '<members names>', 'help': 'List of classes names for which member attributes ' 'should not be checked (useful for classes with ' 'attributes dynamically set). This supports ' 'can work with qualified names.' }), ('zope', utils.deprecated_option(opt_type='yn', help_msg=_ZOPE_DEPRECATED)), ('generated-members', { 'default': (), 'type': 'string', 'metavar': '<members names>', 'help': 'List of members which are set dynamically and \ missed by pylint inference system, and so shouldn\'t trigger E1101 when \ accessed. Python regular expressions are accepted.' }), ) def open(self): # do this in open since config not fully initialized in __init__ self.generated_members = list(self.config.generated_members) def visit_assignattr(self, node): if isinstance(node.assign_type(), astroid.AugAssign): self.visit_attribute(node) def visit_delattr(self, node): self.visit_attribute(node) @check_messages('no-member') def visit_attribute(self, node): """check that the accessed attribute exists to avoid too much false positives for now, we'll consider the code as correct if a single of the inferred nodes has the accessed attribute. function/method, super call and metaclasses are ignored """ # generated_members may contain regular expressions # (surrounded by quote `"` and followed by a comma `,`) # REQUEST,aq_parent,"[a-zA-Z]+_set{1,2}"' => # ('REQUEST', 'aq_parent', '[a-zA-Z]+_set{1,2}') if isinstance(self.config.generated_members, str): gen = shlex.shlex(self.config.generated_members) gen.whitespace += ',' gen.wordchars += '[]-+' self.config.generated_members = tuple( tok.strip('"') for tok in gen) for pattern in self.config.generated_members: # attribute is marked as generated, stop here if re.match(pattern, node.attrname): return try: infered = list(node.expr.infer()) except exceptions.InferenceError: return # list of (node, nodename) which are missing the attribute missingattr = set() inference_failure = False for owner in infered: # skip yes object if owner is astroid.YES: inference_failure = True continue name = getattr(owner, 'name', None) if _is_owner_ignored(owner, name, self.config.ignored_classes, self.config.ignored_modules): continue try: if not [ n for n in owner.getattr(node.attrname) if not isinstance(n.statement(), astroid.AugAssign) ]: missingattr.add((owner, name)) continue except AttributeError: # XXX method / function continue except exceptions.NotFoundError: # This can't be moved before the actual .getattr call, # because there can be more values inferred and we are # stopping after the first one which has the attribute in question. # The problem is that if the first one has the attribute, # but we continue to the next values which doesn't have the # attribute, then we'll have a false positive. # So call this only after the call has been made. if not _emit_no_member(node, owner, name, self.config.ignore_mixin_members): continue missingattr.add((owner, name)) continue # stop on the first found break else: # we have not found any node with the attributes, display the # message for infered nodes done = set() for owner, name in missingattr: if isinstance(owner, astroid.Instance): actual = owner._proxied else: actual = owner if actual in done: continue done.add(actual) confidence = INFERENCE if not inference_failure else INFERENCE_FAILURE self.add_message('no-member', node=node, args=(owner.display_type(), name, node.attrname), confidence=confidence) @check_messages('assignment-from-no-return', 'assignment-from-none') def visit_assign(self, node): """check that if assigning to a function call, the function is possibly returning something valuable """ if not isinstance(node.value, astroid.Call): return function_node = safe_infer(node.value.func) # skip class, generator and incomplete function definition if not (isinstance(function_node, astroid.FunctionDef) and function_node.root().fully_defined()): return if function_node.is_generator() \ or function_node.is_abstract(pass_is_abstract=False): return returns = list( function_node.nodes_of_class(astroid.Return, skip_klass=astroid.FunctionDef)) if len(returns) == 0: self.add_message('assignment-from-no-return', node=node) else: for rnode in returns: if not (isinstance(rnode.value, astroid.Const) and rnode.value.value is None or rnode.value is None): break else: self.add_message('assignment-from-none', node=node) def _check_uninferable_callfunc(self, node): """ Check that the given uninferable CallFunc node does not call an actual function. """ if not isinstance(node.func, astroid.Attribute): return # Look for properties. First, obtain # the lhs of the Getattr node and search the attribute # there. If that attribute is a property or a subclass of properties, # then most likely it's not callable. # TODO: since astroid doesn't understand descriptors very well # we will not handle them here, right now. expr = node.func.expr klass = safe_infer(expr) if (klass is None or klass is astroid.YES or not isinstance(klass, astroid.Instance)): return try: attrs = klass._proxied.getattr(node.func.attrname) except exceptions.NotFoundError: return for attr in attrs: if attr is astroid.YES: continue if not isinstance(attr, astroid.FunctionDef): continue # Decorated, see if it is decorated with a property. # Also, check the returns and see if they are callable. if decorated_with_property(attr): if all(return_node.callable() for return_node in attr.infer_call_result(node)): continue else: self.add_message('not-callable', node=node, args=node.func.as_string()) break @check_messages(*(list(MSGS.keys()))) def visit_call(self, node): """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. call_site = astroid.arguments.CallSite.from_call(node) num_positional_args = len(call_site.positional_arguments) keyword_args = list(call_site.keyword_arguments.keys()) 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('not-callable', node=node, args=node.func.as_string()) self._check_uninferable_callfunc(node) try: called, implicit_args, callable_name = _determine_callable(called) except ValueError: # Any error occurred during determining the function type, most of # those errors are handled by different warnings. return num_positional_args += implicit_args 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 duplicate-argument). We can't really # make sense of the function call in this case, so just return. return # Warn about duplicated keyword arguments, such as `f=24, **{'f': 24}` for keyword in call_site.duplicated_keywords: self.add_message('repeated-keyword', node=node, args=(keyword, )) if call_site.has_invalid_arguments() or call_site.has_invalid_keywords( ): # Can't make sense of this. 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, astroid.Tuple): name = None # Don't store any parameter names within the tuple, since those # are not assignable from keyword arguments. else: assert isinstance(arg, astroid.AssignName) # 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]) kwparams = {} for i, arg in enumerate(called.args.kwonlyargs): if isinstance(arg, astroid.Keyword): name = arg.arg else: assert isinstance(arg, astroid.AssignName) name = arg.name kwparams[name] = [called.args.kw_defaults[i], 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('too-many-function-args', node=node, args=(callable_name, )) 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. # Might be too hardcoded, but this can actually # happen when using str.format and `self` is passed # by keyword argument, as in `.format(self=self)`. # It's perfectly valid to so, so we're just skipping # it if that's the case. if not (keyword == 'self' and called.qname() == STR_FORMAT): self.add_message('redundant-keyword-arg', node=node, args=(keyword, callable_name)) else: parameters[i][1] = True elif keyword in kwparams: if kwparams[keyword][1]: # XXX is that even possible? # Duplicate definition of function parameter. self.add_message('redundant-keyword-arg', node=node, args=(keyword, callable_name)) else: kwparams[keyword][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('unexpected-keyword-arg', node=node, args=(keyword, callable_name)) # 3. Match the **kwargs, if any. if node.kwargs: 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: if name is None: display_name = '<tuple>' else: display_name = repr(name) self.add_message('no-value-for-parameter', node=node, args=(display_name, callable_name)) for name in kwparams: defval, assigned = kwparams[name] if defval is None and not assigned: self.add_message('missing-kwoa', node=node, args=(name, callable_name)) @check_messages('invalid-sequence-index') def visit_extslice(self, node): # Check extended slice objects as if they were used as a sequence # index to check if the object being sliced can support them return self.visit_index(node) @check_messages('invalid-sequence-index') def visit_index(self, node): if not node.parent or not hasattr(node.parent, "value"): return # Look for index operations where the parent is a sequence type. # If the types can be determined, only allow indices to be int, # slice or instances with __index__. parent_type = safe_infer(node.parent.value) if not isinstance(parent_type, (astroid.ClassDef, astroid.Instance)): return # Determine what method on the parent this index will use # The parent of this node will be a Subscript, and the parent of that # node determines if the Subscript is a get, set, or delete operation. operation = node.parent.parent if isinstance(operation, astroid.Assign): methodname = '__setitem__' elif isinstance(operation, astroid.Delete): methodname = '__delitem__' else: methodname = '__getitem__' # Check if this instance's __getitem__, __setitem__, or __delitem__, as # appropriate to the statement, is implemented in a builtin sequence # type. This way we catch subclasses of sequence types but skip classes # that override __getitem__ and which may allow non-integer indices. try: methods = parent_type.getattr(methodname) if methods is astroid.YES: return itemmethod = methods[0] except (exceptions.NotFoundError, IndexError): return if not isinstance(itemmethod, astroid.FunctionDef): return if itemmethod.root().name != BUILTINS: return if not itemmethod.parent: return if itemmethod.parent.name not in SEQUENCE_TYPES: return # For ExtSlice objects coming from visit_extslice, no further # inference is necessary, since if we got this far the ExtSlice # is an error. if isinstance(node, astroid.ExtSlice): index_type = node else: index_type = safe_infer(node) if index_type is None or index_type is astroid.YES: return # Constants must be of type int if isinstance(index_type, astroid.Const): if isinstance(index_type.value, int): return # Instance values must be int, slice, or have an __index__ method elif isinstance(index_type, astroid.Instance): if index_type.pytype() in (BUILTINS + '.int', BUILTINS + '.slice'): return try: index_type.getattr('__index__') return except exceptions.NotFoundError: pass elif isinstance(index_type, astroid.Slice): # Delegate to visit_slice. A slice can be present # here after inferring the index node, which could # be a `slice(...)` call for instance. return self.visit_slice(index_type) # Anything else is an error self.add_message('invalid-sequence-index', node=node) @check_messages('invalid-slice-index') def visit_slice(self, node): # Check the type of each part of the slice for index in (node.lower, node.upper, node.step): if index is None: continue index_type = safe_infer(index) if index_type is None or index_type is astroid.YES: continue # Constants must of type int or None if isinstance(index_type, astroid.Const): if isinstance(index_type.value, (int, type(None))): continue # Instance values must be of type int, None or an object # with __index__ elif isinstance(index_type, astroid.Instance): if index_type.pytype() in (BUILTINS + '.int', BUILTINS + '.NoneType'): continue try: index_type.getattr('__index__') return except exceptions.NotFoundError: pass # Anything else is an error self.add_message('invalid-slice-index', node=node) @check_messages('not-context-manager') def visit_with(self, node): for ctx_mgr, _ in node.items: context = astroid.context.InferenceContext() infered = safe_infer(ctx_mgr, context=context) if infered is None or infered is astroid.YES: continue if isinstance(infered, bases.Generator): # Check if we are dealing with a function decorated # with contextlib.contextmanager. if decorated_with(infered.parent, ['contextlib.contextmanager']): continue # If the parent of the generator is not the context manager itself, # that means that it could have been returned from another # function which was the real context manager. # The following approach is more of a hack rather than a real # solution: walk all the inferred statements for the # given *ctx_mgr* and if you find one function scope # which is decorated, consider it to be the real # manager and give up, otherwise emit not-context-manager. # See the test file for not_context_manager for a couple # of self explaining tests. for path in six.moves.filter(None, _unflatten(context.path)): scope = path.scope() if not isinstance(scope, astroid.FunctionDef): continue if decorated_with(scope, ['contextlib.contextmanager']): break else: self.add_message('not-context-manager', node=node, args=(infered.name, )) else: try: infered.getattr('__enter__') infered.getattr('__exit__') except exceptions.NotFoundError: if isinstance(infered, astroid.Instance): # If we do not know the bases of this class, # just skip it. if not has_known_bases(infered): continue # Just ignore mixin classes. if self.config.ignore_mixin_members: if infered.name[-5:].lower() == 'mixin': continue self.add_message('not-context-manager', node=node, args=(infered.name, )) # Disabled until we'll have a more capable astroid. @check_messages('invalid-unary-operand-type') def _visit_unaryop(self, node): """Detect TypeErrors for unary operands.""" for error in node.type_errors(): # Let the error customize its output. self.add_message('invalid-unary-operand-type', args=str(error), node=node) @check_messages('unsupported-binary-operation') def _visit_binop(self, node): """Detect TypeErrors for binary arithmetic operands.""" self._check_binop_errors(node) @check_messages('unsupported-binary-operation') def _visit_augassign(self, node): """Detect TypeErrors for augmented binary arithmetic operands.""" self._check_binop_errors(node) def _check_binop_errors(self, node): for error in node.type_errors(): # Let the error customize its output. self.add_message('unsupported-binary-operation', args=str(error), node=node) def _check_membership_test(self, node): if is_inside_abstract_class(node): return if is_comprehension(node): return infered = safe_infer(node) if infered is None or infered is astroid.YES: return if not supports_membership_test(infered): self.add_message('unsupported-membership-test', args=node.as_string(), node=node) @check_messages('unsupported-membership-test') def visit_compare(self, node): if len(node.ops) != 1: return operator, right = node.ops[0] if operator in ['in', 'not in']: self._check_membership_test(right) @check_messages('unsubscriptable-object') def visit_subscript(self, node): if isinstance(node.value, (astroid.ListComp, astroid.DictComp)): return if isinstance(node.value, astroid.SetComp): self.add_message('unsubscriptable-object', args=node.value.as_string(), node=node.value) return infered = safe_infer(node.value) if infered is None or infered is astroid.YES: return if is_inside_abstract_class(node): return if not supports_subscript(infered): self.add_message('unsubscriptable-object', args=node.value.as_string(), node=node.value)