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