danmayer-metric_fu 2.1.2
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 +462 -0
data/lib/base/graph.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module MetricFu
|
|
2
|
+
|
|
3
|
+
def self.graph
|
|
4
|
+
@graph ||= Graph.new
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class Graph
|
|
8
|
+
|
|
9
|
+
attr_accessor :clazz
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
self.clazz = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add(graph_type, graph_engine)
|
|
16
|
+
grapher_name = graph_type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } + graph_engine.to_s.capitalize + "Grapher"
|
|
17
|
+
self.clazz.push MetricFu.const_get(grapher_name).new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate
|
|
22
|
+
return if self.clazz.empty?
|
|
23
|
+
puts "Generating graphs"
|
|
24
|
+
Dir[File.join(MetricFu.data_directory, '*.yml')].sort.each do |metric_file|
|
|
25
|
+
puts "Generating graphs for #{metric_file}"
|
|
26
|
+
date_parts = year_month_day_from_filename(metric_file)
|
|
27
|
+
metrics = YAML::load(File.open(metric_file))
|
|
28
|
+
|
|
29
|
+
self.clazz.each do |grapher|
|
|
30
|
+
grapher.get_metrics(metrics, "#{date_parts[:m]}/#{date_parts[:d]}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
self.clazz.each do |grapher|
|
|
34
|
+
grapher.graph!
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
def year_month_day_from_filename(path_to_file_with_date)
|
|
40
|
+
date = path_to_file_with_date.match(/\/(\d+).yml$/)[1]
|
|
41
|
+
{:y => date[0..3].to_i, :m => date[4..5].to_i, :d => date[6..7].to_i}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'ruby_parser'
|
|
2
|
+
module MetricFu
|
|
3
|
+
class LineNumbers
|
|
4
|
+
|
|
5
|
+
def initialize(contents)
|
|
6
|
+
rp = RubyParser.new
|
|
7
|
+
@locations = {}
|
|
8
|
+
file_sexp = rp.parse(contents)
|
|
9
|
+
case file_sexp[0]
|
|
10
|
+
when :class
|
|
11
|
+
process_class(file_sexp)
|
|
12
|
+
when :module
|
|
13
|
+
process_module(file_sexp)
|
|
14
|
+
when :block
|
|
15
|
+
file_sexp.each_of_type(:class) { |sexp| process_class(sexp) }
|
|
16
|
+
else
|
|
17
|
+
end
|
|
18
|
+
rescue Exception
|
|
19
|
+
#catch errors for files ruby_parser fails on
|
|
20
|
+
@locations
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def in_method? line_number
|
|
24
|
+
!!@locations.detect do |method_name, line_number_range|
|
|
25
|
+
line_number_range.include?(line_number)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def method_at_line line_number
|
|
30
|
+
found_method_and_range = @locations.detect do |method_name, line_number_range|
|
|
31
|
+
line_number_range.include?(line_number)
|
|
32
|
+
end
|
|
33
|
+
return nil unless found_method_and_range
|
|
34
|
+
found_method_and_range.first
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def start_line_for_method(method)
|
|
38
|
+
return nil unless @locations.has_key?(method)
|
|
39
|
+
@locations[method].first
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def process_module(sexp)
|
|
45
|
+
module_name = sexp[1]
|
|
46
|
+
sexp.each_of_type(:class) do |sexp|
|
|
47
|
+
process_class(sexp, module_name)
|
|
48
|
+
hide_methods_from_next_round(sexp)
|
|
49
|
+
end
|
|
50
|
+
process_class(sexp)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def process_class(sexp, module_name=nil)
|
|
54
|
+
class_name = sexp[1]
|
|
55
|
+
process_class_self_blocks(sexp, class_name)
|
|
56
|
+
module_name_string = module_name ? "#{module_name}::" : nil
|
|
57
|
+
sexp.each_of_type(:defn) { |s| @locations["#{module_name_string}#{class_name}##{s[1]}"] = (s.line)..(s.last.line) }
|
|
58
|
+
sexp.each_of_type(:defs) { |s| @locations["#{module_name_string}#{class_name}::#{s[2]}"] = (s.line)..(s.last.line) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def process_class_self_blocks(sexp, class_name)
|
|
62
|
+
sexp.each_of_type(:sclass) do |sexp_in_class_self_block|
|
|
63
|
+
sexp_in_class_self_block.each_of_type(:defn) { |s| @locations["#{class_name}::#{s[1]}"] = (s.line)..(s.last.line) }
|
|
64
|
+
hide_methods_from_next_round(sexp_in_class_self_block)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def hide_methods_from_next_round(sexp)
|
|
69
|
+
sexp.find_and_replace_all(:defn, :ignore_me)
|
|
70
|
+
sexp.find_and_replace_all(:defs, :ignore_me)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module MetricFu
|
|
2
|
+
class Location
|
|
3
|
+
include Comparable
|
|
4
|
+
|
|
5
|
+
attr_accessor :class_name, :method_name, :file_path, :simple_method_name, :hash
|
|
6
|
+
|
|
7
|
+
def self.get(file_path, class_name, method_name)
|
|
8
|
+
# This could be more 'confident' using Maybe, but we want it to be as fast as possible
|
|
9
|
+
file_path_copy = file_path == nil ? nil : file_path.clone
|
|
10
|
+
class_name_copy = class_name == nil ? nil : class_name.clone
|
|
11
|
+
method_name_copy = method_name == nil ? nil : method_name.clone
|
|
12
|
+
key = [file_path_copy, class_name_copy, method_name_copy]
|
|
13
|
+
@@locations ||= {}
|
|
14
|
+
if @@locations.has_key?(key)
|
|
15
|
+
@@locations[key]
|
|
16
|
+
else
|
|
17
|
+
location = self.new(file_path_copy, class_name_copy, method_name_copy)
|
|
18
|
+
@@locations[key] = location
|
|
19
|
+
location.freeze # we cache a lot of method call results, so we want location to be immutable
|
|
20
|
+
location
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(file_path, class_name, method_name)
|
|
25
|
+
@file_path = file_path
|
|
26
|
+
@class_name = class_name
|
|
27
|
+
@method_name = method_name
|
|
28
|
+
@simple_method_name = @method_name.sub(@class_name,'') unless @method_name == nil
|
|
29
|
+
@hash = [@file_path, @class_name, @method_name].hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# TODO - we need this method (and hash accessor above) as a temporary hack where we're using Location as a hash key
|
|
33
|
+
def eql?(other)
|
|
34
|
+
[self.file_path.to_s, self.class_name.to_s, self.method_name.to_s] == [other.file_path.to_s, other.class_name.to_s, other.method_name.to_s]
|
|
35
|
+
end
|
|
36
|
+
# END we need these methods as a temporary hack where we're using Location as a hash key
|
|
37
|
+
|
|
38
|
+
def self.for(class_or_method_name)
|
|
39
|
+
class_or_method_name = strip_modules(class_or_method_name)
|
|
40
|
+
if(class_or_method_name)
|
|
41
|
+
begin
|
|
42
|
+
match = class_or_method_name.match(/(.*)((\.|\#|\:\:[a-z])(.+))/)
|
|
43
|
+
rescue => error
|
|
44
|
+
#new error during port to metric_fu occasionally a unintialized
|
|
45
|
+
#MatchData object shows up here. Not expected.
|
|
46
|
+
match = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# reek reports the method with :: not # on modules like
|
|
50
|
+
# module ApplicationHelper \n def signed_in?, convert it so it records correctly
|
|
51
|
+
# but classes have to start with a capital letter... HACK for REEK bug, reported underlying issue to REEK
|
|
52
|
+
if(match)
|
|
53
|
+
class_name = strip_modules(match[1])
|
|
54
|
+
method_name = class_or_method_name.gsub(/\:\:/,"#")
|
|
55
|
+
else
|
|
56
|
+
class_name = strip_modules(class_or_method_name)
|
|
57
|
+
method_name = nil
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
class_name = nil
|
|
61
|
+
method_name = nil
|
|
62
|
+
end
|
|
63
|
+
self.get(nil, class_name, method_name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def <=>(other)
|
|
67
|
+
[self.file_path.to_s, self.class_name.to_s, self.method_name.to_s] <=> [other.file_path.to_s, other.class_name.to_s, other.method_name.to_s]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def self.strip_modules(class_or_method_name)
|
|
73
|
+
# reek reports the method with :: not # on modules like
|
|
74
|
+
# module ApplicationHelper \n def signed_in?, convert it so it records correctly
|
|
75
|
+
# but classes have to start with a capital letter... HACK for REEK bug, reported underlying issue to REEK
|
|
76
|
+
if(class_or_method_name=~/\:\:[A-Z]/)
|
|
77
|
+
class_or_method_name.split("::").last
|
|
78
|
+
else
|
|
79
|
+
class_or_method_name
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -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
|
+
|