metric_fu 2.1.3.4 → 2.1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/{HISTORY → HISTORY.md} +18 -2
- data/README.md +11 -7
- data/TODO +1 -0
- data/lib/data_structures/careful_array.rb +8 -6
- data/lib/data_structures/code_issue.rb +82 -78
- data/lib/data_structures/grouping.rb +2 -2
- data/lib/data_structures/table.rb +77 -75
- data/lib/errors/analysis_error.rb +4 -1
- data/lib/metrics/churn/{churn_analyzer.rb → churn_hotspot.rb} +4 -4
- data/lib/metrics/flay/{flay_analyzer.rb → flay_hotspot.rb} +5 -5
- data/lib/metrics/flog/{flog_analyzer.rb → flog_hotspot.rb} +4 -4
- data/lib/metrics/hotspot_analyzer.rb +328 -0
- data/lib/metrics/hotspots/hotspots.rb +1 -1
- data/lib/metrics/rcov/{rcov_analyzer.rb → rcov_hotspot.rb} +4 -4
- data/lib/metrics/reek/{reek_analyzer.rb → reek_hotspot.rb} +7 -7
- data/lib/metrics/roodi/{roodi_analyzer.rb → roodi_hotspot.rb} +5 -5
- data/lib/metrics/saikuro/{saikuro_analyzer.rb → saikuro_hotspot.rb} +4 -4
- data/lib/metrics/stats/{stats_analyzer.rb → stats_hotspot.rb} +1 -1
- data/lib/scoring_strategies.rb +24 -22
- data/lib/version.rb +1 -1
- data/metric_fu.gemspec +1 -1
- data/spec/base/{metric_analyzer_spec.rb → hotspot_analyzer_spec.rb} +92 -92
- data/spec/generators/hotspots_spec.rb +2 -2
- metadata +356 -330
- data/lib/metrics/metric_analyzer.rb +0 -328
@@ -1,5 +1,5 @@
|
|
1
|
-
class
|
2
|
-
include
|
1
|
+
class ChurnHotspot
|
2
|
+
include MetricFu::HotspotScoringStrategies
|
3
3
|
|
4
4
|
COLUMNS = %w{times_changed}
|
5
5
|
|
@@ -12,11 +12,11 @@ class ChurnAnalyzer
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def map(row)
|
15
|
-
|
15
|
+
MetricFu::HotspotScoringStrategies.present(row)
|
16
16
|
end
|
17
17
|
|
18
18
|
def reduce(scores)
|
19
|
-
|
19
|
+
MetricFu::HotspotScoringStrategies.sum(scores)
|
20
20
|
end
|
21
21
|
|
22
22
|
def score(metric_ranking, item)
|
@@ -1,5 +1,5 @@
|
|
1
|
-
class
|
2
|
-
include
|
1
|
+
class FlayHotspot
|
2
|
+
include MetricFu::HotspotScoringStrategies
|
3
3
|
|
4
4
|
COLUMNS = %w{flay_reason flay_matching_reason}
|
5
5
|
|
@@ -12,15 +12,15 @@ class FlayAnalyzer
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def map(row)
|
15
|
-
|
15
|
+
MetricFu::HotspotScoringStrategies.present(row)
|
16
16
|
end
|
17
17
|
|
18
18
|
def reduce(scores)
|
19
|
-
|
19
|
+
MetricFu::HotspotScoringStrategies.sum(scores)
|
20
20
|
end
|
21
21
|
|
22
22
|
def score(metric_ranking, item)
|
23
|
-
|
23
|
+
MetricFu::HotspotScoringStrategies.percentile(metric_ranking, item)
|
24
24
|
end
|
25
25
|
|
26
26
|
def generate_records(data, table)
|
@@ -1,5 +1,5 @@
|
|
1
|
-
class
|
2
|
-
include
|
1
|
+
class FlogHotspot
|
2
|
+
include MetricFu::HotspotScoringStrategies
|
3
3
|
|
4
4
|
COLUMNS = %w{score}
|
5
5
|
|
@@ -16,11 +16,11 @@ class FlogAnalyzer
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def reduce(scores)
|
19
|
-
|
19
|
+
MetricFu::HotspotScoringStrategies.average(scores)
|
20
20
|
end
|
21
21
|
|
22
22
|
def score(metric_ranking, item)
|
23
|
-
|
23
|
+
MetricFu::HotspotScoringStrategies.identity(metric_ranking, item)
|
24
24
|
end
|
25
25
|
|
26
26
|
def generate_records(data, table)
|
@@ -0,0 +1,328 @@
|
|
1
|
+
%w(record grouping).each do |path|
|
2
|
+
MetricFu.data_structures_require { path }
|
3
|
+
end
|
4
|
+
|
5
|
+
module MetricFu
|
6
|
+
class HotspotAnalyzer
|
7
|
+
|
8
|
+
COMMON_COLUMNS = %w{metric}
|
9
|
+
GRANULARITIES = %w{file_path class_name method_name}
|
10
|
+
|
11
|
+
attr_accessor :table
|
12
|
+
|
13
|
+
def initialize(yaml)
|
14
|
+
if(yaml.is_a?(String))
|
15
|
+
@yaml = YAML.load(yaml)
|
16
|
+
else
|
17
|
+
@yaml = yaml
|
18
|
+
end
|
19
|
+
@file_ranking = MetricFu::Ranking.new
|
20
|
+
@class_ranking = MetricFu::Ranking.new
|
21
|
+
@method_ranking = MetricFu::Ranking.new
|
22
|
+
rankings = [@file_ranking, @class_ranking, @method_ranking]
|
23
|
+
|
24
|
+
tool_analyzers = [ReekHotspot.new, RoodiHotspot.new,
|
25
|
+
FlogHotspot.new, ChurnHotspot.new, SaikuroHotspot.new,
|
26
|
+
FlayHotspot.new, StatsHotspot.new, RcovHotspot.new]
|
27
|
+
# TODO There is likely a clash that will happen between
|
28
|
+
# column names eventually. We should probably auto-prefix
|
29
|
+
# them (e.g. "roodi_problem")
|
30
|
+
columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map{|analyzer| analyzer.columns}.flatten
|
31
|
+
|
32
|
+
@table = make_table(columns)
|
33
|
+
|
34
|
+
# These tables are an optimization. They contain subsets of the master table.
|
35
|
+
# TODO - these should be pushed into the Table class now
|
36
|
+
@tool_tables = make_table_hash(columns)
|
37
|
+
@file_tables = make_table_hash(columns)
|
38
|
+
@class_tables = make_table_hash(columns)
|
39
|
+
@method_tables = make_table_hash(columns)
|
40
|
+
|
41
|
+
tool_analyzers.each do |analyzer|
|
42
|
+
analyzer.generate_records(@yaml[analyzer.name], @table)
|
43
|
+
end
|
44
|
+
|
45
|
+
build_lookups!(table)
|
46
|
+
process_rows!(table)
|
47
|
+
|
48
|
+
tool_analyzers.each do |analyzer|
|
49
|
+
GRANULARITIES.each do |granularity|
|
50
|
+
metric_ranking = calculate_metric_scores(granularity, analyzer)
|
51
|
+
add_to_master_ranking(ranking(granularity), metric_ranking, analyzer)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
rankings.each do |ranking|
|
56
|
+
ranking.delete(nil)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def location(item, value)
|
61
|
+
sub_table = get_sub_table(item, value)
|
62
|
+
if(sub_table.length==0)
|
63
|
+
raise MetricFu::AnalysisError, "The #{item.to_s} '#{value.to_s}' does not have any rows in the analysis table"
|
64
|
+
else
|
65
|
+
first_row = sub_table[0]
|
66
|
+
case item
|
67
|
+
when :class
|
68
|
+
MetricFu::Location.get(first_row.file_path, first_row.class_name, nil)
|
69
|
+
when :method
|
70
|
+
MetricFu::Location.get(first_row.file_path, first_row.class_name, first_row.method_name)
|
71
|
+
when :file
|
72
|
+
MetricFu::Location.get(first_row.file_path, nil, nil)
|
73
|
+
else
|
74
|
+
raise ArgumentError, "Item must be :class, :method, or :file"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#todo redo as item,value, options = {}
|
80
|
+
# Note that the other option for 'details' is :detailed (this isn't
|
81
|
+
# at all clear from this method itself
|
82
|
+
def problems_with(item, value, details = :summary, exclude_details = [])
|
83
|
+
sub_table = get_sub_table(item, value)
|
84
|
+
#grouping = Ruport::Data::Grouping.new(sub_table, :by => 'metric')
|
85
|
+
grouping = get_grouping(sub_table, :by => 'metric')
|
86
|
+
problems = {}
|
87
|
+
grouping.each do |metric, table|
|
88
|
+
if details == :summary || exclude_details.include?(metric)
|
89
|
+
problems[metric] = present_group(metric,table)
|
90
|
+
else
|
91
|
+
problems[metric] = present_group_details(metric,table)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
problems
|
95
|
+
end
|
96
|
+
|
97
|
+
def worst_methods(size = nil)
|
98
|
+
@method_ranking.top(size)
|
99
|
+
end
|
100
|
+
|
101
|
+
def worst_classes(size = nil)
|
102
|
+
@class_ranking.top(size)
|
103
|
+
end
|
104
|
+
|
105
|
+
def worst_files(size = nil)
|
106
|
+
@file_ranking.top(size)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def get_grouping(table, opts)
|
112
|
+
#Ruport::Data::Grouping.new(table, opts)
|
113
|
+
MetricFu::Grouping.new(table, opts)
|
114
|
+
#@grouping_cache ||= {}
|
115
|
+
#@grouping_cache.fetch(grouping_key(table,opts)) do
|
116
|
+
# @grouping_cache[grouping_key(table,opts)] = Ruport::Data::Grouping.new(table, opts)
|
117
|
+
#end
|
118
|
+
end
|
119
|
+
|
120
|
+
def grouping_key(table, opts)
|
121
|
+
"table #{table.object_id} opts #{opts.inspect}"
|
122
|
+
end
|
123
|
+
|
124
|
+
def build_lookups!(table)
|
125
|
+
@class_and_method_to_file ||= {}
|
126
|
+
# Build a mapping from [class,method] => filename
|
127
|
+
# (and make sure the mapping is unique)
|
128
|
+
table.each do |row|
|
129
|
+
# We know that Saikuro provides the wrong data
|
130
|
+
next if row['metric'] == :saikuro
|
131
|
+
key = [row['class_name'], row['method_name']]
|
132
|
+
file_path = row['file_path']
|
133
|
+
@class_and_method_to_file[key] ||= file_path
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def process_rows!(table)
|
138
|
+
# Correct incorrect rows in the table
|
139
|
+
table.each do |row|
|
140
|
+
row_metric = row['metric'] #perf optimization
|
141
|
+
if row_metric == :saikuro
|
142
|
+
fix_row_file_path!(row)
|
143
|
+
end
|
144
|
+
@tool_tables[row_metric] << row
|
145
|
+
@file_tables[row["file_path"]] << row
|
146
|
+
@class_tables[row["class_name"]] << row
|
147
|
+
@method_tables[row["method_name"]] << row
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def fix_row_file_path!(row)
|
152
|
+
# We know that Saikuro rows are broken
|
153
|
+
# next unless row['metric'] == :saikuro
|
154
|
+
key = [row['class_name'], row['method_name']]
|
155
|
+
current_file_path = row['file_path'].to_s
|
156
|
+
correct_file_path = @class_and_method_to_file[key]
|
157
|
+
if(correct_file_path!=nil && correct_file_path.include?(current_file_path))
|
158
|
+
row['file_path'] = correct_file_path
|
159
|
+
else
|
160
|
+
# There wasn't an exact match, so we can do a substring match
|
161
|
+
matching_file_path = file_paths.detect {|file_path|
|
162
|
+
file_path!=nil && file_path.include?(current_file_path)
|
163
|
+
}
|
164
|
+
if(matching_file_path)
|
165
|
+
row['file_path'] = matching_file_path
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def file_paths
|
171
|
+
@file_paths ||= @table.column('file_path').uniq
|
172
|
+
end
|
173
|
+
|
174
|
+
def ranking(column_name)
|
175
|
+
case column_name
|
176
|
+
when "file_path"
|
177
|
+
@file_ranking
|
178
|
+
when "class_name"
|
179
|
+
@class_ranking
|
180
|
+
when "method_name"
|
181
|
+
@method_ranking
|
182
|
+
else
|
183
|
+
raise ArgumentError, "Invalid column name #{column_name}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def calculate_metric_scores(granularity, analyzer)
|
188
|
+
metric_ranking = MetricFu::Ranking.new
|
189
|
+
metric_violations = @tool_tables[analyzer.name]
|
190
|
+
metric_violations.each do |row|
|
191
|
+
location = row[granularity]
|
192
|
+
metric_ranking[location] ||= []
|
193
|
+
metric_ranking[location] << analyzer.map(row)
|
194
|
+
end
|
195
|
+
|
196
|
+
metric_ranking.each do |item, scores|
|
197
|
+
metric_ranking[item] = analyzer.reduce(scores)
|
198
|
+
end
|
199
|
+
|
200
|
+
metric_ranking
|
201
|
+
end
|
202
|
+
|
203
|
+
def add_to_master_ranking(master_ranking, metric_ranking, analyzer)
|
204
|
+
metric_ranking.each do |item, _|
|
205
|
+
master_ranking[item] ||= 0
|
206
|
+
master_ranking[item] += analyzer.score(metric_ranking, item) # scaling? Do we just add in the raw score?
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def most_common_column(column_name, size)
|
211
|
+
#grouping = Ruport::Data::Grouping.new(@table,
|
212
|
+
# :by => column_name,
|
213
|
+
# :order => lambda { |g| -g.size})
|
214
|
+
get_grouping(@table, :by => column_name, :order => lambda {|g| -g.size})
|
215
|
+
values = []
|
216
|
+
grouping.each do |value, _|
|
217
|
+
values << value if value!=nil
|
218
|
+
if(values.size==size)
|
219
|
+
break
|
220
|
+
end
|
221
|
+
end
|
222
|
+
return nil if values.empty?
|
223
|
+
if(values.size == 1)
|
224
|
+
return values.first
|
225
|
+
else
|
226
|
+
return values
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# TODO: As we get fancier, the presenter should
|
231
|
+
# be its own class, not just a method with a long
|
232
|
+
# case statement
|
233
|
+
def present_group(metric, group)
|
234
|
+
occurences = group.size
|
235
|
+
case(metric)
|
236
|
+
when :reek
|
237
|
+
"found #{occurences} code smells"
|
238
|
+
when :roodi
|
239
|
+
"found #{occurences} design problems"
|
240
|
+
when :churn
|
241
|
+
"detected high level of churn (changed #{group[0].times_changed} times)"
|
242
|
+
when :flog
|
243
|
+
complexity = get_mean(group.column("score"))
|
244
|
+
"#{"average " if occurences > 1}complexity is %.1f" % complexity
|
245
|
+
when :saikuro
|
246
|
+
complexity = get_mean(group.column("complexity"))
|
247
|
+
"#{"average " if occurences > 1}complexity is %.1f" % complexity
|
248
|
+
when :flay
|
249
|
+
"found #{occurences} code duplications"
|
250
|
+
when :rcov
|
251
|
+
average_code_uncoverage = get_mean(group.column("percentage_uncovered"))
|
252
|
+
"#{"average " if occurences > 1}uncovered code is %.1f%" % average_code_uncoverage
|
253
|
+
else
|
254
|
+
raise MetricFu::AnalysisError, "Unknown metric #{metric}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def present_group_details(metric, group)
|
259
|
+
occurences = group.size
|
260
|
+
case(metric)
|
261
|
+
when :reek
|
262
|
+
message = "found #{occurences} code smells<br/>"
|
263
|
+
group.each do |item|
|
264
|
+
type = item.data["reek__type_name"]
|
265
|
+
reek_message = item.data["reek__message"]
|
266
|
+
message << "* #{type}: #{reek_message}<br/>"
|
267
|
+
end
|
268
|
+
message
|
269
|
+
when :roodi
|
270
|
+
message = "found #{occurences} design problems<br/>"
|
271
|
+
group.each do |item|
|
272
|
+
problem = item.data["problems"]
|
273
|
+
message << "* #{problem}<br/>"
|
274
|
+
end
|
275
|
+
message
|
276
|
+
when :churn
|
277
|
+
"detected high level of churn (changed #{group[0].times_changed} times)"
|
278
|
+
when :flog
|
279
|
+
complexity = get_mean(group.column("score"))
|
280
|
+
"#{"average " if occurences > 1}complexity is %.1f" % complexity
|
281
|
+
when :saikuro
|
282
|
+
complexity = get_mean(group.column("complexity"))
|
283
|
+
"#{"average " if occurences > 1}complexity is %.1f" % complexity
|
284
|
+
when :flay
|
285
|
+
message = "found #{occurences} code duplications<br/>"
|
286
|
+
group.each do |item|
|
287
|
+
problem = item.data["flay_reason"]
|
288
|
+
problem = problem.gsub(/^[0-9]*\)/,'')
|
289
|
+
problem = problem.gsub(/files\:/,' <br> files:')
|
290
|
+
message << "* #{problem}<br/>"
|
291
|
+
end
|
292
|
+
message
|
293
|
+
else
|
294
|
+
raise MetricFu::AnalysisError, "Unknown metric #{metric}"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def make_table_hash(columns)
|
299
|
+
Hash.new { |hash, key|
|
300
|
+
hash[key] = make_table(columns)
|
301
|
+
}
|
302
|
+
end
|
303
|
+
|
304
|
+
def make_table(columns)
|
305
|
+
MetricFu::Table.new(:column_names => columns)
|
306
|
+
end
|
307
|
+
|
308
|
+
def get_sub_table(item, value)
|
309
|
+
tables = {
|
310
|
+
:class => @class_tables,
|
311
|
+
:method => @method_tables,
|
312
|
+
:file => @file_tables,
|
313
|
+
:tool => @tool_tables
|
314
|
+
}.fetch(item) do
|
315
|
+
raise ArgumentError, "Item must be :class, :method, or :file"
|
316
|
+
end
|
317
|
+
tables[value]
|
318
|
+
end
|
319
|
+
|
320
|
+
def get_mean(collection)
|
321
|
+
collection_length = collection.length
|
322
|
+
sum = 0
|
323
|
+
sum = collection.inject( nil ) { |sum,x| sum ? sum+x : x }
|
324
|
+
(sum.to_f / collection_length.to_f)
|
325
|
+
end
|
326
|
+
|
327
|
+
end
|
328
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
class
|
2
|
-
include
|
1
|
+
class RcovHotspot
|
2
|
+
include MetricFu::HotspotScoringStrategies
|
3
3
|
|
4
4
|
COLUMNS = %w{percentage_uncovered}
|
5
5
|
|
@@ -16,11 +16,11 @@ class RcovAnalyzer
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def reduce(scores)
|
19
|
-
|
19
|
+
MetricFu::HotspotScoringStrategies.average(scores)
|
20
20
|
end
|
21
21
|
|
22
22
|
def score(metric_ranking, item)
|
23
|
-
|
23
|
+
MetricFu::HotspotScoringStrategies.identity(metric_ranking, item)
|
24
24
|
end
|
25
25
|
|
26
26
|
def generate_records(data, table)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
class
|
4
|
-
include
|
3
|
+
class ReekHotspot
|
4
|
+
include MetricFu::HotspotScoringStrategies
|
5
5
|
|
6
6
|
REEK_ISSUE_INFO = {
|
7
7
|
'Uncommunicative Name' =>
|
@@ -71,7 +71,7 @@ class ReekAnalyzer
|
|
71
71
|
|
72
72
|
# Note that in practice, the prefix reek__ is appended to each one
|
73
73
|
# This was a partially implemented idea to avoid column name collisions
|
74
|
-
# but it is only done in the
|
74
|
+
# but it is only done in the ReekHotspot
|
75
75
|
COLUMNS = %w{type_name message value value_description comparable_message}
|
76
76
|
|
77
77
|
def self.issue_link(issue)
|
@@ -87,15 +87,15 @@ class ReekAnalyzer
|
|
87
87
|
end
|
88
88
|
|
89
89
|
def map(row)
|
90
|
-
|
90
|
+
MetricFu::HotspotScoringStrategies.present(row)
|
91
91
|
end
|
92
92
|
|
93
93
|
def reduce(scores)
|
94
|
-
|
94
|
+
MetricFu::HotspotScoringStrategies.sum(scores)
|
95
95
|
end
|
96
96
|
|
97
97
|
def score(metric_ranking, item)
|
98
|
-
|
98
|
+
MetricFu::HotspotScoringStrategies.percentile(metric_ranking, item)
|
99
99
|
end
|
100
100
|
|
101
101
|
def generate_records(data, table)
|
@@ -109,7 +109,7 @@ class ReekAnalyzer
|
|
109
109
|
table << {
|
110
110
|
"metric" => name, # important
|
111
111
|
"file_path" => file_path, # important
|
112
|
-
# NOTE:
|
112
|
+
# NOTE: ReekHotspot is currently different than other hotspots with regard
|
113
113
|
# to column name. Note the COLUMNS constant and #columns method
|
114
114
|
"reek__message" => message,
|
115
115
|
"reek__type_name" => smell_type,
|