doc/commands: introduce graph commands
[nit.git] / src / doc / commands / commands_graph.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Graph commands
16 #
17 # Commands that return graphical representations about a Model or a MEntity.
18 module commands_graph
19
20 import commands_model
21
22 import uml
23 import dot
24
25 # An abstract command that returns a dot graph
26 abstract class CmdGraph
27 super DocCommand
28
29 # Rendering format
30 #
31 # Default is `dot`.
32 # See `allowed_formats`.
33 var format = "dot" is optional, writable
34
35 # Allowed rendering formats.
36 #
37 # Can be `dot` or `svg`.
38 var allowed_formats: Array[String] = ["dot", "svg"]
39
40 # Dot to render
41 var dot: nullable Writable = null is optional, writable
42
43 # Render `dot` depending on `format`
44 fun render: nullable Writable do
45 var dot = self.dot
46 if dot == null then return null
47 if format == "svg" then
48 var proc = new ProcessDuplex("dot", "-Tsvg")
49 var svg = proc.write_and_read(dot.write_to_string)
50 proc.close
51 proc.wait
52 return svg
53 end
54 return dot
55 end
56
57 redef fun init_command do
58 if not allowed_formats.has(format) then
59 return new ErrorBadGraphFormat(format, allowed_formats)
60 end
61 return super
62 end
63 end
64
65 # Bad graph format requested
66 class ErrorBadGraphFormat
67 super CmdError
68
69 # Provided format
70 var format: String
71
72 # Allowed formats
73 var allowed_formats: Array[String]
74
75 redef fun to_s do
76 var allowed_values = new Buffer
77 for allowed in allowed_formats do
78 allowed_values.append "`{allowed}`"
79 if allowed != allowed_formats.last then
80 allowed_values.append ", "
81 end
82 end
83 return "Bad format `{format}`. Allowed values are {allowed_values.write_to_string}."
84 end
85 end
86
87 # UML command
88 #
89 # Return an UML diagram about a `mentity`.
90 class CmdUML
91 super CmdEntity
92 super CmdGraph
93
94 autoinit(view, mentity, mentity_name, format, uml)
95
96 # UML model to return
97 var uml: nullable UMLModel = null is optional, writable
98
99 redef fun init_command do
100 if uml != null then return new CmdSuccess
101
102 var res = super
103 if not res isa CmdSuccess then return res
104 var mentity = self.mentity.as(not null)
105
106 if mentity isa MClassDef then mentity = mentity.mclass
107 if mentity isa MClass then
108 uml = new UMLModel(view, view.mainmodule)
109 else if mentity isa MModule then
110 uml = new UMLModel(view, view.mainmodule)
111 else
112 return new WarningNoUML(mentity)
113 end
114 return res
115 end
116
117 redef fun render do
118 var uml = self.uml
119 if uml == null then return null
120 if mentity isa MClass then
121 dot = uml.generate_class_uml.write_to_string
122 else if mentity isa MModule then
123 dot = uml.generate_package_uml.write_to_string
124 end
125 return super
126 end
127 end
128
129 # No UML model for `mentity`
130 class WarningNoUML
131 super CmdWarning
132
133 # MEntity provided
134 var mentity: MEntity
135
136 redef fun to_s do return "No UML for `{mentity.full_name}`"
137 end
138
139 # Render a hierarchy graph for `mentity` if any.
140 class CmdInheritanceGraph
141 super CmdEntity
142 super CmdGraph
143
144 autoinit(view, mentity, mentity_name, pdepth, cdepth, format, graph)
145
146 # Parents depth to display
147 var pdepth: nullable Int = null is optional, writable
148
149 # Children depth to display
150 var cdepth: nullable Int = null is optional, writable
151
152 # Inheritance graph to return
153 var graph: nullable InheritanceGraph = null is optional, writable
154
155 redef fun init_command do
156 if graph != null then return new CmdSuccess
157
158 var res = super
159 if not res isa CmdSuccess then return res
160 var mentity = self.mentity.as(not null)
161
162 graph = new InheritanceGraph(mentity, view)
163 return res
164 end
165
166 redef fun render do
167 var graph = self.graph
168 if graph == null then return ""
169 self.dot = graph.draw(pdepth, cdepth).to_dot
170 return super
171 end
172 end
173
174 # Graph for mentity hierarchies
175 #
176 # Recursively build parents and children list from a `center`.
177 class InheritanceGraph
178
179 # MEntity at the center of this graph
180 var center: MEntity
181
182 # ModelView used to filter graph
183 var view: ModelView
184
185 # Graph generated
186 var graph: DotGraph is lazy do
187 var graph = new DotGraph("package_diagram", "digraph")
188
189 graph["compound"] = "true"
190 graph["rankdir"] = "BT"
191 graph["ranksep"] = 0.3
192 graph["nodesep"] = 0.3
193
194 graph.nodes_attrs["margin"] = 0.1
195 graph.nodes_attrs["width"] = 0
196 graph.nodes_attrs["height"] = 0
197 graph.nodes_attrs["fontsize"] = 10
198 graph.nodes_attrs["fontname"] = "helvetica"
199
200 graph.edges_attrs["dir"] = "none"
201 graph.edges_attrs["color"] = "gray"
202
203 return graph
204 end
205
206 # Build the graph
207 fun draw(parents_depth, children_depth: nullable Int): DotGraph do
208 draw_node center
209 draw_parents(center, parents_depth)
210 draw_children(center, children_depth)
211 return graph
212 end
213
214 private var nodes = new HashMap[MEntity, DotElement]
215 private var done_parents = new HashSet[MEntity]
216 private var done_children = new HashSet[MEntity]
217
218 # Recursively draw parents of mentity
219 fun draw_parents(mentity: MEntity, max_depth: nullable Int, current_depth: nullable Int) do
220 if done_parents.has(mentity) then return
221 done_parents.add mentity
222 current_depth = current_depth or else 0
223 if max_depth != null and current_depth >= max_depth then
224 from_dotdotdot(mentity)
225 return
226 end
227 var parents = mentity.collect_parents(view)
228 if parents.length > 10 then
229 from_dotdotdot(mentity)
230 return
231 end
232 for parent in parents do
233 if parent isa MModule then
234 var mgroup = parent.mgroup
235 if mgroup != null and mgroup.default_mmodule == parent then parent = mgroup
236 end
237 if parent isa MGroup then
238 if parent.mpackage.mgroups.first == parent then parent = parent.mpackage
239 end
240 draw_edge(mentity, parent)
241 end
242 for parent in parents do
243 if parent isa MModule then
244 var mgroup = parent.mgroup
245 if mgroup != null and mgroup.default_mmodule == parent then parent = mgroup
246 end
247 if parent isa MGroup then
248 if parent.mpackage.mgroups.first == parent then parent = parent.mpackage
249 end
250 draw_parents(parent, max_depth, current_depth + 1)
251 end
252 end
253
254 # Recursively draw children of mentity
255 fun draw_children(mentity: MEntity, max_depth: nullable Int, current_depth: nullable Int) do
256 if done_children.has(mentity) then return
257 done_children.add mentity
258 current_depth = current_depth or else 0
259 if max_depth != null and current_depth >= max_depth then
260 to_dotdotdot(mentity)
261 return
262 end
263 var children = mentity.collect_children(view)
264 if children.length > 10 then
265 to_dotdotdot(mentity)
266 return
267 end
268 for child in children do
269 if child isa MGroup then
270 if child.mpackage.mgroups.first == child then child = child.mpackage
271 end
272 draw_edge(child, mentity)
273 end
274 for child in children do
275 if child isa MGroup then
276 if child.mpackage.mgroups.first == child then child = child.mpackage
277 end
278 draw_children(child, max_depth, current_depth + 1)
279 end
280 end
281
282 # Draw a node from a `mentity`
283 fun draw_node(mentity: MEntity): DotElement do
284 if nodes.has_key(mentity) then return nodes[mentity]
285 var node: DotElement = mentity.to_dot_node
286 if mentity == center then node = highlight(node)
287 nodes[mentity] = node
288 graph.add node
289 return node
290 end
291
292 private var edges = new HashMap2[MEntity, MEntity, DotEdge]
293
294 # Draw a edges between two mentities
295 fun draw_edge(from, to: MEntity): DotEdge do
296 if edges.has(from, to) then return edges[from, to].as(not null)
297 if edges.has(to, from) then return edges[to, from].as(not null)
298 var nfrom = draw_node(from)
299 var nto = draw_node(to)
300 var edge = new DotEdge(nfrom, nto)
301 edges[from, to] = edge
302 graph.add edge
303 return edge
304 end
305
306 private var to_dots = new HashMap[MEntity, DotElement]
307
308 # Create a link from `mentity` to a `...` node
309 fun to_dotdotdot(mentity: MEntity): DotEdge do
310 var nto = draw_node(mentity)
311 var dots = to_dots.get_or_null(mentity)
312 if dots == null then
313 dots = dotdotdot("{nto.id}...")
314 to_dots[mentity] = dots
315 end
316 graph.add dots
317 var edge = new DotEdge(dots, nto)
318 graph.add edge
319 return edge
320 end
321
322 private var from_dots = new HashMap[MEntity, DotElement]
323
324 # Create a link from a `...` node to a `mentity`
325 fun from_dotdotdot(mentity: MEntity): DotEdge do
326 var nfrom = draw_node(mentity)
327 var dots = to_dots.get_or_null(mentity)
328 if dots == null then
329 dots = dotdotdot("...{nfrom.id}")
330 from_dots[mentity] = dots
331 end
332 graph.add dots
333 var edge = new DotEdge(dots, nfrom)
334 graph.add edge
335 return edge
336 end
337
338 # Change the border color of the node
339 fun highlight(dot: DotElement): DotElement do
340 dot["color"] = "#1E9431"
341 return dot
342 end
343
344 # Generate a `...` node
345 fun dotdotdot(id: String): DotNode do
346 var node = new DotNode(id)
347 node["label"] = "..."
348 node["shape"] = "none"
349 return node
350 end
351 end
352
353 redef class MEntity
354 # Return `self` as a DotNode
355 fun to_dot_node: DotNode do
356 var node = new DotNode(full_name)
357 node["label"] = name
358 return node
359 end
360 end
361
362 redef class MPackage
363 redef fun to_dot_node do
364 var node = super
365 node["shape"] = "tab"
366 return node
367 end
368 end
369
370 redef class MGroup
371 redef fun to_dot_node do
372 var node = super
373 node["shape"] = "folder"
374 return node
375 end
376 end
377
378 redef class MModule
379 redef fun to_dot_node do
380 var node = super
381 node["shape"] = "note"
382 return node
383 end
384 end
385
386 redef class MClass
387 redef fun to_dot_node do
388 var node = super
389 node["shape"] = "box"
390 return node
391 end
392 end