metric_fu 4.1.0 → 4.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 (62) hide show
  1. data/Gemfile +1 -1
  2. data/HISTORY.md +12 -1
  3. data/README.md +3 -0
  4. data/TODO.md +0 -41
  5. data/lib/metric_fu.rb +9 -1
  6. data/lib/metric_fu/configuration.rb +9 -1
  7. data/lib/metric_fu/initial_requires.rb +11 -11
  8. data/lib/metric_fu/load_files.rb +1 -0
  9. data/lib/metric_fu/metrics/cane/cane.rb +5 -4
  10. data/lib/metric_fu/metrics/cane/cane_bluff_grapher.rb +15 -0
  11. data/lib/metric_fu/metrics/cane/cane_grapher.rb +1 -0
  12. data/lib/metric_fu/metrics/cane/init.rb +1 -0
  13. data/lib/metric_fu/metrics/churn/churn_hotspot.rb +9 -1
  14. data/lib/metric_fu/metrics/flay/flay.rb +0 -2
  15. data/lib/metric_fu/metrics/flay/flay_bluff_grapher.rb +15 -0
  16. data/lib/metric_fu/metrics/flay/flay_gchart_grapher.rb +17 -0
  17. data/lib/metric_fu/metrics/flay/flay_grapher.rb +1 -0
  18. data/lib/metric_fu/metrics/flay/flay_hotspot.rb +18 -1
  19. data/lib/metric_fu/metrics/flay/init.rb +1 -0
  20. data/lib/metric_fu/metrics/flog/flog.rb +6 -4
  21. data/lib/metric_fu/metrics/flog/flog_bluff_grapher.rb +16 -0
  22. data/lib/metric_fu/metrics/flog/flog_gchart_grapher.rb +21 -0
  23. data/lib/metric_fu/metrics/flog/flog_grapher.rb +1 -0
  24. data/lib/metric_fu/metrics/flog/flog_hotspot.rb +13 -1
  25. data/lib/metric_fu/metrics/flog/init.rb +1 -0
  26. data/lib/metric_fu/metrics/hotspots/analysis/analyzed_problems.rb +73 -0
  27. data/lib/metric_fu/metrics/hotspots/analysis/analyzer_tables.rb +116 -0
  28. data/lib/metric_fu/metrics/hotspots/analysis/groupings.rb +21 -0
  29. data/lib/metric_fu/metrics/hotspots/analysis/problems.rb +21 -0
  30. data/lib/metric_fu/metrics/hotspots/analysis/rankings.rb +81 -0
  31. data/lib/metric_fu/metrics/hotspots/hotspot.rb +29 -0
  32. data/lib/metric_fu/metrics/hotspots/hotspot_analyzer.rb +62 -308
  33. data/lib/metric_fu/metrics/hotspots/hotspots.rb +1 -27
  34. data/lib/metric_fu/metrics/rails_best_practices/rails_best_practices_bluff_grapher.rb +15 -0
  35. data/lib/metric_fu/metrics/rails_best_practices/rails_best_practices_gchart_grapher.rb +21 -0
  36. data/lib/metric_fu/metrics/rails_best_practices/rails_best_practices_grapher.rb +1 -0
  37. data/lib/metric_fu/metrics/rcov/rcov_bluff_grapher.rb +15 -0
  38. data/lib/metric_fu/metrics/rcov/rcov_gchart_grapher.rb +17 -0
  39. data/lib/metric_fu/metrics/rcov/rcov_grapher.rb +1 -0
  40. data/lib/metric_fu/metrics/rcov/rcov_hotspot.rb +28 -16
  41. data/lib/metric_fu/metrics/reek/reek_bluff_grapher.rb +20 -0
  42. data/lib/metric_fu/metrics/reek/reek_gchart_grapher.rb +25 -0
  43. data/lib/metric_fu/metrics/reek/reek_grapher.rb +1 -0
  44. data/lib/metric_fu/metrics/reek/reek_hotspot.rb +16 -1
  45. data/lib/metric_fu/metrics/roodi/roodi_bluff_grapher.rb +15 -0
  46. data/lib/metric_fu/metrics/roodi/roodi_gchart_grapher.rb +17 -0
  47. data/lib/metric_fu/metrics/roodi/roodi_grapher.rb +1 -0
  48. data/lib/metric_fu/metrics/roodi/roodi_hotspot.rb +16 -1
  49. data/lib/metric_fu/metrics/saikuro/saikuro_hotspot.rb +13 -1
  50. data/lib/metric_fu/metrics/stats/stats_bluff_grapher.rb +16 -0
  51. data/lib/metric_fu/metrics/stats/stats_gchart_grapher.rb +20 -0
  52. data/lib/metric_fu/metrics/stats/stats_grapher.rb +1 -0
  53. data/lib/metric_fu/metrics/stats/stats_hotspot.rb +1 -1
  54. data/lib/metric_fu/reporting/graphs/engines/bluff.rb +0 -114
  55. data/lib/metric_fu/reporting/graphs/engines/gchart.rb +0 -123
  56. data/lib/metric_fu/run.rb +1 -0
  57. data/lib/metric_fu/version.rb +1 -1
  58. data/metric_fu.gemspec +1 -0
  59. data/spec/metric_fu/metrics/cane/cane_spec.rb +17 -0
  60. data/spec/metric_fu/metrics/hotspots/hotspot_spec.rb +11 -0
  61. data/spec/metric_fu/metrics/hotspots/hotspots_spec.rb +7 -0
  62. metadata +180 -134
