1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
16 module codesmells_metrics
19 import nitsmell_toolcontext
20 import method_analyze_metrics
21 import mclassdef_collect
23 redef class ToolContext
24 var codesmells_metrics_phase
= new CodeSmellsMetricsPhase(self, null)
27 class CodeSmellsMetricsPhase
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
34 redef fun process_mainmodule
(mainmodule
, given_mmodules
) do
35 print toolcontext
.format_h1
("--- Code Smells Metrics ---")
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]
44 for mclass
in mainmodule
.flatten_mclass_hierarchy
do
45 mclass_codesmell
.collect
(mclass
.mclassdefs
,self)
47 if toolcontext
.opt_get_all
.value
then
48 mclass_codesmell
.print_all
50 mclass_codesmell
.print_top
(10)
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
63 class BadConceptonController
68 var bad_conception_elements
= new Array[BadConceptionFinder]
70 # Print all collected code smell sort in decroissant order
72 for bad_conception
in self.sort
do
73 bad_conception
.print_collected_data
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
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
)
93 # Sort the bad_conception_elements array
94 fun sort
: Array[BadConceptionFinder]
96 var res
= bad_conception_elements
97 var sorter
= new BadConceptionComparator
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]
107 if list
.length
<= number
*2 then min
= list
.length
109 var t
= list
[list
.length-i-1
]
116 class BadConceptionFinder
117 var mclassdef
: MClassDef
118 var array_badconception
= new Array[BadConception]
119 var phase
: CodeSmellsMetricsPhase
123 # Collect code smell with selected toolcontext option
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
)
136 # Compute global score
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
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
159 abstract class BadConception
160 var phase
: CodeSmellsMetricsPhase
167 fun name
: String is abstract
170 fun desc
: String is abstract
173 fun collect
(mclassdef
: MClassDef, model_builder
: ModelBuilder): Bool is abstract
175 # Show results in console
176 fun print_result
is abstract
178 # Compute code smell score to sort
186 var number_attribut
= 0
188 var number_method
= 0
190 redef fun name
do return "LARGC"
192 redef fun desc
do return "Large class"
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
199 return self.number_method
.to_f
> phase
.average_number_of_method
and self.number_attribut
.to_f
> phase
.average_number_of_attribute
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)")
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
)
211 class LongParameterList
213 var bad_methods
= new Array[MMethodDef]
215 redef fun name
do return "LONGPL"
217 redef fun desc
do return "Long parameter list"
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
)
232 return self.bad_methods
.not_empty
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"
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
254 var bad_methods
= new Array[MMethodDef]
256 redef fun name
do return "FEM"
258 redef fun desc
do return "Feature envy"
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
)
269 return self.bad_methods
.not_empty
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]}"
283 print
" -{method.name}({method.msignature.mparameters.join(", ")}) {method.total_self_call}/{method.class_call[max_class_call]} move to {max_class_call}"
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
299 var bad_methods
= new Array[MMethodDef]
301 redef fun name
do return "LONGMETH"
303 redef fun desc
do return "Long method"
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
311 for mmethoddef
in mmethoddefs
do
312 if mmethoddef
.line_number
<= threshold_value
then continue
313 self.bad_methods
.add
(mmethoddef
)
316 return self.bad_methods
.not_empty
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"
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
336 class NoAbstractImplementation
338 var bad_methods
= new Array[MMethodDef]
340 redef fun name
do return "LONGMETH"
342 redef fun desc
do return "No Implemented abstract property"
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
))
351 return bad_methods
.not_empty
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}"
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
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
384 return counter
.avg
+ counter
.std_dev
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
393 return counter
.avg
+ counter
.std_dev
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
402 return counter
.avg
+ counter
.std_dev
405 fun get_avg_linenumber
(model_builder
: ModelBuilder): Float do
406 var methods_analyse_metrics
= new Counter[MClassDef]
407 for mclassdef
in mclassdefs
do
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
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
419 return methods_analyse_metrics
.avg
+ methods_analyse_metrics
.std_dev
423 class BadConceptionComparator
425 redef type COMPARED: BadConceptionFinder
426 redef fun compare
(a
,b
) do
427 var test
= a
.array_badconception
.length
<=> b
.array_badconception
.length
429 return a
.score
<=> b
.score
431 return a
.array_badconception
.length
<=> b
.array_badconception
.length