classes.py

download
# 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.
""" Copyright (c) 2002-2005 LOGILAB S.A. (Paris, FRANCE).
 http://www.logilab.fr/ -- mailto:contact@logilab.fr

 basic checker for Python code
"""
from __future__ import generators

__revision__ = "$Id: classes.py,v 1.52 2005/01/18 17:32:11 syt Exp $"

from logilab.common import astng

from logilab.pylint.interfaces import IASTNGChecker
from logilab.pylint.checkers import BaseChecker
from logilab.pylint.checkers.utils import is_abstract, is_interface, \
     is_exception, is_metaclass, get_nodes_from_class


MSGS = {
    'F0201': ('Unable to check method %r of interface %s',
              'Used when PyLint has been unable to fetch a method declared in \
              an interface (either in the class or in the interface) and so to\
              check its implementation.'),
    'F0202': ('Unable to check methods signature (%s / %s)',
              'Used when PyLint has been unable to check methods signature \
              compatibility for an unexpected raison. Please report this kind \
              if you don\'t make sense of it.'),
    'F0203': ('Unable to resolve %s',
              'Used when PyLint has been unable to resolve a name.'),

    'E0201': ('Access to undefined member %r',
              'Used when an instance member not defined in the instance, its\
              class or its ancestors is accessed.'),
    'E0202': ('Method hide an inherited attribute from %s',
              'Used when a class defines a method which hide an attribute from \
              an ancestor class.'),
    'E0203': ('Access to member %r before its definition line %s',
              'Used when an instance member is accessed before it\'s actually\
              assigned.'),
    'W0201': ('Attribute %r defined outside __init__',
              'Used when an instance attribute is defined outside the __init__\
              method.'),
    
    'E0211': ('Method has no argument',
              'Used when a method which should have the bound instance as \
              first argument has no argument defined.'),
    'E0212': ('Class method should have "cls" as first argument',
              'Used when a class method has an attribute different than "cls"\
              as first argument, to easily differentiate them from regular \
              instance methods.'),
    'E0213': ('Method doesn\'t have "self" as first argument',
              'Used when a method has an attribute different the "self" as\
              first argument.'),
    'E0214': ('Metaclass method doesn\'t have "mcs" as first argument',
              'Used when a metaclass method has an attribute different the \
              "mcs" as first argument.'),
    'W0211': ('Static method with %r as first argument',
              'Used when a static method has "self" or "cls" as first argument.'
              ),
    
    'E0221': ('Interface %s is not a class (%s)',
              'Used when a class claims to implement an interface which is not \
              a class.'),
    'E0222': ('Missing method %r from %s interface' ,
              'Used when a method declared in an interface is missing from a \
              class implementing this interface'),
    'W0221': ('Arguments number differs from %s method',
              'Used when a method has a different number of arguments than in \
              the implemented interface or in an overriden method.'),
    'W0222': ('Signature differs from %s method',
              'Used when a method signature is different than in the \
              implemented interface or in an overriden method.'),
    'W0223': ('Method %r is abstract in class %r but is not overriden',
              'Used when an abstract method (ie raise NotImplementedError) is \
              not overriden in concrete class.'
              ),
    
    'W0231': ('__init__ method from base class %r is 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',
              'Used when a class has no __init__ method, neither its parent \
              classes.'),
    'W0233': ('__init__ method from a non direct base class %r is called',
              'Used when an __init__ method is called on a class which is not \
              in the direct ancestors for the analysed class.'),
    
    }