@@ -0,0 +1,21 @@
1
+ MetricFu.metrics_require { 'flog/flog_grapher' }
2
+ module MetricFu
3
+ class FlogGchartGrapher < FlogGrapher
4
+ def graph!
5
+ determine_y_axis_scale(@top_five_percent_average + @flog_average)
6
+ url = Gchart.line(
7
+ :size => GCHART_GRAPH_SIZE,
8
+ :title => URI.escape("Flog: code complexity"),
9
+ :data => [@flog_average, @top_five_percent_average],
10
+ :stacked => false,
11
+ :bar_colors => COLORS[0..1],
12
+ :legend => ['average', 'top 5% average'],
13
+ :custom => "chdlp=t",
14
+ :max_value => @max_value,
15
+ :axis_with_labels => 'x,y',
16
+ :axis_labels => [@labels.values, @yaxis],
17
+ :format => 'file',
18
+ :filename => File.join(MetricFu.output_directory, 'flog.png'))
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,4 @@
1
+ MetricFu.reporting_require { 'graphs/grapher' }
1
2
  module MetricFu
2
3
  class FlogGrapher < Grapher
3
4
  attr_accessor :flog_average, :labels, :top_five_percent_average
@@ -1,4 +1,4 @@
1
- class FlogHotspot
1
+ class FlogHotspot < MetricFu::Hotspot
2
2
  include MetricFu::HotspotScoringStrategies
3
3
 
4
4
  COLUMNS = %w{score}
@@ -40,4 +40,16 @@ class FlogHotspot
40
40
  end
41
41
  end
42
42
 
43
+ def present_group(group)
44
+ occurences = group.size
45
+ complexity = get_mean(group.column("score"))
46
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
47
+ end
48
+
49
+ def present_group_details(group)
50
+ occurences = group.size
51
+ complexity = get_mean(group.column("score"))
52
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
53
+ end
54
+
43
55
  end
@@ -1,4 +1,5 @@
1
1
  MetricFu::Configuration.run do |config|
2
+ require 'flog'
2
3
  config.add_metric(:flog)
3
4
  config.add_graph(:flog)
