diff options
Diffstat (limited to 'checkers/classes.py')
-rw-r--r-- | checkers/classes.py | 767 |
1 files changed, 0 insertions, 767 deletions
diff --git a/checkers/classes.py b/checkers/classes.py deleted file mode 100644 index 85a8507..0000000 --- a/checkers/classes.py +++ /dev/null @@ -1,767 +0,0 @@ -# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). -# http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -"""classes checker for Python code -""" -from __future__ import generators -import sys -import astroid -from astroid import YES, Instance, are_exclusive, AssAttr -from astroid.bases import Generator - -from pylint.interfaces import IAstroidChecker -from pylint.checkers import BaseChecker -from pylint.checkers.utils import (PYMETHODS, overrides_a_method, - check_messages, is_attr_private, is_attr_protected, node_frame_class) - -if sys.version_info >= (3, 0): - NEXT_METHOD = '__next__' -else: - NEXT_METHOD = 'next' -ITER_METHODS = ('__iter__', '__getitem__') - -def class_is_abstract(node): - """return true if the given class node should be considered as an abstract - class - """ - for method in node.methods(): - if method.parent.frame() is node: - if method.is_abstract(pass_is_abstract=False): - return True - return False - - -MSGS = { - 'F0202': ('Unable to check methods signature (%s / %s)', - 'method-check-failed', - 'Used when PyLint has been unable to check methods signature \ - compatibility for an unexpected reason. Please report this kind \ - if you don\'t make sense of it.'), - - 'E0202': ('An attribute defined in %s line %s hides this method', - 'method-hidden', - 'Used when a class defines a method which is hidden by an ' - 'instance attribute from an ancestor class or set by some ' - 'client code.'), - 'E0203': ('Access to member %r before its definition line %s', - 'access-member-before-definition', - 'Used when an instance member is accessed before it\'s actually\ - assigned.'), - 'W0201': ('Attribute %r defined outside __init__', - 'attribute-defined-outside-init', - 'Used when an instance attribute is defined outside the __init__\ - method.'), - - 'W0212': ('Access to a protected member %s of a client class', # E0214 - 'protected-access', - 'Used when a protected member (i.e. class member with a name \ - beginning with an underscore) is access outside the class or a \ - descendant of the class where it\'s defined.'), - - 'E0211': ('Method has no argument', - 'no-method-argument', - 'Used when a method which should have the bound instance as \ - first argument has no argument defined.'), - 'E0213': ('Method should have "self" as first argument', - 'no-self-argument', - 'Used when a method has an attribute different the "self" as\ - first argument. This is considered as an error since this is\ - a so common convention that you shouldn\'t break it!'), - 'C0202': ('Class method %s should have %s as first argument', # E0212 - 'bad-classmethod-argument', - 'Used when a class method has a first argument named differently ' - 'than the value specified in valid-classmethod-first-arg option ' - '(default to "cls"), recommended to easily differentiate them ' - 'from regular instance methods.'), - 'C0203': ('Metaclass method %s should have %s as first argument', # E0214 - 'bad-mcs-method-argument', - 'Used when a metaclass method has a first agument named ' - 'differently than the value specified in valid-classmethod-first' - '-arg option (default to "cls"), recommended to easily ' - 'differentiate them from regular instance methods.'), - 'C0204': ('Metaclass class method %s should have %s as first argument', - 'bad-mcs-classmethod-argument', - 'Used when a metaclass class method has a first argument named ' - 'differently than the value specified in valid-metaclass-' - 'classmethod-first-arg option (default to "mcs"), recommended to ' - 'easily differentiate them from regular instance methods.'), - - 'W0211': ('Static method with %r as first argument', - 'bad-staticmethod-argument', - 'Used when a static method has "self" or a value specified in ' - 'valid-classmethod-first-arg option or ' - 'valid-metaclass-classmethod-first-arg option as first argument.' - ), - 'R0201': ('Method could be a function', - 'no-self-use', - 'Used when a method doesn\'t use its bound instance, and so could\ - be written as a function.' - ), - - 'E0221': ('Interface resolved to %s is not a class', - 'interface-is-not-class', - 'Used when a class claims to implement an interface which is not \ - a class.'), - 'E0222': ('Missing method %r from %s interface', - 'missing-interface-method', - 'Used when a method declared in an interface is missing from a \ - class implementing this interface'), - 'W0221': ('Arguments number differs from %s method', - 'arguments-differ', - 'Used when a method has a different number of arguments than in \ - the implemented interface or in an overridden method.'), - 'W0222': ('Signature differs from %s method', - 'signature-differs', - 'Used when a method signature is different than in the \ - implemented interface or in an overridden method.'), - 'W0223': ('Method %r is abstract in class %r but is not overridden', - 'abstract-method', - 'Used when an abstract method (i.e. raise NotImplementedError) is \ - not overridden in concrete class.' - ), - 'F0220': ('failed to resolve interfaces implemented by %s (%s)', # W0224 - 'unresolved-interface', - 'Used when a PyLint as failed to find interfaces implemented by \ - a class'), - - - 'W0231': ('__init__ method from base class %r is not called', - 'super-init-not-called', - 'Used when an ancestor class method has an __init__ method \ - which is not called by a derived class.'), - 'W0232': ('Class has no __init__ method', - 'no-init', - 'Used when a class has no __init__ method, neither its parent \ - classes.'), - 'W0233': ('__init__ method from a non direct base class %r is called', - 'non-parent-init-called', - 'Used when an __init__ method is called on a class which is not \ - in the direct ancestors for the analysed class.'), - 'W0234': ('__iter__ returns non-iterator', - 'non-iterator-returned', - 'Used when an __iter__ method returns something which is not an \ - iterable (i.e. has no `%s` method)' % NEXT_METHOD), - 'E0235': ('__exit__ must accept 3 arguments: type, value, traceback', - 'bad-context-manager', - 'Used when the __exit__ special method, belonging to a \ - context manager, does not accept 3 arguments \ - (type, value, traceback).'), - 'E0236': ('Invalid object %r in __slots__, must contain ' - 'only non empty strings', - 'invalid-slots-object', - 'Used when an invalid (non-string) object occurs in __slots__.'), - 'E0238': ('Invalid __slots__ object', - 'invalid-slots', - 'Used when an invalid __slots__ is found in class. ' - 'Only a string, an iterable or a sequence is permitted.') - - - } - - -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 - * supported interfaces implementation - * unreachable code - """ - - __implements__ = (IAstroidChecker,) - - # configuration section name - name = 'classes' - # messages - msgs = MSGS - priority = -2 - # configuration options - options = (('ignore-iface-methods', - {'default' : (#zope interface - 'isImplementedBy', 'deferred', 'extends', 'names', - 'namesAndDescriptions', 'queryDescriptionFor', 'getBases', - 'getDescriptionFor', 'getDoc', 'getName', 'getTaggedValue', - 'getTaggedValueTags', 'isEqualOrExtendedBy', 'setTaggedValue', - 'isImplementedByInstancesOf', - # twisted - 'adaptWith', - # logilab.common interface - 'is_implemented_by'), - 'type' : 'csv', - 'metavar' : '<method names>', - 'help' : 'List of interface methods to ignore, \ -separated by a comma. This is used for instance to not check methods defines \ -in Zope\'s Interface base class.'} - ), - - ('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.'} - ), - - ) - - def __init__(self, linter=None): - BaseChecker.__init__(self, linter) - self._accessed = [] - self._first_attrs = [] - self._meth_could_be_func = None - - def visit_class(self, node): - """init visit variable _accessed and check interfaces - """ - self._accessed.append({}) - self._check_bases_classes(node) - self._check_interfaces(node) - # if not an interface, exception, metaclass - if node.type == 'class': - try: - node.local_attr('__init__') - except astroid.NotFoundError: - self.add_message('W0232', args=node, node=node) - self._check_slots(node) - - @check_messages('E0203', 'W0201') - def leave_class(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 - 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 'W0201' not in self.active_msgs: - return - defining_methods = self.config.defining_attr_methods - for attr, nodes in cnode.instance_attrs.iteritems(): - nodes = [n for n in nodes if not - isinstance(n.statement(), (astroid.Delete, astroid.AugAssign))] - if not nodes: - continue # error detected by typechecking - attr_defined = False - # check if any method attr is defined in is a defining method - for node in nodes: - if node.frame().name in defining_methods: - attr_defined = True - if not attr_defined: - # 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: - self.add_message('W0201', args=attr, node=node) - - def visit_function(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.Function): - continue - self._check_signature(node, meth_node, 'overridden') - break - if node.decorators: - for decorator in node.decorators.nodes: - if isinstance(decorator, astroid.Getattr) 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 - args = (overridden.root().name, overridden.fromlineno) - self.add_message('E0202', args=args, node=node) - except astroid.NotFoundError: - pass - - # check non-iterators in __iter__ - if node.name == '__iter__': - self._check_iter(node) - elif node.name == '__exit__': - self._check_exit(node) - - 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 - for meth in ITER_METHODS: - try: - slots.getattr(meth) - break - except astroid.NotFoundError: - continue - else: - 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 YES: - return - - for elt in values: - if elt is YES: - continue - if (not isinstance(elt, astroid.Const) or - not isinstance(elt.value, str)): - self.add_message('invalid-slots-object', - args=elt.as_string(), - node=elt) - continue - if not elt.value: - self.add_message('invalid-slots-object', - args=elt.as_string(), - node=elt) - - def _check_iter(self, node): - try: - infered = node.infer_call_result(node) - except astroid.InferenceError: - return - - for infered_node in infered: - if (infered_node is YES - or isinstance(infered_node, Generator)): - continue - if isinstance(infered_node, astroid.Instance): - try: - infered_node.local_attr(NEXT_METHOD) - except astroid.NotFoundError: - self.add_message('non-iterator-returned', - node=node) - break - - def _check_exit(self, node): - positional = sum(1 for arg in node.args.args if arg.name != 'self') - if positional < 3 and not node.args.vararg: - self.add_message('bad-context-manager', - node=node) - elif positional > 3: - self.add_message('bad-context-manager', - node=node) - - def leave_function(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 and any - kind of method defined in an interface for this warning - """ - if node.is_method(): - if node.args.args is not None: - self._first_attrs.pop() - if 'R0201' not in self.active_msgs: - return - class_node = node.parent.frame() - if (self._meth_could_be_func and node.type == 'method' - and not node.name in PYMETHODS - and not (node.is_abstract() or - overrides_a_method(class_node, node.name)) - and class_node.type != 'interface'): - self.add_message('R0201', node=node) - - def visit_getattr(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].setdefault(attrname, []).append(node) - return - if 'W0212' not in self.active_msgs: - return - - self._check_protected_attribute_access(node) - - def visit_assign(self, assign_node): - if 'W0212' not in self.active_msgs: - return - - node = assign_node.targets[0] - if not isinstance(node, AssAttr): - return - - if self.is_first_attr(node): - return - - self._check_protected_attribute_access(node) - - 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): - - 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('W0212', node=node, args=attrname) - return - - # If the expression begins with a call to super, that's ok. - if isinstance(node.expr, astroid.CallFunc) 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): - self.add_message('W0212', 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 - for attr, nodes in accessed.iteritems(): - # deactivate "except doesn't do anything", that's expected - # pylint: disable=W0704 - # is it a class attribute ? - try: - node.local_attr(attr) - # yes, stop here - continue - except astroid.NotFoundError: - pass - # is it an instance attribute of a parent class ? - try: - node.instance_attr_ancestors(attr).next() - # yes, stop here - continue - except StopIteration: - pass - # is it an instance attribute ? - try: - defstmts = node.instance_attr(attr) - except astroid.NotFoundError: - pass - else: - 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 are_exclusive(_node.statement(), defstmt, ('AttributeError', 'Exception', 'BaseException')): - self.add_message('E0203', 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('W0211', args=first, node=node) - return - self._first_attrs[-1] = None - # class / regular method with no args - elif not node.args.args: - self.add_message('E0211', 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, - 'C0204', node.name) - # metaclass regular method - else: - self._check_first_arg_config(first, - self.config.valid_classmethod_first_arg, node, 'C0203', - node.name) - # regular class - else: - # class method - if node.type == 'classmethod': - self._check_first_arg_config(first, - self.config.valid_classmethod_first_arg, node, 'C0202', - node.name) - # regular method without self as argument - elif first != 'self': - self.add_message('E0213', 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 - """ - # check if this class abstract - if class_is_abstract(node): - return - for method in node.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 method.name in node.locals: - # it is redefined as an attribute or with a descriptor - continue - if method.is_abstract(pass_is_abstract=False): - self.add_message('W0223', node=node, - args=(method.name, owner.name)) - - def _check_interfaces(self, node): - """check that the given class node really implements declared - interfaces - """ - e0221_hack = [False] - def iface_handler(obj): - """filter interface objects, it should be classes""" - if not isinstance(obj, astroid.Class): - e0221_hack[0] = True - self.add_message('E0221', node=node, - args=(obj.as_string(),)) - return False - return True - ignore_iface_methods = self.config.ignore_iface_methods - try: - for iface in node.interfaces(handler_func=iface_handler): - for imethod in iface.methods(): - name = imethod.name - if name.startswith('_') or name in ignore_iface_methods: - # don't check method beginning with an underscore, - # usually belonging to the interface implementation - continue - # get class method astroid - try: - method = node_method(node, name) - except astroid.NotFoundError: - self.add_message('E0222', args=(name, iface.name), - node=node) - continue - # ignore inherited methods - if method.parent.frame() is not node: - continue - # check signature - self._check_signature(method, imethod, - '%s interface' % iface.name) - except astroid.InferenceError: - if e0221_hack[0]: - return - implements = Instance(node).getattr('__implements__')[0] - assignment = implements.parent - assert isinstance(assignment, astroid.Assign) - # assignment.expr can be a Name or a Tuple or whatever. - # Use as_string() for the message - # FIXME: in case of multiple interfaces, find which one could not - # be resolved - self.add_message('F0220', node=implements, - args=(node.name, assignment.value.as_string())) - - def _check_init(self, node): - """check that the __init__ method call super or ancestors'__init__ - method - """ - if not set(('W0231', 'W0233')) & self.active_msgs: - 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.CallFunc): - expr = stmt.func - if not isinstance(expr, astroid.Getattr) \ - or expr.attrname != '__init__': - continue - # skip the test if using super - if isinstance(expr.expr, astroid.CallFunc) and \ - isinstance(expr.expr.func, astroid.Name) and \ - expr.expr.func.name == 'super': - return - try: - klass = expr.expr.infer().next() - if klass is YES: - continue - try: - del not_called_yet[klass] - except KeyError: - if klass not in to_call: - self.add_message('W0233', node=expr, args=klass.name) - except astroid.InferenceError: - continue - for klass, method in not_called_yet.iteritems(): - if klass.name == 'object' or method.parent.name == 'object': - continue - self.add_message('W0231', args=klass.name, node=node) - - def _check_signature(self, method1, refmethod, class_type): - """check that the signature of the two given methods match - - class_type is in 'class', 'interface' - """ - if not (isinstance(method1, astroid.Function) - and isinstance(refmethod, astroid.Function)): - self.add_message('F0202', args=(method1, refmethod), node=method1) - return - # 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 - if is_attr_private(method1.name): - return - if len(method1.args.args) != len(refmethod.args.args): - self.add_message('W0221', args=class_type, node=method1) - elif len(method1.args.defaults) < len(refmethod.args.defaults): - self.add_message('W0222', args=class_type, 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] - -def _ancestors_to_call(klass_node, method='__init__'): - """return a dictionary where keys are the list of base classes providing - the queried method, and so that should/may be called from the method node - """ - to_call = {} - for base_node in klass_node.ancestors(recurs=False): - try: - to_call[base_node] = base_node.igetattr(method).next() - except astroid.InferenceError: - continue - return to_call - - -def node_method(node, method_name): - """get astroid for <method_name> on the given class node, ensuring it - is a Function node - """ - for n in node.local_attr(method_name): - if isinstance(n, astroid.Function): - return n - raise astroid.NotFoundError(method_name) - -def register(linter): - """required method to auto register this checker """ - linter.register_checker(ClassChecker(linter)) |