#
# Copyright (C) 2005 by Yasushi Saito (yasushi.saito@gmail.com)
# 
# Cxxchecker 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, or (at your option) any
# later version.
#
# Cxxchecker 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.
#
import re
import options
from cxxcheckerutils.elemtypes import *
from cxxcheckerutils.error import *


def stripTemplate(name):
    mo = re.match("([^<]*)<", name)
    if mo:
        return mo.group(1)
    return name

def run(objs):
    checkNames(objs)
    checkStyles(objs)

def isInheritedFrom(obj, baseName):
    for base in obj.bases:
        x = base.enclosingNamespace() + "::" + base.name
        if x.startswith(baseName):
            return True
        if isInheritedFrom(base, baseName):
            return True
    return False

def checkPublicDataMembers(obj):
    """Check if a class has a public data member."""

    if obj.elemType != "Class":
        # For a type declared as "struct" or "union", we allow
        # public data members.
        return

    # Allow public members in a private/protected class.
    if getattr(obj, "access", "public") == "public":
      for m in getattr(obj, "members", []):
          if m.elemType == "Field" and getattr(m, "access", "public") == "public":
              message(m, "Public data member found in class")
            
def checkCopyConstructor(obj):
    """
    Argument OBJ is a Class object. Checks that
    if the class has a pointer member, then the class has a copy constructor.
    """
    
    if obj.elemType != "Class":
        # For a type declared as "struct" or "union", we allow
        # pointer data members without a copy constructor.
        return

    #
    # Check that a class with a pointer either
    # has a copy constructor or is derived from boost:noncopyable.
    #
    hasPointer = None
    hasCopyConstructor = False
    for m in getattr(obj, "members", []):
        if m.elemType == "Field" and m.type.elemType == "PointerType":
            if (m.type.type.elemType == "FundamentalType"
                and m.type.type.name == "void"):
                # void* is treated specially; because it's 
                # an opaque pointer, we can copy it perhaps
                pass
            else:
                hasPointer = m
        elif m.elemType == "Constructor":
            nonDefaultArgs = 0;
            for arg in getattr(m, 'arguments', []):
                if not getattr(arg, "default", False):
                    nonDefaultArgs = nonDefaultArgs + 1
            if nonDefaultArgs == 1 and getattr(m.arguments[0].type, "elemType", False):
                if (m.arguments[0].type.elemType == "ReferenceType"
                    and m.arguments[0].type.type.elemType == "CvQualifiedType"):
                    if( m.arguments[0].type.type.type == obj ):
                        hasCopyConstructor = True
    if hasPointer:
        if not hasCopyConstructor:
            # See if this class is derived from boost::noncopyable
            if not isInheritedFrom(obj, "boost::noncopyable"):
                message(obj, "Class has a pointer member (%s) but no copy constructor" % hasPointer.name)
                
def checkPublicConstructorInAbstractClass(obj):
    """Check if an abstract base class has a public constructor."""
    hasPureVirtual = False
    hasPublicConstructor = False
    thePublicConstructor = ""
    for m in getattr(obj, "members", []):
        if m.elemType == "Method" and getattr(m, "pure_virtual", False):
            hasPureVirtual = True
        elif (m.elemType == "Constructor"
              and getattr(m, "access", "public") == "public"):
            
            if m.name != stripTemplate(m.name):
                pass
            if (getattr(m, "artificial", False)
                and isInheritedFrom(obj, "boost::noncopyable")):
                # Using default constructor in a boost::noncopyable-derived
                # class will cause a compile-time error.
                # So we ignore such a class.
                pass
            else:
                thePublicConstructor = m.demangled
                hasPublicConstructor = True
                
    if hasPureVirtual and hasPublicConstructor:
        message(obj, "Abstract base class should not have a public constructor: " + thePublicConstructor)
        
def checkProtectedMembersInStructs(obj):
    """Looks for protected members in struct or union."""
    if obj.elemType == "Class": 
        return
    if obj.elemType == "Struct":
        if obj.name != stripTemplate(obj.name):
            return
    for m in getattr(obj, "members", []):
        if getattr(m, "access", "public") != "public":
            message(m, "Members of a struct/union should always be public")
        if ( m.elemType != "Field" 
             and m.elemType != "Constructor" 
             and m.elemType != "Destructor" 
             and m.elemType != "Enumeration" 
             and m.elemType != "Union" 
             #and not re.match("\._[\d]+$", m.name) 
             and m.line != obj.line ):
            if not getattr(m, "name", False):
                m.name = '<unnamed>'
            if not getattr(m, "mangled", False):
                m.mangled = '<unmangled>'
            message(m, "Struct/union should not have a (%s) (%s) (%s)"  % (m.elemType, m.name, m.mangled))

