rferraz-metric_fu 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. data/HISTORY +237 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README +29 -0
  4. data/Rakefile +18 -0
  5. data/TODO +6 -0
  6. data/lib/base/base_template.rb +172 -0
  7. data/lib/base/churn_analyzer.rb +38 -0
  8. data/lib/base/code_issue.rb +97 -0
  9. data/lib/base/configuration.rb +199 -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/line_numbers.rb +74 -0
  15. data/lib/base/location.rb +85 -0
  16. data/lib/base/md5_tracker.rb +52 -0
  17. data/lib/base/metric_analyzer.rb +404 -0
  18. data/lib/base/ranking.rb +34 -0
  19. data/lib/base/rcov_analyzer.rb +43 -0
  20. data/lib/base/reek_analyzer.rb +163 -0
  21. data/lib/base/report.rb +108 -0
  22. data/lib/base/roodi_analyzer.rb +37 -0
  23. data/lib/base/saikuro_analyzer.rb +48 -0
  24. data/lib/base/scoring_strategies.rb +29 -0
  25. data/lib/base/stats_analyzer.rb +37 -0
  26. data/lib/base/table.rb +102 -0
  27. data/lib/generators/churn.rb +28 -0
  28. data/lib/generators/flay.rb +31 -0
  29. data/lib/generators/flog.rb +111 -0
  30. data/lib/generators/hotspots.rb +52 -0
  31. data/lib/generators/rails_best_practices.rb +53 -0
  32. data/lib/generators/rcov.rb +122 -0
  33. data/lib/generators/reek.rb +81 -0
  34. data/lib/generators/roodi.rb +35 -0
  35. data/lib/generators/saikuro.rb +256 -0
  36. data/lib/generators/stats.rb +58 -0
  37. data/lib/graphs/engines/bluff.rb +113 -0
  38. data/lib/graphs/engines/gchart.rb +157 -0
  39. data/lib/graphs/flay_grapher.rb +18 -0
  40. data/lib/graphs/flog_grapher.rb +57 -0
  41. data/lib/graphs/grapher.rb +11 -0
  42. data/lib/graphs/rails_best_practices_grapher.rb +19 -0
  43. data/lib/graphs/rcov_grapher.rb +18 -0
  44. data/lib/graphs/reek_grapher.rb +30 -0
  45. data/lib/graphs/roodi_grapher.rb +18 -0
  46. data/lib/graphs/stats_grapher.rb +20 -0
  47. data/lib/metric_fu.rb +40 -0
  48. data/lib/templates/awesome/awesome_template.rb +73 -0
  49. data/lib/templates/awesome/churn.html.erb +58 -0
  50. data/lib/templates/awesome/css/buttons.css +82 -0
  51. data/lib/templates/awesome/css/default.css +91 -0
  52. data/lib/templates/awesome/css/integrity.css +334 -0
  53. data/lib/templates/awesome/css/reset.css +7 -0
  54. data/lib/templates/awesome/css/syntax.css +19 -0
  55. data/lib/templates/awesome/flay.html.erb +34 -0
  56. data/lib/templates/awesome/flog.html.erb +55 -0
  57. data/lib/templates/awesome/hotspots.html.erb +62 -0
  58. data/lib/templates/awesome/index.html.erb +34 -0
  59. data/lib/templates/awesome/layout.html.erb +30 -0
  60. data/lib/templates/awesome/rails_best_practices.html.erb +27 -0
  61. data/lib/templates/awesome/rcov.html.erb +42 -0
  62. data/lib/templates/awesome/reek.html.erb +40 -0
  63. data/lib/templates/awesome/roodi.html.erb +27 -0
  64. data/lib/templates/awesome/saikuro.html.erb +71 -0
  65. data/lib/templates/awesome/stats.html.erb +51 -0
  66. data/lib/templates/javascripts/bluff-min.js +1 -0
  67. data/lib/templates/javascripts/excanvas.js +35 -0
  68. data/lib/templates/javascripts/js-class.js +1 -0
  69. data/lib/templates/standard/churn.html.erb +31 -0
  70. data/lib/templates/standard/default.css +64 -0
  71. data/lib/templates/standard/flay.html.erb +34 -0
  72. data/lib/templates/standard/flog.html.erb +57 -0
  73. data/lib/templates/standard/hotspots.html.erb +54 -0
  74. data/lib/templates/standard/index.html.erb +41 -0
  75. data/lib/templates/standard/rails_best_practices.html.erb +29 -0
  76. data/lib/templates/standard/rcov.html.erb +43 -0
  77. data/lib/templates/standard/reek.html.erb +42 -0
  78. data/lib/templates/standard/roodi.html.erb +29 -0
  79. data/lib/templates/standard/saikuro.html.erb +84 -0
  80. data/lib/templates/standard/standard_template.rb +26 -0
  81. data/lib/templates/standard/stats.html.erb +55 -0
  82. data/spec/base/base_template_spec.rb +177 -0
  83. data/spec/base/configuration_spec.rb +276 -0
  84. data/spec/base/generator_spec.rb +223 -0
  85. data/spec/base/graph_spec.rb +61 -0
  86. data/spec/base/line_numbers_spec.rb +62 -0
  87. data/spec/base/md5_tracker_spec.rb +57 -0
  88. data/spec/base/report_spec.rb +146 -0
  89. data/spec/generators/churn_spec.rb +41 -0
  90. data/spec/generators/flay_spec.rb +105 -0
  91. data/spec/generators/flog_spec.rb +70 -0
  92. data/spec/generators/rails_best_practices_spec.rb +52 -0
  93. data/spec/generators/rcov_spec.rb +180 -0
  94. data/spec/generators/reek_spec.rb +134 -0
  95. data/spec/generators/roodi_spec.rb +24 -0
  96. data/spec/generators/saikuro_spec.rb +74 -0
  97. data/spec/generators/stats_spec.rb +74 -0
  98. data/spec/graphs/engines/bluff_spec.rb +19 -0
  99. data/spec/graphs/engines/gchart_spec.rb +156 -0
  100. data/spec/graphs/flay_grapher_spec.rb +56 -0
  101. data/spec/graphs/flog_grapher_spec.rb +108 -0
  102. data/spec/graphs/rails_best_practices_grapher_spec.rb +61 -0
  103. data/spec/graphs/rcov_grapher_spec.rb +56 -0
  104. data/spec/graphs/reek_grapher_spec.rb +65 -0
  105. data/spec/graphs/roodi_grapher_spec.rb +56 -0
  106. data/spec/graphs/stats_grapher_spec.rb +68 -0
  107. data/spec/resources/line_numbers/foo.rb +33 -0
  108. data/spec/resources/line_numbers/module.rb +11 -0
  109. data/spec/resources/line_numbers/module_surrounds_class.rb +15 -0
  110. data/spec/resources/line_numbers/two_classes.rb +11 -0
  111. data/spec/resources/saikuro/app/controllers/sessions_controller.rb_cyclo.html +10 -0
  112. data/spec/resources/saikuro/app/controllers/users_controller.rb_cyclo.html +16 -0
  113. data/spec/resources/saikuro/index_cyclo.html +155 -0
  114. data/spec/resources/saikuro_sfiles/thing.rb_cyclo.html +11 -0
  115. data/spec/resources/yml/20090630.yml +7922 -0
  116. data/spec/resources/yml/metric_missing.yml +1 -0
  117. data/spec/spec.opts +6 -0
  118. data/spec/spec_helper.rb +7 -0
  119. data/tasks/metric_fu.rake +22 -0
  120. metadata +448 -0
