Modules |
Files |
Inheritance Tree |
Inheritance Graph |
Name Index |
Config
File: Synopsis/Formatter/Dot.py
1| # $Id: Dot.py,v 1.34 2002/11/20 15:18:41 stefan Exp $
2| #
3| # This file is a part of Synopsis.
4| # Copyright (C) 2000, 2001 Stefan Seefeld
5| #
6| # Synopsis is free software; you can redistribute it and/or modify it
7| # under the terms of the GNU General Public License as published by
8| # the Free Software Foundation; either version 2 of the License, or
9| # (at your option) any later version.
10| #
11| # This program is distributed in the hope that it will be useful,
12| # but WITHOUT ANY WARRANTY; without even the implied warranty of
13| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14| # General Public License for more details.
15| #
16| # You should have received a copy of the GNU General Public License
17| # along with this program; if not, write to the Free Software
18| # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
19| # 02111-1307, USA.
20| #
21| # $Log: Dot.py,v $
22| # Revision 1.34 2002/11/20 15:18:41 stefan
23| # don't quote dirname part of a file name
24| #
25| # Revision 1.33 2002/11/01 04:26:34 chalky
26| # Fix wrong-ordered imagemap coords
27| #
28| # Revision 1.32 2002/11/01 03:39:20 chalky
29| # Cleaning up HTML after using 'htmltidy'
30| #
31| # Revision 1.31 2002/10/29 15:01:21 chalky
32| # Better display of template types, and support names with spaces
33| #
34| # Revision 1.30 2002/10/28 16:27:22 chalky
35| # Support horizontal inheritance graphs
36| #
37| # Revision 1.29 2002/10/28 11:44:16 chalky
38| # Support being given a prefix name to strip from class names.
39| # Display template parameters in class labels
40| #
41| # Revision 1.28 2002/10/20 15:38:08 chalky
42| # Much improved template support, including Function Templates.
43| #
44| # Revision 1.27 2002/10/20 02:21:50 chalky
45| # Fix up quoting (thanks David).
46| #
47| # Revision 1.26 2002/08/21 03:36:54 stefan
48| # * use UML inheritance arrows
49| # * display operations and attributes if requested
50| #
51| # Revision 1.25 2002/07/11 02:09:34 chalky
52| # Patch from Patrick Mauritz: Use png support in latest graphviz. If dot not
53| # present, don't subvert what the user asked for but instead tell them.
54| #
55| # Revision 1.24 2002/04/26 01:21:14 chalky
56| # Bugs and cleanups
57| #
58| # Revision 1.23 2002/01/09 11:43:41 chalky
59| # Inheritance pics
60| #
61| # Revision 1.22 2001/07/19 04:03:05 chalky
62| # New .syn file format.
63| #
64| # Revision 1.21 2001/07/12 00:53:12 stefan
65| # ...wasn't meant to be left for the commit
66| #
67| # Revision 1.20 2001/07/11 01:45:02 stefan
68| # fix Dot and HTML formatters to adjust the references depending on the filename of the output
69| #
70| # Revision 1.19 2001/06/28 07:22:18 stefan
71| # more refactoring/cleanup in the HTML formatter
72| #
73| # Revision 1.18 2001/06/26 04:32:15 stefan
74| # A whole slew of changes mostly to fix the HTML formatter's output generation,
75| # i.e. to make the output more robust towards changes in the layout of files.
76| #
77| # the rpm script now works, i.e. it generates source and binary packages.
78| #
79| # Revision 1.17 2001/06/06 04:44:54 uid20151
80| # Prune names of each class to the base which is the most common of its parents.
81| # Makes for simpler graphs with short names :)
82| #
83| # Revision 1.16 2001/05/25 13:45:49 stefan
84| # fix problem with getopt error reporting
85| #
86| # Revision 1.15 2001/04/06 02:38:14 chalky
87| # Dont reset toc
88| #
89| # Revision 1.14 2001/04/06 01:40:19 chalky
90| # Fixed the unknown nodes bug! Wasn't clearing the nodes dict on each run!
91| #
92| # Revision 1.13 2001/03/19 07:53:45 chalky
93| # Small fixes.
94| #
95| # Revision 1.12 2001/02/13 06:55:23 chalky
96| # Made synopsis -l work again
97| #
98| # Revision 1.11 2001/02/06 16:54:15 chalky
99| # Added -n to Dot which stops those nested classes
100| #
101| # Revision 1.10 2001/02/06 15:00:42 stefan
102| # Dot.py now references the template, not the last parameter, when displaying a Parametrized; replaced logo with a simple link
103| #
104| # Revision 1.9 2001/02/05 05:25:08 chalky
105| # Added hspace and vspace in _format_html
106| #
107| # Revision 1.8 2001/02/02 17:42:50 stefan
108| # cleanup in the Makefiles, more work on the Dot formatter
109| #
110| # Revision 1.7 2001/02/02 02:01:01 stefan
111| # synopsis now supports inlined inheritance tree generation
112| #
113| # Revision 1.6 2001/02/01 04:04:23 stefan
114| # added support for html page generation
115| #
116| # Revision 1.5 2001/01/31 21:53:11 stefan
117| # some more work on the dot formatter
118| #
119| # Revision 1.4 2001/01/31 06:51:24 stefan
120| # add support for '-v' to all modules; modified toc lookup to use additional url as prefix
121| #
122| # Revision 1.3 2001/01/24 01:38:36 chalky
123| # Added docstrings to all modules
124| #
125| # Revision 1.2 2001/01/23 21:31:36 stefan
126| # bug fixes
127| #
128| # Revision 1.1 2001/01/23 19:50:42 stefan
129| # Dot: an inheritance/collaboration graph generator
130| #
131| #
132|
133| """
134| Uses 'dot' from graphviz to generate various graphs.
135| """
136| # THIS-IS-A-FORMATTER
137|
138| import sys, tempfile, getopt, os, os.path, string, types, errno, re
139| from Synopsis.Core import AST, Type, Util
140| from Synopsis.Formatter import TOC
141|
142| verbose = 0
143| toc = None
144| nodes = {}
145| name_prefix = None
146| direction = 'vertical'
147|
148| class SystemError:
149| """Error thrown by the system() function. Attributes are 'retval', encoded
150| as per os.wait(): low-byte is killing signal number, high-byte is return
151| value of command."""
152| def __init__(self, retval, command):
153| self.retval = retval
154| self.command = command
155| def __repr__(self):
156| return "SystemError: %(retval)x\"%(command)s\" failed."%self.__dict__
157|
158| def system(command):
159| """Run the command. If the command fails, an exception SystemError is
160| thrown."""
161| ret = os.system(command)
162| if (ret>>8) != 0:
163| raise SystemError(ret, command)
164|
165| class InheritanceFormatter(AST.Visitor, Type.Visitor):
166| """A Formatter that generates an inheritance graph"""
167| def __init__(self, os, operations, attributes):
168| self.__os = os
169| if operations: self.__operations = []
170| else: self.__operations = None
171| if attributes: self.__attributes = []
172| else: self.__attributes = None
173| self.__scope = []
174| if name_prefix: self.__scope = string.split(name_prefix, '::')
175| self.__type_ref = None
176| self.__type_label = ''
177| def scope(self): return self.__scope
178| def write(self, text): self.__os.write(text)
179| def type_ref(self): return self.__type_ref
180| def type_label(self): return self.__type_label
181| def parameter(self): return self.__parameter
182|
183| def formatType(self, typeObj):
184| "Returns a reference string for the given type object"
185| if typeObj is None: return "(unknown)"
186| typeObj.accept(self)
187| return self.type_label()
188|
189| def clearType(self):
190| self.__type_ref = None
191| self.__type_label = ''
192|
193| def writeNode(self, ref, name, label, **attr):
194| """helper method to generate output for a given node"""
195| if nodes.has_key(name): return
196| nodes[name] = len(nodes)
197| number = nodes[name]
198|
199| # Quote to remove characters that dot can't handle
200| label = re.sub('<',r'\<',label)
201| label = re.sub('>',r'\>',label)
202| label = re.sub('{',r'\{',label)
203| label = re.sub('}',r'\}',label)
204|
205| self.write("Node" + str(number) + " [shape=\"record\", label=\"" + label + "\"")
206| self.write(", fontSize = 10, height = 0.2, width = 0.4")
207| self.write(string.join(map(lambda item:', %s="%s"'%item, attr.items())))
208| if ref: self.write(", URL=\"" + ref + "\"")
209| self.write("];\n")
210|
211| def writeEdge(self, parent, child, label, **attr):
212| self.write("Node" + str(nodes[parent]) + " -> ")
213| self.write("Node" + str(nodes[child]))
214| self.write("[ color=\"black\", fontsize=10, dir=back, arrowtail=empty, " + string.join(map(lambda item:', %s="%s"'%item, attr.items())) + "];\n")
215|
216| def getClassName(self, node):
217| """Returns the name of the given class node, relative to all its
218| parents. This makes the graph simpler by making the names shorter"""
219| base = node.name()
220| for i in node.parents():
221| try:
222| parent = i.parent()
223| pname = parent.name()
224| for j in range(len(base)):
225| if j > len(pname) or pname[j] != base[j]:
226| # Base is longer than parent name, or found a difference
227| base[j:] = []
228| break
229| except: pass # typedefs etc may cause errors here.. ignore
230| if not node.parents():
231| base = self.scope()
232| return Util.ccolonName(node.name(), base)
233|
234| #################### Type Visitor ###########################################
235| def visitModifier(self, type):
236| self.formatType(type.alias())
237| self.__type_label = string.join(type.premod()) + self.__type_label
238| self.__type_label = self.__type_label + string.join(type.postmod())
239|
240| def visitUnknown(self, type):
241| self.__type_ref = toc[type.link()]
242| self.__type_label = Util.ccolonName(type.name(), self.scope())
243|
244| def visitBase(self, type):
245| self.__type_ref = None
246| self.__type_label = type.name()[-1]
247|
248| def visitDependent(self, type):
249| self.__type_ref = None
250| self.__type_label = type.name()[-1]
251|
252| def visitDeclared(self, type):
253| self.__type_ref = toc[type.declaration().name()]
254| if isinstance(type.declaration(), AST.Class):
255| self.__type_label = self.getClassName(type.declaration())
256| else:
257| self.__type_label = Util.ccolonName(type.declaration().name(), self.scope())
258|
259| def visitParametrized(self, type):
260| if type.template():
261| type_ref = toc[type.template().name()]
262| type_label = Util.ccolonName(type.template().name(), self.scope())
263| else:
264| type_ref = None
265| type_label = "(unknown)"
266| parameters_label = []
267| for p in type.parameters():
268| parameters_label.append(self.formatType(p))
269| self.__type_ref = type_ref
270| self.__type_label = type_label + "<" + string.join(parameters_label, ",") + ">"
271|
272| def visitTemplate(self, type):
273| self.__type_ref = None
274| def clip(x, max=20):
275| if len(x) > max: return '...'
276| return x
277| self.__type_label = "template<%s>"%(clip(string.join(map(clip, map(self.formatType, type.parameters())), ","),40))
278|
279| #################### AST Visitor ############################################
280|
281| def visitInheritance(self, node):
282| self.formatType(node.parent())
283| if self.type_ref():
284| self.writeNode(self.type_ref().link, self.type_label(), self.type_label())
285| else:
286| self.writeNode('', self.type_label(), self.type_label(), color='gray75', fontcolor='gray75')
287|
288| def visitClass(self, node):
289| if self.__operations is not None: self.__operations.append([])
290| if self.__attributes is not None: self.__attributes.append([])
291| name = self.getClassName(node)
292| ref = toc[node.name()]
293| for d in node.declarations(): d.accept(self)
294| # NB: old version of dot needed the label surrounded in {}'s (?)
295| label = name
296| if node.template():
297| if direction == 'vertical':
298| label = self.formatType(node.template()) + '\\n' + label
299| else:
300| label = self.formatType(node.template()) + ' ' + label
301| if self.__operations or self.__attributes:
302| label = label + '\\n'
303| if self.__operations:
304| label = label + '|' + string.join(map(lambda x:x[-1] + '()\\l', self.__operations[-1]))
305| if self.__attributes:
306| label = label + '|' + string.join(map(lambda x:x[-1] + '\\l', self.__attributes[-1]))
307| if ref:
308| self.writeNode(ref.link, name, label)
309| else:
310| self.writeNode('', name, label, color='gray75', fontcolor='gray75')
311| for inheritance in node.parents():
312| inheritance.accept(self)
313| self.writeEdge(self.type_label(), name, None)
314| if no_descend: return
315| if self.__operations: self.__operations = self.__operations[:-1]
316| if self.__attributes: self.__attributes = self.__attributes[:-1]
317|
318| def visitOperation(self, operation):
319| if self.__operations:
320| self.__operations[-1].append(operation.realname())
321|
322| def visitVariable(self, variable):
323| if self.__attributes:
324| self.__attributes[-1].append(variable.name())
325|
326| class SingleInheritanceFormatter(InheritanceFormatter):
327| """A Formatter that generates an inheritance graph for a specific class.
328| This Visitor visits the AST upwards, i.e. following the inheritance links, instead of
329| the declarations contained in a given scope."""
330| #base = InheritanceFormatter
331| def __init__(self, os, operations, attributes, levels, types):
332| InheritanceFormatter.__init__(self, os, operations, attributes)
333| self.__levels = levels
334| self.__types = types
335| self.__current = 1
336| self.__visited_classes = {} # classes already visited, to prevent recursion
337|
338| #################### Type Visitor ###########################################
339|
340| def visitDeclared(self, type):
341| if self.__current < self.__levels or self.__levels == -1:
342| self.__current = self.__current + 1
343| type.declaration().accept(self)
344| self.__current = self.__current - 1
345| # to restore the ref/label...
346| InheritanceFormatter.visitDeclared(self, type)
347| #################### AST Visitor ############################################
348|
349| def visitInheritance(self, node):
350| node.parent().accept(self)
351| if self.type_label():
352| if self.type_ref():
353| self.writeNode(self.type_ref().link, self.type_label(), self.type_label())
354| else:
355| self.writeNode('', self.type_label(), self.type_label(), color='gray75', fontcolor='gray75')
356|
357| def visitClass(self, node):
358| # Prevent recursion
359| if self.__visited_classes.has_key(id(node)): return
360| self.__visited_classes[id(node)] = None
361|
362| name = self.getClassName(node)
363| if self.__current == 1:
364| self.writeNode('', name, name, style='filled', color='lightgrey')
365| else:
366| ref = toc[node.name()]
367| if ref:
368| self.writeNode(ref.link, name, name)
369| else:
370| self.writeNode('', name, name, color='gray75', fontcolor='gray75')
371| for inheritance in node.parents():
372| inheritance.accept(self)
373| if nodes.has_key(self.type_label()):
374| self.writeEdge(self.type_label(), name, None)
375| # if this is the main class and if there is a type dictionary,
376| # look for classes that are derived from this class
377|
378| # if this is the main class
379| if self.__current == 1 and self.__types:
380| # fool the visitDeclared method to stop walking upwards
381| self.__levels = 0
382| for t in self.__types.values():
383| if isinstance(t, Type.Declared):
384| child = t.declaration()
385| if isinstance(child, AST.Class):
386| for inheritance in child.parents():
387| type = inheritance.parent()
388| type.accept(self)
389| if self.type_ref():
390| if self.type_ref().name == node.name():
391| child_label = self.getClassName(child)
392| ref = toc[child.name()]
393| if ref:
394| self.writeNode(ref.link, child_label, child_label)
395| else:
396| self.writeNode('', child_label, child_label, color='gray75', fontcolor='gray75')
397| self.writeEdge(name, child_label, None)
398|
399| class CollaborationFormatter(AST.Visitor, Type.Visitor):
400| """A Formatter that generates a collaboration graph"""
401| def __init__(self, output):
402| self.__output = output
403| def write(self, text): self.__output.write(text)
404| def visitClass(self, node):
405| name = node.name()
406| for inheritance in node.parents():
407| parent = inheritance.parent()
408| if hasattr(parent, 'name'):
409| self.write(parent.name()[-1] + " -> " + name[-1] + ";\n")
410| for d in node.declarations(): d.accept(self)
411| def visitVariable(self, node):
412| node.vtype().accept(self)
413| print node.name(), self.__type_label
414|
415| def visitBaseType(self, type):
416| self.__type_label = type.name()
417| def visitUnknown(self, type):
418| self.__type_label = type.name()
419| def visitDeclared(self, type):
420| self.__type_label = type.name()
421| def visitModifier(self, type):
422| alias = type.alias()
423| pre = string.join(map(lambda x:x+" ", type.premod()), '')
424| post = string.join(type.postmod(), '')
425| self.__type_label = "%s%s%s"%(pre,alias,post)
426| def visitTemplate(self, type):
427| self.__type_label = "<template>"
428| def visitParametrized(self, type):
429| self.__type_label = "<parametrized>"
430| def visitFunctionType(self, type):
431| self.__type_label = "<function>"
432|
433|
434| def usage():
435| """Print usage to stdout"""
436| print \
437| """
438| -o <filename> Output directory, created if it doesn't exist.
439| -t <title> Associate <title> with the generated graph
440| -i Generate an inheritance graph
441| -s Generate an inheritance graph for a single class
442| -O show operations
443| -A show attributs
444| -c Generate a collaboration graph
445| -f <format> Generate output in format 'dot', 'ps' (default), 'png', 'gif', 'map', 'html'
446| -r <filename> Read in toc for an external data base that is to be referenced (for map/html output)
447| -R <filename> Provide a reference URL which is used to compute relative links from the toc entries (for map/html output)
448| -n Don't descend AST (for internal use)
449| -p <prefix> Specify a prefix to strip from all class names
450| -d <direction> Direction of graph: 'vertical' or 'horizontal'"""
451|
452| formats = {
453| 'dot' : 'dot',
454| 'ps' : 'ps',
455| 'png' : 'png',
456| 'gif' : 'gif',
457| 'map' : 'imap',
458| 'html' : 'html',
459| }
460|
461| def __parseArgs(args, config_obj):
462| global output, title, type, operations, attributes, oformat, verbose
463| global toc_in, origin, no_descend, nodes, name_prefix, direction
464| output = ''
465| title = 'NoTitle'
466| type = ''
467| operations = 0
468| attributes = 0
469| oformat = 'ps'
470| toc_in = []
471| origin = ''
472| no_descend = 0
473| nodes = {}
474| name_prefix = None
475| direction = 'vertical'
476| try:
477| opts,remainder = Util.getopt_spec(args, "o:t:OAf:r:R:p:d:icsvn")
478| except Util.getopt.error, e:
479| sys.stderr.write("Error in arguments: " + str(e) + "\n")
480| sys.exit(1)
481|
482| for opt in opts:
483| o,a = opt
484| if o == "-o": output = a
485| elif o == "-t": title = a
486| elif o == "-i": type = "inheritance"
487| elif o == "-s": type = "single"
488| elif o == "-O": operations = 1
489| elif o == "-A": attributes = 1
490| elif o == "-c":
491| type = "collaboration"
492| sys.stderr.write("sorry, collaboration diagrams not yet implemented\n");
493| sys.exit(-1)
494| elif o == "-f":
495| if formats.has_key(a): oformat = formats[a]
496| else:
497| print "Error: Unknown format. Available formats are:",string.join(formats.keys(), ', ')
498| sys.exit(1)
499| elif o == "-r": toc_in.append(a)
500| elif o == "-R": origin = a
501| elif o == "-v": verbose = 1
502| elif o == "-n": no_descend = 1
503| elif o == "-p": name_prefix = a
504| elif o == "-d": direction = a
505|
506| def _rel(frm, to):
507| "Find link to to relative to frm"
508| frm = string.split(frm, '/'); to = string.split(to, '/')
509| for l in range((len(frm)<len(to)) and len(frm)-1 or len(to)-1):
510| if to[0] == frm[0]: del to[0]; del frm[0]
511| else: break
512| if frm: to = ['..'] * (len(frm) - 1) + to
513| return string.join(to,'/')
514|
515| def _convert_map(input, output):
516| """convert map generated from Dot to a html region map.
517| input and output are (open) streams"""
518| line = input.readline()
519| while line:
520| line = line[:-1]
521| if line[0:4] == "rect":
522| url, x1y1, x2y2 = string.split(line[4:])
523| x1, y1 = string.split(x1y1, ",")
524| x2, y2 = string.split(x2y2, ",")
525| output.write('<area alt="'+url+'" href="' + _rel(origin, url) + '" shape="rect" coords="')
526| output.write(str(x1) + ", " + str(y1) + ", " + str(x2) + ", " + str(y2) + '">\n')
527| line = input.readline()
528|
529| def _format(input, output, format):
530| command = 'dot -T%s -o "%s" "%s"'%(format, output, input)
531| if verbose: print "Dot Formatter: running command '" + command + "'"
532| system(command)
533|
534| def _format_png(input, output):
535| _format(input, output, "png")
536|
537| def _format_html(input, output):
538| """generate (active) image for html.
539| input and output are file names. If output ends
540| in '.html', its stem is used with an '.png' suffix for the
541| actual image."""
542| if output[-5:] == ".html": output = output[:-5]
543| _format_png(input, output + ".png")
544| _format(input, output + ".map", "imap")
545| prefix, name = os.path.split(output)
546| reference = name + ".png"
547| html = Util.open(output + ".html")
548| html.write('<img alt="'+name+'" src="' + reference + '" hspace="8" vspace="8" border="0" usemap="#')
549| html.write(name + "_map\">\n")
550| html.write("<map name=\"" + name + "_map\">")
551| dotmap = open(output + ".map", "r+")
552| _convert_map(dotmap, html)
553| dotmap.close()
554| os.remove(output + ".map")
555| html.write("</map>\n")
556|
557| def format(args, ast, config_obj):
558| global output, title, type, operations, attributes, oformat, verbose, toc, toc_in
559| __parseArgs(args, config_obj)
560|
561| # Create table of contents index
562| if not toc: toc = TOC.TableOfContents(TOC.Linker())
563| for t in toc_in: toc.load(t)
564|
565| head, tail = os.path.split(output)
566| tmpfile = os.path.join(head, Util.quote(tail)) + ".dot"
567| if verbose: print "Dot Formatter: Writing dot file..."
568| dotfile = Util.open(tmpfile)
569| dotfile.write("digraph \"%s\" {\n"%(title))
570| if direction == 'horizontal':
571| dotfile.write('rankdir="LR";\n')
572| dotfile.write('ranksep="1.0";\n')
573| dotfile.write("node[shape=record, fontsize=10, height=0.2, width=0.4, color=black]\n")
574| if type == "inheritance":
575| generator = InheritanceFormatter(dotfile, operations, attributes)
576| elif type == "single":
577| generator = SingleInheritanceFormatter(dotfile, operations, attributes, -1, ast.types())
578| else:
579| generator = CollaborationFormatter(dotfile)
580| for d in ast.declarations():
581| d.accept(generator)
582| dotfile.write("}\n")
583| dotfile.close()
584| if oformat == "dot":
585| os.rename(tmpfile, output)
586| elif oformat == "png":
587| _format_png(tmpfile, output)
588| #os.remove(tmpfile)
589| elif oformat == "html":
590| _format_html(tmpfile, output)
591| #os.remove(tmpfile)
592| else:
593| _format(tmpfile, output, oformat)
594| os.remove(tmpfile)