class ClassChecker(BaseChecker):
    """checks for :                                                            
    * methods without self as first argument                                   
    * overriden methods signature                                              
    * access only to existant members via self                                 
    * attributes not defined in the __init__ method                            
    * supported interfaces implementation                                      
    * unreachable code                                                         
    """
    
    __implements__ = (IASTNGChecker,)

    # 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.'}
                ),
               ('ignore-mixin-members',
                {'default' : 1, 'type' : 'yn', 'metavar': '<y_or_n>',
                 'help' : 'Tells wether missing members accessed in mixin \
class should be ignored. A mixin class is detected if its name ends with \
"mixin" (case insensitive).'}
                ),
               )

    def __init__(self, linter=None):
        BaseChecker.__init__(self, linter)
        self._accessed = []
        self._first_attrs = []
        
    def visit_class(self, node):
        """init visit variable _accessed and check interfaces
        """        
        self._accessed.append({})
        node.metaclass = is_metaclass(node)
        self._check_bases_classes(node)
        self._check_interfaces(node)
        if not (is_interface(node) or is_exception(node) or node.metaclass):
            try:
                node.get_method('__init__')
            except astng.NotFoundError:
                self.add_message('W0232', args=node, node=node)
            
    def leave_class(self, class_node):
        """close a class node :
        check that instance attributes are defined in __init__ and check
        access to existant members
        """
        # checks attributes are defined in __init__
        for attr, node in class_node.instance_attrs.items():
            frame = node.get_frame()
            if frame.name not in ('__init__', '__new__', 'setUp'):
                try:
                    par_node = class_node.get_ancestor_for_attribute(attr)
                    frame = par_node.instance_attrs[attr].get_frame()
                    if frame.name not in ('__init__', '__new__',
                                          'setUp'):
                        self.add_message('W0201', args=attr, node=node)
                except astng.NotFoundError:
                    self.add_message('W0201', args=attr, node=node)
        # check access to existant members
        accessed = self._accessed.pop()
        if not self.config.ignore_mixin_members or \
               class_node.name[-5:].lower() != 'mixin':
            self._check_accessed_members(class_node, accessed)
            
    def visit_function(self, node):
        """check method arguments, overriding"""
        # check first argument is self if this is actually a method
        if node.is_method():
            klass = node.parent.get_frame()
            self._check_first_arg_for_type(node, klass.metaclass)
            if node.name == '__init__':
                self._check_init(node)
                return
            # check signature if the method overrload an herited method
            try:
                overriden = klass.get_ancestor_for_method(node.name)
            except (astng.NotFoundError, astng.ASTNGBuildingException):
                pass
            else:
                # get astng for the searched method
                try:
                    meth_node = overriden.locals[node.name]
                    self._check_signature(node, meth_node, 'overriden')
                except KeyError:
                    # we have found the method but it's not in the local
                    # dictionnary.
                    # This may happen with astng build from living objects only
                    pass
            # check if the method overload an attribute
            try:
                overriden = klass.get_ancestor_for_attribute(node.name)
                self.add_message('E0202', args=overriden.name, node=node)
            except astng.NotFoundError:
                pass
                
    def leave_function(self, node):
        """check method arguments, overriding"""
        # check first argument is self if this is actually a method
        if node.is_method() and node.argnames is not None:
            self._first_attrs.pop()

    def visit_getattr(self, node):
        """check if the name handle an access to a class member
        if so, register it
        """
        if self._first_attrs and isinstance(node.expr, astng.Name):
            if node.expr.name == self._first_attrs[-1]:
                self._accessed[-1].setdefault(node.attrname, []).append(node)

                
    def _check_accessed_members(self, node, accessed):
        """check that accessed members are defined"""
        for attr, nodes in accessed.items():
            # is it a builtin attribute
            if attr in ('__dict__', '__class__', '__doc__'):
                continue
            # is it an instance attribute ?
            if node.instance_attrs.has_key(attr):
                frame = node.instance_attrs[attr].get_frame()
                lineno = node.instance_attrs[attr].source_line()
                # check that if the node is accessed in the same method as
                # it's defined, it's accessed after the initial assigment
                for _node in nodes:
                    if _node.get_frame() is frame and _node.lineno < lineno:
                        self.add_message('E0203', node=_node,
                                         args=(attr, lineno))
                continue
            # or a class attribute ?
            if node.locals.has_key(attr):
                continue
            # or an inherited method / attribute ?
            try:
                node.get_ancestor_for_method(attr)
            except astng.NotFoundError:
                pass
            else:
                continue
            try:
                node.get_ancestor_for_attribute(attr)
            except astng.NotFoundError:
                pass
            else:
                continue
            for _node in nodes:
                self.add_message('E0201', node=_node, args=attr)
        
    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
        * 'mcs' for a metaclass
        * not one of the above for a static method
        """
        # don't care about functions with unknown argument (builtins)
        if node.argnames is None:
            return
        self._first_attrs.append(node.argnames and node.argnames[0])
        # metaclass method
        if metaclass:
            if self._first_attrs[-1] != 'mcs':
                self.add_message('E0214', node=node)
        # static method
        elif node.is_static_method():
            if node.argnames and node.argnames[0] in ('self', 'cls', 'mcs'):
                self.add_message('W0211', args=node.argnames[0], node=node)
            self._first_attrs[-1] = None
        # class / regular method with no args
        elif not node.argnames:
            self.add_message('E0211', node=node)
        # class method
        elif node.is_class_method():
            if self._first_attrs[-1] != 'cls':
                self.add_message('E0212', node=node)
        # regular method without self as argument
        elif self._first_attrs[-1] != 'self':
            self.add_message('E0213', node=node)

    def _check_bases_classes(self, node):
        """check that the given class node implements abstract methods from
        base classes
        """
        for method in node.methods():
            owner = method.parent.get_frame()
            if owner is node:
                continue
            # check that the ancestor's method is not abstract
            if is_abstract(method, pass_is_abstract=0):
                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
        """
        def iface_handler(klass, iface_node):
            """filter interface objects, it should be classes"""
            try:
                obj = klass.resolve_dotted(iface_node.as_string())
                if not isinstance(obj, astng.Class):
                    self.add_message('E0221', node=node,
                                     args=(iface_node.as_string(),
                                           obj.__class__.__name__))
                else:
                    yield obj
            except:
                self.add_message('F0203', node=node,
                                 args=(iface_node.as_string()))
                
        implements = astng.utils.get_interfaces(node, handler_func=iface_handler)
        for iface in implements:
            for imethod in iface.methods():
                name = imethod.name
                if name.startswith('_') or \
                       name in self.config.ignore_iface_methods:
                    # don't check method begining with an underscore, usually
                    # belonging to the interface implementation
                    continue
                # get class method astng
                try:
                    method = self._get_method(node, name)
                except astng.NotFoundError:
                    self.add_message('E0222', args=(name, iface.name),
                                     node=node)
                    continue
                if method is None:
                    continue
                # don't go further if it is an inherited method
                if not method.parent.get_frame() is node:
                    continue
                # check signature
                self._check_signature(method, imethod,
                                     '%s interface' % iface.name)

    def _check_init(self, node):
        """check that the __init__ method call super or ancestors'__init__
        method
        """
        klass_node = node.parent.get_frame()        
        to_call, unresolved = self._ancestors_to_call(node, klass_node)
        for stmt in get_nodes_from_class(node, astng.CallFunc):
            expr = stmt.node
            if not (isinstance(expr, astng.Name) or
                    isinstance(expr, astng.Getattr)):
                continue
            func_str = expr.as_string()
            if func_str.endswith('.__init__'):
                klass_name = '.'.join(func_str.split('.')[:-1])
                try:
                    del to_call[klass_name]
                except KeyError:
                    if unresolved.has_key(klass_name) \
                           or klass_name in klass_node.basenames:
                        continue
                    if klass_name.startswith('super('):
                        return
                    self.add_message('W0233', node=expr, args=klass_name)
        for klass_name in to_call.keys():
            self.add_message('W0231', args=klass_name, node=node)

    def _check_signature(self, method1, method2, class_type):
        """check that the signature of the two given methods match
        
        class_type is in 'class', 'interface'
        """
        if not (isinstance(method1, astng.Function)
                and isinstance(method2, astng.Function)):
            self.add_message('F0202', args=(method1, method2), node=method1)
            return
        # don't care about functions with unknown argument (builtins)
        if method1.argnames is None or method2.argnames is None:
            return
        if len(method1.argnames) != len(method2.argnames):
            self.add_message('W0221', args=class_type, node=method1)
        elif len(method1.defaults) != len(method2.defaults):
            self.add_message('W0222', args=class_type, node=method1)
            
    def _get_method(self, node, method_name, fatal=1):
        """get astng for <method_name> on the given class node"""
        try:
            return node.get_method(method_name)
        except astng.NotFoundError:
            raise
        except Exception:
            if fatal:
                self.add_message('F0201', args=(method_name, node.name),
                                 node=node)
            
    def _ancestors_to_call(self, node, klass_node, method='__init__'):
        """return two dictionarys :
        
        * one where keys are the list of base classes names with the given
          method that should be called from the method node
        * a second where keys are base classes names which could not have been
          resolved correctly
        """
        to_call = {}
        unresolved = {}
        for base in klass_node.basenames:
            parts = base.split('.')
            cbase = parts[0]
            try:
                baseastng = klass_node.resolve(cbase)
            except (astng.ResolveError, astng.NotFoundError):
                unresolved[base] = 1
                self.add_message('F0203', node=node, args=cbase)
                continue
            for part in parts[1:]:
                try:
                    cbase = '%s.%s' % (cbase, part)
                    baseastng = baseastng.resolve(part)
                except astng.ResolveError:
                    unresolved[base] = 1
                    #import traceback
                    #traceback.print_exc()
                    self.add_message('F0203', node=node, args=cbase)
                    break
            else:
                try:
                    baseastng.get_method(method)
                    to_call[base] = 1
                except astng.NotFoundError:
                    continue
        return to_call, unresolved
        
        
def register(linter):
    """required method to auto register this checker """
    linter.register_checker(ClassChecker(linter))
download