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 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]
43 for mclass
in mainmodule
.flatten_mclass_hierarchy
do
44 mclass_codesmell
.collect
(mclass
.mclassdefs
,self)
46 if toolcontext
.opt_get_all
.value
then
47 mclass_codesmell
.print_all
49 mclass_codesmell
.print_top
(10)
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
62 class BadConceptonController
67 var bad_conception_elements
= new Array[BadConceptionFinder]
69 # Print all collected code smell sort in decroissant order
71 for bad_conception
in self.sort
do
72 bad_conception
.print_collected_data
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
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
)
92 # Sort the bad_conception_elements array
93 fun sort
: Array[BadConceptionFinder]
95 var res
= bad_conception_elements
96 var sorter
= new BadConceptionComparator
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]
106 if list
.length
<= number
*2 then min
= list
.length
108 var t
= list
[list
.length-i-1
]
115 class BadConceptionFinder
116 var mclassdef
: MClassDef
117 var array_badconception
= new Array[BadConception]
118 var phase
: CodeSmellsMetricsPhase
122 # Collect code smell with selected toolcontext option
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
)
135 # Compute global score
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
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
158 abstract class BadConception
159 var phase
: CodeSmellsMetricsPhase
166 fun name
: String is abstract
169 fun desc
: String is abstract
172 fun collect
(mclassdef
: MClassDef, model_builder
: ModelBuilder): Bool is abstract
174 # Show results in console
175 fun print_result
is abstract
177 # Compute code smell score to sort
185 var number_attribut
= 0
187 var number_method
= 0
189 redef fun name
do return "LARGC"
191 redef fun desc
do return "Large class"
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
198 return self.number_method
.to_f
> phase
.average_number_of_method
and self.number_attribut
.to_f
> phase
.average_number_of_attribute
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)")
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
)
210 class LongParameterList
212 var bad_methods
= new Array[MMethodDef]
214 redef fun name
do return "LONGPL"
216 redef fun desc
do return "Long parameter list"
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
)
231 return self.bad_methods
.not_empty
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"
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
253 var bad_methods
= new Array[MMethodDef]
255 redef fun name
do return "FEM"
257 redef fun desc
do return "Feature envy"
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
)
268 return self.bad_methods
.not_empty
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]}"
282 print
" -{method.name}({method.msignature.mparameters.join(", ")}) {method.total_self_call}/{method.class_call[max_class_call]} move to {max_class_call}"
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
298 var bad_methods
= new Array[MMethodDef]
300 redef fun name
do return "LONGMETH"
302 redef fun desc
do return "Long method"
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
310 for mmethoddef
in mmethoddefs
do
311 if mmethoddef
.line_number
<= threshold_value
then continue
312 self.bad_methods
.add
(mmethoddef
)
315 return self.bad_methods
.not_empty
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"
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
335 class NoAbstractImplementation
337 var bad_methods
= new Array[MMethodDef]
339 redef fun name
do return "LONGMETH"
341 redef fun desc
do return "No Implemented abstract property"
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
))
350 return bad_methods
.not_empty
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}"
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
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
383 return counter
.avg
+ counter
.std_dev
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
392 return counter
.avg
+ counter
.std_dev
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
401 return counter
.avg
+ counter
.std_dev
404 fun get_avg_linenumber
(model_builder
: ModelBuilder): Float do
405 var methods_analyse_metrics
= new Counter[MClassDef]
406 for mclassdef
in mclassdefs
do
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
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
418 return methods_analyse_metrics
.avg
+ methods_analyse_metrics
.std_dev
422 class BadConceptionComparator
424 redef type COMPARED: BadConceptionFinder
425 redef fun compare
(a
,b
) do
426 var test
= a
.array_badconception
.length
<=> b
.array_badconception
.length
428 return a
.score
<=> b
.score
430 return a
.array_badconception
.length
<=> b
.array_badconception
.length