bf4-metric_fu 2.1.3.1

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.
Files changed (127) hide show
  1. data/HISTORY +252 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README.md +49 -0
  4. data/Rakefile +22 -0
  5. data/TODO +6 -0
  6. data/lib/base/base_template.rb +182 -0
  7. data/lib/base/churn_analyzer.rb +38 -0
  8. data/lib/base/code_issue.rb +100 -0
  9. data/lib/base/configuration.rb +219 -0
  10. data/lib/base/flay_analyzer.rb +50 -0
  11. data/lib/base/flog_analyzer.rb +43 -0
  12. data/lib/base/generator.rb +166 -0
  13. data/lib/base/graph.rb +44 -0
  14. data/lib/base/grouping.rb +42 -0
  15. data/lib/base/line_numbers.rb +79 -0
  16. data/lib/base/location.rb +87 -0
  17. data/lib/base/md5_tracker.rb +52 -0
  18. data/lib/base/metric_analyzer.rb +331 -0
  19. data/lib/base/ranking.rb +34 -0
  20. data/lib/base/rcov_analyzer.rb +43 -0
  21. data/lib/base/record.rb +43 -0
  22. data/lib/base/reek_analyzer.rb +164 -0
  23. data/lib/base/report.rb +110 -0
  24. data/lib/base/roodi_analyzer.rb +37 -0
  25. data/lib/base/saikuro_analyzer.rb +48 -0
  26. data/lib/base/scoring_strategies.rb +29 -0
  27. data/lib/base/stats_analyzer.rb +37 -0
  28. data/lib/base/table.rb +108 -0
  29. data/lib/generators/churn.rb +28 -0
  30. data/lib/generators/flay.rb +31 -0
  31. data/lib/generators/flog.rb +113 -0
  32. data/lib/generators/hotspots.rb +52 -0
  33. data/lib/generators/rails_best_practices.rb +53 -0
  34. data/lib/generators/rcov.rb +124 -0
  35. data/lib/generators/reek.rb +81 -0
  36. data/lib/generators/roodi.rb +35 -0
  37. data/lib/generators/saikuro.rb +259 -0
  38. data/lib/generators/stats.rb +58 -0
  39. data/lib/graphs/engines/bluff.rb +113 -0
  40. data/lib/graphs/engines/gchart.rb +157 -0
  41. data/lib/graphs/flay_grapher.rb +18 -0
  42. data/lib/graphs/flog_grapher.rb +57 -0
  43. data/lib/graphs/grapher.rb +11 -0
  44. data/lib/graphs/rails_best_practices_grapher.rb +19 -0
  45. data/lib/graphs/rcov_grapher.rb +18 -0
  46. data/lib/graphs/reek_grapher.rb +30 -0
  47. data/lib/graphs/roodi_grapher.rb +18 -0
  48. data/lib/graphs/stats_grapher.rb +20 -0
  49. data/lib/metric_fu.rb +80 -0
  50. data/lib/tasks/metric_fu.rake +36 -0
  51. data/lib/templates/awesome/awesome_template.rb +92 -0
  52. data/lib/templates/awesome/churn.html.erb +58 -0
  53. data/lib/templates/awesome/css/buttons.css +82 -0
  54. data/lib/templates/awesome/css/default.css +91 -0
  55. data/lib/templates/awesome/css/integrity.css +334 -0
  56. data/lib/templates/awesome/css/reset.css +7 -0
  57. data/lib/templates/awesome/css/syntax.css +19 -0
  58. data/lib/templates/awesome/flay.html.erb +34 -0
  59. data/lib/templates/awesome/flog.html.erb +55 -0
  60. data/lib/templates/awesome/hotspots.html.erb +62 -0
  61. data/lib/templates/awesome/index.html.erb +34 -0
  62. data/lib/templates/awesome/layout.html.erb +30 -0
  63. data/lib/templates/awesome/rails_best_practices.html.erb +27 -0
  64. data/lib/templates/awesome/rcov.html.erb +42 -0
  65. data/lib/templates/awesome/reek.html.erb +40 -0
  66. data/lib/templates/awesome/roodi.html.erb +27 -0
  67. data/lib/templates/awesome/saikuro.html.erb +71 -0
  68. data/lib/templates/awesome/stats.html.erb +51 -0
  69. data/lib/templates/javascripts/bluff-min.js +1 -0
  70. data/lib/templates/javascripts/excanvas.js +35 -0
  71. data/lib/templates/javascripts/js-class.js +1 -0
  72. data/lib/templates/standard/churn.html.erb +31 -0
  73. data/lib/templates/standard/default.css +64 -0
  74. data/lib/templates/standard/flay.html.erb +34 -0
  75. data/lib/templates/standard/flog.html.erb +57 -0
  76. data/lib/templates/standard/hotspots.html.erb +54 -0
  77. data/lib/templates/standard/index.html.erb +41 -0
  78. data/lib/templates/standard/rails_best_practices.html.erb +29 -0
  79. data/lib/templates/standard/rcov.html.erb +43 -0
  80. data/lib/templates/standard/reek.html.erb +42 -0
  81. data/lib/templates/standard/roodi.html.erb +29 -0
  82. data/lib/templates/standard/saikuro.html.erb +84 -0
  83. data/lib/templates/standard/standard_template.rb +27 -0
  84. data/lib/templates/standard/stats.html.erb +55 -0
  85. data/spec/base/base_template_spec.rb +194 -0
  86. data/spec/base/configuration_spec.rb +277 -0
  87. data/spec/base/generator_spec.rb +223 -0
  88. data/spec/base/graph_spec.rb +61 -0
  89. data/spec/base/line_numbers_spec.rb +62 -0
  90. data/spec/base/location_spec.rb +127 -0
  91. data/spec/base/md5_tracker_spec.rb +57 -0
  92. data/spec/base/metric_analyzer_spec.rb +452 -0
  93. data/spec/base/ranking_spec.rb +42 -0
  94. data/spec/base/report_spec.rb +146 -0
  95. data/spec/base/table_spec.rb +36 -0
  96. data/spec/generators/churn_spec.rb +41 -0
  97. data/spec/generators/flay_spec.rb +105 -0
  98. data/spec/generators/flog_spec.rb +70 -0
  99. data/spec/generators/hotspots_spec.rb +88 -0
  100. data/spec/generators/rails_best_practices_spec.rb +52 -0
  101. data/spec/generators/rcov_spec.rb +180 -0
  102. data/spec/generators/reek_spec.rb +134 -0
  103. data/spec/generators/roodi_spec.rb +24 -0
  104. data/spec/generators/saikuro_spec.rb +74 -0
  105. data/spec/generators/stats_spec.rb +74 -0
  106. data/spec/graphs/engines/bluff_spec.rb +19 -0
  107. data/spec/graphs/engines/gchart_spec.rb +156 -0
  108. data/spec/graphs/flay_grapher_spec.rb +56 -0
  109. data/spec/graphs/flog_grapher_spec.rb +108 -0
  110. data/spec/graphs/rails_best_practices_grapher_spec.rb +61 -0
  111. data/spec/graphs/rcov_grapher_spec.rb +56 -0
  112. data/spec/graphs/reek_grapher_spec.rb +65 -0
  113. data/spec/graphs/roodi_grapher_spec.rb +56 -0
  114. data/spec/graphs/stats_grapher_spec.rb +68 -0
  115. data/spec/resources/line_numbers/foo.rb +33 -0
  116. data/spec/resources/line_numbers/module.rb +11 -0
  117. data/spec/resources/line_numbers/module_surrounds_class.rb +15 -0
  118. data/spec/resources/line_numbers/two_classes.rb +11 -0
  119. data/spec/resources/saikuro/app/controllers/sessions_controller.rb_cyclo.html +10 -0
  120. data/spec/resources/saikuro/app/controllers/users_controller.rb_cyclo.html +16 -0
  121. data/spec/resources/saikuro/index_cyclo.html +155 -0
  122. data/spec/resources/saikuro_sfiles/thing.rb_cyclo.html +11 -0
  123. data/spec/resources/yml/20090630.yml +7922 -0
  124. data/spec/resources/yml/metric_missing.yml +1 -0
  125. data/spec/spec.opts +6 -0
  126. data/spec/spec_helper.rb +7 -0
  127. metadata +560 -0
