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.
- data/Gemfile +1 -1
- data/HISTORY.md +12 -1
- data/README.md +3 -0
- data/TODO.md +0 -41
- data/lib/metric_fu.rb +9 -1
- data/lib/metric_fu/configuration.rb +9 -1
- data/lib/metric_fu/initial_requires.rb +11 -11
- data/lib/metric_fu/load_files.rb +1 -0
- data/lib/metric_fu/metrics/cane/cane.rb +5 -4
- data/lib/metric_fu/metrics/cane/cane_bluff_grapher.rb +15 -0
- data/lib/metric_fu/metrics/cane/cane_grapher.rb +1 -0
- data/lib/metric_fu/metrics/cane/init.rb +1 -0
- data/lib/metric_fu/metrics/churn/churn_hotspot.rb +9 -1
- data/lib/metric_fu/metrics/flay/flay.rb +0 -2
- data/lib/metric_fu/metrics/flay/flay_bluff_grapher.rb +15 -0
- data/lib/metric_fu/metrics/flay/flay_gchart_grapher.rb +17 -0
- data/lib/metric_fu/metrics/flay/flay_grapher.rb +1 -0
- data/lib/metric_fu/metrics/flay/flay_hotspot.rb +18 -1
- data/lib/metric_fu/metrics/flay/init.rb +1 -0
- data/lib/metric_fu/metrics/flog/flog.rb +6 -4
- data/lib/metric_fu/metrics/flog/flog_bluff_grapher.rb +16 -0
- data/lib/metric_fu/metrics/flog/flog_gchart_grapher.rb +21 -0
- data/lib/metric_fu/metrics/flog/flog_grapher.rb +1 -0
- data/lib/metric_fu/metrics/flog/flog_hotspot.rb +13 -1
- data/lib/metric_fu/metrics/flog/init.rb +1 -0
- data/lib/metric_fu/metrics/hotspots/analysis/analyzed_problems.rb +73 -0
- data/lib/metric_fu/metrics/hotspots/analysis/analyzer_tables.rb +116 -0
- data/lib/metric_fu/metrics/hotspots/analysis/groupings.rb +21 -0
- data/lib/metric_fu/metrics/hotspots/analysis/problems.rb +21 -0
- data/lib/metric_fu/metrics/hotspots/analysis/rankings.rb +81 -0
- data/lib/metric_fu/metrics/hotspots/hotspot.rb +29 -0
- data/lib/metric_fu/metrics/hotspots/hotspot_analyzer.rb +62 -308
- data/lib/metric_fu/metrics/hotspots/hotspots.rb +1 -27
- data/lib/metric_fu/metrics/rails_best_practices/rails_best_practices_bluff_grapher.rb +15 -0
- data/lib/metric_fu/metrics/rails_best_practices/rails_best_practices_gchart_grapher.rb +21 -0
- data/lib/metric_fu/metrics/rails_best_practices/rails_best_practices_grapher.rb +1 -0
- data/lib/metric_fu/metrics/rcov/rcov_bluff_grapher.rb +15 -0
- data/lib/metric_fu/metrics/rcov/rcov_gchart_grapher.rb +17 -0
- data/lib/metric_fu/metrics/rcov/rcov_grapher.rb +1 -0
- data/lib/metric_fu/metrics/rcov/rcov_hotspot.rb +28 -16
- data/lib/metric_fu/metrics/reek/reek_bluff_grapher.rb +20 -0
- data/lib/metric_fu/metrics/reek/reek_gchart_grapher.rb +25 -0
- data/lib/metric_fu/metrics/reek/reek_grapher.rb +1 -0
- data/lib/metric_fu/metrics/reek/reek_hotspot.rb +16 -1
- data/lib/metric_fu/metrics/roodi/roodi_bluff_grapher.rb +15 -0
- data/lib/metric_fu/metrics/roodi/roodi_gchart_grapher.rb +17 -0
- data/lib/metric_fu/metrics/roodi/roodi_grapher.rb +1 -0
- data/lib/metric_fu/metrics/roodi/roodi_hotspot.rb +16 -1
- data/lib/metric_fu/metrics/saikuro/saikuro_hotspot.rb +13 -1
- data/lib/metric_fu/metrics/stats/stats_bluff_grapher.rb +16 -0
- data/lib/metric_fu/metrics/stats/stats_gchart_grapher.rb +20 -0
- data/lib/metric_fu/metrics/stats/stats_grapher.rb +1 -0
- data/lib/metric_fu/metrics/stats/stats_hotspot.rb +1 -1
- data/lib/metric_fu/reporting/graphs/engines/bluff.rb +0 -114
- data/lib/metric_fu/reporting/graphs/engines/gchart.rb +0 -123
- data/lib/metric_fu/run.rb +1 -0
- data/lib/metric_fu/version.rb +1 -1
- data/metric_fu.gemspec +1 -0
- data/spec/metric_fu/metrics/cane/cane_spec.rb +17 -0
- data/spec/metric_fu/metrics/hotspots/hotspot_spec.rb +11 -0
- data/spec/metric_fu/metrics/hotspots/hotspots_spec.rb +7 -0
- 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,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
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
85
|
-
|
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
|
103
|
-
|
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
|
107
|
-
|
28
|
+
# def worst_items
|
29
|
+
def hotspots
|
30
|
+
analyzed_problems.worst_items
|
108
31
|
end
|
109
|
-
|
110
|
-
def
|
111
|
-
@
|
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
|
117
|
-
#
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
#
|
122
|
-
#
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
@
|
131
|
-
|
132
|
-
#
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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> 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
|