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