eric6/Utilities/ClassBrowsers/rbclbr.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2005 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Parse a Ruby file and retrieve classes, modules, methods and attributes.
8
9 Parse enough of a Ruby file to recognize class, module and method definitions
10 and to find out the superclasses of a class as well as its attributes.
11
12 It is based on the Python class browser found in this package.
13 """
14
15 from __future__ import unicode_literals
16
17 import re
18
19 import Utilities
20 import Utilities.ClassBrowsers as ClassBrowsers
21 from . import ClbrBaseClasses
22
23 SUPPORTED_TYPES = [ClassBrowsers.RB_SOURCE]
24
25 _getnext = re.compile(
26 r"""
27 (?P<String>
28 =begin .*? =end
29
30 | <<-? (?P<HereMarker1> [a-zA-Z0-9_]+? ) [ \t]* .*? (?P=HereMarker1)
31
32 | <<-? ['"] (?P<HereMarker2> [^'"]+? ) ['"] [ \t]* .*? (?P=HereMarker2)
33
34 | " [^"\\\n]* (?: \\. [^"\\\n]*)* "
35
36 | ' [^'\\\n]* (?: \\. [^'\\\n]*)* '
37 )
38
39 | (?P<CodingLine>
40 ^ \# \s* [*_-]* \s* coding[:=] \s* (?P<Coding> [-\w_.]+ ) \s* [*_-]* $
41 )
42
43 | (?P<Comment>
44 ^
45 [ \t]* \#+ .*? $
46 )
47
48 | (?P<Method>
49 ^
50 (?P<MethodIndent> [ \t]* )
51 def [ \t]+
52 (?:
53 (?P<MethodName2> [a-zA-Z0-9_]+ (?: \. | :: )
54 [a-zA-Z_] [a-zA-Z0-9_?!=]* )
55 |
56 (?P<MethodName> [a-zA-Z_] [a-zA-Z0-9_?!=]* )
57 |
58 (?P<MethodName3> [^( \t]{1,3} )
59 )
60 [ \t]*
61 (?:
62 \( (?P<MethodSignature> (?: [^)] | \)[ \t]*,? )*? ) \)
63 )?
64 [ \t]*
65 )
66
67 | (?P<Class>
68 ^
69 (?P<ClassIndent> [ \t]* )
70 class
71 (?:
72 [ \t]+
73 (?P<ClassName> [A-Z] [a-zA-Z0-9_]* )
74 [ \t]*
75 (?P<ClassSupers> < [ \t]* [A-Z] [a-zA-Z0-9_:]* )?
76 |
77 [ \t]* << [ \t]*
78 (?P<ClassName2> [a-zA-Z_] [a-zA-Z0-9_:]* )
79 )
80 [ \t]*
81 )
82
83 | (?P<ClassIgnored>
84 \(
85 [ \t]*
86 class
87 .*?
88 end
89 [ \t]*
90 \)
91 )
92
93 | (?P<Module>
94 ^
95 (?P<ModuleIndent> [ \t]* )
96 module [ \t]+
97 (?P<ModuleName> [A-Z] [a-zA-Z0-9_:]* )
98 [ \t]*
99 )
100
101 | (?P<AccessControl>
102 ^
103 (?P<AccessControlIndent> [ \t]* )
104 (?:
105 (?P<AccessControlType> private | public | protected ) [^_]
106 |
107 (?P<AccessControlType2>
108 private_class_method | public_class_method )
109 )
110 \(?
111 [ \t]*
112 (?P<AccessControlList> (?: : [a-zA-Z0-9_]+ , \s* )*
113 (?: : [a-zA-Z0-9_]+ )+ )?
114 [ \t]*
115 \)?
116 )
117
118 | (?P<Attribute>
119 ^
120 (?P<AttributeIndent> [ \t]* )
121 (?P<AttributeName> (?: @ | @@ ) [a-zA-Z0-9_]* )
122 [ \t]* =
123 )
124
125 | (?P<Attr>
126 ^
127 (?P<AttrIndent> [ \t]* )
128 attr
129 (?P<AttrType> (?: _accessor | _reader | _writer ) )?
130 \(?
131 [ \t]*
132 (?P<AttrList> (?: : [a-zA-Z0-9_]+ , \s* )*
133 (?: : [a-zA-Z0-9_]+ | true | false )+ )
134 [ \t]*
135 \)?
136 )
137
138 | (?P<Begin>
139 ^
140 [ \t]*
141 (?: def | if | unless | case | while | until | for | begin )
142 \b [^_]
143 |
144 [ \t]* do [ \t]* (?: \| .*? \| )? [ \t]* $
145 )
146
147 | (?P<BeginEnd>
148 \b (?: if ) \b [^_] .*? $
149 |
150 \b (?: if ) \b [^_] .*? end [ \t]* $
151 )
152
153 | (?P<End>
154 [ \t]*
155 (?:
156 end [ \t]* $
157 |
158 end \b [^_]
159 )
160 )""",
161 re.VERBOSE | re.DOTALL | re.MULTILINE).search
162
163 _commentsub = re.compile(r"""#[^\n]*\n|#[^\n]*$""").sub
164
165 _modules = {} # cache of modules we've seen
166
167
168 class VisibilityMixin(ClbrBaseClasses.ClbrVisibilityMixinBase):
169 """
170 Mixin class implementing the notion of visibility.
171 """
172 def __init__(self):
173 """
174 Constructor
175 """
176 self.setPublic()
177
178
179 class Class(ClbrBaseClasses.Class, VisibilityMixin):
180 """
181 Class to represent a Ruby class.
182 """
183 def __init__(self, module, name, superClasses, file, lineno):
184 """
185 Constructor
186
187 @param module name of the module containing this class
188 @param name name of this class
189 @param superClasses list of class names this class is inherited from
190 @param file filename containing this class
191 @param lineno linenumber of the class definition
192 """
193 ClbrBaseClasses.Class.__init__(self, module, name, superClasses, file,
194 lineno)
195 VisibilityMixin.__init__(self)
196
197
198 class Module(ClbrBaseClasses.Module, VisibilityMixin):
199 """
200 Class to represent a Ruby module.
201 """
202 def __init__(self, module, name, file, lineno):
203 """
204 Constructor
205
206 @param module name of the module containing this class
207 @param name name of this class
208 @param file filename containing this class
209 @param lineno linenumber of the class definition
210 """
211 ClbrBaseClasses.Module.__init__(self, module, name, file, lineno)
212 VisibilityMixin.__init__(self)
213
214
215 class Function(ClbrBaseClasses.Function, VisibilityMixin):
216 """
217 Class to represent a Ruby function.
218 """
219 def __init__(self, module, name, file, lineno, signature='',
220 separator=','):
221 """
222 Constructor
223
224 @param module name of the module containing this function
225 @param name name of this function
226 @param file filename containing this class
227 @param lineno linenumber of the class definition
228 @param signature parameterlist of the method
229 @param separator string separating the parameters
230 """
231 ClbrBaseClasses.Function.__init__(self, module, name, file, lineno,
232 signature, separator)
233 VisibilityMixin.__init__(self)
234
235
236 class Attribute(ClbrBaseClasses.Attribute, VisibilityMixin):
237 """
238 Class to represent a class or module attribute.
239 """
240 def __init__(self, module, name, file, lineno):
241 """
242 Constructor
243
244 @param module name of the module containing this class
245 @param name name of this class
246 @param file filename containing this attribute
247 @param lineno linenumber of the class definition
248 """
249 ClbrBaseClasses.Attribute.__init__(self, module, name, file, lineno)
250 VisibilityMixin.__init__(self)
251 self.setPrivate()
252
253
254 def readmodule_ex(module, path=None):
255 """
256 Read a Ruby file and return a dictionary of classes, functions and modules.
257
258 @param module name of the Ruby file (string)
259 @param path path the file should be searched in (list of strings)
260 @return the resulting dictionary
261 """
262 global _modules
263
264 dictionary = {}
265 dict_counts = {}
266
267 if module in _modules:
268 # we've seen this file before...
269 return _modules[module]
270
271 # search the path for the file
272 f = None
273 fullpath = [] if path is None else path[:]
274 f, file, (suff, mode, type) = ClassBrowsers.find_module(module, fullpath)
275 if f:
276 f.close()
277 if type not in SUPPORTED_TYPES:
278 # not Ruby source, can't do anything with this module
279 _modules[module] = dictionary
280 return dictionary
281
282 _modules[module] = dictionary
283 classstack = [] # stack of (class, indent) pairs
284 acstack = [] # stack of (access control, indent) pairs
285 indent = 0
286 try:
287 src = Utilities.readEncodedFile(file)[0]
288 except (UnicodeError, IOError):
289 # can't do anything with this module
290 _modules[module] = dictionary
291 return dictionary
292
293 lineno, last_lineno_pos = 1, 0
294 cur_obj = None
295 lastGlobalEntry = None
296 i = 0
297 while True:
298 m = _getnext(src, i)
299 if not m:
300 break
301 start, i = m.span()
302
303 if m.start("Method") >= 0:
304 # found a method definition or function
305 thisindent = indent
306 indent += 1
307 meth_name = m.group("MethodName") or \
308 m.group("MethodName2") or \
309 m.group("MethodName3")
310 meth_sig = m.group("MethodSignature")
311 meth_sig = meth_sig and meth_sig.replace('\\\n', '') or ''
312 meth_sig = _commentsub('', meth_sig)
313 lineno = lineno + src.count('\n', last_lineno_pos, start)
314 last_lineno_pos = start
315 if meth_name.startswith('self.'):
316 meth_name = meth_name[5:]
317 elif meth_name.startswith('self::'):
318 meth_name = meth_name[6:]
319 # close all classes/modules indented at least as much
320 while classstack and \
321 classstack[-1][1] >= thisindent:
322 if classstack[-1][0] is not None:
323 # record the end line
324 classstack[-1][0].setEndLine(lineno - 1)
325 del classstack[-1]
326 while acstack and \
327 acstack[-1][1] >= thisindent:
328 del acstack[-1]
329 if classstack:
330 # it's a class/module method
331 cur_class = classstack[-1][0]
332 if isinstance(cur_class, Class) or \
333 isinstance(cur_class, Module):
334 # it's a method
335 f = Function(None, meth_name,
336 file, lineno, meth_sig)
337 cur_class._addmethod(meth_name, f)
338 else:
339 f = cur_class
340 # set access control
341 if acstack:
342 accesscontrol = acstack[-1][0]
343 if accesscontrol == "private":
344 f.setPrivate()
345 elif accesscontrol == "protected":
346 f.setProtected()
347 elif accesscontrol == "public":
348 f.setPublic()
349 # else it's a nested def
350 else:
351 # it's a function
352 f = Function(module, meth_name,
353 file, lineno, meth_sig)
354 if meth_name in dict_counts:
355 dict_counts[meth_name] += 1
356 meth_name = "{0}_{1:d}".format(
357 meth_name, dict_counts[meth_name])
358 else:
359 dict_counts[meth_name] = 0
360 dictionary[meth_name] = f
361 if not classstack:
362 if lastGlobalEntry:
363 lastGlobalEntry.setEndLine(lineno - 1)
364 lastGlobalEntry = f
365 if cur_obj and isinstance(cur_obj, Function):
366 cur_obj.setEndLine(lineno - 1)
367 cur_obj = f
368 classstack.append((f, thisindent)) # Marker for nested fns
369
370 elif m.start("String") >= 0:
371 pass
372
373 elif m.start("Comment") >= 0:
374 pass
375
376 elif m.start("ClassIgnored") >= 0:
377 pass
378
379 elif m.start("Class") >= 0:
380 # we found a class definition
381 thisindent = indent
382 indent += 1
383 lineno = lineno + src.count('\n', last_lineno_pos, start)
384 last_lineno_pos = start
385 # close all classes/modules indented at least as much
386 while classstack and \
387 classstack[-1][1] >= thisindent:
388 if classstack[-1][0] is not None:
389 # record the end line
390 classstack[-1][0].setEndLine(lineno - 1)
391 del classstack[-1]
392 class_name = m.group("ClassName") or m.group("ClassName2")
393 inherit = m.group("ClassSupers")
394 if inherit:
395 # the class inherits from other classes
396 inherit = inherit[1:].strip()
397 inherit = [_commentsub('', inherit)]
398 # remember this class
399 cur_class = Class(module, class_name, inherit,
400 file, lineno)
401 if not classstack:
402 if class_name in dictionary:
403 cur_class = dictionary[class_name]
404 else:
405 dictionary[class_name] = cur_class
406 else:
407 cls = classstack[-1][0]
408 if class_name in cls.classes:
409 cur_class = cls.classes[class_name]
410 elif cls.name == class_name or class_name == "self":
411 cur_class = cls
412 else:
413 cls._addclass(class_name, cur_class)
414 if not classstack:
415 if lastGlobalEntry:
416 lastGlobalEntry.setEndLine(lineno - 1)
417 lastGlobalEntry = cur_class
418 cur_obj = cur_class
419 classstack.append((cur_class, thisindent))
420 while acstack and \
421 acstack[-1][1] >= thisindent:
422 del acstack[-1]
423 acstack.append(["public", thisindent])
424 # default access control is 'public'
425
426 elif m.start("Module") >= 0:
427 # we found a module definition
428 thisindent = indent
429 indent += 1
430 lineno = lineno + src.count('\n', last_lineno_pos, start)
431 last_lineno_pos = start
432 # close all classes/modules indented at least as much
433 while classstack and \
434 classstack[-1][1] >= thisindent:
435 if classstack[-1][0] is not None:
436 # record the end line
437 classstack[-1][0].setEndLine(lineno - 1)
438 del classstack[-1]
439 module_name = m.group("ModuleName")
440 # remember this class
441 cur_class = Module(module, module_name, file, lineno)
442 if not classstack:
443 if module_name in dictionary:
444 cur_class = dictionary[module_name]
445 else:
446 dictionary[module_name] = cur_class
447 else:
448 cls = classstack[-1][0]
449 if module_name in cls.classes:
450 cur_class = cls.classes[module_name]
451 elif cls.name == module_name:
452 cur_class = cls
453 else:
454 cls._addclass(module_name, cur_class)
455 if not classstack:
456 if lastGlobalEntry:
457 lastGlobalEntry.setEndLine(lineno - 1)
458 lastGlobalEntry = cur_class
459 cur_obj = cur_class
460 classstack.append((cur_class, thisindent))
461 while acstack and \
462 acstack[-1][1] >= thisindent:
463 del acstack[-1]
464 acstack.append(["public", thisindent])
465 # default access control is 'public'
466
467 elif m.start("AccessControl") >= 0:
468 aclist = m.group("AccessControlList")
469 if aclist is None:
470 index = -1
471 while index >= -len(acstack):
472 if acstack[index][1] < indent:
473 actype = m.group("AccessControlType") or \
474 m.group("AccessControlType2").split('_')[0]
475 acstack[index][0] = actype.lower()
476 break
477 else:
478 index -= 1
479 else:
480 index = -1
481 while index >= -len(classstack):
482 if classstack[index][0] is not None and \
483 not isinstance(classstack[index][0], Function) and \
484 not classstack[index][1] >= indent:
485 parent = classstack[index][0]
486 actype = m.group("AccessControlType") or \
487 m.group("AccessControlType2").split('_')[0]
488 actype = actype.lower()
489 for name in aclist.split(","):
490 name = name.strip()[1:] # get rid of leading ':'
491 acmeth = parent._getmethod(name)
492 if acmeth is None:
493 continue
494 if actype == "private":
495 acmeth.setPrivate()
496 elif actype == "protected":
497 acmeth.setProtected()
498 elif actype == "public":
499 acmeth.setPublic()
500 break
501 else:
502 index -= 1
503
504 elif m.start("Attribute") >= 0:
505 lineno = lineno + src.count('\n', last_lineno_pos, start)
506 last_lineno_pos = start
507 index = -1
508 while index >= -len(classstack):
509 if classstack[index][0] is not None and \
510 not isinstance(classstack[index][0], Function) and \
511 not classstack[index][1] >= indent:
512 attr = Attribute(
513 module, m.group("AttributeName"), file, lineno)
514 classstack[index][0]._addattribute(attr)
515 break
516 else:
517 index -= 1
518 if lastGlobalEntry:
519 lastGlobalEntry.setEndLine(lineno - 1)
520 lastGlobalEntry = None
521
522 elif m.start("Attr") >= 0:
523 lineno = lineno + src.count('\n', last_lineno_pos, start)
524 last_lineno_pos = start
525 index = -1
526 while index >= -len(classstack):
527 if classstack[index][0] is not None and \
528 not isinstance(classstack[index][0], Function) and \
529 not classstack[index][1] >= indent:
530 parent = classstack[index][0]
531 if m.group("AttrType") is None:
532 nv = m.group("AttrList").split(",")
533 if not nv:
534 break
535 name = nv[0].strip()[1:] # get rid of leading ':'
536 attr = parent._getattribute("@" + name) or \
537 parent._getattribute("@@" + name) or \
538 Attribute(module, "@" + name, file, lineno)
539 if len(nv) == 1 or nv[1].strip() == "false":
540 attr.setProtected()
541 elif nv[1].strip() == "true":
542 attr.setPublic()
543 parent._addattribute(attr)
544 else:
545 access = m.group("AttrType")
546 for name in m.group("AttrList").split(","):
547 name = name.strip()[1:] # get rid of leading ':'
548 attr = parent._getattribute("@" + name) or \
549 parent._getattribute("@@" + name) or \
550 Attribute(module, "@" + name, file, lineno)
551 if access == "_accessor":
552 attr.setPublic()
553 elif access == "_reader" or access == "_writer":
554 if attr.isPrivate():
555 attr.setProtected()
556 elif attr.isProtected():
557 attr.setPublic()
558 parent._addattribute(attr)
559 break
560 else:
561 index -= 1
562
563 elif m.start("Begin") >= 0:
564 # a begin of a block we are not interested in
565 indent += 1
566
567 elif m.start("End") >= 0:
568 # an end of a block
569 indent -= 1
570 if indent < 0:
571 # no negative indent allowed
572 if classstack:
573 # it's a class/module method
574 indent = classstack[-1][1]
575 else:
576 indent = 0
577
578 elif m.start("BeginEnd") >= 0:
579 pass
580
581 elif m.start("CodingLine") >= 0:
582 # a coding statement
583 coding = m.group("Coding")
584 lineno = lineno + src.count('\n', last_lineno_pos, start)
585 last_lineno_pos = start
586 if "@@Coding@@" not in dictionary:
587 dictionary["@@Coding@@"] = ClbrBaseClasses.Coding(
588 module, file, lineno, coding)
589
590 else:
591 assert 0, "regexp _getnext found something unexpected"
592
593 return dictionary

eric ide

mercurial