modelviz: filter out modules without groups to avoid NPE error
[nit.git] / src / model_viz.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 # Visualisation of Nit models
16 module model_viz
17
18 import model
19 import ordered_tree
20
21 # A simple specialisation of OrderedTree to display projects, groups and modules
22 # FIXME do not use Object, but a better common interface of MModule and MGroup
23 class MProjectTree
24 super OrderedTree[Object]
25
26 # The model where to look for information
27 var model: Model
28
29 init(model: Model) do self.model = model
30
31 redef fun display(a) do
32 if a isa MGroup then
33 if a.parent == null then return "{a.mproject.name} ({a.filepath})"
34 return a.name + " (group)"
35 else if a isa MModule then
36 return a.name
37 else
38 abort
39 end
40 end
41
42 var alpha_comparator = new AlphaComparator
43
44 var linex_comparator: nullable LinexComparator = null
45
46 # Sort modules and groups with their names
47 fun sort_with_alpha
48 do
49 sort_with(alpha_comparator)
50 end
51
52 # Sort modules and groups with a loosly adaptation of the linerarisation of modules
53 fun sort_with_linex
54 do
55 var c = linex_comparator
56 if c == null then
57 c = new LinexComparator(self)
58 linex_comparator = c
59 end
60 sort_with(c)
61 end
62 end
63
64 # Just compare objects by using the `to_s` method
65 private class AlphaComparator
66 super AbstractSorter[Object]
67 redef fun compare(a,b) do return a.to_s <=> b.to_s
68 end
69
70 # Compare modules and groups using the
71 # FIXME do not use Object, but a better common interface of MModule and MGroup
72 private class LinexComparator
73 super AbstractSorter[Object]
74 var mins = new HashMap [MGroup, nullable MModule]
75 var maxs = new HashMap [MGroup, nullable MModule]
76 fun min(o: Object): nullable MModule do
77 if o isa MModule then return o
78 assert o isa MGroup
79 if not mins.has_key(o) then computeminmax(o)
80 return mins[o]
81 end
82 fun max(o: Object): nullable MModule do
83 if o isa MModule then return o
84 assert o isa MGroup
85 if not maxs.has_key(o) then computeminmax(o)
86 return maxs[o]
87 end
88 fun computeminmax(o: MGroup) do
89 if not tree.sub.has_key(o) then
90 mins[o] = null
91 maxs[o] = null
92 return
93 end
94 var subs = tree.sub[o]
95 var minres = min(subs.first)
96 var maxres = max(subs.first)
97 var order = minres.model.mmodule_importation_hierarchy
98 for o2 in subs do
99 var c = min(o2)
100 if c == null then continue
101 if minres == null or order.compare(minres, c) > 0 then minres = c
102 c = max(o2)
103 if c == null then continue
104 if maxres == null or order.compare(maxres, c) < 0 then maxres = c
105 end
106 mins[o] = minres
107 maxs[o] = maxres
108 #if minres != maxres then print "* {o} {minres}..{maxres}"
109 end
110 redef fun compare(a,b) do
111 var ma = min(a)
112 var mb = min(b)
113 if ma == null then
114 if mb == null then return 0 else return -1
115 else if mb == null then
116 return 1
117 end
118 var order = ma.model.mmodule_importation_hierarchy
119 return order.compare(ma, mb)
120 end
121 var tree: OrderedTree[Object]
122 end
123
124 redef class Model
125 # Generate a MProjectTree based on the projects, groups and modules known in the model
126 fun to_mproject_tree: MProjectTree
127 do
128 var res = new MProjectTree(self)
129 for p in mprojects do
130 for g in p.mgroups do
131 res.add(g.parent, g)
132 for m in g.mmodules do
133 res.add(g, m)
134 end
135 end
136 end
137 return res
138 end
139 end
140
141 # Generate graphiz files based on projects, groups and modules
142 #
143 # Interessing elements must be selected. See `mmodules`, ``
144 # Display configuration can be set. See `cluster_group`, `project_group`
145 class MProjectDot
146 # The model where to look for information
147 var model: Model
148
149 # Set of projects to expand fully (ie all groups and modules are displayed)
150 # Initially empty, projects can be added
151 var mprojects = new HashSet[MProject]
152
153 # Set of modules to display
154 # Initially empty, modules can be added
155 var mmodules = new HashSet[MModule]
156
157 private fun node_for(mmodule: MModule): nullable String
158 do
159 return "m_{mmodule.object_id}"
160 end
161
162 # Should groups be shown as clusters?
163 var cluster_group writable = true
164
165 # Should projects be shown as clusters?
166 var project_group writable = true
167
168 # Recursively generate noed ans clusters for a mroup
169 private fun dot_cluster(o: OStream, mgroup: MGroup)
170 do
171 # Open the cluster, if required
172 if mgroup.parent == null then
173 # is is a root group, so display the project
174 if project_group then
175 o.write("subgraph cluster_{mgroup.object_id} \{\nlabel=\"{mgroup.mproject.name}\\n({mgroup.filepath})\"\ncolor=black\nstyle=dotted\n")
176 end
177 else
178 if cluster_group then
179 o.write("subgraph cluster_{mgroup.object_id} \{\nlabel=\"{mgroup.name}\"\ncolor=blue\nstyle=dotted\n")
180 end
181 end
182
183 # outputs the mmodules to show
184 for mmodule in mgroup.mmodules do
185 if not mmodules.has(mmodule) then continue
186 o.write("\t{node_for(mmodule)} [label=\"{mmodule.name}\",color=green]\n")
187 end
188
189 # recusively progress on sub-clusters
190 for d in mgroup.in_nesting.direct_smallers do
191 dot_cluster(o, d)
192 end
193
194 # close the cluster if required
195 if mgroup.parent == null then
196 if project_group then o.write("\}\n")
197 else
198 if cluster_group then o.write("\}\n")
199 end
200 end
201
202 # Extends the set of `mmodules` by recurdively adding the most specific imported modules of foreign projects
203 fun collect_important_importation
204 do
205 var todo = new List[MModule]
206 todo.add_all(mmodules)
207 while not todo.is_empty do
208 var m = todo.pop
209
210 for psm in m.in_importation.greaters do
211 if m.mgroup.mproject != psm.mgroup.mproject then continue
212 for ssm in psm.in_importation.direct_greaters do
213 if psm.mgroup.mproject == ssm.mgroup.mproject then continue
214 mmodules.add(ssm)
215 todo.add(ssm)
216 end
217 end
218 end
219 end
220
221 # Generate the dot-file named `filepath` with the current configuration
222 fun render(filepath: String)
223 do
224 # Collect interessing nodes
225 for m in model.mmodules do
226 # filter out modules outside wanted projects
227 if m.mgroup == null then continue
228 if not mprojects.has(m.mgroup.mproject) then continue
229
230 mmodules.add(m)
231 end
232
233 collect_important_importation
234
235 # Collect interessing edges
236 var sub_hierarchy = new POSet[MModule]
237 for m in mmodules do
238 sub_hierarchy.add_node(m)
239 for sm in m.in_importation.greaters do
240 if sm == m then continue
241 if not mmodules.has(sm) then continue
242 sub_hierarchy.add_edge(m, sm)
243 end
244 end
245
246 print "generating {filepath}"
247 var dot = new OFStream.open(filepath)
248 dot.write("digraph g \{\n")
249 dot.write("rankdir=BT;node[shape=box];\n")
250 # Generate the nodes
251 for p in model.mprojects do
252 dot_cluster(dot, p.root.as(not null))
253 end
254 # Generate the edges
255 for m in mmodules do
256 for sm in sub_hierarchy[m].direct_greaters do
257 var nm = node_for(m)
258 var nsm = node_for(sm)
259 if m.in_importation.direct_greaters.has(sm) then
260 dot.write("\t{nm} -> {nsm}[style=bold]\n")
261 else
262 dot.write("\t{nm} -> {nsm}[style=solid]\n")
263 end
264 end
265 end
266 dot.write("\}\n")
267 dot.close
268 # sys.system("xdot -f dot {filepath}")
269 end
270 end