nitmetrics: use model filters
[nit.git] / src / metrics / codesmells_metrics.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 # Detect the code smells and antipatterns in the code.
15
16 module codesmells_metrics
17
18 import frontend
19 import nitsmell_toolcontext
20 import method_analyze_metrics
21 import mclassdef_collect
22
23 redef class ToolContext
24 var codesmells_metrics_phase = new CodeSmellsMetricsPhase(self, null)
25 end
26
27 class CodeSmellsMetricsPhase
28 super Phase
29 var average_number_of_lines = 0.0
30 var average_number_of_parameter = 0.0
31 var average_number_of_method = 0.0
32 var average_number_of_attribute = 0.0
33
34 redef fun process_mainmodule(mainmodule, given_mmodules) do
35 print toolcontext.format_h1("--- Code Smells Metrics ---")
36
37 var filter = new ModelFilter(private_visibility)
38 var view = new ModelView(toolcontext.modelbuilder.model, mainmodule, filter)
39 self.set_all_average_metrics(view)
40 var mclass_codesmell = new BadConceptonController(view)
41 var collect = new Counter[MClassDef]
42 var mclassdefs = new Array[MClassDef]
43
44 for mclass in mainmodule.flatten_mclass_hierarchy do
45 mclass_codesmell.collect(mclass.mclassdefs,self)
46 end
47 if toolcontext.opt_get_all.value then
48 mclass_codesmell.print_all
49 else
50 mclass_codesmell.print_top(10)
51 end
52 end
53
54 fun set_all_average_metrics(view: ModelView) do
55 var model_builder = toolcontext.modelbuilder
56 self.average_number_of_lines = view.get_avg_linenumber(model_builder)
57 self.average_number_of_parameter = view.get_avg_parameter
58 self.average_number_of_method = view.get_avg_method
59 self.average_number_of_attribute = view.get_avg_attribut
60 end
61 end
62
63 class BadConceptonController
64
65 var view: ModelView
66
67 # Code smell list
68 var bad_conception_elements = new Array[BadConceptionFinder]
69
70 # Print all collected code smell sort in decroissant order
71 fun print_all do
72 for bad_conception in self.sort do
73 bad_conception.print_collected_data
74 end
75 end
76
77 # Print the n top element
78 fun print_top(number: Int) do
79 for bad_conception in self.get_numbers_of_elements(number) do
80 bad_conception.print_collected_data
81 end
82 end
83
84 # Collect method take Array of mclassdef to find the code smells for every class
85 fun collect(mclassdefs: Array[MClassDef],phase: CodeSmellsMetricsPhase) do
86 for mclassdef in mclassdefs do
87 var bad_conception_class = new BadConceptionFinder(mclassdef, phase, view)
88 bad_conception_class.collect
89 bad_conception_elements.add(bad_conception_class)
90 end
91 end
92
93 # Sort the bad_conception_elements array
94 fun sort: Array[BadConceptionFinder]
95 do
96 var res = bad_conception_elements
97 var sorter = new BadConceptionComparator
98 sorter.sort(res)
99 return res
100 end
101
102 # Return an array with n elements
103 fun get_numbers_of_elements(number : Int) : Array[BadConceptionFinder]do
104 var return_values = new Array[BadConceptionFinder]
105 var list = self.sort
106 var min = number
107 if list.length <= number*2 then min = list.length
108 for i in [0..min[ do
109 var t = list[list.length-i-1]
110 return_values.add(t)
111 end
112 return return_values
113 end
114 end
115
116 class BadConceptionFinder
117 var mclassdef: MClassDef
118 var array_badconception = new Array[BadConception]
119 var phase: CodeSmellsMetricsPhase
120 var view: ModelView
121 var score = 0.0
122
123 # Collect code smell with selected toolcontext option
124 fun collect do
125 var bad_conception_elements = new Array[BadConception]
126 # Check toolcontext option
127 if phase.toolcontext.opt_feature_envy.value or phase.toolcontext.opt_all.value then bad_conception_elements.add(new FeatureEnvy(phase, view))
128 if phase.toolcontext.opt_long_method.value or phase.toolcontext.opt_all.value then bad_conception_elements.add(new LongMethod(phase, view))
129 if phase.toolcontext.opt_long_params.value or phase.toolcontext.opt_all.value then bad_conception_elements.add(new LongParameterList(phase, view))
130 if phase.toolcontext.opt_no_abstract_implementation.value or phase.toolcontext.opt_all.value then bad_conception_elements.add(new NoAbstractImplementation(phase, view))
131 if phase.toolcontext.opt_large_class.value or phase.toolcontext.opt_all.value then bad_conception_elements.add(new LargeClass(phase, view))
132 # Collected all code smell if their state is true
133 for bad_conception_element in bad_conception_elements do
134 if bad_conception_element.collect(self.mclassdef,phase.toolcontext.modelbuilder) then array_badconception.add(bad_conception_element)
135 end
136 # Compute global score
137 collect_global_score
138 end
139
140 fun print_collected_data do
141 if array_badconception.length != 0 then
142 print "--------------------"
143 print phase.toolcontext.format_h1("Full name: {mclassdef.full_name} Location: {mclassdef.location}")
144 for bad_conception in array_badconception do
145 bad_conception.print_result
146 end
147 end
148 end
149
150 fun collect_global_score do
151 if array_badconception.not_empty then
152 for bad_conception in array_badconception do
153 self.score += bad_conception.score
154 end
155 end
156 end
157 end
158
159 abstract class BadConception
160 var phase: CodeSmellsMetricsPhase
161
162 var view: ModelView
163
164 var score = 0.0
165
166 # Name
167 fun name: String is abstract
168
169 # Description
170 fun desc: String is abstract
171
172 # Collection method
173 fun collect(mclassdef: MClassDef, model_builder: ModelBuilder): Bool is abstract
174
175 # Show results in console
176 fun print_result is abstract
177
178 # Compute code smell score to sort
179 fun score_rate do
180 score = 1.0
181 end
182 end
183
184 class LargeClass
185 super BadConception
186 var number_attribut = 0
187
188 var number_method = 0
189
190 redef fun name do return "LARGC"
191
192 redef fun desc do return "Large class"
193
194 redef fun collect(mclassdef, model_builder): Bool do
195 self.number_attribut = mclassdef.collect_intro_and_redef_mattributes(view).length
196 # Get the number of methods (Accessor include) (subtract the get and set of attibutes with (numberAtribut*2))
197 self.number_method = mclassdef.collect_intro_and_redef_methods(view).length
198 self.score_rate
199 return self.number_method.to_f > phase.average_number_of_method and self.number_attribut.to_f > phase.average_number_of_attribute
200 end
201
202 redef fun print_result do
203 print phase.toolcontext.format_h2("{desc}: {number_attribut} attributes and {number_method} methods ({phase.average_number_of_attribute}A {phase.average_number_of_method}M Average)")
204 end
205
206 redef fun score_rate do
207 score = (number_method.to_f + number_attribut.to_f) / (phase.average_number_of_method + phase.average_number_of_attribute)
208 end
209 end
210
211 class LongParameterList
212 super BadConception
213 var bad_methods = new Array[MMethodDef]
214
215 redef fun name do return "LONGPL"
216
217 redef fun desc do return "Long parameter list"
218
219 redef fun collect(mclassdef, model_builder): Bool do
220 for meth in mclassdef.collect_intro_and_redef_mpropdefs(view) do
221 var threshold_value = 4
222 # Get the threshold value from the toolcontext command
223 if phase.toolcontext.opt_long_params_threshold.value != 0 then threshold_value = phase.toolcontext.opt_long_params_threshold.value
224 # Check if the property is a method definition
225 if not meth isa MMethodDef then continue
226 # Check if method has a signature
227 if meth.msignature == null then continue
228 if meth.msignature.mparameters.length <= threshold_value then continue
229 self.bad_methods.add(meth)
230 end
231 self.score_rate
232 return self.bad_methods.not_empty
233 end
234
235 redef fun print_result do
236 print phase.toolcontext.format_h2("{desc}:")
237 if self.bad_methods.not_empty then
238 print " Affected method(s):"
239 for method in self.bad_methods do
240 print " -{method.name} has {method.msignature.mparameters.length} parameters"
241 end
242 end
243 end
244
245 redef fun score_rate do
246 if self.bad_methods.not_empty then
247 self.score = self.bad_methods.length.to_f/ phase.average_number_of_method
248 end
249 end
250 end
251
252 class FeatureEnvy
253 super BadConception
254 var bad_methods = new Array[MMethodDef]
255
256 redef fun name do return "FEM"
257
258 redef fun desc do return "Feature envy"
259
260 redef fun collect(mclassdef, model_builder): Bool do
261 var mmethoddefs = call_analyze_methods(mclassdef,model_builder, view)
262 for mmethoddef in mmethoddefs do
263 var max_class_call = mmethoddef.class_call.max
264 # Check if the class with the maximum call is >= auto-call and the maximum call class is != of this class
265 if mmethoddef.class_call[max_class_call] <= mmethoddef.total_self_call or max_class_call.mclass.full_name == mclassdef.mclass.full_name then continue
266 self.bad_methods.add(mmethoddef)
267 end
268 self.score_rate
269 return self.bad_methods.not_empty
270 end
271
272 redef fun print_result do
273 print phase.toolcontext.format_h2("{desc}:")
274 if self.bad_methods.not_empty then
275 print " Affected method(s):"
276 for method in self.bad_methods do
277 var max_class_call = method.class_call.max
278 if max_class_call != null then
279 # Check if the type of max call class is generique
280 if max_class_call.mclass.mclass_type isa MGenericType and not phase.toolcontext.opt_move_generics.value then
281 print " -{method.name}({method.msignature.mparameters.join(", ")}) {method.total_self_call}/{method.class_call[max_class_call]}"
282 else
283 print " -{method.name}({method.msignature.mparameters.join(", ")}) {method.total_self_call}/{method.class_call[max_class_call]} move to {max_class_call}"
284 end
285 end
286 end
287 end
288 end
289
290 redef fun score_rate do
291 if self.bad_methods.not_empty then
292 self.score = self.bad_methods.length.to_f / phase.average_number_of_method
293 end
294 end
295 end
296
297 class LongMethod
298 super BadConception
299 var bad_methods = new Array[MMethodDef]
300
301 redef fun name do return "LONGMETH"
302
303 redef fun desc do return "Long method"
304
305 redef fun collect(mclassdef, model_builder): Bool do
306 var mmethoddefs = call_analyze_methods(mclassdef,model_builder, view)
307 var threshold_value = phase.average_number_of_lines.to_i
308 # Get the threshold value from the toolcontext command
309 if phase.toolcontext.opt_long_method_threshold.value != 0 then threshold_value = phase.toolcontext.opt_long_method_threshold.value
310
311 for mmethoddef in mmethoddefs do
312 if mmethoddef.line_number <= threshold_value then continue
313 self.bad_methods.add(mmethoddef)
314 end
315 self.score_rate
316 return self.bad_methods.not_empty
317 end
318
319 redef fun print_result do
320 print phase.toolcontext.format_h2("{desc}: Average {phase.average_number_of_lines.to_i} lines")
321 if self.bad_methods.not_empty then
322 print " Affected method(s):"
323 for method in self.bad_methods do
324 print " -{method.name} has {method.line_number} lines"
325 end
326 end
327 end
328
329 redef fun score_rate do
330 if self.bad_methods.not_empty then
331 self.score = self.bad_methods.length.to_f / phase.average_number_of_method
332 end
333 end
334 end
335
336 class NoAbstractImplementation
337 super BadConception
338 var bad_methods = new Array[MMethodDef]
339
340 redef fun name do return "LONGMETH"
341
342 redef fun desc do return "No Implemented abstract property"
343
344 redef fun collect(mclassdef, model_builder): Bool do
345 if not mclassdef.mclass.is_abstract and not mclassdef.mclass.is_interface then
346 if mclassdef.collect_abstract_methods(view).not_empty then
347 bad_methods.add_all(mclassdef.collect_not_define_properties(view))
348 end
349 end
350 self.score_rate
351 return bad_methods.not_empty
352 end
353
354 redef fun print_result do
355 print phase.toolcontext.format_h2("{desc}:")
356 if self.bad_methods.not_empty then
357 print " Affected method(s):"
358 for method in self.bad_methods do
359 print " -{method.name}"
360 end
361 end
362 end
363
364 redef fun score_rate do
365 if self.bad_methods.not_empty then
366 self.score = self.bad_methods.length.to_f / phase.average_number_of_method
367 end
368 end
369 end
370
371 redef class ModelView
372 fun get_avg_parameter: Float do
373 var counter = new Counter[MMethodDef]
374 for mclassdef in mclassdefs do
375 for method in mclassdef.collect_intro_and_redef_mpropdefs(self) do
376 # check if the property is a method definition
377 if not method isa MMethodDef then continue
378 #Check if method has a signature
379 if method.msignature == null then continue
380 if method.msignature.mparameters.length == 0 then continue
381 counter[method] = method.msignature.mparameters.length
382 end
383 end
384 return counter.avg + counter.std_dev
385 end
386
387 fun get_avg_attribut: Float do
388 var counter = new Counter[MClassDef]
389 for mclassdef in mclassdefs do
390 var number_attributs = mclassdef.collect_intro_and_redef_mattributes(self).length
391 if number_attributs != 0 then counter[mclassdef] = number_attributs
392 end
393 return counter.avg + counter.std_dev
394 end
395
396 fun get_avg_method: Float do
397 var counter = new Counter[MClassDef]
398 for mclassdef in mclassdefs do
399 var number_methodes = mclassdef.collect_intro_and_redef_methods(self).length
400 if number_methodes != 0 then counter[mclassdef] = number_methodes
401 end
402 return counter.avg + counter.std_dev
403 end
404
405 fun get_avg_linenumber(model_builder: ModelBuilder): Float do
406 var methods_analyse_metrics = new Counter[MClassDef]
407 for mclassdef in mclassdefs do
408 var result = 0
409 var count = 0
410 for mmethoddef in call_analyze_methods(mclassdef,model_builder, self) do
411 result += mmethoddef.line_number
412 if mmethoddef.line_number == 0 then continue
413 count += 1
414 end
415 if not mclassdef.collect_local_mproperties(self).length != 0 then continue
416 if count == 0 then continue
417 methods_analyse_metrics[mclassdef] = (result/count).to_i
418 end
419 return methods_analyse_metrics.avg + methods_analyse_metrics.std_dev
420 end
421 end
422
423 class BadConceptionComparator
424 super Comparator
425 redef type COMPARED: BadConceptionFinder
426 redef fun compare(a,b) do
427 var test = a.array_badconception.length <=> b.array_badconception.length
428 if test == 0 then
429 return a.score <=> b.score
430 end
431 return a.array_badconception.length <=> b.array_badconception.length
432 end
433 end