@@ -0,0 +1,331 @@
1
+ class AnalysisError < RuntimeError; end;
2
+
3
+ [ '/base/record',
4
+ '/base/grouping'].each do |path|
5
+ require File.expand_path(File.join(MetricFu::LIB_ROOT,path))
6
+ end
7
+
8
+ class MetricAnalyzer
9
+
10
+ COMMON_COLUMNS = %w{metric}
11
+ GRANULARITIES = %w{file_path class_name method_name}
12
+
13
+ attr_accessor :table
14
+
15
+ def initialize(yaml)
16
+ if(yaml.is_a?(String))
17
+ @yaml = YAML.load(yaml)
18
+ else
19
+ @yaml = yaml
20
+ end
21
+ @file_ranking = MetricFu::Ranking.new
22
+ @class_ranking = MetricFu::Ranking.new
23
+ @method_ranking = MetricFu::Ranking.new
24
+ rankings = [@file_ranking, @class_ranking, @method_ranking]
25
+
26
+ tool_analyzers = [ReekAnalyzer.new, RoodiAnalyzer.new,
27
+ FlogAnalyzer.new, ChurnAnalyzer.new, SaikuroAnalyzer.new,
28
+ FlayAnalyzer.new, StatsAnalyzer.new, RcovAnalyzer.new]
29
+ # TODO There is likely a clash that will happen between
30
+ # column names eventually. We should probably auto-prefix
31
+ # them (e.g. "roodi_problem")
32
+ columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map{|analyzer| analyzer.columns}.flatten
33
+
34
+ @table = make_table(columns)
35
+
36
+ # These tables are an optimization. They contain subsets of the master table.
37
+ # TODO - these should be pushed into the Table class now
38
+ @tool_tables = make_table_hash(columns)
39
+ @file_tables = make_table_hash(columns)
40
+ @class_tables = make_table_hash(columns)
41
+ @method_tables = make_table_hash(columns)
42
+
43
+ tool_analyzers.each do |analyzer|
44
+ analyzer.generate_records(@yaml[analyzer.name], @table)
45
+ end
46
+
47
+ build_lookups!(table)
48
+ process_rows!(table)
49
+
50
+ tool_analyzers.each do |analyzer|
51
+ GRANULARITIES.each do |granularity|
52
+ metric_ranking = calculate_metric_scores(granularity, analyzer)
53
+ add_to_master_ranking(ranking(granularity), metric_ranking, analyzer)
54
+ end
55
+ end
56
+
57
+ rankings.each do |ranking|
58
+ ranking.delete(nil)
59
+ end
60
+ end
61
+
62
+ def location(item, value)
63
+ sub_table = get_sub_table(item, value)
64
+ if(sub_table.length==0)
65
+ raise AnalysisError, "The #{item.to_s} '#{value.to_s}' does not have any rows in the analysis table"
66
+ else
67
+ first_row = sub_table[0]
68
+ case item
69
+ when :class
70
+ MetricFu::Location.get(first_row.file_path, first_row.class_name, nil)
71
+ when :method
72
+ MetricFu::Location.get(first_row.file_path, first_row.class_name, first_row.method_name)
73
+ when :file
74
+ MetricFu::Location.get(first_row.file_path, nil, nil)
75
+ else
76
+ raise ArgumentError, "Item must be :class, :method, or :file"
77
+ end
78
+ end
79
+ end
80
+
81
+ #todo redo as item,value, options = {}
82
+ # Note that the other option for 'details' is :detailed (this isn't
83
+ # at all clear from this method itself
84
+ def problems_with(item, value, details = :summary, exclude_details = [])
85
+ sub_table = get_sub_table(item, value)
86
+ #grouping = Ruport::Data::Grouping.new(sub_table, :by => 'metric')
87
+ grouping = get_grouping(sub_table, :by => 'metric')
88
+ problems = {}
89
+ grouping.each do |metric, table|
90
+ if details == :summary || exclude_details.include?(metric)
91
+ problems[metric] = present_group(metric,table)
92
+ else
93
+ problems[metric] = present_group_details(metric,table)
94
+ end
95
+ end
96
+ problems
97
+ end
98
+
99
+ def worst_methods(size = nil)
100
+ @method_ranking.top(size)
101
+ end
102
+
103
+ def worst_classes(size = nil)
104
+ @class_ranking.top(size)
105
+ end
106
+
107
+ def worst_files(size = nil)
108
+ @file_ranking.top(size)
109
+ end
110
+
111
+ private
112
+
113
+ def get_grouping(table, opts)
114
+ #Ruport::Data::Grouping.new(table, opts)
115
+ MetricFu::Grouping.new(table, opts)
116
+ #@grouping_cache ||= {}
117
+ #@grouping_cache.fetch(grouping_key(table,opts)) do
118
+ # @grouping_cache[grouping_key(table,opts)] = Ruport::Data::Grouping.new(table, opts)
119
+ #end
120
+ end
121
+
122
+ def grouping_key(table, opts)
123
+ "table #{table.object_id} opts #{opts.inspect}"
124
+ end
125
+
126
+ def build_lookups!(table)
127
+ @class_and_method_to_file ||= {}
128
+ # Build a mapping from [class,method] => filename
129
+ # (and make sure the mapping is unique)
130
+ table.each do |row|
131
+ # We know that Saikuro provides the wrong data
132
+ next if row['metric'] == :saikuro
133
+ key = [row['class_name'], row['method_name']]
134
+ file_path = row['file_path']
135
+ @class_and_method_to_file[key] ||= file_path
136
+ end
137
+ end
138
+
139
+ def process_rows!(table)
140
+ # Correct incorrect rows in the table
141
+ table.each do |row|
142
+ row_metric = row['metric'] #perf optimization
143
+ if row_metric == :saikuro
144
+ fix_row_file_path!(row)
145
+ end
146
+ @tool_tables[row_metric] << row
147
+ @file_tables[row["file_path"]] << row
148
+ @class_tables[row["class_name"]] << row
149
+ @method_tables[row["method_name"]] << row
150
+ end
151
+ end
152
+
153
+ def fix_row_file_path!(row)
154
+ # We know that Saikuro rows are broken
155
+ # next unless row['metric'] == :saikuro
156
+ key = [row['class_name'], row['method_name']]
157
+ current_file_path = row['file_path'].to_s
158
+ correct_file_path = @class_and_method_to_file[key]
159
+ if(correct_file_path!=nil && correct_file_path.include?(current_file_path))
160
+ row['file_path'] = correct_file_path
161
+ else
162
+ # There wasn't an exact match, so we can do a substring match
163
+ matching_file_path = file_paths.detect {|file_path|
164
+ file_path!=nil && file_path.include?(current_file_path)
165
+ }
166
+ if(matching_file_path)
167
+ row['file_path'] = matching_file_path
168
+ end
169
+ end
170
+ end
171
+
172
+ def file_paths
173
+ @file_paths ||= @table.column('file_path').uniq
174
+ end
175
+
176
+ def ranking(column_name)
177
+ case column_name
178
+ when "file_path"
179
+ @file_ranking
180
+ when "class_name"
181
+ @class_ranking
182
+ when "method_name"
183
+ @method_ranking
184
+ else
185
+ raise ArgumentError, "Invalid column name #{column_name}"
186
+ end
187
+ end
188
+
189
+ def calculate_metric_scores(granularity, analyzer)
190
+ metric_ranking = MetricFu::Ranking.new
191
+ metric_violations = @tool_tables[analyzer.name]
192
+ metric_violations.each do |row|
193
+ location = row[granularity]
194
+ metric_ranking[location] ||= []
195
+ metric_ranking[location] << analyzer.map(row)
196
+ end
197
+
198
+ metric_ranking.each do |item, scores|
199
+ metric_ranking[item] = analyzer.reduce(scores)
200
+ end
201
+
202
+ metric_ranking
203
+ end
204
+
205
+ def add_to_master_ranking(master_ranking, metric_ranking, analyzer)
206
+ metric_ranking.each do |item, _|
207
+ master_ranking[item] ||= 0
208
+ master_ranking[item] += analyzer.score(metric_ranking, item) # scaling? Do we just add in the raw score?
209
+ end
210
+ end
211
+
212
+ def most_common_column(column_name, size)
213
+ #grouping = Ruport::Data::Grouping.new(@table,
214
+ # :by => column_name,
215
+ # :order => lambda { |g| -g.size})
216
+ get_grouping(@table, :by => column_name, :order => lambda {|g| -g.size})
217
+ values = []
218
+ grouping.each do |value, _|
219
+ values << value if value!=nil
220
+ if(values.size==size)
221
+ break
222
+ end
223
+ end
224
+ return nil if values.empty?
225
+ if(values.size == 1)
226
+ return values.first
227
+ else
228
+ return values
229
+ end
230
+ end
231
+
232
+ # TODO: As we get fancier, the presenter should
233
+ # be its own class, not just a method with a long
234
+ # case statement
235
+ def present_group(metric, group)
236
+ occurences = group.size
237
+ case(metric)
238
+ when :reek
239
+ "found #{occurences} code smells"
240
+ when :roodi
241
+ "found #{occurences} design problems"
242
+ when :churn
243
+ "detected high level of churn (changed #{group[0].times_changed} times)"
244
+ when :flog
245
+ complexity = get_mean(group.column("score"))
246
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
247
+ when :saikuro
248
+ complexity = get_mean(group.column("complexity"))
249
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
250
+ when :flay
251
+ "found #{occurences} code duplications"
252
+ when :rcov
253
+ average_code_uncoverage = get_mean(group.column("percentage_uncovered"))
254
+ "#{"average " if occurences > 1}uncovered code is %.1f%" % average_code_uncoverage
255
+ else
256
+ raise AnalysisError, "Unknown metric #{metric}"
257
+ end
258
+ end
259
+
260
+ def present_group_details(metric, group)
261
+ occurences = group.size
262
+ case(metric)
263
+ when :reek
264
+ message = "found #{occurences} code smells<br/>"
265
+ group.each do |item|
266
+ type = item.data["reek__type_name"]
267
+ reek_message = item.data["reek__message"]
268
+ message << "* #{type}: #{reek_message}<br/>"
269
+ end
270
+ message
271
+ when :roodi
272
+ message = "found #{occurences} design problems<br/>"
273
+ group.each do |item|
274
+ problem = item.data["problems"]
275
+ message << "* #{problem}<br/>"
276
+ end
277
+ message
278
+ when :churn
279
+ "detected high level of churn (changed #{group[0].times_changed} times)"
280
+ when :flog
281
+ complexity = get_mean(group.column("score"))
282
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
283
+ when :saikuro
284
+ complexity = get_mean(group.column("complexity"))
285
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
286
+ when :flay
287
+ message = "found #{occurences} code duplications<br/>"
288
+ group.each do |item|
289
+ problem = item.data["flay_reason"]
290
+ problem = problem.gsub(/^[0-9]*\)/,'')
291
+ problem = problem.gsub(/files\:/,' <br>&nbsp;&nbsp;&nbsp;files:')
292
+ message << "* #{problem}<br/>"
293
+ end
294
+ message
295
+ else
296
+ raise AnalysisError, "Unknown metric #{metric}"
297
+ end
298
+ end
299
+
300
+ def make_table_hash(columns)
301
+ Hash.new { |hash, key|
302
+ hash[key] = make_table(columns)
303
+ }
304
+ end
305
+
306
+ def make_table(columns)
307
+ Table.new(:column_names => columns)
308
+ end
309
+
310
+ def get_sub_table(item, value)
311
+ tables = {
312
+ :class => @class_tables,
313
+ :method => @method_tables,
314
+ :file => @file_tables,
315
+ :tool => @tool_tables
316
+ }.fetch(item) do
317
+ raise ArgumentError, "Item must be :class, :method, or :file"
318
+ end
319
+ tables[value]
320
+ end
321
+
322
+ def get_mean(collection)
323
+ collection_length = collection.length
324
+ sum = 0
325
+ sum = collection.inject( nil ) { |sum,x| sum ? sum+x : x }
326
+ (sum.to_f / collection_length.to_f)
327
+ end
328
+
329
+ end
330
+
331
+
@@ -0,0 +1,34 @@
1
+ require 'forwardable'
2
+ module MetricFu
3
+ class Ranking
4
+ extend Forwardable
5
+
6
+ def initialize
7
+ @items_to_score = {}
8
+ end
9
+
10
+ def top(num=nil)
11
+ if(num.is_a?(Numeric))
12
+ sorted_items[0,num]
13
+ else
14
+ sorted_items
15
+ end
16
+ end
17
+
18
+ def percentile(item)
19
+ index = sorted_items.index(item)
20
+ worse_item_count = (length - (index+1))
21
+ worse_item_count.to_f/length
22
+ end
23
+
24
+ def_delegator :@items_to_score, :has_key?, :scored?
25
+ def_delegators :@items_to_score, :[], :[]=, :length, :each, :delete
26
+
27
+ private
28
+
29
+ def sorted_items
30
+ @sorted_items ||= @items_to_score.sort_by {|item, score| -score}.map {|item, score| item}
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ class RcovAnalyzer
2
+ include ScoringStrategies
3
+
4
+ COLUMNS = %w{percentage_uncovered}
5
+
6
+ def columns
7
+ COLUMNS
8
+ end
9
+
10
+ def name
11
+ :rcov
12
+ end
13
+
14
+ def map(row)
15
+ row.percentage_uncovered
16
+ end
17
+
18
+ def reduce(scores)
19
+ ScoringStrategies.average(scores)
20
+ end
21
+
22
+ def score(metric_ranking, item)
23
+ ScoringStrategies.identity(metric_ranking, item)
24
+ end
25
+
26
+ def generate_records(data, table)
27
+ return if data==nil
28
+ data.each do |file_name, info|
29
+ next if (file_name == :global_percent_run) || (info[:methods].nil?)
30
+ info[:methods].each do |method_name, percentage_uncovered|
31
+ location = MetricFu::Location.for(method_name)
32
+ table << {
33
+ "metric" => :rcov,
34
+ 'file_path' => file_name,
35
+ 'class_name' => location.class_name,
36
+ "method_name" => location.method_name,
37
+ "percentage_uncovered" => percentage_uncovered
38
+ }
39
+ end
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,43 @@
1
+ module MetricFu
2
+ class Record
3
+
4
+ attr_reader :data
5
+
6
+ def initialize(data, columns)
7
+ @data = data
8
+ @columns = columns
9
+ end
10
+
11
+ def method_missing(name, *args, &block)
12
+ key = name.to_s
13
+ if @data.has_key?(key)
14
+ @data[key]
15
+ elsif @columns.member?(key)
16
+ nil
17
+ else
18
+ super(name, *args, &block)
19
+ end
20
+ end
21
+
22
+ def []=(key, value)
23
+ @data[key]=value
24
+ end
25
+
26
+ def [](key)
27
+ @data[key]
28
+ end
29
+
30
+ def keys
31
+ @data.keys
32
+ end
33
+
34
+ def has_key?(key)
35
+ @data.has_key?(key)
36
+ end
37
+
38
+ def attributes
39
+ @columns
40
+ end
41
+
42
+ end
43
+ end