|
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 |