def checkGetSet(obj):
    """If a class has a member setFoo, make sure that there's either
    foo() or getFoo()."""
    for m in getattr(obj, "members", []):
        if m.elemType == "Method":
            mo = (re.match("set([a-zA-Z].*)$", m.name)
                  or re.match("set_([a-zA-Z].*)$", m.name))
            if not mo:
                continue
            n = mo.group(1)
            if n[0].isupper():
                n = n[0].lower() + n[1:]
            found = False
            for m2 in obj.members:
                if m2.elemType == "Method" and m2.name == n:
                    found = True
                    break
            if not found:
                message(obj, "Class has method %s but no %s" % (m.name, n))
    

def checkStyles(objs):
    for obj in objs.values():
        if isinstance(obj, Class):
            if "copy-constructor" in options.warningOptions:
                checkCopyConstructor(obj)
            if "public-data-in-class" in options.warningOptions:
                checkPublicDataMembers(obj)
            if "public-constructor-in-base-class" in options.warningOptions: 
                checkPublicConstructorInAbstractClass(obj)
            if "protected-data-in-struct" in options.warningOptions: 
                checkProtectedMembersInStructs(obj)
            if "get-set-matching" in options.warningOptions: 
                checkGetSet(obj)
            
def checkNames(objs):
    if "name-consistency" not in options.warningOptions:
        return
    for obj in objs.values():
        if getattr(obj, "arguments", False):
            # Some kind of function
            for arg in getattr(obj, "arguments", []):
                name = getattr(arg, "name", None)
                if (name and name != "_ctor_arg"
                    and not re.match(options.REGEX_ARGUMENT, name)):
                    message(obj, "Parameter name '%s' violates the style"
                            % name)
                    
        if isinstance(obj, Type):
            name = getattr(obj, "name", "")
            if not name[0] in ("_", ".") and not re.match(options.REGEX_TYPE_NAME, name):
                message(obj, "Type name violation")
            if isinstance(obj, Enumeration):
                for val in obj.values:
                    if not re.match(options.REGEX_ENUM_TAG, val.name):
                        message(val, "Enum constant naming violation")
        elif obj.elemType == "Namespace":
            
            # grandfather standard namespaces
            if obj.name.startswith("_") or obj.name in options.STANDARD_NAMESPACES:
                continue
            if (getattr(obj, "context", None)
                and obj.context.name in options.STANDARD_NAMESPACES):
                continue
            
            if not re.match(options.REGEX_NAMESPACE, obj.name):
                message(obj, "Namespace name violation")
                
        elif obj.elemType == "Function":
            # standalone function
            name = stripTemplate(obj.name)
            if name[0] not in (".", "_") and not re.match(options.REGEX_FUNCTION, name):
                message(obj, "Function naming violation")
        elif obj.elemType == "Method" and getattr(obj, "static", False):
            # static method.
            name = stripTemplate(obj.name)
            if name[0] not in (".", "_") and not re.match(options.REGEX_STATIC_METHOD, name):
                message(obj, "Static method naming violation")

        elif obj.elemType == "Variable":
            if getattr(obj.type, "const", False):
                # Const variables should have CONST_VAR type of names.
                if (not obj.name.startswith("_")
                    and not re.match(options.REGEX_STATIC_CONST_VARIABLE, obj.name)):
                    # Names starting with "_" is probably a vtable.
                    message(obj, "Constant variable naming violation")
            else:
                context = getattr(obj, "context", None)
                if isinstance(context, Class):
                    # static class member.
                    if not re.match(options.REGEX_STATIC_DATA_MEMBER, obj.name):
                        message(obj, "Static data member naming violation")
                elif context == None or isinstance(context, Namespace):
                    # static variable.
                    if not re.match(options.REGEX_GLOBAL_VARIABLE, obj.name):
                        message(obj, "Global variable naming violation")
        elif obj.elemType == "Method":
            # Non-static methods
            if not re.match(options.REGEX_BOUND_METHOD, obj.name):
                message(obj, "Class method naming violation")
        elif obj.elemType == "Field" and obj.name != "":
            if not re.match(options.REGEX_DATA_MEMBER, obj.name):
                message(obj, "Class member naming violation")
        elif isinstance(obj, Class):
            # For templetized classes, just check the basename part.
            name = stripTemplate(obj.name)
            if (not re.match(options.REGEX_CLASS, name)
                and getattr(obj, "incomplete", "0") != "1"):
                message(obj, "Class naming violation")
