File: Synopsis/Formatters/Dot.py
  1#
  2# Copyright (C) 2000 Stefan Seefeld
  3# Copyright (C) 2000 Stephen Davies
  4# All rights reserved.
  5# Licensed to the public under the terms of the GNU LGPL (>= 2),
  6# see the file COPYING for details.
  7#
  8
  9"""
 10Uses 'dot' from graphviz to generate various graphs.
 11"""
 12
 13from Synopsis.Processor import *
 14from Synopsis.QualifiedName import *
 15from Synopsis import ASG
 16from Synopsis.Formatters import TOC
 17from Synopsis.Formatters import quote_name, open_file
 18import sys, os
 19
 20verbose = False
 21debug = False
 22
 23class SystemError:
 24   """Error thrown by the system() function. Attributes are 'retval', encoded
 25   as per os.wait(): low-byte is killing signal number, high-byte is return
 26   value of command."""
 27
 28   def __init__(self, retval, command):
 29
 30      self.retval = retval
 31      self.command = command
 32
 33   def __repr__(self):
 34
 35      return 'SystemError: %(retval)x"%(command)s" failed.'%self.__dict__
 36
 37def system(command):
 38   """Run the command. If the command fails, an exception SystemError is
 39   thrown."""
 40
 41   ret = os.system(command)
 42   if (ret>>8) != 0:
 43      raise SystemError(ret, command)
 44
 45
 46def normalize(color):
 47   """Generate a color triplet from a color string."""
 48
 49   if type(color) is str and color[0] == '#':
 50      return (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16))
 51   elif type(color) is tuple:
 52      return (color[0] * 255, color[1] * 255, color[2] * 255)
 53
 54
 55def light(color):
 56
 57   import colorsys
 58   hsv = colorsys.rgb_to_hsv(*color)
 59   return colorsys.hsv_to_rgb(hsv[0], hsv[1], hsv[2]/2)
 60
 61
 62class DotFileGenerator:
 63   """A class that encapsulates the dot file generation"""
 64   def __init__(self, os, direction, bgcolor):
 65
 66      self.__os = os
 67      self.direction = direction
 68      self.bgcolor = bgcolor and '"#%X%X%X"'%bgcolor
 69      self.light_color = bgcolor and '"#%X%X%X"'%light(bgcolor) or 'gray75'
 70      self.nodes = {}
 71
 72   def write(self, text): self.__os.write(text)
 73
 74   def write_node(self, ref, name, label, **attr):
 75      """helper method to generate output for a given node"""
 76
 77      if self.nodes.has_key(name): return
 78      self.nodes[name] = len(self.nodes)
 79      number = self.nodes[name]
 80
 81      # Quote to remove characters that dot can't handle
 82      for p in [('<', '\<'), ('>', '\>'), ('{','\{'), ('}','\}')]:
 83         label = label.replace(*p)
 84
 85      if self.bgcolor:
 86         attr['fillcolor'] = self.bgcolor
 87         attr['style'] = 'filled'
 88
 89      self.write("Node" + str(number) + " [shape=\"record\", label=\"{" + label + "}\"")
 90      #self.write(", fontSize = 10, height = 0.2, width = 0.4")
 91      self.write(''.join([', %s=%s'%item for item in attr.items()]))
 92      if ref: self.write(', URL="' + ref + '"')
 93      self.write('];\n')
 94
 95   def write_edge(self, parent, child, **attr):
 96
 97      self.write("Node" + str(self.nodes[parent]) + " -> ")
 98      self.write("Node" + str(self.nodes[child]))
 99      self.write('[ color="black", fontsize=10, dir=back' + ''.join([', %s="%s"'%item for item in attr.items()]) + '];\n')
