%w(record grouping).each do |path|
MetricFu.data_structures_require { path }
end
class MetricAnalyzer
COMMON_COLUMNS = %w{metric}
GRANULARITIES = %w{file_path class_name method_name}
attr_accessor :table
def initialize(yaml)
if(yaml.is_a?(String))
@yaml = YAML.load(yaml)
else
@yaml = yaml
end
@file_ranking = MetricFu::Ranking.new
@class_ranking = MetricFu::Ranking.new
@method_ranking = MetricFu::Ranking.new
rankings = [@file_ranking, @class_ranking, @method_ranking]
tool_analyzers = [ReekAnalyzer.new, RoodiAnalyzer.new,
FlogAnalyzer.new, ChurnAnalyzer.new, SaikuroAnalyzer.new,
FlayAnalyzer.new, StatsAnalyzer.new, RcovAnalyzer.new]
# TODO There is likely a clash that will happen between
# column names eventually. We should probably auto-prefix
# them (e.g. "roodi_problem")
columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map{|analyzer| analyzer.columns}.flatten
@table = make_table(columns)
# These tables are an optimization. They contain subsets of the master table.
# TODO - these should be pushed into the Table class now
@tool_tables = make_table_hash(columns)
@file_tables = make_table_hash(columns)
@class_tables = make_table_hash(columns)
@method_tables = make_table_hash(columns)
tool_analyzers.each do |analyzer|
analyzer.generate_records(@yaml[analyzer.name], @table)
end
build_lookups!(table)
process_rows!(table)
tool_analyzers.each do |analyzer|
GRANULARITIES.each do |granularity|
metric_ranking = calculate_metric_scores(granularity, analyzer)
add_to_master_ranking(ranking(granularity), metric_ranking, analyzer)
end
end
rankings.each do |ranking|
ranking.delete(nil)
end
end
def location(item, value)
sub_table = get_sub_table(item, value)
if(sub_table.length==0)
raise AnalysisError, "The #{item.to_s} '#{value.to_s}' does not have any rows in the analysis table"
else
first_row = sub_table[0]
case item
when :class
MetricFu::Location.get(first_row.file_path, first_row.class_name, nil)
when :method
MetricFu::Location.get(first_row.file_path, first_row.class_name, first_row.method_name)
when :file
MetricFu::Location.get(first_row.file_path, nil, nil)
else
raise ArgumentError, "Item must be :class, :method, or :file"
end
end
end
#todo redo as item,value, options = {}
# Note that the other option for 'details' is :detailed (this isn't
# at all clear from this method itself
def problems_with(item, value, details = :summary, exclude_details = [])
sub_table = get_sub_table(item, value)
#grouping = Ruport::Data::Grouping.new(sub_table, :by => 'metric')
grouping = get_grouping(sub_table, :by => 'metric')
problems = {}
grouping.each do |metric, table|
if details == :summary || exclude_details.include?(metric)
problems[metric] = present_group(metric,table)
else
problems[metric] = present_group_details(metric,table)
end
end
problems
end
def worst_methods(size = nil)
@method_ranking.top(size)
end
def worst_classes(size = nil)
@class_ranking.top(size)
end
def worst_files(size = nil)
@file_ranking.top(size)
end
private
def get_grouping(table, opts)
#Ruport::Data::Grouping.new(table, opts)
MetricFu::Grouping.new(table, opts)
#@grouping_cache ||= {}
#@grouping_cache.fetch(grouping_key(table,opts)) do
# @grouping_cache[grouping_key(table,opts)] = Ruport::Data::Grouping.new(table, opts)
#end
end
def grouping_key(table, opts)
"table #{table.object_id} opts #{opts.inspect}"
end
def build_lookups!(table)
@class_and_method_to_file ||= {}
# Build a mapping from [class,method] => filename
# (and make sure the mapping is unique)
table.each do |row|
# We know that Saikuro provides the wrong data
next if row['metric'] == :saikuro
key = [row['class_name'], row['method_name']]
file_path = row['file_path']
@class_and_method_to_file[key] ||= file_path
end
end
def process_rows!(table)
# Correct incorrect rows in the table
table.each do |row|
row_metric = row['metric'] #perf optimization
if row_metric == :saikuro
fix_row_file_path!(row)
end
@tool_tables[row_metric] << row
@file_tables[row["file_path"]] << row
@class_tables[row["class_name"]] << row
@method_tables[row["method_name"]] << row
end
end
def fix_row_file_path!(row)
# We know that Saikuro rows are broken
# next unless row['metric'] == :saikuro
key = [row['class_name'], row['method_name']]
current_file_path = row['file_path'].to_s
correct_file_path = @class_and_method_to_file[key]
if(correct_file_path!=nil && correct_file_path.include?(current_file_path))
row['file_path'] = correct_file_path
else
# There wasn't an exact match, so we can do a substring match
matching_file_path = file_paths.detect {|file_path|
file_path!=nil && file_path.include?(current_file_path)
}
if(matching_file_path)
row['file_path'] = matching_file_path
end
end
end
def file_paths
@file_paths ||= @table.column('file_path').uniq
end
def ranking(column_name)
case column_name
when "file_path"
@file_ranking
when "class_name"
@class_ranking
when "method_name"
@method_ranking
else
raise ArgumentError, "Invalid column name #{column_name}"
end
end
def calculate_metric_scores(granularity, analyzer)
metric_ranking = MetricFu::Ranking.new
metric_violations = @tool_tables[analyzer.name]
metric_violations.each do |row|
location = row[granularity]
metric_ranking[location] ||= []
metric_ranking[location] << analyzer.map(row)
end
metric_ranking.each do |item, scores|
metric_ranking[item] = analyzer.reduce(scores)
end
metric_ranking
end
def add_to_master_ranking(master_ranking, metric_ranking, analyzer)
metric_ranking.each do |item, _|
master_ranking[item] ||= 0
master_ranking[item] += analyzer.score(metric_ranking, item) # scaling? Do we just add in the raw score?
end
end
def most_common_column(column_name, size)
#grouping = Ruport::Data::Grouping.new(@table,
# :by => column_name,
# :order => lambda { |g| -g.size})
get_grouping(@table, :by => column_name, :order => lambda {|g| -g.size})
values = []
grouping.each do |value, _|
values << value if value!=nil
if(values.size==size)
break
end
end
return nil if values.empty?
if(values.size == 1)
return values.first
else
return values
end
end
# TODO: As we get fancier, the presenter should
# be its own class, not just a method with a long
# case statement
def present_group(metric, group)
occurences = group.size
case(metric)
when :reek
"found #{occurences} code smells"
when :roodi
"found #{occurences} design problems"
when :churn
"detected high level of churn (changed #{group[0].times_changed} times)"
when :flog
complexity = get_mean(group.column("score"))
"#{"average " if occurences > 1}complexity is %.1f" % complexity
when :saikuro
complexity = get_mean(group.column("complexity"))
"#{"average " if occurences > 1}complexity is %.1f" % complexity
when :flay
"found #{occurences} code duplications"
when :rcov
average_code_uncoverage = get_mean(group.column("percentage_uncovered"))
"#{"average " if occurences > 1}uncovered code is %.1f%" % average_code_uncoverage
else
raise AnalysisError, "Unknown metric #{metric}"
end
end
def present_group_details(metric, group)
occurences = group.size
case(metric)
when :reek
message = "found #{occurences} code smells
"
group.each do |item|
type = item.data["reek__type_name"]
reek_message = item.data["reek__message"]
message << "* #{type}: #{reek_message}
"
end
message
when :roodi
message = "found #{occurences} design problems
"
group.each do |item|
problem = item.data["problems"]
message << "* #{problem}
"
end
message
when :churn
"detected high level of churn (changed #{group[0].times_changed} times)"
when :flog
complexity = get_mean(group.column("score"))
"#{"average " if occurences > 1}complexity is %.1f" % complexity
when :saikuro
complexity = get_mean(group.column("complexity"))
"#{"average " if occurences > 1}complexity is %.1f" % complexity
when :flay
message = "found #{occurences} code duplications
"
group.each do |item|
problem = item.data["flay_reason"]
problem = problem.gsub(/^[0-9]*\)/,'')
problem = problem.gsub(/files\:/,'
files:')
message << "* #{problem}
"
end
message
else
raise AnalysisError, "Unknown metric #{metric}"
end
end
def make_table_hash(columns)
Hash.new { |hash, key|
hash[key] = make_table(columns)
}
end
def make_table(columns)
Table.new(:column_names => columns)
end
def get_sub_table(item, value)
tables = {
:class => @class_tables,
:method => @method_tables,
:file => @file_tables,
:tool => @tool_tables
}.fetch(item) do
raise ArgumentError, "Item must be :class, :method, or :file"
end
tables[value]
end
def get_mean(collection)
collection_length = collection.length
sum = 0
sum = collection.inject( nil ) { |sum,x| sum ? sum+x : x }
(sum.to_f / collection_length.to_f)
end
end