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