--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Utilities/ClassBrowsers/rbclbr.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,613 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2005 - 2022 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 re + +import Utilities +import Utilities.ClassBrowsers as ClassBrowsers +from . import ClbrBaseClasses + +SUPPORTED_TYPES = [ClassBrowsers.RB_SOURCE] + +_getnext = re.compile( + r""" + (?P<String> + =begin .*? =end + + | <<-? (?P<HereMarker1> [a-zA-Z0-9_]+? ) [ \t]* .*? (?P=HereMarker1) + + | <<-? ['"] (?P<HereMarker2> [^'"]+? ) ['"] [ \t]* .*? (?P=HereMarker2) + + | " [^"\\\n]* (?: \\. [^"\\\n]*)* " + + | ' [^'\\\n]* (?: \\. [^'\\\n]*)* ' + ) + +| (?P<CodingLine> + ^ \# \s* [*_-]* \s* coding[:=] \s* (?P<Coding> [-\w_.]+ ) \s* [*_-]* $ + ) + +| (?P<Comment> + ^ + [ \t]* \#+ .*? $ + ) + +| (?P<Method> + ^ + (?P<MethodIndent> [ \t]* ) + def [ \t]+ + (?: + (?P<MethodName2> [a-zA-Z0-9_]+ (?: \. | :: ) + [a-zA-Z_] [a-zA-Z0-9_?!=]* ) + | + (?P<MethodName> [a-zA-Z_] [a-zA-Z0-9_?!=]* ) + | + (?P<MethodName3> [^( \t]{1,3} ) + ) + [ \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 ) [^_] + | + (?P<AccessControlType2> + private_class_method | public_class_method ) + ) + \(? + [ \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<BeginEnd> + \b (?: if ) \b [^_] .*? $ + | + \b (?: if ) \b [^_] .*? end [ \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): + """ + Constructor + """ + self.setPublic() + + +class Class(ClbrBaseClasses.Class, VisibilityMixin): + """ + Class to represent a Ruby class. + """ + def __init__(self, module, name, superClasses, file, lineno): + """ + Constructor + + @param module name of the module containing this class + @param name name of this class + @param superClasses 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, superClasses, 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, 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 attribute + @param lineno linenumber of the class definition + """ + ClbrBaseClasses.Attribute.__init__(self, module, name, file, lineno) + VisibilityMixin.__init__(self) + self.setPrivate() + + +def readmodule_ex(module, path=None): + """ + 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 + """ + global _modules + + if module in _modules: + # we've seen this file before... + return _modules[module] + + # search the path for the file + f = None + fullpath = [] if path is None else path[:] + f, file, (suff, mode, type) = ClassBrowsers.find_module(module, fullpath) + if f: + f.close() + if type not in SUPPORTED_TYPES: + # not Ruby source, can't do anything with this module + _modules[module] = {} + return {} + + try: + src = Utilities.readEncodedFile(file)[0] + except (UnicodeError, OSError): + # can't do anything with this module + _modules[module] = {} + return {} + + _modules[module] = scan(src, file, module) + return _modules[module] + + +def scan(src, file, module): + """ + Public method to scan the given source text. + + @param src source text to be scanned + @type str + @param file file name associated with the source text + @type str + @param module module name associated with the source text + @type str + @return dictionary containing the extracted data + @rtype dict + """ + # convert eol markers the Python style + src = src.replace("\r\n", "\n").replace("\r", "\n") + + dictionary = {} + dict_counts = {} + + classstack = [] # stack of (class, indent) pairs + acstack = [] # stack of (access control, indent) pairs + indent = 0 + + lineno, last_lineno_pos = 1, 0 + cur_obj = None + lastGlobalEntry = None + i = 0 + while True: + 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") or + m.group("MethodName3") + ) + meth_sig = m.group("MethodSignature") + meth_sig = meth_sig and meth_sig.replace('\\\n', '') or '' + meth_sig = _commentsub('', meth_sig) + lineno += src.count('\n', last_lineno_pos, start) + last_lineno_pos = start + if meth_name.startswith('self.'): + meth_name = meth_name[5:] + elif meth_name.startswith('self::'): + meth_name = meth_name[6:] + # close all classes/modules indented at least as much + while classstack and classstack[-1][1] >= thisindent: + if classstack[-1][0] is not None: + # record the end line + classstack[-1][0].setEndLine(lineno - 1) + 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, Module)): + # it's a method + f = Function(None, meth_name, + file, lineno, meth_sig) + cur_class._addmethod(meth_name, f) + else: + f = cur_class + # set access control + if acstack: + 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 meth_name in dict_counts: + dict_counts[meth_name] += 1 + meth_name = "{0}_{1:d}".format( + meth_name, dict_counts[meth_name]) + else: + dict_counts[meth_name] = 0 + dictionary[meth_name] = f + if not classstack: + if lastGlobalEntry: + lastGlobalEntry.setEndLine(lineno - 1) + lastGlobalEntry = f + if cur_obj and isinstance(cur_obj, Function): + cur_obj.setEndLine(lineno - 1) + cur_obj = f + classstack.append((f, thisindent)) # Marker for nested fns + + elif ( + m.start("String") >= 0 or + m.start("Comment") >= 0 or + m.start("ClassIgnored") >= 0 or + m.start("BeginEnd") >= 0 + ): + pass + + elif m.start("Class") >= 0: + # we found a class definition + thisindent = indent + indent += 1 + lineno += src.count('\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: + if classstack[-1][0] is not None: + # record the end line + classstack[-1][0].setEndLine(lineno - 1) + del classstack[-1] + 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 class_name in dictionary: + cur_class = dictionary[class_name] + else: + dictionary[class_name] = cur_class + else: + cls = classstack[-1][0] + if class_name in cls.classes: + cur_class = cls.classes[class_name] + elif class_name in (cls.name, "self"): + cur_class = cls + else: + cls._addclass(class_name, cur_class) + if not classstack: + if lastGlobalEntry: + lastGlobalEntry.setEndLine(lineno - 1) + lastGlobalEntry = cur_class + cur_obj = 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 + lineno += src.count('\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: + if classstack[-1][0] is not None: + # record the end line + classstack[-1][0].setEndLine(lineno - 1) + del classstack[-1] + module_name = m.group("ModuleName") + # remember this class + cur_class = Module(module, module_name, file, lineno) + if not classstack: + if module_name in dictionary: + cur_class = dictionary[module_name] + else: + dictionary[module_name] = cur_class + else: + cls = classstack[-1][0] + if module_name in cls.classes: + cur_class = cls.classes[module_name] + elif cls.name == module_name: + cur_class = cls + else: + cls._addclass(module_name, cur_class) + if not classstack: + if lastGlobalEntry: + lastGlobalEntry.setEndLine(lineno - 1) + lastGlobalEntry = cur_class + cur_obj = 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: + actype = ( + m.group("AccessControlType") or + m.group("AccessControlType2").split('_')[0] + ) + acstack[index][0] = actype.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 + classstack[index][1] < indent + ): + parent = classstack[index][0] + actype = ( + m.group("AccessControlType") or + m.group("AccessControlType2").split('_')[0] + ) + actype = actype.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: + lineno += src.count('\n', last_lineno_pos, start) + last_lineno_pos = start + index = -1 + while index >= -len(classstack): + if ( + classstack[index][0] is not None and + not isinstance(classstack[index][0], Function) and + classstack[index][1] < indent + ): + attr = Attribute( + module, m.group("AttributeName"), file, lineno) + classstack[index][0]._addattribute(attr) + break + else: + index -= 1 + if lastGlobalEntry: + lastGlobalEntry.setEndLine(lineno - 1) + lastGlobalEntry = None + + elif m.start("Attr") >= 0: + lineno += src.count('\n', last_lineno_pos, start) + last_lineno_pos = start + index = -1 + while index >= -len(classstack): + if ( + classstack[index][0] is not None and + not isinstance(classstack[index][0], Function) and + 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(module, "@" + name, file, lineno) + ) + 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(module, "@" + name, file, lineno) + ) + if access == "_accessor": + attr.setPublic() + elif access in ("_reader", "_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 + if indent < 0: + # no negative indent allowed + if classstack: + # it's a class/module method + indent = classstack[-1][1] + else: + indent = 0 + + elif m.start("CodingLine") >= 0: + # a coding statement + coding = m.group("Coding") + lineno += src.count('\n', last_lineno_pos, start) + last_lineno_pos = start + if "@@Coding@@" not in dictionary: + dictionary["@@Coding@@"] = ClbrBaseClasses.Coding( + module, file, lineno, coding) + + return dictionary