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.
@@ -1 +1,4 @@
1
- class AnalysisError < RuntimeError; end;
1
+ module MetricFu
2
+ class AnalysisError < RuntimeError
3
+ end
4
+ end
@@ -1,5 +1,5 @@
1
- class ChurnAnalyzer
2
- include ScoringStrategies
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
- ScoringStrategies.present(row)
15
+ MetricFu::HotspotScoringStrategies.present(row)
16
16
  end
17
17
 
18
18
  def reduce(scores)
19
- ScoringStrategies.sum(scores)
19
+ MetricFu::HotspotScoringStrategies.sum(scores)
20
20
  end
21
21
 
22
22
  def score(metric_ranking, item)
@@ -1,5 +1,5 @@
1
- class FlayAnalyzer
2
- include ScoringStrategies
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
- ScoringStrategies.present(row)
15
+ MetricFu::HotspotScoringStrategies.present(row)
16
16
  end
17
17
 
18
18
  def reduce(scores)
19
- ScoringStrategies.sum(scores)
19
+ MetricFu::HotspotScoringStrategies.sum(scores)
20
20
  end
21
21
 
22
22
  def score(metric_ranking, item)
23
- ScoringStrategies.percentile(metric_ranking, item)
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 FlogAnalyzer
2
- include ScoringStrategies
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
- ScoringStrategies.average(scores)
19
+ MetricFu::HotspotScoringStrategies.average(scores)
20
20
  end
21
21
 
22
22
  def score(metric_ranking, item)
23
- ScoringStrategies.identity(metric_ranking, item)
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>&nbsp;&nbsp;&nbsp;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
@@ -11,7 +11,7 @@ module MetricFu
11
11
  end
12
12
 
13
13
  def emit
14
- @analyzer = MetricAnalyzer.new(MetricFu.report.report_hash)
14
+ @analyzer = MetricFu::HotspotAnalyzer.new(MetricFu.report.report_hash)
15
15
  end
16
16
 
17
17
  def analyze
@@ -1,5 +1,5 @@
1
- class RcovAnalyzer
2
- include ScoringStrategies
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
- ScoringStrategies.average(scores)
19
+ MetricFu::HotspotScoringStrategies.average(scores)
20
20
  end
21
21
 
22
22
  def score(metric_ranking, item)
23
- ScoringStrategies.identity(metric_ranking, item)
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 ReekAnalyzer
4
- include ScoringStrategies
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 ReekAnalyzer
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
- ScoringStrategies.present(row)
90
+ MetricFu::HotspotScoringStrategies.present(row)
91
91
  end
92
92
 
93
93
  def reduce(scores)
94
- ScoringStrategies.sum(scores)
94
+ MetricFu::HotspotScoringStrategies.sum(scores)
95
95
  end
96
96
 
97
97
  def score(metric_ranking, item)
98
- ScoringStrategies.percentile(metric_ranking, item)
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: ReekAnalyzer is currently different than other analyzers with regard
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,