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.
- data/HISTORY +237 -0
- data/MIT-LICENSE +22 -0
- data/README +29 -0
- data/Rakefile +18 -0
- data/TODO +6 -0
- data/lib/base/base_template.rb +172 -0
- data/lib/base/churn_analyzer.rb +38 -0
- data/lib/base/code_issue.rb +97 -0
- data/lib/base/configuration.rb +199 -0
- data/lib/base/flay_analyzer.rb +50 -0
- data/lib/base/flog_analyzer.rb +43 -0
- data/lib/base/generator.rb +166 -0
- data/lib/base/graph.rb +44 -0
- data/lib/base/line_numbers.rb +74 -0
- data/lib/base/location.rb +85 -0
- data/lib/base/md5_tracker.rb +52 -0
- data/lib/base/metric_analyzer.rb +404 -0
- data/lib/base/ranking.rb +34 -0
- data/lib/base/rcov_analyzer.rb +43 -0
- data/lib/base/reek_analyzer.rb +163 -0
- data/lib/base/report.rb +108 -0
- data/lib/base/roodi_analyzer.rb +37 -0
- data/lib/base/saikuro_analyzer.rb +48 -0
- data/lib/base/scoring_strategies.rb +29 -0
- data/lib/base/stats_analyzer.rb +37 -0
- data/lib/base/table.rb +102 -0
- data/lib/generators/churn.rb +28 -0
- data/lib/generators/flay.rb +31 -0
- data/lib/generators/flog.rb +111 -0
- data/lib/generators/hotspots.rb +52 -0
- data/lib/generators/rails_best_practices.rb +53 -0
- data/lib/generators/rcov.rb +122 -0
- data/lib/generators/reek.rb +81 -0
- data/lib/generators/roodi.rb +35 -0
- data/lib/generators/saikuro.rb +256 -0
- data/lib/generators/stats.rb +58 -0
- data/lib/graphs/engines/bluff.rb +113 -0
- data/lib/graphs/engines/gchart.rb +157 -0
- data/lib/graphs/flay_grapher.rb +18 -0
- data/lib/graphs/flog_grapher.rb +57 -0
- data/lib/graphs/grapher.rb +11 -0
- data/lib/graphs/rails_best_practices_grapher.rb +19 -0
- data/lib/graphs/rcov_grapher.rb +18 -0
- data/lib/graphs/reek_grapher.rb +30 -0
- data/lib/graphs/roodi_grapher.rb +18 -0
- data/lib/graphs/stats_grapher.rb +20 -0
- data/lib/metric_fu.rb +40 -0
- data/lib/templates/awesome/awesome_template.rb +73 -0
- data/lib/templates/awesome/churn.html.erb +58 -0
- data/lib/templates/awesome/css/buttons.css +82 -0
- data/lib/templates/awesome/css/default.css +91 -0
- data/lib/templates/awesome/css/integrity.css +334 -0
- data/lib/templates/awesome/css/reset.css +7 -0
- data/lib/templates/awesome/css/syntax.css +19 -0
- data/lib/templates/awesome/flay.html.erb +34 -0
- data/lib/templates/awesome/flog.html.erb +55 -0
- data/lib/templates/awesome/hotspots.html.erb +62 -0
- data/lib/templates/awesome/index.html.erb +34 -0
- data/lib/templates/awesome/layout.html.erb +30 -0
- data/lib/templates/awesome/rails_best_practices.html.erb +27 -0
- data/lib/templates/awesome/rcov.html.erb +42 -0
- data/lib/templates/awesome/reek.html.erb +40 -0
- data/lib/templates/awesome/roodi.html.erb +27 -0
- data/lib/templates/awesome/saikuro.html.erb +71 -0
- data/lib/templates/awesome/stats.html.erb +51 -0
- data/lib/templates/javascripts/bluff-min.js +1 -0
- data/lib/templates/javascripts/excanvas.js +35 -0
- data/lib/templates/javascripts/js-class.js +1 -0
- data/lib/templates/standard/churn.html.erb +31 -0
- data/lib/templates/standard/default.css +64 -0
- data/lib/templates/standard/flay.html.erb +34 -0
- data/lib/templates/standard/flog.html.erb +57 -0
- data/lib/templates/standard/hotspots.html.erb +54 -0
- data/lib/templates/standard/index.html.erb +41 -0
- data/lib/templates/standard/rails_best_practices.html.erb +29 -0
- data/lib/templates/standard/rcov.html.erb +43 -0
- data/lib/templates/standard/reek.html.erb +42 -0
- data/lib/templates/standard/roodi.html.erb +29 -0
- data/lib/templates/standard/saikuro.html.erb +84 -0
- data/lib/templates/standard/standard_template.rb +26 -0
- data/lib/templates/standard/stats.html.erb +55 -0
- data/spec/base/base_template_spec.rb +177 -0
- data/spec/base/configuration_spec.rb +276 -0
- data/spec/base/generator_spec.rb +223 -0
- data/spec/base/graph_spec.rb +61 -0
- data/spec/base/line_numbers_spec.rb +62 -0
- data/spec/base/md5_tracker_spec.rb +57 -0
- data/spec/base/report_spec.rb +146 -0
- data/spec/generators/churn_spec.rb +41 -0
- data/spec/generators/flay_spec.rb +105 -0
- data/spec/generators/flog_spec.rb +70 -0
- data/spec/generators/rails_best_practices_spec.rb +52 -0
- data/spec/generators/rcov_spec.rb +180 -0
- data/spec/generators/reek_spec.rb +134 -0
- data/spec/generators/roodi_spec.rb +24 -0
- data/spec/generators/saikuro_spec.rb +74 -0
- data/spec/generators/stats_spec.rb +74 -0
- data/spec/graphs/engines/bluff_spec.rb +19 -0
- data/spec/graphs/engines/gchart_spec.rb +156 -0
- data/spec/graphs/flay_grapher_spec.rb +56 -0
- data/spec/graphs/flog_grapher_spec.rb +108 -0
- data/spec/graphs/rails_best_practices_grapher_spec.rb +61 -0
- data/spec/graphs/rcov_grapher_spec.rb +56 -0
- data/spec/graphs/reek_grapher_spec.rb +65 -0
- data/spec/graphs/roodi_grapher_spec.rb +56 -0
- data/spec/graphs/stats_grapher_spec.rb +68 -0
- data/spec/resources/line_numbers/foo.rb +33 -0
- data/spec/resources/line_numbers/module.rb +11 -0
- data/spec/resources/line_numbers/module_surrounds_class.rb +15 -0
- data/spec/resources/line_numbers/two_classes.rb +11 -0
- data/spec/resources/saikuro/app/controllers/sessions_controller.rb_cyclo.html +10 -0
- data/spec/resources/saikuro/app/controllers/users_controller.rb_cyclo.html +16 -0
- data/spec/resources/saikuro/index_cyclo.html +155 -0
- data/spec/resources/saikuro_sfiles/thing.rb_cyclo.html +11 -0
- data/spec/resources/yml/20090630.yml +7922 -0
- data/spec/resources/yml/metric_missing.yml +1 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +7 -0
- data/tasks/metric_fu.rake +22 -0
- 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> 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
|
+
|
data/lib/base/ranking.rb
ADDED
@@ -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
|