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