# -*- coding: utf-8 -*-

# Copyright (c) 2005 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Parse a Ruby file and retrieve classes, modules, methods and attributes.

Parse enough of a Ruby file to recognize class, module and method definitions
and to find out the superclasses of a class as well as its attributes.

It is based on the Python class browser found in this package.
"""

import sys
import os
import re
import string

import Utilities.ClassBrowsers as ClassBrowsers
import ClbrBaseClasses

TABWIDTH = 8

SUPPORTED_TYPES = [ClassBrowsers.RB_SOURCE]
    
_getnext = re.compile(r"""
    (?P<String>
        =begin .*? =end

    |   " [^"\\\n]* (?: \\. [^"\\\n]*)* "

    |   ' [^'\\\n]* (?: \\. [^'\\\n]*)* '
    
    |   <<-? ['"]? (?P<HereMarker> \w*? ) ['"]? [ \t]* $ .*? (?P=HereMarker)
    )

|   (?P<Comment>
        ^
        [ \t]* \#+ .*? $
    )

|   (?P<Method>
        ^
        (?P<MethodIndent> [ \t]* )
        def [ \t]+
        (?:
            (?P<MethodName2> \w+ \. [a-zA-Z_] [a-zA-Z0-9_?!=]* )
        |
            (?P<MethodName> [a-zA-Z_] [a-zA-Z0-9_?!=]* )
        )
        [ \t]* 
        (?: 
            \( (?P<MethodSignature> (?: [^)] | \)[ \t]*,? )*? ) \)
        )?
        [ \t]*
    )

|   (?P<Class>
        ^
        (?P<ClassIndent> [ \t]* )
        class
        (?:
            [ \t]+
            (?P<ClassName> [A-Z] [a-zA-Z0-9_]* )
            [ \t]*
            (?P<ClassSupers> < [ \t]* [A-Z] [a-zA-Z0-9_:]* )?
        |
            [ \t]* << [ \t]* 
            (?P<ClassName2> [a-zA-Z_] [a-zA-Z0-9_:]* )
        )
        [ \t]*
    )

|   (?P<ClassIgnored>
        \(
        [ \t]*
        class
        .*?
        end
        [ \t]*
        \)
    )

|   (?P<Module>
        ^
        (?P<ModuleIndent> [ \t]* )
        module [ \t]+
        (?P<ModuleName> [A-Z] [a-zA-Z0-9_:]* )
        [ \t]*
    )

|   (?P<AccessControl>
        ^
        (?P<AccessControlIndent> [ \t]* )
        (?P<AccessControlType> private | public | protected )
        \(?
        [ \t]*
        (?P<AccessControlList> (?: : [a-zA-Z0-9_]+ , \s* )* (?: : [a-zA-Z0-9_]+ )+ )?
        [ \t]*
        \)?
    )

|   (?P<Attribute>
        ^
        (?P<AttributeIndent> [ \t]* )
        (?P<AttributeName> (?: @ | @@ ) [a-zA-Z0-9_]* )
        [ \t]* =
    )

|   (?P<Attr>
        ^
        (?P<AttrIndent> [ \t]* )
        attr 
        (?P<AttrType> (?: _accessor | _reader | _writer ) )?
        \(?
        [ \t]*
        (?P<AttrList> (?: : [a-zA-Z0-9_]+ , \s* )* (?: : [a-zA-Z0-9_]+ | true | false )+ )
        [ \t]*
        \)?
    )

|   (?P<Begin>
            ^
            [ \t]*
            (?: def | if | unless | case | while | until | for | begin ) \b [^_]
        |
            [ \t]* do [ \t]* (?: \| .*? \| )? [ \t]* $
    )

|   (?P<End>
        (?:
            ^
        |
            ; 
        )
        [ \t]*
        (?:
            end [ \t]* $
        |
            end \b [^_]
        )
    )
""", re.VERBOSE | re.DOTALL | re.MULTILINE).search

_commentsub = re.compile(r"""#[^\n]*\n|#[^\n]*$""").sub

_modules = {}                           # cache of modules we've seen

class VisibilityMixin(ClbrBaseClasses.ClbrVisibilityMixinBase):
    """
    Mixin class implementing the notion of visibility.
    """
    def __init__(self):
        """
        Method to initialize the visibility.
        """
        self.setPublic()

class Class(ClbrBaseClasses.Class, VisibilityMixin):
    """
    Class to represent a Ruby class.
    """
    def __init__(self, module, name, super, file, lineno):
        """
        Constructor
        
        @param module name of the module containing this class
        @param name name of this class
        @param super list of class names this class is inherited from
        @param file filename containing this class
        @param lineno linenumber of the class definition
        """
        ClbrBaseClasses.Class.__init__(self, module, name, super, file, lineno)
        VisibilityMixin.__init__(self)

class Module(ClbrBaseClasses.Module, VisibilityMixin):
    """
    Class to represent a Ruby module.
    """
    def __init__(self, module, name, file, lineno):
        """
        Constructor
        
        @param module name of the module containing this class
        @param name name of this class
        @param file filename containing this class
        @param lineno linenumber of the class definition
        """
        ClbrBaseClasses.Module.__init__(self, module, name, file, lineno)
        VisibilityMixin.__init__(self)

class Function(ClbrBaseClasses.Function, VisibilityMixin):
    """
    Class to represent a Ruby function.
    """
    def __init__(self, module, name, file, lineno, signature = '', separator = ','):
        """
        Constructor
        
        @param module name of the module containing this function
        @param name name of this function
        @param file filename containing this class
        @param lineno linenumber of the class definition
        @param signature parameterlist of the method
        @param separator string separating the parameters
        """
        ClbrBaseClasses.Function.__init__(self, module, name, file, lineno, signature, separator)
        VisibilityMixin.__init__(self)

class Attribute(ClbrBaseClasses.Attribute, VisibilityMixin):
    """
    Class to represent a class or module attribute.
    """
    def __init__(self, name):
        """
        Constructor
        
        @param name name of this attribute
        """
        ClbrBaseClasses.Attribute.__init__(self, name)
        VisibilityMixin.__init__(self)
        self.setPrivate()

def readmodule_ex(module, path=[]):
    '''
    Read a Ruby file and return a dictionary of classes, functions and modules.

    @param module name of the Ruby file (string)
    @param path path the file should be searched in (list of strings)
    @return the resulting dictionary
    '''

    dict = {}
    dict_counts = {}

    if _modules.has_key(module):
        # we've seen this file before...
        return _modules[module]

    # search the path for the file
    f = None
    fullpath = list(path)
    f, file, (suff, mode, type) = ClassBrowsers.find_module(module, fullpath)
    if type not in SUPPORTED_TYPES:
        # not Ruby source, can't do anything with this module
        f.close()
        _modules[module] = dict
        return dict

    _modules[module] = dict
    classstack = [] # stack of (class, indent) pairs
    acstack = []    # stack of (access control, indent) pairs
    indent = 0
    src = f.read()
    f.close()

    # To avoid having to stop the regexp at each newline, instead
    # when we need a line number we simply string.count the number of
    # newlines in the string since the last time we did this; i.e.,
    #    lineno = lineno + \
    #             string.count(src, '\n', last_lineno_pos, here)
    #    last_lineno_pos = here
    countnl = string.count
    lineno, last_lineno_pos = 1, 0
    i = 0
    while 1:
        m = _getnext(src, i)
        if not m:
            break
        start, i = m.span()

        if m.start("Method") >= 0:
            # found a method definition or function
            thisindent = indent
            indent += 1
            meth_name = m.group("MethodName") or m.group("MethodName2")
            meth_sig = m.group("MethodSignature")
            meth_sig = meth_sig and meth_sig.replace('\\\n', '') or ''
            meth_sig = _commentsub('', meth_sig)
            lineno = lineno + \
                     countnl(src, '\n',
                             last_lineno_pos, start)
            last_lineno_pos = start
            # close all classes/modules indented at least as much
            while classstack and \
                  classstack[-1][1] >= thisindent:
                del classstack[-1]
            while acstack and \
                  acstack[-1][1] >= thisindent:
                del acstack[-1]
            if classstack:
                # it's a class/module method
                cur_class = classstack[-1][0]
                if isinstance(cur_class, Class) or isinstance(cur_class, Module):
                    # it's a method
                    f = Function(None, meth_name,
                                 file, lineno, meth_sig)
                    cur_class._addmethod(meth_name, f)
                # set access control
                accesscontrol = acstack[-1][0]
                if accesscontrol == "private":
                    f.setPrivate()
                elif accesscontrol == "protected":
                    f.setProtected()
                elif accesscontrol == "public":
                    f.setPublic()
                # else it's a nested def
            else:
                # it's a function
                f = Function(module, meth_name,
                             file, lineno, meth_sig)
                if dict.has_key(meth_name):
                    dict_counts[meth_name] += 1
                    meth_name = "%s_%d" % (meth_name, dict_counts[meth_name])
                else:
                    dict_counts[meth_name] = 0
                dict[meth_name] = f
            classstack.append((f, thisindent)) # Marker for nested fns

        elif m.start("String") >= 0:
            pass

        elif m.start("Comment") >= 0:
            pass

        elif m.start("ClassIgnored") >= 0:
            pass

        elif m.start("Class") >= 0:
            # we found a class definition
            thisindent = indent
            indent += 1
            # close all classes/modules indented at least as much
            while classstack and \
                  classstack[-1][1] >= thisindent:
                del classstack[-1]
            lineno = lineno + \
                     countnl(src, '\n', last_lineno_pos, start)
            last_lineno_pos = start
            class_name = m.group("ClassName") or m.group("ClassName2")
            inherit = m.group("ClassSupers")
            if inherit:
                # the class inherits from other classes
                inherit = inherit[1:].strip()
                inherit = [_commentsub('', inherit)]
            # remember this class
            cur_class = Class(module, class_name, inherit,
                              file, lineno)
            if not classstack:
                if dict.has_key(class_name):
                    cur_class = dict[class_name]
                else:
                    dict[class_name] = cur_class
            else:
                cls = classstack[-1][0]
                if cls.classes.has_key(class_name):
                    cur_class = cls.classes[class_name]
                elif cls.name == class_name:
                    cur_class = cls
                else:
                    cls._addclass(class_name, cur_class)
            classstack.append((cur_class, thisindent))
            while acstack and \
                  acstack[-1][1] >= thisindent:
                del acstack[-1]
            acstack.append(["public", thisindent])  # default access control is 'public'

        elif m.start("Module") >= 0:
            # we found a module definition
            thisindent = indent
            indent += 1
            # close all classes/modules indented at least as much
            while classstack and \
                  classstack[-1][1] >= thisindent:
                del classstack[-1]
            lineno = lineno + \
                     countnl(src, '\n', last_lineno_pos, start)
            last_lineno_pos = start
            module_name = m.group("ModuleName")
            # remember this class
            cur_class = Module(module, module_name, file, lineno)
            if not classstack:
                if dict.has_key(module_name):
                    cur_class = dict[module_name]
                else:
                    dict[module_name] = cur_class
            else:
                cls = classstack[-1][0]
                if cls.classes.has_key(module_name):
                    cur_class = cls.classes[module_name]
                elif cls.name == module_name:
                    cur_class = cls
                else:
                    cls._addclass(module_name, cur_class)
            classstack.append((cur_class, thisindent))
            while acstack and \
                  acstack[-1][1] >= thisindent:
                del acstack[-1]
            acstack.append(["public", thisindent])  # default access control is 'public'

        elif m.start("AccessControl") >= 0:
            aclist = m.group("AccessControlList")
            if aclist is None:
                index = -1
                while index >= -len(acstack):
                    if acstack[index][1] < indent:
                        acstack[index][0] = m.group("AccessControlType").lower()
                        break
                    else:
                        index -= 1
            else:
                index = -1
                while index >= -len(classstack):
                    if classstack[index][0] is not None and \
                       not isinstance(classstack[index][0], Function) and \
                       not classstack[index][1] >= indent:
                        parent = classstack[index][0]
                        actype = m.group("AccessControlType").lower()
                        for name in aclist.split(","):
                            name = name.strip()[1:]     # get rid of leading ':'
                            acmeth = parent._getmethod(name)
                            if acmeth is None:
                                continue
                            if actype == "private":
                                acmeth.setPrivate()
                            elif actype == "protected":
                                acmeth.setProtected()
                            elif actype == "public":
                                acmeth.setPublic()
                        break
                    else:
                        index -= 1

        elif m.start("Attribute") >= 0:
            index = -1
            while index >= -len(classstack):
                if classstack[index][0] is not None and \
                   not isinstance(classstack[index][0], Function) and \
                   not classstack[index][1] >= indent:
                    attr = Attribute(m.group("AttributeName"))
                    classstack[index][0]._addattribute(attr)
                    break
                else:
                    index -= 1

        elif m.start("Attr") >= 0:
            index = -1
            while index >= -len(classstack):
                if classstack[index][0] is not None and \
                   not isinstance(classstack[index][0], Function) and \
                   not classstack[index][1] >= indent:
                    parent = classstack[index][0]
                    if m.group("AttrType") is None:
                        nv = m.group("AttrList").split(",")
                        if not nv:
                            break
                        name = nv[0].strip()[1:]    # get rid of leading ':'
                        attr = parent._getattribute("@"+name) or \
                               parent._getattribute("@@"+name) or \
                               Attribute("@"+name)
                        if len(nv) == 1 or nv[1].strip() == "false":
                            attr.setProtected()
                        elif nv[1].strip() == "true":
                            attr.setPublic()
                        parent._addattribute(attr)
                    else:
                        access = m.group("AttrType")
                        for name in m.group("AttrList").split(","):
                            name = name.strip()[1:]     # get rid of leading ':'
                            attr = parent._getattribute("@"+name) or \
                                   parent._getattribute("@@"+name) or \
                                   Attribute("@"+name)
                            if access == "_accessor":
                                attr.setPublic()
                            elif access == "_reader" or access == "_writer":
                                if attr.isPrivate():
                                    attr.setProtected()
                                elif attr.isProtected():
                                    attr.setPublic()
                            parent._addattribute(attr)
                    break
                else:
                    index -= 1

        elif m.start("Begin") >= 0:
            # a begin of a block we are not interested in
            indent += 1

        elif m.start("End") >= 0:
            # an end of a block
            indent -= 1

        else:
            assert 0, "regexp _getnext found something unexpected"

    return dict