@@ -0,0 +1,52 @@
1
+ require 'digest/md5'
2
+ require 'fileutils'
3
+
4
+ module MetricFu
5
+ class MD5Tracker
6
+
7
+ @@unchanged_md5s = []
8
+
9
+ class << self
10
+ def md5_dir(path_to_file, base_dir)
11
+ File.join(base_dir,
12
+ path_to_file.split('/')[0..-2].join('/'))
13
+ end
14
+
15
+ def md5_file(path_to_file, base_dir)
16
+ File.join(md5_dir(path_to_file, base_dir),
17
+ path_to_file.split('/').last.sub(/\.[a-z]+/, '.md5'))
18
+ end
19
+
20
+ def track(path_to_file, base_dir)
21
+ md5 = Digest::MD5.hexdigest(File.read(path_to_file))
22
+ FileUtils.mkdir_p(md5_dir(path_to_file, base_dir), :verbose => false)
23
+ f = File.new(md5_file(path_to_file, base_dir), "w")
24
+ f.puts(md5)
25
+ f.close
26
+ md5
27
+ end
28
+
29
+ def file_changed?(path_to_file, base_dir)
30
+ orig_md5_file = md5_file(path_to_file, base_dir)
31
+ return !!track(path_to_file, base_dir) unless File.exist?(orig_md5_file)
32
+
33
+ current_md5 = ""
34
+ file = File.open(orig_md5_file, 'r')
35
+ file.each_line { |line| current_md5 << line }
36
+ file.close
37
+ current_md5.chomp!
38
+
39
+ new_md5 = Digest::MD5.hexdigest(File.read(path_to_file))
40
+ new_md5.chomp!
41
+
42
+ @@unchanged_md5s << path_to_file if new_md5 == current_md5
43
+
44
+ return new_md5 != current_md5
45
+ end
46
+
47
+ def file_already_counted?(path_to_file)
48
+ return @@unchanged_md5s.include?(path_to_file)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,404 @@
1
+ class AnalysisError < RuntimeError; end;
2
+
3
+ class MetricAnalyzer
4
+
5
+ COMMON_COLUMNS = %w{metric}
6
+ GRANULARITIES = %w{file_path class_name method_name}
7
+
8
+ attr_accessor :table
9
+
10
+ def initialize(yaml)
11
+ if(yaml.is_a?(String))
12
+ @yaml = YAML.load(yaml)
13
+ else
14
+ @yaml = yaml
15
+ end
16
+ @file_ranking = MetricFu::Ranking.new
17
+ @class_ranking = MetricFu::Ranking.new
18
+ @method_ranking = MetricFu::Ranking.new
19
+ rankings = [@file_ranking, @class_ranking, @method_ranking]
20
+
21
+ tool_analyzers = [ReekAnalyzer.new, RoodiAnalyzer.new,
22
+ FlogAnalyzer.new, ChurnAnalyzer.new, SaikuroAnalyzer.new,
23
+ FlayAnalyzer.new, StatsAnalyzer.new, RcovAnalyzer.new]
24
+ # TODO There is likely a clash that will happen between
25
+ # column names eventually. We should probably auto-prefix
26
+ # them (e.g. "roodi_problem")
27
+ columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map{|analyzer| analyzer.columns}.flatten
28
+
29
+ @table = make_table(columns)
30
+
31
+ # These tables are an optimization. They contain subsets of the master table.
32
+ # TODO - these should be pushed into the Table class now
33
+ @tool_tables = make_table_hash(columns)
34
+ @file_tables = make_table_hash(columns)
35
+ @class_tables = make_table_hash(columns)
36
+ @method_tables = make_table_hash(columns)
37
+
38
+ tool_analyzers.each do |analyzer|
39
+ analyzer.generate_records(@yaml[analyzer.name], @table)
40
+ end
41
+
42
+ build_lookups!(table)
43
+ process_rows!(table)
44
+
45
+ tool_analyzers.each do |analyzer|
46
+ GRANULARITIES.each do |granularity|
47
+ metric_ranking = calculate_metric_scores(granularity, analyzer)
48
+ add_to_master_ranking(ranking(granularity), metric_ranking, analyzer)
49
+ end
50
+ end
51
+
52
+ rankings.each do |ranking|
53
+ ranking.delete(nil)
54
+ end
55
+ end
56
+
57
+ def location(item, value)
58
+ sub_table = get_sub_table(item, value)
59
+ if(sub_table.length==0)
60
+ raise AnalysisError, "The #{item.to_s} '#{value.to_s}' does not have any rows in the analysis table"
61
+ else
62
+ first_row = sub_table[0]
63
+ case item
64
+ when :class
65
+ MetricFu::Location.get(first_row.file_path, first_row.class_name, nil)
66
+ when :method
67
+ MetricFu::Location.get(first_row.file_path, first_row.class_name, first_row.method_name)
68
+ when :file
69
+ MetricFu::Location.get(first_row.file_path, nil, nil)
70
+ else
71
+ raise ArgumentError, "Item must be :class, :method, or :file"
72
+ end
73
+ end
74
+ end
75
+
76
+ #todo redo as item,value, options = {}
77
+ # Note that the other option for 'details' is :detailed (this isn't
78
+ # at all clear from this method itself
79
+ def problems_with(item, value, details = :summary, exclude_details = [])
80
+ sub_table = get_sub_table(item, value)
81
+ #grouping = Ruport::Data::Grouping.new(sub_table, :by => 'metric')
82
+ grouping = get_grouping(sub_table, :by => 'metric')
83
+ problems = {}
84
+ grouping.each do |metric, table|
85
+ if details == :summary || exclude_details.include?(metric)
86
+ problems[metric] = present_group(metric,table)
87
+ else
88
+ problems[metric] = present_group_details(metric,table)
89
+ end
90
+ end
91
+ problems
92
+ end
93
+
94
+ def worst_methods(size = nil)
95
+ @method_ranking.top(size)
96
+ end
97
+
98
+ def worst_classes(size = nil)
99
+ @class_ranking.top(size)
100
+ end
101
+
102
+ def worst_files(size = nil)
103
+ @file_ranking.top(size)
104
+ end
105
+
106
+ private
107
+
108
+ def get_grouping(table, opts)
109
+ #Ruport::Data::Grouping.new(table, opts)
110
+ Grouping.new(table, opts)
111
+ #@grouping_cache ||= {}
112
+ #@grouping_cache.fetch(grouping_key(table,opts)) do
113
+ # @grouping_cache[grouping_key(table,opts)] = Ruport::Data::Grouping.new(table, opts)
114
+ #end
115
+ end
116
+
117
+ def grouping_key(table, opts)
118
+ "table #{table.object_id} opts #{opts.inspect}"
119
+ end
120
+
121
+ def build_lookups!(table)
122
+ @class_and_method_to_file ||= {}
123
+ # Build a mapping from [class,method] => filename
124
+ # (and make sure the mapping is unique)
125
+ table.each do |row|
126
+ # We know that Saikuro provides the wrong data
127
+ next if row['metric'] == :saikuro
128
+ key = [row['class_name'], row['method_name']]
129
+ file_path = row['file_path']
130
+ @class_and_method_to_file[key] ||= file_path
131
+ end
132
+ end
133
+
134
+ def process_rows!(table)
135
+ # Correct incorrect rows in the table
136
+ table.each do |row|
137
+ row_metric = row['metric'] #perf optimization
138
+ if row_metric == :saikuro
139
+ fix_row_file_path!(row)
140
+ end
141
+ @tool_tables[row_metric] << row
142
+ @file_tables[row["file_path"]] << row
143
+ @class_tables[row["class_name"]] << row
144
+ @method_tables[row["method_name"]] << row
145
+ end
146
+ end
147
+
148
+ def fix_row_file_path!(row)
149
+ # We know that Saikuro rows are broken
150
+ # next unless row['metric'] == :saikuro
151
+ key = [row['class_name'], row['method_name']]
152
+ current_file_path = row['file_path'].to_s
153
+ correct_file_path = @class_and_method_to_file[key]
154
+ if(correct_file_path!=nil && correct_file_path.include?(current_file_path))
155
+ row['file_path'] = correct_file_path
156
+ else
157
+ # There wasn't an exact match, so we can do a substring match
158
+ matching_file_path = file_paths.detect {|file_path|
159
+ file_path!=nil && file_path.include?(current_file_path)
160
+ }
161
+ if(matching_file_path)
162
+ row['file_path'] = matching_file_path
163
+ end
164
+ end
165
+ end
166
+
167
+ def file_paths
168
+ @file_paths ||= @table.column('file_path').uniq
169
+ end
170
+
171
+ def ranking(column_name)
172
+ case column_name
173
+ when "file_path"
174
+ @file_ranking
175
+ when "class_name"
176
+ @class_ranking
177
+ when "method_name"
178
+ @method_ranking
179
+ else
180
+ raise ArgumentError, "Invalid column name #{column_name}"
181
+ end
182
+ end
183
+
184
+ def calculate_metric_scores(granularity, analyzer)
185
+ metric_ranking = MetricFu::Ranking.new
186
+ metric_violations = @tool_tables[analyzer.name]
187
+ metric_violations.each do |row|
188
+ location = row[granularity]
189
+ metric_ranking[location] ||= []
190
+ metric_ranking[location] << analyzer.map(row)
191
+ end
192
+
193
+ metric_ranking.each do |item, scores|
194
+ metric_ranking[item] = analyzer.reduce(scores)
195
+ end
196
+
197
+ metric_ranking
198
+ end
199
+
200
+ def add_to_master_ranking(master_ranking, metric_ranking, analyzer)
201
+ metric_ranking.each do |item, _|
202
+ master_ranking[item] ||= 0
203
+ master_ranking[item] += analyzer.score(metric_ranking, item) # scaling? Do we just add in the raw score?
204
+ end
205
+ end
206
+
207
+ def most_common_column(column_name, size)
208
+ #grouping = Ruport::Data::Grouping.new(@table,
209
+ # :by => column_name,
210
+ # :order => lambda { |g| -g.size})
211
+ get_grouping(@table, :by => column_name, :order => lambda {|g| -g.size})
212
+ values = []
213
+ grouping.each do |value, _|
214
+ values << value if value!=nil
215
+ if(values.size==size)
216
+ break
217
+ end
218
+ end
219
+ return nil if values.empty?
220
+ if(values.size == 1)
221
+ return values.first
222
+ else
223
+ return values
224
+ end
225
+ end
226
+
227
+ # TODO: As we get fancier, the presenter should
228
+ # be its own class, not just a method with a long
229
+ # case statement
230
+ def present_group(metric, group)
231
+ occurences = group.size
232
+ case(metric)
233
+ when :reek
234
+ "found #{occurences} code smells"
235
+ when :roodi
236
+ "found #{occurences} design problems"
237
+ when :churn
238
+ "detected high level of churn (changed #{group[0].times_changed} times)"
239
+ when :flog
240
+ complexity = get_mean(group.column("score"))
241
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
242
+ when :saikuro
243
+ complexity = get_mean(group.column("complexity"))
244
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
245
+ when :flay
246
+ "found #{occurences} code duplications"
247
+ when :rcov
248
+ average_code_uncoverage = get_mean(group.column("percentage_uncovered"))
249
+ "#{"average " if occurences > 1}uncovered code is %.1f%" % average_code_uncoverage
250
+ else
251
+ raise AnalysisError, "Unknown metric #{metric}"
252
+ end
253
+ end
254
+
255
+ def present_group_details(metric, group)
256
+ occurences = group.size
257
+ case(metric)
258
+ when :reek
259
+ message = "found #{occurences} code smells<br/>"
260
+ group.each do |item|
261
+ type = item.data["reek__type_name"]
262
+ reek_message = item.data["reek__message"]
263
+ message << "* #{type}: #{reek_message}<br/>"
264
+ end
265
+ message
266
+ when :roodi
267
+ message = "found #{occurences} design problems<br/>"
268
+ group.each do |item|
269
+ problem = item.data["problems"]
270
+ message << "* #{problem}<br/>"
271
+ end
272
+ message
273
+ when :churn
274
+ "detected high level of churn (changed #{group[0].times_changed} times)"
275
+ when :flog
276
+ complexity = get_mean(group.column("score"))
277
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
278
+ when :saikuro
279
+ complexity = get_mean(group.column("complexity"))
280
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
281
+ when :flay
282
+ message = "found #{occurences} code duplications<br/>"
283
+ group.each do |item|
284
+ problem = item.data["flay_reason"]
285
+ problem = problem.gsub(/^[0-9]*\)/,'')
286
+ problem = problem.gsub(/files\:/,' <br>&nbsp;&nbsp;&nbsp;files:')
287
+ message << "* #{problem}<br/>"
288
+ end
289
+ message
290
+ else
291
+ raise AnalysisError, "Unknown metric #{metric}"
292
+ end
293
+ end
294
+
295
+ def make_table_hash(columns)
296
+ Hash.new { |hash, key|
297
+ hash[key] = make_table(columns)
298
+ }
299
+ end
300
+
301
+ def make_table(columns)
302
+ Table.new(:column_names => columns)
303
+ end
304
+
305
+ def get_sub_table(item, value)
306
+ tables = {
307
+ :class => @class_tables,
308
+ :method => @method_tables,
309
+ :file => @file_tables,
310
+ :tool => @tool_tables
311
+ }.fetch(item) do
312
+ raise ArgumentError, "Item must be :class, :method, or :file"
313
+ end
314
+ tables[value]
315
+ end
316
+
317
+ def get_mean(collection)
318
+ collection_length = collection.length
319
+ sum = 0
320
+ sum = collection.inject( nil ) { |sum,x| sum ? sum+x : x }
321
+ (sum.to_f / collection_length.to_f)
322
+ end
323
+
324
+ end
325
+
326
+ class Record
327
+
328
+ attr_reader :data
329
+
330
+ def initialize(data, columns)
331
+ @data = data
332
+ @columns = columns
333
+ end
334
+
335
+ def method_missing(name, *args, &block)
336
+ key = name.to_s
337
+ if @data.has_key?(key)
338
+ @data[key]
339
+ elsif @columns.member?(key)
340
+ nil
341
+ else
342
+ super(name, *args, &block)
343
+ end
344
+ end
345
+
346
+ def []=(key, value)
347
+ @data[key]=value
348
+ end
349
+
350
+ def [](key)
351
+ @data[key]
352
+ end
353
+
354
+ def keys
355
+ @data.keys
356
+ end
357
+
358
+ def has_key?(key)
359
+ @data.has_key?(key)
360
+ end
361
+
362
+ def attributes
363
+ @columns
364
+ end
365
+
366
+ end
367
+
368
+ class Grouping
369
+
370
+ def initialize(table, opts)
371
+ column_name = opts.fetch(:by)
372
+ order = opts.fetch(:order) { nil }
373
+ hash = {}
374
+ if column_name.to_sym == :metric # special optimized case
375
+ hash = table.group_by_metric
376
+ else
377
+ table.each do |row|
378
+ hash[row[column_name]] ||= Table.new(:column_names => row.attributes)
379
+ hash[row[column_name]] << row
380
+ end
381
+ end
382
+ if order
383
+ @arr = hash.sort_by &order
384
+ else
385
+ @arr = hash.to_a
386
+ end
387
+ end
388
+
389
+ def [](key)
390
+ @arr.each do |group_key, table|
391
+ return table if group_key == key
392
+ end
393
+ return nil
394
+ end
395
+
396
+ def each
397
+ @arr.each do |value, rows|
398
+ yield value, rows
399
+ end
400
+ end
401
+
402
+ end
403
+
404
+
@@ -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,163 @@
1
+ # coding: utf-8
2
+
3
+ class ReekAnalyzer
4
+ include ScoringStrategies
5
+
6
+ REEK_ISSUE_INFO = {
7
+ 'Uncommunicative Name' =>
8
+ {'link' => 'http://wiki.github.com/kevinrutherford/reek/uncommunicative-name',
9
+ 'info' => 'An Uncommunicative Name is a name that doesn’t communicate its intent well enough.'},
10
+ 'Class Variable' =>
11
+ {'link' => 'http://wiki.github.com/kevinrutherford/reek/class-variable',
12
+ 'info' => 'Class variables form part of the global runtime state, and as such make it ' +
13
+ 'easy for one part of the system to accidentally or inadvertently depend on ' +
14
+ 'another part of the system.'},
15
+ 'Duplication' =>
16
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/duplication',
17
+ 'info' => 'Duplication occurs when two fragments of code look nearly identical, or when ' +
18
+ 'two fragments of code have nearly identical effects at some conceptual level.'},
19
+ 'Low Cohesion' =>
20
+ {'link' => 'http://en.wikipedia.org/wiki/Cohesion_(computer_science)',
21
+ 'info' => 'Low cohesion is associated with undesirable traits such as being difficult to ' +
22
+ 'maintain, difficult to test, difficult to reuse, and even difficult to understand.'},
23
+ 'Nested Iterators' =>
24
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/nested-iterators',
25
+ 'info' => 'Nested Iterator occurs when a block contains another block.'},
26
+ 'Control Couple' =>
27
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/control-couple',
28
+ 'info' => 'Control coupling occurs when a method or block checks the value of a parameter in ' +
29
+ 'order to decide which execution path to take. The offending parameter is often called a “Control Couple”.'},
30
+ 'Irresponsible Module' =>
31
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/irresponsible-module',
32
+ 'info' => 'Classes and modules are the units of reuse and release. It is therefore considered ' +
33
+ 'good practice to annotate every class and module with a brief comment outlining its responsibilities.'},
34
+ 'Long Parameter List' =>
35
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/long-parameter-list',
36
+ 'info' => 'A Long Parameter List occurs when a method has more than one or two parameters, ' +
37
+ 'or when a method yields more than one or two objects to an associated block.'},
38
+ 'Data Clump' =>
39
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/data-clump',
40
+ 'info' => 'In general, a Data Clump occurs when the same two or three items frequently appear ' +
41
+ 'together in classes and parameter lists, or when a group of instance variable names ' +
42
+ 'start or end with similar substrings.'},
43
+ 'Simulated Polymorphism' =>
44
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/simulated-polymorphism',
45
+ 'info' => 'Simulated Polymorphism occurs when, code uses a case statement (especially on a ' +
46
+ 'type field) or code uses instance_of?, kind_of?, is_a?, or === to decide what code to execute'},
47
+ 'Large Class' =>
48
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/large-class',
49
+ 'info' => 'A Large Class is a class or module that has a large number of instance variables, ' +
50
+ 'methods or lines of code in any one piece of its specification.'},
51
+ 'Long Method' =>
52
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/long-method',
53
+ 'info' => 'Long methods can be hard to read and understand. They often are harder to test and ' +
54
+ 'maintain as well, which can lead to buggier code.'},
55
+ 'Feature Envy' =>
56
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/feature-envy',
57
+ 'info' => 'Feature Envy occurs when a code fragment references another object more often than ' +
58
+ 'it references itself, or when several clients do the same series of manipulations ' +
59
+ 'on a particular type of object.'},
60
+ 'Utility Function' =>
61
+ {'link' =>'http://wiki.github.com/kevinrutherford/reek/utility-function',
62
+ 'info' => 'A Utility Function is any instance method that has no dependency on the state of the ' +
63
+ 'instance. It reduces the code’s ability to communicate intent. Code that “belongs” on ' +
64
+ 'one class but which is located in another can be hard to find.'},
65
+ 'Attribute' =>
66
+ {'link' => 'http://wiki.github.com/kevinrutherford/reek/attribute',
67
+ 'info' => 'A class that publishes a getter or setter for an instance variable invites client ' +
68
+ 'classes to become too intimate with its inner workings, and in particular with its ' +
69
+ 'representation of state.'}
70
+ }
71
+
72
+ # Note that in practice, the prefix reek__ is appended to each one
73
+ # This was a partially implemented idea to avoid column name collisions
74
+ # but it is only done in the ReekAnalyzer
75
+ COLUMNS = %w{type_name message value value_description comparable_message}
76
+
77
+ def self.issue_link(issue)
78
+ REEK_ISSUE_INFO[issue]
79
+ end
80
+
81
+ def columns
82
+ COLUMNS.map{|column| "#{name}__#{column}"}
83
+ end
84
+
85
+ def name
86
+ :reek
87
+ end
88
+
89
+ def map(row)
90
+ ScoringStrategies.present(row)
91
+ end
92
+
93
+ def reduce(scores)
94
+ ScoringStrategies.sum(scores)
95
+ end
96
+
97
+ def score(metric_ranking, item)
98
+ ScoringStrategies.percentile(metric_ranking, item)
99
+ end
100
+
101
+ def generate_records(data, table)
102
+ return if data==nil
103
+ data[:matches].each do |match|
104
+ file_path = match[:file_path]
105
+ match[:code_smells].each do |smell|
106
+ location = MetricFu::Location.for(smell[:method])
107
+ smell_type = smell[:type]
108
+ message = smell[:message]
109
+ table << {
110
+ "metric" => name, # important
111
+ "file_path" => file_path, # important
112
+ # NOTE: ReekAnalyzer is currently different than other analyzers with regard
113
+ # to column name. Note the COLUMNS constant and #columns method
114
+ "reek__message" => message,
115
+ "reek__type_name" => smell_type,
116
+ "reek__value" => parse_value(message),
117
+ "reek__value_description" => build_value_description(smell_type, message),
118
+ "reek__comparable_message" => comparable_message(smell_type, message),
119
+ "class_name" => location.class_name, # important
120
+ "method_name" => location.method_name, # important
121
+ }
122
+ end
123
+ end
124
+ end
125
+
126
+ def self.numeric_smell?(type)
127
+ ["Large Class", "Long Method", "Long Parameter List"].include?(type)
128
+ end
129
+
130
+ private
131
+
132
+ def comparable_message(type_name, message)
133
+ if self.class.numeric_smell?(type_name)
134
+ match = message.match(/\d+/)
135
+ if(match)
136
+ match.pre_match + match.post_match
137
+ else
138
+ message
139
+ end
140
+ else
141
+ message
142
+ end
143
+ end
144
+
145
+ def build_value_description(type_name, message)
146
+ item_type = message.match(/\d+ (.*)$/)
147
+ if(item_type)
148
+ "number of #{item_type[1]} in #{type_name.downcase}"
149
+ else
150
+ nil
151
+ end
152
+ end
153
+
154
+ def parse_value(message)
155
+ match = message.match(/\d+/)
156
+ if(match)
157
+ match[0].to_i
158
+ else
159
+ nil
160
+ end
161
+ end
162
+
163
+ end