Synopsis - Cross-Reference
File: Synopsis/Formatters/Dot.py1# 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
Generated on Tue May 13 02:39:48 2008 by
synopsis (version 0.10)
synopsis (version 0.10)