rferraz-metric_fu 2.1.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 (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