vcg_printer.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # Copyright (c) 2015-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
  2. # Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
  3. # Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
  4. # Copyright (c) 2020-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  5. # Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com>
  6. # Copyright (c) 2020 Ram Rachum <ram@rachum.com>
  7. # Copyright (c) 2020 谭九鼎 <109224573@qq.com>
  8. # Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
  9. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  10. # Copyright (c) 2021 Andreas Finkler <andi.finkler@gmail.com>
  11. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  12. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  13. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  14. """Functions to generate files readable with Georg Sander's vcg
  15. (Visualization of Compiler Graphs).
  16. You can download vcg at https://rw4.cs.uni-sb.de/~sander/html/gshome.html
  17. Note that vcg exists as a debian package.
  18. See vcg's documentation for explanation about the different values that
  19. maybe used for the functions parameters.
  20. """
  21. from typing import Any, Dict, Mapping, Optional
  22. from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer
  23. ATTRS_VAL = {
  24. "algos": (
  25. "dfs",
  26. "tree",
  27. "minbackward",
  28. "left_to_right",
  29. "right_to_left",
  30. "top_to_bottom",
  31. "bottom_to_top",
  32. "maxdepth",
  33. "maxdepthslow",
  34. "mindepth",
  35. "mindepthslow",
  36. "mindegree",
  37. "minindegree",
  38. "minoutdegree",
  39. "maxdegree",
  40. "maxindegree",
  41. "maxoutdegree",
  42. ),
  43. "booleans": ("yes", "no"),
  44. "colors": (
  45. "black",
  46. "white",
  47. "blue",
  48. "red",
  49. "green",
  50. "yellow",
  51. "magenta",
  52. "lightgrey",
  53. "cyan",
  54. "darkgrey",
  55. "darkblue",
  56. "darkred",
  57. "darkgreen",
  58. "darkyellow",
  59. "darkmagenta",
  60. "darkcyan",
  61. "gold",
  62. "lightblue",
  63. "lightred",
  64. "lightgreen",
  65. "lightyellow",
  66. "lightmagenta",
  67. "lightcyan",
  68. "lilac",
  69. "turquoise",
  70. "aquamarine",
  71. "khaki",
  72. "purple",
  73. "yellowgreen",
  74. "pink",
  75. "orange",
  76. "orchid",
  77. ),
  78. "shapes": ("box", "ellipse", "rhomb", "triangle"),
  79. "textmodes": ("center", "left_justify", "right_justify"),
  80. "arrowstyles": ("solid", "line", "none"),
  81. "linestyles": ("continuous", "dashed", "dotted", "invisible"),
  82. }
  83. # meaning of possible values:
  84. # O -> string
  85. # 1 -> int
  86. # list -> value in list
  87. GRAPH_ATTRS = {
  88. "title": 0,
  89. "label": 0,
  90. "color": ATTRS_VAL["colors"],
  91. "textcolor": ATTRS_VAL["colors"],
  92. "bordercolor": ATTRS_VAL["colors"],
  93. "width": 1,
  94. "height": 1,
  95. "borderwidth": 1,
  96. "textmode": ATTRS_VAL["textmodes"],
  97. "shape": ATTRS_VAL["shapes"],
  98. "shrink": 1,
  99. "stretch": 1,
  100. "orientation": ATTRS_VAL["algos"],
  101. "vertical_order": 1,
  102. "horizontal_order": 1,
  103. "xspace": 1,
  104. "yspace": 1,
  105. "layoutalgorithm": ATTRS_VAL["algos"],
  106. "late_edge_labels": ATTRS_VAL["booleans"],
  107. "display_edge_labels": ATTRS_VAL["booleans"],
  108. "dirty_edge_labels": ATTRS_VAL["booleans"],
  109. "finetuning": ATTRS_VAL["booleans"],
  110. "manhattan_edges": ATTRS_VAL["booleans"],
  111. "smanhattan_edges": ATTRS_VAL["booleans"],
  112. "port_sharing": ATTRS_VAL["booleans"],
  113. "edges": ATTRS_VAL["booleans"],
  114. "nodes": ATTRS_VAL["booleans"],
  115. "splines": ATTRS_VAL["booleans"],
  116. }
  117. NODE_ATTRS = {
  118. "title": 0,
  119. "label": 0,
  120. "color": ATTRS_VAL["colors"],
  121. "textcolor": ATTRS_VAL["colors"],
  122. "bordercolor": ATTRS_VAL["colors"],
  123. "width": 1,
  124. "height": 1,
  125. "borderwidth": 1,
  126. "textmode": ATTRS_VAL["textmodes"],
  127. "shape": ATTRS_VAL["shapes"],
  128. "shrink": 1,
  129. "stretch": 1,
  130. "vertical_order": 1,
  131. "horizontal_order": 1,
  132. }
  133. EDGE_ATTRS = {
  134. "sourcename": 0,
  135. "targetname": 0,
  136. "label": 0,
  137. "linestyle": ATTRS_VAL["linestyles"],
  138. "class": 1,
  139. "thickness": 0,
  140. "color": ATTRS_VAL["colors"],
  141. "textcolor": ATTRS_VAL["colors"],
  142. "arrowcolor": ATTRS_VAL["colors"],
  143. "backarrowcolor": ATTRS_VAL["colors"],
  144. "arrowsize": 1,
  145. "backarrowsize": 1,
  146. "arrowstyle": ATTRS_VAL["arrowstyles"],
  147. "backarrowstyle": ATTRS_VAL["arrowstyles"],
  148. "textmode": ATTRS_VAL["textmodes"],
  149. "priority": 1,
  150. "anchor": 1,
  151. "horizontal_order": 1,
  152. }
  153. SHAPES: Dict[NodeType, str] = {
  154. NodeType.PACKAGE: "box",
  155. NodeType.CLASS: "box",
  156. NodeType.INTERFACE: "ellipse",
  157. }
  158. ARROWS: Dict[EdgeType, Dict] = {
  159. EdgeType.USES: dict(arrowstyle="solid", backarrowstyle="none", backarrowsize=0),
  160. EdgeType.INHERITS: dict(
  161. arrowstyle="solid", backarrowstyle="none", backarrowsize=10
  162. ),
  163. EdgeType.IMPLEMENTS: dict(
  164. arrowstyle="solid",
  165. backarrowstyle="none",
  166. linestyle="dotted",
  167. backarrowsize=10,
  168. ),
  169. EdgeType.ASSOCIATION: dict(
  170. arrowstyle="solid", backarrowstyle="none", textcolor="green"
  171. ),
  172. }
  173. ORIENTATION: Dict[Layout, str] = {
  174. Layout.LEFT_TO_RIGHT: "left_to_right",
  175. Layout.RIGHT_TO_LEFT: "right_to_left",
  176. Layout.TOP_TO_BOTTOM: "top_to_bottom",
  177. Layout.BOTTOM_TO_TOP: "bottom_to_top",
  178. }
  179. # Misc utilities ###############################################################
  180. class VCGPrinter(Printer):
  181. def _open_graph(self) -> None:
  182. """Emit the header lines"""
  183. self.emit("graph:{\n")
  184. self._inc_indent()
  185. self._write_attributes(
  186. GRAPH_ATTRS,
  187. title=self.title,
  188. layoutalgorithm="dfs",
  189. late_edge_labels="yes",
  190. port_sharing="no",
  191. manhattan_edges="yes",
  192. )
  193. if self.layout:
  194. self._write_attributes(GRAPH_ATTRS, orientation=ORIENTATION[self.layout])
  195. def _close_graph(self) -> None:
  196. """Emit the lines needed to properly close the graph."""
  197. self._dec_indent()
  198. self.emit("}")
  199. def emit_node(
  200. self,
  201. name: str,
  202. type_: NodeType,
  203. properties: Optional[NodeProperties] = None,
  204. ) -> None:
  205. """Create a new node. Nodes can be classes, packages, participants etc."""
  206. if properties is None:
  207. properties = NodeProperties(label=name)
  208. elif properties.label is None:
  209. properties.label = name
  210. self.emit(f'node: {{title:"{name}"', force_newline=False)
  211. self._write_attributes(
  212. NODE_ATTRS,
  213. label=self._build_label_for_node(properties),
  214. shape=SHAPES[type_],
  215. )
  216. self.emit("}")
  217. @staticmethod
  218. def _build_label_for_node(properties: NodeProperties) -> str:
  219. fontcolor = "\f09" if properties.fontcolor == "red" else ""
  220. label = fr"\fb{fontcolor}{properties.label}\fn"
  221. if properties.attrs is None and properties.methods is None:
  222. # return a compact form which only displays the classname in a box
  223. return label
  224. attrs = properties.attrs or []
  225. methods = properties.methods or []
  226. method_names = [func.name for func in methods]
  227. # box width for UML like diagram
  228. maxlen = max(len(name) for name in [properties.label] + method_names + attrs)
  229. line = "_" * (maxlen + 2)
  230. label = fr"{label}\n\f{line}"
  231. for attr in attrs:
  232. label = fr"{label}\n\f08{attr}"
  233. if attrs:
  234. label = fr"{label}\n\f{line}"
  235. for func in method_names:
  236. label = fr"{label}\n\f10{func}()"
  237. return label
  238. def emit_edge(
  239. self,
  240. from_node: str,
  241. to_node: str,
  242. type_: EdgeType,
  243. label: Optional[str] = None,
  244. ) -> None:
  245. """Create an edge from one node to another to display relationships."""
  246. self.emit(
  247. f'edge: {{sourcename:"{from_node}" targetname:"{to_node}"',
  248. force_newline=False,
  249. )
  250. attributes = ARROWS[type_]
  251. if label:
  252. attributes["label"] = label
  253. self._write_attributes(
  254. EDGE_ATTRS,
  255. **attributes,
  256. )
  257. self.emit("}")
  258. def _write_attributes(self, attributes_dict: Mapping[str, Any], **args) -> None:
  259. """write graph, node or edge attributes"""
  260. for key, value in args.items():
  261. try:
  262. _type = attributes_dict[key]
  263. except KeyError as e:
  264. raise Exception(
  265. f"no such attribute {key}\npossible attributes are {attributes_dict.keys()}"
  266. ) from e
  267. if not _type:
  268. self.emit(f'{key}:"{value}"\n')
  269. elif _type == 1:
  270. self.emit(f"{key}:{int(value)}\n")
  271. elif value in _type:
  272. self.emit(f"{key}:{value}\n")
  273. else:
  274. raise Exception(
  275. f"value {value} isn't correct for attribute {key} correct values are {type}"
  276. )