100
101class InheritanceGenerator(DotFileGenerator, ASG.Visitor):
102   """A Formatter that generates an inheritance graph. If the 'toc' argument is not None,
103   it is used to generate URLs. If no reference could be found in the toc, the node will
104   be grayed out."""
105   def __init__(self, os, direction, operations, attributes, aggregation,
106                toc, prefix, no_descend, bgcolor):
107
108      DotFileGenerator.__init__(self, os, direction, bgcolor)
109      if operations: self.__operations = []
110      else: self.__operations = None
111      if attributes: self.__attributes = []
112      else: self.__attributes = None
113      self.aggregation = aggregation
114      self.toc = toc
115      self.scope = QualifiedName()
116      if prefix:
117         if prefix.contains('::'):
118            self.scope = QualifiedCxxName(prefix.split('::'))
119         elif prefix.contains('.'):
120            self.scope = QualifiedPythonName(prefix.split('.'))
121         else:
122            self.scope = QualifiedName((prefix,))
123      self.__type_ref = None
124      self.__type_label = ''
125      self.__no_descend = no_descend
126      self.nodes = {}
127
128   def type_ref(self): return self.__type_ref
129   def type_label(self): return self.__type_label
130   def parameter(self): return self.__parameter
131
132   def format_type(self, typeObj):
133      "Returns a reference string for the given type object"
134
135      if typeObj is None: return "(unknown)"
136      typeObj.accept(self)
137      return self.type_label()
138
139   def clear_type(self):
140
141      self.__type_ref = None
142      self.__type_label = ''
143
144   def get_class_name(self, node):
145      """Returns the name of the given class node, relative to all its
146      parents. This makes the graph simpler by making the names shorter"""
147
148      base = node.name
149      for i in node.parents:
150         try:
151            parent = i.parent
152            pname = parent.name
153            for j in range(len(base)):
154               if j > len(pname) or pname[j] != base[j]:
155                  # Base is longer than parent name, or found a difference
156                  base[j:] = []
157                  break
158         except: pass # typedefs etc may cause errors here.. ignore
159      if not node.parents:
160         base = self.scope
161      return str(self.scope.prune(node.name))
162
163   #################### Type Visitor ##########################################
164
165   def visit_modifier_type(self, type):
166
167      self.format_type(type.alias)
168      self.__type_label = ''.join(type.premod) + self.__type_label
169      self.__type_label = self.__type_label + ''.join(type.postmod)
170
171   def visit_unknown_type(self, type):
172
173      self.__type_ref = self.toc and self.toc[type.link] or None
174      self.__type_label = str(self.scope.prune(type.name))
175
176   def visit_builtin_type_id(self, type):
177
178      self.__type_ref = None
179      self.__type_label = type.name[-1]
180
181   def visit_dependent_type_id(self, type):
182
183      self.__type_ref = None
184      self.__type_label = type.name[-1]
185
186   def visit_declared_type_id(self, type):
187
188      self.__type_ref = self.toc and self.toc[type.declaration.name] or None
189      if isinstance(type.declaration, ASG.Class):
190         self.__type_label = self.get_class_name(type.declaration)
191      else:
192         self.__type_label = str(self.scope.prune(type.declaration.name))
193
194   def visit_parametrized_type_id(self, type):
195
196      if type.template:
197         type_ref = self.toc and self.toc[type.template.name] or None
198         type_label = str(self.scope.prune(type.template.name))
199      else:
200         type_ref = None
201         type_label = "(unknown)"
202      parameters_label = []
203      for p in type.parameters:
204         parameters_label.append(self.format_type(p))
205      self.__type_ref = type_ref
206      self.__type_label = type_label + "<" + ','.join(parameters_label) + ">"
207
208   def visit_template_id(self, type):
209      self.__type_ref = None
210      def clip(x, max=20):
211         if len(x) > max: return '...'
212         return x
213      self.__type_label = "template<%s>"%(clip(','.join([clip(self.format_type(p)) for p in type.parameters]), 40))
214
215   #################### ASG Visitor ###########################################
216
217   def visit_inheritance(self, node):
218
219      self.format_type(node.parent)
220      if self.type_ref():
221         self.write_node(self.type_ref().link, self.type_label(), self.type_label())
222      elif self.toc:
223         self.write_node('', self.type_label(), self.type_label(), color=self.light_color, fontcolor=self.light_color)
224      else:
225         self.write_node('', self.type_label(), self.type_label())
226
227   def visit_class(self, node):
228
229      if self.__operations is not None: self.__operations.append([])
230      if self.__attributes is not None: self.__attributes.append([])
231      name = self.get_class_name(node)
232      ref = self.toc and self.toc[node.name] or None
233      for d in node.declarations: d.accept(self)
234      # NB: old version of dot needed the label surrounded in {}'s (?)
235      label = name
236      if type(node) is ASG.ClassTemplate and node.template:
237         if self.direction == 'vertical':
238            label = self.format_type(node.template) + '\\n' + label
239         else:
240            label = self.format_type(node.template) + ' ' + label
241      if self.__operations or self.__attributes:
242         label = label + '\\n'
243         if self.__operations:
244            label += '|' + ''.join([x[-1] + '()\\l' for x in self.__operations[-1]])
245         if self.__attributes:
246            label += '|' + ''.join([x[-1] + '\\l' for x in self.__attributes[-1]])
247      if ref:
248         self.write_node(ref.link, name, label)
249      elif self.toc:
250         self.write_node('', name, label, color=self.light_color, fontcolor=self.light_color)
251      else:
252         self.write_node('', name, label)
253
254      if self.aggregation:
255         #FIXME: we shouldn't only be looking for variables of the exact type,
256         #       but also derived types such as pointers, references, STL containers, etc.
257         #
258         # find attributes of type 'Class' so we can link to it
259         for a in filter(lambda a:isinstance(a, ASG.Variable), node.declarations):
260            if isinstance(a.vtype, ASG.DeclaredTypeId):
261               d = a.vtype.declaration
262               if isinstance(d, ASG.Class) and self.nodes.has_key(self.get_class_name(d)):
263                  self.write_edge(self.get_class_name(node), self.get_class_name(d),
264                                  arrowtail='ediamond')
265
266      for p in node.parents:
267         p.accept(self)
268         self.write_edge(self.type_label(), name, arrowtail='empty')
269      if self.__no_descend: return
270      if self.__operations: self.__operations.pop()
271      if self.__attributes: self.__attributes.pop()
272
273   def visit_operation(self, operation):
274
275      if self.__operations:
276         self.__operations[-1].append(operation.real_name)
277
278   def visit_variable(self, variable):
279
280      if self.__attributes:
281         self.__attributes[-1].append(variable.name)
282
283class SingleInheritanceGenerator(InheritanceGenerator):
284   """A Formatter that generates an inheritance graph for a specific class.
285   This Visitor visits the ASG upwards, i.e. following the inheritance links, instead of
286   the declarations contained in a given scope."""
287
288   def __init__(self, os, direction, operations, attributes, levels, types,
289                toc, prefix, no_descend, bgcolor):
290      InheritanceGenerator.__init__(self, os, direction, operations, attributes, False,
291                                    toc, prefix, no_descend, bgcolor)
292      self.__levels = levels
293      self.__types = types
294      self.__current = 1
295      self.__visited_classes = {} # classes already visited, to prevent recursion
296
297   #################### Type Visitor ##########################################
298
299   def visit_declared_type_id(self, type):
300      if self.__current < self.__levels or self.__levels == -1:
301         self.__current = self.__current + 1
302         type.declaration.accept(self)
303         self.__current = self.__current - 1
304      # to restore the ref/label...
305      InheritanceGenerator.visit_declared_type_id(self, type)
306
307   #################### ASG Visitor ###########################################
308
309   def visit_inheritance(self, node):
310
311      node.parent.accept(self)
312      if self.type_label():
313         if self.type_ref():
314            self.write_node(self.type_ref().link, self.type_label(), self.type_label())
315         elif self.toc:
316            self.write_node('', self.type_label(), self.type_label(), color=self.light_color, fontcolor=self.light_color)
317         else:
318            self.write_node('', self.type_label(), self.type_label())
319
320   def visit_class(self, node):
321
322      # Prevent recursion
323      if self.__visited_classes.has_key(id(node)): return
324      self.__visited_classes[id(node)] = None
325
326      name = self.get_class_name(node)
327      if self.__current == 1:
328         self.write_node('', name, name, style='filled', color=self.light_color, fontcolor=self.light_color)
329      else:
330         ref = self.toc and self.toc[node.name] or None
331         if ref:
332            self.write_node(ref.link, name, name)
333         elif self.toc:
334            self.write_node('', name, name, color=self.light_color, fontcolor=self.light_color)
335         else:
336            self.write_node('', name, name)
337
338      for p in node.parents:
339         p.accept(self)
340         if self.nodes.has_key(self.type_label()):
341            self.write_edge(self.type_label(), name, arrowtail='empty')
342      # if this is the main class and if there is a type dictionary,
343      # look for classes that are derived from this class
344
345      # if this is the main class
346      if self.__current == 1 and self.__types:
347         # fool the visit_declared_type_id method to stop walking upwards
348         self.__levels = 0
349         for t in self.__types.values():
350            if isinstance(t, ASG.DeclaredTypeId):
351               child = t.declaration
352               if isinstance(child, ASG.Class):
353                  for i in child.parents:
354                     type = i.parent
355                     type.accept(self)
356                     if self.type_ref():
357                        if self.type_ref().name == node.name:
358                           child_label = self.get_class_name(child)
359                           ref = self.toc and self.toc[child.name] or None
360                           if ref:
361                              self.write_node(ref.link, child_label, child_label)
362                           elif self.toc:
363                              self.write_node('', child_label, child_label, color=self.light_color, fontcolor=self.light_color)
364                           else:
365                              self.write_node('', child_label, child_label)
366
367                           self.write_edge(name, child_label, arrowtail='empty')
368
369class FileDependencyGenerator(DotFileGenerator, ASG.Visitor):
370   """A Formatter that generates a file dependency graph"""
371
372   def visit_file(self, file):
373      if file.annotations['primary']:
374         self.write_node('', file.name, file.name)
375      for i in file.includes:
376         target = i.target
377         if target.annotations['primary']:
378            self.write_node('', target.name, target.name)
379            name = i.name
380            name = name.replace('"', '\\"')
381            self.write_edge(target.name, file.name, label=name, style='dashed')
382
383def _rel(frm, to):
384   "Find link to to relative to frm"
385
386   frm = frm.split('/'); to = to.split('/')
387   for l in range((len(frm)<len(to)) and len(frm)-1 or len(to)-1):
388      if to[0] == frm[0]: del to[0]; del frm[0]
389      else: break
390   if frm: to = ['..'] * (len(frm) - 1) + to
391   return '/'.join(to)
392
393def _convert_map(input, output, base_url):
394   """convert map generated from Dot to a html region map.
395   input and output are (open) streams"""
396
397   line = input.readline()
398   while line:
399      line = line[:-1]
400      if line[0:4] == "rect":
401         url, x1y1, x2y2 = line[4:].split()
402         x1, y1 = x1y1.split(',')
403         x2, y2 = x2y2.split(',')
404         output.write('<area alt="'+url+'" href="' + _rel(base_url, url) + '" shape="rect" coords="')
405         output.write(str(x1) + ", " + str(y1) + ", " + str(x2) + ", " + str(y2) + '" />\n')
406      line = input.readline()
407
408def _format(input, output, format):
409
410   command = 'dot -T%s -o "%s" "%s"'%(format, output, input)
411   if verbose: print "Dot Formatter: running command '" + command + "'"
412   try:
413      system(command)
414   except SystemError, e:
415      if debug:
416         print 'failed to execute "%s"'%command
417      raise InvalidCommand, "could not execute 'dot'"
418
419def _format_png(input, output): _format(input, output, "png")
420
421def _format_html(input, output, base_url):
422   """generate (active) image for html.
423   input and output are file names. If output ends
424   in '.html', its stem is used with an '.png' suffix for the
425   actual image."""
426
427   if output[-5:] == ".html": output = output[:-5]
428   _format_png(input, output + ".png")
429   _format(input, output + ".map", "imap")
430   prefix, name = os.path.split(output)
431   reference = name + ".png"
432   html = open_file(output + ".html")
433   html.write('<img alt="'+name+'" src="' + reference + '" hspace="8" vspace="8" border="0" usemap="#')
434   html.write(name + "_map\" />\n")
435   html.write("<map name=\"" + name + "_map\">")
436   dotmap = open(output + ".map", "r+")
437   _convert_map(dotmap, html, base_url)
438   dotmap.close()
439   os.remove(output + ".map")
440   html.write("</map>\n")
441
442class Formatter(Processor):
443   """The Formatter class acts merely as a frontend to
444   the various InheritanceGenerators"""
445
446   title = Parameter('Inheritance Graph', 'the title of the graph')
447   type = Parameter('class', 'type of graph (one of "file", "class", "single"')
448   hide_operations = Parameter(True, 'hide operations')
449   hide_attributes = Parameter(True, 'hide attributes')
450   show_aggregation = Parameter(False, 'show aggregation')
451   bgcolor = Parameter(None, 'background color for nodes')
452   format = Parameter('ps', 'Generate output in format "dot", "ps", "png", "svg", "gif", "map", "html"')
453   layout = Parameter('vertical', 'Direction of graph')
454   prefix = Parameter(None, 'Prefix to strip from all class names')
455   toc_in = Parameter([], 'list of table of content files to use for symbol lookup')
456   base_url = Parameter(None, 'base url to use for generated links')
457
458   def process(self, ir, **kwds):
459      global verbose, debug
460
461      self.set_parameters(kwds)
462      if self.bgcolor:
463         bgcolor = normalize(self.bgcolor)
464         if not bgcolor:
465            raise InvalidArgument('bgcolor=%s'%repr(self.bgcolor))
466         else:
467            self.bgcolor = bgcolor
468
469      self.ir = self.merge_input(ir)
470      verbose = self.verbose
471      debug = self.debug
472
473      formats = {'dot' : 'dot',
474                 'ps' : 'ps',
475                 'png' : 'png',
476                 'gif' : 'gif',
477                 'svg' : 'svg',
478                 'map' : 'imap',
479                 'html' : 'html'}
480
481      if formats.has_key(self.format): format = formats[self.format]
482      else:
483         print "Error: Unknown format. Available formats are:",
484         print ', '.join(formats.keys())
485         return self.ir
486
487      # we only need the toc if format=='html'
488      if format == 'html':
489         # beware: HTML.Fragments.ClassHierarchyGraph sets self.toc !!
490         toc = getattr(self, 'toc', TOC.TOC(TOC.Linker()))
491         for t in self.toc_in: toc.load(t)
492      else:
493         toc = None
494
495      head, tail = os.path.split(self.output)
496      tmpfile = os.path.join(head, quote_name(tail)) + ".dot"
497      if self.verbose: print "Dot Formatter: Writing dot file..."
498      dotfile = open_file(tmpfile)
499      dotfile.write("digraph \"%s\" {\n"%(self.title))
500      if self.layout == 'horizontal':
501         dotfile.write('rankdir="LR";\n')
502         dotfile.write('ranksep="1.0";\n')
503      dotfile.write("node[shape=record, fontsize=10, height=0.2, width=0.4, color=black]\n")
504      if self.type == 'single':
505         generator = SingleInheritanceGenerator(dotfile, self.layout,
506                                                not self.hide_operations,
507                                                not self.hide_attributes,
508                                                -1, self.ir.asg.types,
509                                                toc, self.prefix, False,
510                                                self.bgcolor)
511      elif self.type == 'class':
512         generator = InheritanceGenerator(dotfile, self.layout,
513                                          not self.hide_operations,
514                                          not self.hide_attributes,
515                                          self.show_aggregation,
516                                          toc, self.prefix, False,
517                                          self.bgcolor)
518      elif self.type == 'file':
519         generator = FileDependencyGenerator(dotfile, self.layout, self.bgcolor)
520      else:
521         sys.stderr.write("Dot: unknown type\n");
522
523
524      if self.type == 'file':
525         for f in self.ir.files.values():
526            generator.visit_file(f)
527      else:
528         for d in self.ir.asg.declarations:
529            d.accept(generator)
530      dotfile.write("}\n")
531      dotfile.close()
532      if format == "dot":
533         os.rename(tmpfile, self.output)
534      elif format == "png":
535         _format_png(tmpfile, self.output)
536         os.remove(tmpfile)
537      elif format == "html":
538         _format_html(tmpfile, self.output, self.base_url)
539         os.remove(tmpfile)
540      else:
541         _format(tmpfile, self.output, format)
542         os.remove(tmpfile)
543
544      return self.ir
545
546