Utilities/ClassBrowsers/rbclbr.py

changeset 0
de9c2efb9d02
child 12
1d8dd9706f46
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
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

eric ide

mercurial