metric_fu 2.1.3.4 → 2.1.3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,