4
5
  config.configure_metric(:flog,
@@ -0,0 +1,73 @@
1
+ module MetricFu
2
+ class HotspotAnalyzedProblems
3
+
4
+
5
+ def initialize(hotspot_rankings, analyzer_tables)
6
+ @hotspot_rankings = hotspot_rankings
7
+ @analyzer_tables = analyzer_tables
8
+ end
9
+ def worst_items
10
+ num = nil
11
+ worst_items = {}
12
+ worst_items[:files] =
13
+ @hotspot_rankings.worst_files(num).inject([]) do |array, worst_file|
14
+ array <<
15
+ {:location => self.location(:file, worst_file),
16
+ :details => self.problems_with(:file, worst_file)}
17
+ array
18
+ end
19
+ worst_items[:classes] = @hotspot_rankings.worst_classes(num).inject([]) do |array, class_name|
20
+ location = self.location(:class, class_name)
21
+ array <<
22
+ {:location => location,
23
+ :details => self.problems_with(:class, class_name)}
24
+ array
25
+ end
26
+ worst_items[:methods] = @hotspot_rankings.worst_methods(num).inject([]) do |array, method_name|
27
+ location = self.location(:method, method_name)
28
+ array <<
29
+ {:location => location,
30
+ :details => self.problems_with(:method, method_name)}
31
+ array
32
+ end
33
+ worst_items
34
+ end
35
+ private
36
+ #todo redo as item,value, options = {}
37
+ # Note that the other option for 'details' is :detailed (this isn't
38
+ # at all clear from this method itself
39
+ def problems_with(item, value, details = :summary, exclude_details = [])
40
+ sub_table = get_sub_table(item, value)
41
+ #grouping = Ruport::Data::Grouping.new(sub_table, :by => 'metric')
42
+ grouping = get_grouping(sub_table, :by => 'metric')
43
+ MetricFu::HotspotProblems.new(grouping, details, exclude_details).problems
44
+ end
45
+ def location(item, value)
46
+ sub_table = get_sub_table(item, value)
47
+ if(sub_table.length==0)
48
+ raise MetricFu::AnalysisError, "The #{item.to_s} '#{value.to_s}' does not have any rows in the analysis table"
49
+ else
50
+ first_row = sub_table[0]
51
+ case item
52
+ when :class
53
+ MetricFu::Location.get(first_row.file_path, first_row.class_name, nil)
54
+ when :method
55
+ MetricFu::Location.get(first_row.file_path, first_row.class_name, first_row.method_name)
56
+ when :file
57
+ MetricFu::Location.get(first_row.file_path, nil, nil)
58
+ else
59
+ raise ArgumentError, "Item must be :class, :method, or :file"
60
+ end
61
+ end
62
+ end
63
+ # just for testing
64
+ public :location, :problems_with
65
+ def get_sub_table(item, value)
66
+ tables = @analyzer_tables.tables_for(item)
67
+ tables[value]
68
+ end
69
+ def get_grouping(table, opts)
70
+ MetricFu::HotspotGroupings.new(table, opts).get_grouping
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,116 @@
1
+ module MetricFu
2
+ class AnalyzerTables
3
+ %w(table).each do |path|
4
+ MetricFu.metrics_require { "hotspots/analysis/#{path}" }
5
+ end
6
+
7
+ def initialize(analyzer_columns)
8
+ @columns = analyzer_columns
9
+ end
10
+
11
+ def generate_records
12
+ build_lookups!
13
+ process_rows!
14
+ end
15
+
16
+ def tool_tables
17
+ @tool_tables ||= make_table_hash(@columns)
18
+ end
19
+
20
+ def table
21
+ @table ||= make_table(@columns)
22
+ end
23
+
24
+ def tables_for(item)
25
+ {
26
+ :class => @class_tables,
27
+ :method => @method_tables,
28
+ :file => @file_tables,
29
+ :tool => @tool_tables
30
+ }.fetch(item) do
31
+ raise ArgumentError, "Item must be :class, :method, or :file, but was #{item}"
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def make_table(columns)
38
+ MetricFu::Table.new(:column_names => columns)
39
+ end
40
+
41
+ def make_table_hash(columns)
42
+ Hash.new { |hash, key|
43
+ hash[key] = make_table(columns)
44
+ }
45
+ end
46
+
47
+ def build_lookups!
48
+ @class_and_method_to_file ||= {}
49
+ # Build a mapping from [class,method] => filename
50
+ # (and make sure the mapping is unique)
51
+ table.each do |row|
52
+ # We know that Saikuro provides the wrong data
53
+ # TODO inject Saikuro reference
54
+ next if row['metric'] == :saikuro
55
+ key = [row['class_name'], row['method_name']]
56
+ file_path = row['file_path']
57
+ @class_and_method_to_file[key] ||= file_path
58
+ end
59
+ end
60
+
61
+ def process_rows!
62
+ # Correct incorrect rows in the table
63
+ table.each do |row|
64
+ row_metric = row['metric'] #perf optimization
65
+ # TODO inject Saikuro reference
66
+ if row_metric == :saikuro
67
+ fix_row_file_path!(row)
68
+ end
69
+ tool_tables[row_metric] << row
70
+ file_tables[row["file_path"]] << row
71
+ class_tables[row["class_name"]] << row
72
+ method_tables[row["method_name"]] << row
73
+ end
74
+ end
75
+
76
+
77
+ def fix_row_file_path!(row)
78
+ # We know that Saikuro rows are broken
79
+ # next unless row['metric'] == :saikuro
80
+ key = [row['class_name'], row['method_name']]
81
+ current_file_path = row['file_path'].to_s
82
+ correct_file_path = @class_and_method_to_file[key]
83
+ if(correct_file_path!=nil && correct_file_path.include?(current_file_path))
84
+ row['file_path'] = correct_file_path
85
+ else
86
+ # There wasn't an exact match, so we can do a substring match
87
+ matching_file_path = file_paths.detect {|file_path|
88
+ file_path!=nil && file_path.include?(current_file_path)
89
+ }
90
+ if(matching_file_path)
91
+ row['file_path'] = matching_file_path
92
+ end
93
+ end
94
+ end
95
+
96
+ def file_paths
97
+ @file_paths ||= @table.column('file_path').uniq
98
+ end
99
+
100
+ # These tables are an optimization. They contain subsets of the master table.
101
+ # TODO - these should be pushed into the Table class now
102
+ def optimized_tables
103
+ @optimized_tables ||= make_table_hash(@columns)
104
+ end
105
+
106
+ def file_tables
107
+ @file_tables ||= make_table_hash(@columns)
108
+ end
109
+ def class_tables
110
+ @class_tables ||= make_table_hash(@columns)
111
+ end
112
+ def method_tables
113
+ @method_tables ||= make_table_hash(@columns)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,21 @@
1
+ module MetricFu
2
+ class HotspotGroupings
3
+
4
+ def initialize(table, opts)
5
+ @table, @opts = table, opts
6
+ end
7
+ def get_grouping
8
+ #Ruport::Data::Grouping.new(table, opts)
9
+ MetricFu::Grouping.new(@table, @opts)
10
+ #@grouping_cache ||= {}
11
+ #@grouping_cache.fetch(grouping_key(table,opts)) do
12
+ # @grouping_cache[grouping_key(table,opts)] = Ruport::Data::Grouping.new(table, opts)
13
+ #end
14
+ end
15
+
16
+ # UNUSED
17
+ # def grouping_key(table, opts)
18
+ # "table #{table.object_id} opts #{opts.inspect}"
19
+ # end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module MetricFu
2
+ class HotspotProblems
3
+
4
+ def initialize(grouping, details, exclude_details)
5
+ @grouping, @details, @exclude_details = grouping, details, exclude_details
6
+ end
7
+
8
+ def problems
9
+ problems = {}
10
+ @grouping.each do |metric, table|
11
+ if @details == :summary || @exclude_details.include?(metric)
12
+ problems[metric] = MetricFu::Hotspot.analyzer_for_metric(metric).present_group(table)
13
+ else
14
+ problems[metric] = MetricFu::Hotspot.analyzer_for_metric(metric).present_group_details(table)
15
+ end
16
+ end
17
+ problems
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,81 @@
1
+ module MetricFu
2
+ class HotspotRankings
3
+
4
+ def initialize(tool_tables)
5
+ @tool_tables = tool_tables
6
+ @file_ranking = MetricFu::Ranking.new
7
+ @class_ranking = MetricFu::Ranking.new
8
+ @method_ranking = MetricFu::Ranking.new
9
+ end
10
+
11
+ def calculate_scores(tool_analyzers, granularities)
12
+ tool_analyzers.each do |analyzer|
13
+ calculate_scores_by_granularities(analyzer, granularities)
14
+ end
15
+ end
16
+
17
+ def worst_methods(size = nil)
18
+ @method_ranking.delete(nil)
19
+ @method_ranking.top(size)
20
+ end
21
+
22
+ def worst_classes(size = nil)
23
+ @class_ranking.delete(nil)
24
+ @class_ranking.top(size)
25
+ end
26
+
27
+ def worst_files(size = nil)
28
+ @file_ranking.delete(nil)
29
+ @file_ranking.top(size)
30
+ end
31
+
32
+ private
33
+
34
+ def calculate_scores_by_granularities(analyzer, granularities)
35
+ granularities.each do |granularity|
36
+ calculate_score_for_granularity(analyzer, granularity)
37
+ end
38
+ end
39
+
40
+ def calculate_score_for_granularity(analyzer, granularity)
41
+ metric_ranking = calculate_metric_scores(granularity, analyzer)
42
+ add_to_master_ranking(ranking(granularity), metric_ranking, analyzer)
43
+ end
44
+ def calculate_metric_scores(granularity, analyzer)
45
+ metric_ranking = MetricFu::Ranking.new
46
+ metric_violations = @tool_tables[analyzer.name]
47
+ metric_violations.each do |row|
48
+ location = row[granularity]
49
+ metric_ranking[location] ||= []
50
+ metric_ranking[location] << analyzer.map(row)
51
+ end
52
+
53
+ metric_ranking.each do |item, scores|
54
+ metric_ranking[item] = analyzer.reduce(scores)
55
+ end
56
+
57
+ metric_ranking
58
+ end
59
+
60
+ def ranking(column_name)
61
+ case column_name
62
+ when "file_path"
63
+ @file_ranking
64
+ when "class_name"
65
+ @class_ranking
66
+ when "method_name"
67
+ @method_ranking
68
+ else
69
+ raise ArgumentError, "Invalid column name #{column_name}"
70
+ end
71
+ end
72
+
73
+ def add_to_master_ranking(master_ranking, metric_ranking, analyzer)
74
+ metric_ranking.each do |item, _|
75
+ master_ranking[item] ||= 0
76
+ master_ranking[item] += analyzer.score(metric_ranking, item) # scaling? Do we just add in the raw score?
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,29 @@
1
+ module MetricFu
2
+ class Hotspot
3
+ def self.metric
4
+ self.name.split('Hotspot')[0].downcase.to_sym
5
+ end
6
+ @analyzers = {}
7
+ def self.analyzers
8
+ @analyzers.values
9
+ end
10
+ def self.analyzer_for_metric(metric)
11
+ mf_debug "Getting analyzer for #{metric}"
12
+ @analyzers.fetch(metric.to_sym) {
13
+ raise MetricFu::AnalysisError, "Unknown metric #{metric}. We only know #{@analyzers.keys.inspect}"
14
+ }
15
+ end
16
+ def self.inherited(subclass)
17
+ mf_debug "Adding #{subclass} to #{@analyzers.inspect}"
18
+ @analyzers[subclass.metric] = subclass.new
19
+ end
20
+
21
+ # TODO simplify calculation
22
+ def get_mean(collection)
23
+ collection_length = collection.length
24
+ sum = 0
25
+ sum = collection.inject( nil ) { |sum,x| sum ? sum+x : x }
26
+ (sum.to_f / collection_length.to_f)
27
+ end
28
+ end
29
+ end
@@ -1,11 +1,9 @@
1
1
  require File.expand_path('analysis_error', MetricFu.errors_dir)
2
2
  MetricFu.data_structures_require { 'location' }
3
- %w(table record grouping ranking).each do |path|
3
+ %w(table record grouping ranking problems).each do |path|
4
4
  MetricFu.metrics_require { "hotspots/analysis/#{path}" }
5
5
  end
6
- %w(reek roodi flog churn saikuro flay stats rcov).each do |path|
7
- MetricFu.metrics_require { "#{path}/#{path}_hotspot" }
8
- end
6
+ MetricFu.metrics_require { 'hotspots/hotspot' }
9
7
 
10
8
  module MetricFu
11
9
  class HotspotAnalyzer
@@ -13,321 +11,77 @@ module MetricFu
13
11
  COMMON_COLUMNS = %w{metric}
14
12
  GRANULARITIES = %w{file_path class_name method_name}
15
13
 
16
- attr_accessor :table
17
-
18
- def initialize(yaml)
19
- if(yaml.is_a?(String))
20
- @yaml = YAML.load(yaml)
21
- else
22
- @yaml = yaml
23
- end
24
- @file_ranking = MetricFu::Ranking.new
25
- @class_ranking = MetricFu::Ranking.new
26
- @method_ranking = MetricFu::Ranking.new
27
- rankings = [@file_ranking, @class_ranking, @method_ranking]
28
-
29
- tool_analyzers = [ReekHotspot.new, RoodiHotspot.new,
30
- FlogHotspot.new, ChurnHotspot.new, SaikuroHotspot.new,
31
- FlayHotspot.new, StatsHotspot.new, RcovHotspot.new]
32
- # TODO There is likely a clash that will happen between
33
- # column names eventually. We should probably auto-prefix
34
- # them (e.g. "roodi_problem")
35
- columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map{|analyzer| analyzer.columns}.flatten
36
-
37
- @table = make_table(columns)
38
-
39
- # These tables are an optimization. They contain subsets of the master table.
40
- # TODO - these should be pushed into the Table class now
41
- @tool_tables = make_table_hash(columns)
42
- @file_tables = make_table_hash(columns)
43
- @class_tables = make_table_hash(columns)
44
- @method_tables = make_table_hash(columns)
45
-
46
- tool_analyzers.each do |analyzer|
47
- analyzer.generate_records(@yaml[analyzer.name], @table)
48
- end
49
-
50
- build_lookups!(table)
51
- process_rows!(table)
52
-
53
- tool_analyzers.each do |analyzer|
54
- GRANULARITIES.each do |granularity|
55
- metric_ranking = calculate_metric_scores(granularity, analyzer)
56
- add_to_master_ranking(ranking(granularity), metric_ranking, analyzer)
57
- end
58
- end
59
-
60
- rankings.each do |ranking|
61
- ranking.delete(nil)
62
- end
63
- end
64
-
65
- def location(item, value)
66
- sub_table = get_sub_table(item, value)
67
- if(sub_table.length==0)
68
- raise MetricFu::AnalysisError, "The #{item.to_s} '#{value.to_s}' does not have any rows in the analysis table"
69
- else
70
- first_row = sub_table[0]
71
- case item
72
- when :class
73
- MetricFu::Location.get(first_row.file_path, first_row.class_name, nil)
74
- when :method
75
- MetricFu::Location.get(first_row.file_path, first_row.class_name, first_row.method_name)
76
- when :file
77
- MetricFu::Location.get(first_row.file_path, nil, nil)
78
- else
79
- raise ArgumentError, "Item must be :class, :method, or :file"
80
- end
81
- end
82
- end
14
+ # UNUSED
15
+ # attr_accessor :table
83
16
 
84
- #todo redo as item,value, options = {}
85
- # Note that the other option for 'details' is :detailed (this isn't
86
- # at all clear from this method itself
87
- def problems_with(item, value, details = :summary, exclude_details = [])
88
- sub_table = get_sub_table(item, value)
89
- #grouping = Ruport::Data::Grouping.new(sub_table, :by => 'metric')
90
- grouping = get_grouping(sub_table, :by => 'metric')
91
- problems = {}
92
- grouping.each do |metric, table|
93
- if details == :summary || exclude_details.include?(metric)
94
- problems[metric] = present_group(metric,table)
95
- else
96
- problems[metric] = present_group_details(metric,table)
97
- end
98
- end
99
- problems
17
+ def tool_analyzers
18
+ MetricFu::Hotspot.analyzers
100
19
  end
101
20
 
102
- def worst_methods(size = nil)
103
- @method_ranking.top(size)
21
+ def initialize(report_hash)
22
+ # we can't depend on the Report
23
+ # returning a parsed yaml file as a hash?
24
+ report_hash = YAML::load(report_hash) if report_hash.is_a?(String)
25
+ setup(report_hash)
104
26
  end
105
27
 
106
- def worst_classes(size = nil)
107
- @class_ranking.top(size)
28
+ # def worst_items
29
+ def hotspots
30
+ analyzed_problems.worst_items
108
31
  end
109
-
110
- def worst_files(size = nil)
111
- @file_ranking.top(size)
32
+ # just for testing
33
+ def analyzed_problems
34
+ @analyzed_problems = MetricFu::HotspotAnalyzedProblems.new(@rankings, @analyzer_tables)
112
35
  end
113
-
36
+ alias_method :worst_items, :hotspots
37
+ extend Forwardable
38
+ def_delegators :@analyzer_tables, :table
39
+ def_delegators :@analyzed_problems, :problems_with, :location
40
+ def_delegators :@rankings, :worst_files, :worst_methods, :worst_classes
114
41
  private
115
42
 
116
- def get_grouping(table, opts)
117
- #Ruport::Data::Grouping.new(table, opts)
118
- MetricFu::Grouping.new(table, opts)
119
- #@grouping_cache ||= {}
120
- #@grouping_cache.fetch(grouping_key(table,opts)) do
121
- # @grouping_cache[grouping_key(table,opts)] = Ruport::Data::Grouping.new(table, opts)
122
- #end
123
- end
124
-
125
- def grouping_key(table, opts)
126
- "table #{table.object_id} opts #{opts.inspect}"
127
- end
128
-
129
- def build_lookups!(table)
130
- @class_and_method_to_file ||= {}
131
- # Build a mapping from [class,method] => filename
132
- # (and make sure the mapping is unique)
133
- table.each do |row|
134
- # We know that Saikuro provides the wrong data
135
- next if row['metric'] == :saikuro
136
- key = [row['class_name'], row['method_name']]
137
- file_path = row['file_path']
138
- @class_and_method_to_file[key] ||= file_path
139
- end
140
- end
141
-
142
- def process_rows!(table)
143
- # Correct incorrect rows in the table
144
- table.each do |row|
145
- row_metric = row['metric'] #perf optimization
146
- if row_metric == :saikuro
147
- fix_row_file_path!(row)
148
- end
149
- @tool_tables[row_metric] << row
150
- @file_tables[row["file_path"]] << row
151
- @class_tables[row["class_name"]] << row
152
- @method_tables[row["method_name"]] << row
153
- end
154
- end
155
-
156
- def fix_row_file_path!(row)
157
- # We know that Saikuro rows are broken
158
- # next unless row['metric'] == :saikuro
159
- key = [row['class_name'], row['method_name']]
160
- current_file_path = row['file_path'].to_s
161
- correct_file_path = @class_and_method_to_file[key]
162
- if(correct_file_path!=nil && correct_file_path.include?(current_file_path))
163
- row['file_path'] = correct_file_path
164
- else
165
- # There wasn't an exact match, so we can do a substring match
166
- matching_file_path = file_paths.detect {|file_path|
167
- file_path!=nil && file_path.include?(current_file_path)
168
- }
169
- if(matching_file_path)
170
- row['file_path'] = matching_file_path
171
- end
172
- end
173
- end
174
-
175
- def file_paths
176
- @file_paths ||= @table.column('file_path').uniq
177
- end
178
-
179
- def ranking(column_name)
180
- case column_name
181
- when "file_path"
182
- @file_ranking
183
- when "class_name"
184
- @class_ranking
185
- when "method_name"
186
- @method_ranking
187
- else
188
- raise ArgumentError, "Invalid column name #{column_name}"
189
- end
190
- end
191
-
192
- def calculate_metric_scores(granularity, analyzer)
193
- metric_ranking = MetricFu::Ranking.new
194
- metric_violations = @tool_tables[analyzer.name]
195
- metric_violations.each do |row|
196
- location = row[granularity]
197
- metric_ranking[location] ||= []
198
- metric_ranking[location] << analyzer.map(row)
199
- end
200
-
201
- metric_ranking.each do |item, scores|
202
- metric_ranking[item] = analyzer.reduce(scores)
203
- end
204
-
205
- metric_ranking
206
- end
207
-
208
- def add_to_master_ranking(master_ranking, metric_ranking, analyzer)
209
- metric_ranking.each do |item, _|
210
- master_ranking[item] ||= 0
211
- master_ranking[item] += analyzer.score(metric_ranking, item) # scaling? Do we just add in the raw score?
212
- end
213
- end
214
-
215
- def most_common_column(column_name, size)
216
- #grouping = Ruport::Data::Grouping.new(@table,
217
- # :by => column_name,
218
- # :order => lambda { |g| -g.size})
219
- get_grouping(@table, :by => column_name, :order => lambda {|g| -g.size})
220
- values = []
221
- grouping.each do |value, _|
222
- values << value if value!=nil
223
- if(values.size==size)
224
- break
225
- end
226
- end
227
- return nil if values.empty?
228
- if(values.size == 1)
229
- return values.first
230
- else
231
- return values
232
- end
233
- end
234
-
235
- # TODO: As we get fancier, the presenter should
236
- # be its own class, not just a method with a long
237
- # case statement
238
- def present_group(metric, group)
239
- occurences = group.size
240
- case(metric)
241
- when :reek
242
- "found #{occurences} code smells"
243
- when :roodi
244
- "found #{occurences} design problems"
245
- when :churn
246
- "detected high level of churn (changed #{group[0].times_changed} times)"
247
- when :flog
248
- complexity = get_mean(group.column("score"))
249
- "#{"average " if occurences > 1}complexity is %.1f" % complexity
250
- when :saikuro
251
- complexity = get_mean(group.column("complexity"))
252
- "#{"average " if occurences > 1}complexity is %.1f" % complexity
253
- when :flay
254
- "found #{occurences} code duplications"
255
- when :rcov
256
- average_code_uncoverage = get_mean(group.column("percentage_uncovered"))
257
- "#{"average " if occurences > 1}uncovered code is %.1f%" % average_code_uncoverage
258
- else
259
- raise MetricFu::AnalysisError, "Unknown metric #{metric}"
260
- end
261
- end
262
-
263
- def present_group_details(metric, group)
264
- occurences = group.size
265
- case(metric)
266
- when :reek
267
- message = "found #{occurences} code smells<br/>"
268
- group.each do |item|
269
- type = item.data["reek__type_name"]
270
- reek_message = item.data["reek__message"]
271
- message << "* #{type}: #{reek_message}<br/>"
272
- end
273
- message
274
- when :roodi
275
- message = "found #{occurences} design problems<br/>"
276
- group.each do |item|
277
- problem = item.data["problems"]
278
- message << "* #{problem}<br/>"
279
- end
280
- message
281
- when :churn
282
- "detected high level of churn (changed #{group[0].times_changed} times)"
283
- when :flog
284
- complexity = get_mean(group.column("score"))
285
- "#{"average " if occurences > 1}complexity is %.1f" % complexity
286
- when :saikuro
287
- complexity = get_mean(group.column("complexity"))
288
- "#{"average " if occurences > 1}complexity is %.1f" % complexity
289
- when :flay
290
- message = "found #{occurences} code duplications<br/>"
291
- group.each do |item|
292
- problem = item.data["flay_reason"]
293
- problem = problem.gsub(/^[0-9]*\)/,'')
294
- problem = problem.gsub(/files\:/,' <br>&nbsp;&nbsp;&nbsp;files:')
295
- message << "* #{problem}<br/>"
296
- end
297
- message
298
- else
299
- raise MetricFu::AnalysisError, "Unknown metric #{metric}"
300
- end
301
- end
302
-
303
- def make_table_hash(columns)
304
- Hash.new { |hash, key|
305
- hash[key] = make_table(columns)
306
- }
307
- end
308
-
309
- def make_table(columns)
310
- MetricFu::Table.new(:column_names => columns)
311
- end
43
+ def setup(report_hash)
44
+ # TODO There is likely a clash that will happen between
45
+ # column names eventually. We should probably auto-prefix
46
+ # them (e.g. "roodi_problem")
47
+ analyzer_columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map{|analyzer| analyzer.columns}.flatten
48
+ # though the tool_analyzers aren't returned, they are processed in
49
+ # various places here, then by the analyzer tables
50
+ # then by the rankings
51
+ # to ultimately generate the hotspots
52
+ @analyzer_tables = MetricFu::AnalyzerTables.new(analyzer_columns)
53
+ tool_analyzers.each do |analyzer|
54
+ analyzer.generate_records(report_hash[analyzer.name], @analyzer_tables.table)
55
+ end
56
+ @analyzer_tables.generate_records
57
+ @rankings = MetricFu::HotspotRankings.new(@analyzer_tables.tool_tables)
58
+ @rankings.calculate_scores(tool_analyzers, GRANULARITIES)
59
+ # just for testing
60
+ analyzed_problems
61
+ end
62
+
63
+ # UNUSED
64
+ # def most_common_column(column_name, size)
65
+ # #grouping = Ruport::Data::Grouping.new(@table,
66
+ # # :by => column_name,
67
+ # # :order => lambda { |g| -g.size})
68
+ # get_grouping(@table, :by => column_name, :order => lambda {|g| -g.size})
69
+ # values = []
70
+ # grouping.each do |value, _|
71
+ # values << value if value!=nil
72
+ # if(values.size==size)
73
+ # break
74
+ # end
75
+ # end
76
+ # return nil if values.empty?
77
+ # if(values.size == 1)
78
+ # return values.first
79
+ # else
80
+ # return values
81
+ # end
82
+ # end
312
83
 
313
- def get_sub_table(item, value)
314
- tables = {
315
- :class => @class_tables,
316
- :method => @method_tables,
317
- :file => @file_tables,
318
- :tool => @tool_tables
319
- }.fetch(item) do
320
- raise ArgumentError, "Item must be :class, :method, or :file"
321
- end
322
- tables[value]
323
- end
324
84
 
325
- def get_mean(collection)
326
- collection_length = collection.length
327
- sum = 0
328
- sum = collection.inject( nil ) { |sum,x| sum ? sum+x : x }
329
- (sum.to_f / collection_length.to_f)
330
- end
331
85
 
332
86
  end
333
87
  end