jscruggs-metric_fu 0.9.0 → 1.0.0
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 +10 -0
- data/README +1 -154
- data/Rakefile +37 -2
- data/TODO +2 -1
- data/lib/base/base_template.rb +134 -0
- data/lib/base/configuration.rb +187 -0
- data/lib/base/generator.rb +144 -0
- data/lib/{metric_fu → base}/md5_tracker.rb +0 -0
- data/lib/base/report.rb +100 -0
- data/lib/{metric_fu → generators}/churn.rb +20 -22
- data/lib/generators/flay.rb +29 -0
- data/lib/generators/flog.rb +130 -0
- data/lib/generators/rcov.rb +74 -0
- data/lib/generators/reek.rb +31 -0
- data/lib/generators/roodi.rb +28 -0
- data/lib/generators/saikuro.rb +201 -0
- data/lib/generators/stats.rb +43 -0
- data/lib/metric_fu.rb +20 -3
- data/lib/templates/{churn.html.erb → standard/churn.html.erb} +12 -4
- data/lib/templates/{default.css → standard/default.css} +20 -1
- data/lib/templates/{flay.html.erb → standard/flay.html.erb} +12 -9
- data/lib/templates/standard/flog.html.erb +52 -0
- data/lib/templates/standard/index.html.erb +38 -0
- data/lib/templates/standard/rcov.html.erb +42 -0
- data/lib/templates/standard/reek.html.erb +41 -0
- data/lib/templates/{roodi.html.erb → standard/roodi.html.erb} +10 -8
- data/lib/templates/standard/saikuro.html.erb +83 -0
- data/lib/templates/standard/standard_template.rb +26 -0
- data/lib/templates/standard/stats.html.erb +54 -0
- data/spec/base/base_template_spec.rb +140 -0
- data/spec/base/configuration_spec.rb +303 -0
- data/spec/base/generator_spec.rb +159 -0
- data/spec/{md5_tracker_spec.rb → base/md5_tracker_spec.rb} +1 -1
- data/spec/base/report_spec.rb +139 -0
- data/spec/generators/churn_spec.rb +152 -0
- data/spec/generators/flay_spec.rb +101 -0
- data/spec/generators/flog_spec.rb +189 -0
- data/spec/generators/reek_spec.rb +47 -0
- data/spec/generators/saikuro_spec.rb +35 -0
- data/spec/generators/stats_spec.rb +74 -0
- data/spec/spec_helper.rb +24 -7
- data/tasks/metric_fu.rake +14 -0
- data/{lib/tasks → tasks}/railroad.rake +0 -0
- data/{lib/metric_fu → vendor}/saikuro/saikuro.rb +0 -0
- metadata +58 -47
- data/lib/metric_fu/base.rb +0 -160
- data/lib/metric_fu/flay.rb +0 -17
- data/lib/metric_fu/flog.rb +0 -129
- data/lib/metric_fu/reek.rb +0 -17
- data/lib/metric_fu/roodi.rb +0 -17
- data/lib/tasks/churn.rake +0 -9
- data/lib/tasks/coverage.rake +0 -54
- data/lib/tasks/flay.rake +0 -6
- data/lib/tasks/flog.rake +0 -69
- data/lib/tasks/metric_fu.rake +0 -24
- data/lib/tasks/metric_fu.rb +0 -6
- data/lib/tasks/reek.rake +0 -6
- data/lib/tasks/roodi.rake +0 -7
- data/lib/tasks/saikuro.rake +0 -35
- data/lib/tasks/stats.rake +0 -14
- data/lib/templates/flog.html.erb +0 -38
- data/lib/templates/flog_page.html.erb +0 -25
- data/lib/templates/reek.html.erb +0 -30
- data/spec/base_spec.rb +0 -57
- data/spec/churn_spec.rb +0 -117
- data/spec/config_spec.rb +0 -110
- data/spec/flay_spec.rb +0 -19
- data/spec/flog_spec.rb +0 -208
- data/spec/reek_spec.rb +0 -26
data/lib/base/report.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module MetricFu
|
2
|
+
|
3
|
+
# MetricFu.report memoizes access to a Report object, that will be
|
4
|
+
# used throughout the lifecycle of the MetricFu app.
|
5
|
+
def self.report
|
6
|
+
@report ||= Report.new
|
7
|
+
end
|
8
|
+
|
9
|
+
# = Report
|
10
|
+
#
|
11
|
+
# The Report class is responsible two things:
|
12
|
+
#
|
13
|
+
# It adds information to the yaml report, produced by the system
|
14
|
+
# as a whole, for each of the generators used in this test run.
|
15
|
+
#
|
16
|
+
# It also handles passing the information from each generator used
|
17
|
+
# in this test run out to the template class set in
|
18
|
+
# MetricFu::Configuration.
|
19
|
+
class Report
|
20
|
+
|
21
|
+
# Renders the result of the report_hash into a yaml serialization
|
22
|
+
# ready for writing out to a file.
|
23
|
+
#
|
24
|
+
# @return YAML
|
25
|
+
# A YAML object containing the results of the report generation
|
26
|
+
# process
|
27
|
+
def to_yaml
|
28
|
+
report_hash.to_yaml
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def report_hash #:nodoc:
|
33
|
+
@report_hash ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Instantiates a new template class based on the configuration set
|
37
|
+
# in MetricFu::Configuration, or through the MetricFu.config block
|
38
|
+
# in your rake file (defaults to the included StandardTemplate) and
|
39
|
+
# assigns the report_hash to the report_hash to the template and
|
40
|
+
# asks itself to write itself out.
|
41
|
+
def save_templatized_report
|
42
|
+
@template = MetricFu.template_class.new
|
43
|
+
@template.report = report_hash
|
44
|
+
@template.write
|
45
|
+
end
|
46
|
+
|
47
|
+
# Adds a hash from a passed report, produced by one of the Generator
|
48
|
+
# classes to the aggregate report_hash managed by this hash.
|
49
|
+
#
|
50
|
+
# @param report_type Hash
|
51
|
+
# The hash to add to the aggregate report_hash
|
52
|
+
def add(report_type)
|
53
|
+
clazz = MetricFu.const_get(report_type.to_s.capitalize)
|
54
|
+
report_hash.merge!(clazz.generate_report)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Saves the passed in content to the passed in directory. If
|
58
|
+
# a filename is passed in it will be used as the name of the
|
59
|
+
# file, otherwise it will default to 'index.html'
|
60
|
+
#
|
61
|
+
# @param content String
|
62
|
+
# A string containing the content (usually html) to be written
|
63
|
+
# to the file.
|
64
|
+
#
|
65
|
+
# @param dir String
|
66
|
+
# A dir containing the path to the directory to write the file in.
|
67
|
+
#
|
68
|
+
# @param file String
|
69
|
+
# A filename to save the path as. Defaults to 'index.html'.
|
70
|
+
#
|
71
|
+
def save_output(content, dir, file='index.html')
|
72
|
+
open("#{dir}/#{file}", "w") do |f|
|
73
|
+
f.puts content
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Checks to discover whether we should try and open the results
|
78
|
+
# of the report in the browser on this system. We only try and open
|
79
|
+
# in the browser if we're on OS X and we're not running in a
|
80
|
+
# CruiseControl.rb environment. See MetricFu.configuration for more
|
81
|
+
# details about how we make those guesses.
|
82
|
+
#
|
83
|
+
# @return Boolean
|
84
|
+
# Should we open in the browser or not?
|
85
|
+
def open_in_browser?
|
86
|
+
MetricFu.configuration.platform.include?('darwin') &&
|
87
|
+
! MetricFu.configuration.is_cruise_control_rb?
|
88
|
+
end
|
89
|
+
|
90
|
+
# Shows 'index.html' from the passed directory in the browser
|
91
|
+
# if we're able to open the browser on this platform.
|
92
|
+
#
|
93
|
+
# @param dir String
|
94
|
+
# The directory path where the 'index.html' we want to open is
|
95
|
+
# stored
|
96
|
+
def show_in_browser(dir)
|
97
|
+
system("open #{dir}/index.html") if open_in_browser?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -1,27 +1,32 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
require 'generator'
|
1
3
|
module MetricFu
|
2
4
|
|
3
|
-
|
4
|
-
Churn.generate_report(MetricFu.churn)
|
5
|
-
system("open #{Churn.metric_dir}/index.html") if open_in_browser?
|
6
|
-
end
|
7
|
-
|
8
|
-
class Churn < Base::Generator
|
5
|
+
class Churn < Generator
|
9
6
|
|
10
7
|
def initialize(options={})
|
11
|
-
|
8
|
+
super
|
12
9
|
if File.exist?(".git")
|
13
|
-
@source_control = Git.new(
|
10
|
+
@source_control = Git.new(MetricFu.churn[:start_date])
|
14
11
|
elsif File.exist?(".svn")
|
15
|
-
@source_control = Svn.new(
|
12
|
+
@source_control = Svn.new(MetricFu.churn[:start_date])
|
16
13
|
else
|
17
14
|
raise "Churning requires a subversion or git repo"
|
18
15
|
end
|
16
|
+
@minimum_churn_count = MetricFu.churn[:minimum_churn_count] || 5
|
17
|
+
end
|
19
18
|
|
20
|
-
|
19
|
+
def emit
|
20
|
+
@changes = parse_log_for_changes.reject {|file, change_count| change_count < @minimum_churn_count}
|
21
21
|
end
|
22
22
|
|
23
23
|
def analyze
|
24
|
-
@changes =
|
24
|
+
@changes = @changes.to_a.sort {|x,y| y[1] <=> x[1]}
|
25
|
+
@changes = @changes.map {|change| {:file_path => change[0], :times_changed => change[1] }}
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_h
|
29
|
+
{:churn => {:changes => @changes}}
|
25
30
|
end
|
26
31
|
|
27
32
|
private
|
@@ -41,13 +46,6 @@ module MetricFu
|
|
41
46
|
def initialize(start_date=nil)
|
42
47
|
@start_date = start_date
|
43
48
|
end
|
44
|
-
|
45
|
-
private
|
46
|
-
def require_rails_env
|
47
|
-
# not sure if the following works because active_support might only be in vendor/rails
|
48
|
-
# require 'activesupport'
|
49
|
-
require RAILS_ROOT + '/config/environment'
|
50
|
-
end
|
51
49
|
end
|
52
50
|
|
53
51
|
class Git < SourceControl
|
@@ -58,8 +56,8 @@ module MetricFu
|
|
58
56
|
private
|
59
57
|
def date_range
|
60
58
|
if @start_date
|
61
|
-
|
62
|
-
"--after=#{
|
59
|
+
date = Chronic.parse(@start_date)
|
60
|
+
"--after=#{date.strftime('%Y-%m-%d')}"
|
63
61
|
end
|
64
62
|
end
|
65
63
|
|
@@ -73,8 +71,8 @@ module MetricFu
|
|
73
71
|
private
|
74
72
|
def date_range
|
75
73
|
if @start_date
|
76
|
-
|
77
|
-
"--revision {#{
|
74
|
+
date = Chronic.parse(@start_date)
|
75
|
+
"--revision {#{date.strftime('%Y-%m-%d')}}:{#{Time.now.strftime('%Y-%m-%d')}}"
|
78
76
|
end
|
79
77
|
end
|
80
78
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'generator'
|
2
|
+
module MetricFu
|
3
|
+
|
4
|
+
class Flay < Generator
|
5
|
+
|
6
|
+
def emit
|
7
|
+
files_to_flay = MetricFu.flay[:dirs_to_flay].map{|dir| Dir[File.join(dir, "**/*.rb")] }
|
8
|
+
@output = `flay #{files_to_flay.join(" ")}`
|
9
|
+
end
|
10
|
+
|
11
|
+
def analyze
|
12
|
+
@matches = @output.chomp.split("\n\n").map{|m| m.split("\n ") }
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
target = []
|
17
|
+
total_score = @matches.shift.first.split('=').last.strip
|
18
|
+
@matches.each do |problem|
|
19
|
+
reason = problem.shift.strip
|
20
|
+
lines_info = problem.map do |full_line|
|
21
|
+
name, line = full_line.split(":")
|
22
|
+
{:name => name.strip, :line => line.strip}
|
23
|
+
end
|
24
|
+
target << [:reason => reason, :matches => lines_info]
|
25
|
+
end
|
26
|
+
{:flay => {:total_score => total_score, :matches => target.flatten}}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module MetricFu
|
2
|
+
|
3
|
+
class Flog < Generator
|
4
|
+
attr_reader :pages
|
5
|
+
|
6
|
+
SCORE_FORMAT = "%0.2f"
|
7
|
+
METHOD_LINE_REGEX = /(\d+\.\d+):\s+([A-Za-z]+#.*)/
|
8
|
+
OPERATOR_LINE_REGEX = /\s*(\d+\.\d+):\s(.*)$/
|
9
|
+
|
10
|
+
def emit
|
11
|
+
metric_dir = MetricFu::Flog.metric_directory
|
12
|
+
MetricFu.flog[:dirs_to_flog].each do |directory|
|
13
|
+
Dir.glob("#{directory}/**/*.rb").each do |filename|
|
14
|
+
output_dir = "#{metric_dir}/#{filename.split("/")[0..-2].join("/")}"
|
15
|
+
mkdir_p(output_dir, :verbose => false) unless File.directory?(output_dir)
|
16
|
+
if MetricFu::MD5Tracker.file_changed?(filename, metric_dir)
|
17
|
+
`flog -ad #{filename} > #{metric_dir}/#{filename.split('.')[0]}.txt`
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
rescue LoadError
|
22
|
+
if RUBY_PLATFORM =~ /java/
|
23
|
+
puts 'running in jruby - flog tasks not available'
|
24
|
+
else
|
25
|
+
puts 'sudo gem install flog # if you want the flog tasks'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse(text)
|
30
|
+
summary, methods_summary = text.split "\n\n"
|
31
|
+
score, average = summary.split("\n").map {|line| line[OPERATOR_LINE_REGEX, 1]}
|
32
|
+
return nil unless score && methods_summary
|
33
|
+
page = Flog::Page.new(score, average)
|
34
|
+
methods_summary.each_line do |method_line|
|
35
|
+
if match = method_line.match(METHOD_LINE_REGEX)
|
36
|
+
page.scanned_methods << ScannedMethod.new(match[2], match[1])
|
37
|
+
elsif match = method_line.match(OPERATOR_LINE_REGEX)
|
38
|
+
return if page.scanned_methods.empty?
|
39
|
+
page.scanned_methods.last.operators << Operator.new(match[1], match[2])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
page
|
43
|
+
end
|
44
|
+
|
45
|
+
def analyze
|
46
|
+
@pages = []
|
47
|
+
flog_results.each do |path|
|
48
|
+
page = parse(open(path, "r") { |f| f.read })
|
49
|
+
if page
|
50
|
+
page.path = path.sub(metric_directory, "").sub(".txt", ".rb")
|
51
|
+
@pages << page
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_h
|
57
|
+
number_of_methods = @pages.inject(0) {|count, page| count += page.scanned_methods.size}
|
58
|
+
total_flog_score = @pages.inject(0) {|total, page| total += page.score}
|
59
|
+
sorted_pages = @pages.sort_by {|page| page.score }.reverse
|
60
|
+
{:flog => { :total => total_flog_score,
|
61
|
+
:average => round_to_tenths(total_flog_score/number_of_methods),
|
62
|
+
:pages => sorted_pages.map {|page| page.to_h}}}
|
63
|
+
end
|
64
|
+
|
65
|
+
def round_to_tenths(decimal)
|
66
|
+
(decimal * 10).round / 10.0
|
67
|
+
end
|
68
|
+
def flog_results
|
69
|
+
Dir.glob("#{metric_directory}/**/*.txt")
|
70
|
+
end
|
71
|
+
|
72
|
+
class Operator
|
73
|
+
attr_accessor :score, :operator
|
74
|
+
|
75
|
+
def initialize(score, operator)
|
76
|
+
@score = score.to_f
|
77
|
+
@operator = operator
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_h
|
81
|
+
{:score => @score, :operator => @operator}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class ScannedMethod
|
86
|
+
attr_accessor :name, :score, :operators
|
87
|
+
|
88
|
+
def initialize(name, score, operators = [])
|
89
|
+
@name = name
|
90
|
+
@score = score.to_f
|
91
|
+
@operators = operators
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_h
|
95
|
+
{:name => @name,
|
96
|
+
:score => @score,
|
97
|
+
:operators => @operators.map {|o| o.to_h}}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
class Flog::Page < MetricFu::Generator
|
104
|
+
attr_accessor :path, :score, :scanned_methods, :average_score
|
105
|
+
|
106
|
+
def initialize(score, average_score, scanned_methods = [])
|
107
|
+
@score = score.to_f
|
108
|
+
@scanned_methods = scanned_methods
|
109
|
+
@average_score = average_score.to_f
|
110
|
+
end
|
111
|
+
|
112
|
+
def filename
|
113
|
+
File.basename(path, ".txt")
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_h
|
117
|
+
{:score => @score,
|
118
|
+
:scanned_methods => @scanned_methods.map {|sm| sm.to_h},
|
119
|
+
:highest_score => highest_score,
|
120
|
+
:average_score => average_score,
|
121
|
+
:path => path}
|
122
|
+
end
|
123
|
+
|
124
|
+
def highest_score
|
125
|
+
scanned_methods.inject(0) do |highest, m|
|
126
|
+
m.score > highest ? m.score : highest
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module MetricFu
|
4
|
+
|
5
|
+
class Rcov < Generator
|
6
|
+
NEW_FILE_MARKER = ("=" * 80) + "\n"
|
7
|
+
|
8
|
+
|
9
|
+
class Line
|
10
|
+
attr_accessor :content, :was_run
|
11
|
+
|
12
|
+
def initialize(content, was_run)
|
13
|
+
@content = content
|
14
|
+
@was_run = was_run
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_h
|
18
|
+
{:content => @content, :was_run => @was_run}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def emit
|
23
|
+
begin
|
24
|
+
FileUtils.rm_rf(MetricFu::Rcov.metric_directory, :verbose => false)
|
25
|
+
Dir.mkdir(MetricFu::Rcov.metric_directory)
|
26
|
+
test_files = FileList[*MetricFu.rcov[:test_files]].join(' ')
|
27
|
+
rcov_opts = MetricFu.rcov[:rcov_opts].join(' ')
|
28
|
+
output = ">> #{MetricFu::Rcov.metric_directory}/rcov.txt"
|
29
|
+
`rcov --include-file #{test_files} #{rcov_opts} #{output}`
|
30
|
+
rescue LoadError
|
31
|
+
if RUBY_PLATFORM =~ /java/
|
32
|
+
puts 'running in jruby - rcov tasks not available'
|
33
|
+
else
|
34
|
+
puts 'sudo gem install rcov # if you want the rcov tasks'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def analyze
|
41
|
+
output = File.open(MetricFu::Rcov.metric_directory + '/rcov.txt').read
|
42
|
+
output = output.split(NEW_FILE_MARKER)
|
43
|
+
# Throw away the first entry - it's the execution time etc.
|
44
|
+
output.shift
|
45
|
+
files = {}
|
46
|
+
output.each_slice(2) {|out| files[out.first.strip] = out.last}
|
47
|
+
files.each_pair {|fname, content| files[fname] = content.split("\n") }
|
48
|
+
files.each_pair do |fname, content|
|
49
|
+
content.map! do |raw_line|
|
50
|
+
if raw_line.match(/^!!/)
|
51
|
+
line = Line.new(raw_line.gsub('!!', ' '), false).to_h
|
52
|
+
else
|
53
|
+
line = Line.new(raw_line, true).to_h
|
54
|
+
end
|
55
|
+
end
|
56
|
+
files[fname] = {:lines => content}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Calculate the percentage of lines run in each file
|
60
|
+
files.each_pair do |fname, content|
|
61
|
+
lines = content[:lines]
|
62
|
+
lines_run = lines.find_all {|line| line[:was_run] == true }.length
|
63
|
+
total_lines = lines.length
|
64
|
+
percent_run = ((lines_run.to_f / total_lines.to_f) * 100).round
|
65
|
+
files[fname][:percent_run] = percent_run
|
66
|
+
end
|
67
|
+
@rcov = files
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_h
|
71
|
+
{:rcov => @rcov}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module MetricFu
|
2
|
+
|
3
|
+
class Reek < Generator
|
4
|
+
REEK_REGEX = /^(\S+) (.*) \((.*)\)$/
|
5
|
+
|
6
|
+
def emit
|
7
|
+
files_to_reek = MetricFu.reek[:dirs_to_reek].map{|dir| Dir[File.join(dir, "**/*.rb")] }
|
8
|
+
@output = `reek #{files_to_reek.join(" ")}`
|
9
|
+
end
|
10
|
+
|
11
|
+
def analyze
|
12
|
+
@matches = @output.chomp.split("\n\n").map{|m| m.split("\n") }
|
13
|
+
@matches = @matches.map do |match|
|
14
|
+
file_path = match.shift.split('--').first
|
15
|
+
file_path = file_path.gsub('"', ' ').strip
|
16
|
+
code_smells = match.map do |smell|
|
17
|
+
match_object = smell.match(REEK_REGEX)
|
18
|
+
{:method => match_object[1].strip,
|
19
|
+
:message => match_object[2].strip,
|
20
|
+
:type => match_object[3].strip}
|
21
|
+
end
|
22
|
+
{:file_path => file_path, :code_smells => code_smells}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h
|
27
|
+
{:reek => {:matches => @matches}}
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module MetricFu
|
2
|
+
|
3
|
+
|
4
|
+
class Roodi < Generator
|
5
|
+
|
6
|
+
def emit
|
7
|
+
files_to_analyze = MetricFu.roodi[:dirs_to_roodi].map{|dir| Dir[File.join(dir, "**/*.rb")] }
|
8
|
+
@output = `roodi #{files_to_analyze.join(" ")}`
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
def analyze
|
13
|
+
@matches = @output.chomp.split("\n").map{|m| m.split(" - ") }
|
14
|
+
total = @matches.pop
|
15
|
+
@matches.reject! {|array| array.empty? }
|
16
|
+
@matches.map! do |match|
|
17
|
+
file, line = match[0].split(':')
|
18
|
+
problem = match[1]
|
19
|
+
{:file => file, :line => line, :problem => problem}
|
20
|
+
end
|
21
|
+
@roodi_results = {:total => total, :problems => @matches}
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_h
|
25
|
+
{:roodi => @roodi_results}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
module MetricFu
|
2
|
+
|
3
|
+
class Saikuro < Generator
|
4
|
+
|
5
|
+
|
6
|
+
def emit
|
7
|
+
relative_path = [File.dirname(__FILE__), '..', '..',
|
8
|
+
'vendor', 'saikuro', 'saikuro.rb']
|
9
|
+
saikuro = File.expand_path(File.join(relative_path))
|
10
|
+
options_string = MetricFu.saikuro.inject("") do |o, h|
|
11
|
+
o + "--#{h.join(' ')} "
|
12
|
+
end
|
13
|
+
sh %{ruby "#{saikuro}" #{options_string}} do |ok, response|
|
14
|
+
unless ok
|
15
|
+
puts "Saikuro failed with exit status: #{response.exitstatus}"
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def analyze
|
22
|
+
@files = []
|
23
|
+
saikuro_results.each do |path|
|
24
|
+
if Saikuro::SFile.is_valid_text_file?(path)
|
25
|
+
file = Saikuro::SFile.new(path)
|
26
|
+
if file
|
27
|
+
@files << file
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
@files = @files.sort_by do |file|
|
32
|
+
file.elements.
|
33
|
+
max {|a,b| a.complexity.to_i <=> b.complexity.to_i}.
|
34
|
+
complexity.to_i
|
35
|
+
end
|
36
|
+
@files.reverse!
|
37
|
+
klasses = []
|
38
|
+
@files.each {|f| klasses << f.elements}
|
39
|
+
klasses.flatten!
|
40
|
+
@classes = klasses.sort_by {|k| k.complexity.to_i}
|
41
|
+
@classes.reverse!
|
42
|
+
meths = []
|
43
|
+
@files.each {|f|
|
44
|
+
f.elements.each {|el|
|
45
|
+
el.defs.each {|defn|
|
46
|
+
defn.name = "#{el.name}##{defn.name}"
|
47
|
+
meths << defn}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
meths = meths.sort_by {|meth| meth.complexity.to_i}
|
51
|
+
@meths = meths.reverse
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_h
|
56
|
+
files = @files.map do |file|
|
57
|
+
my_file = file.to_h
|
58
|
+
my_file[:filename] = file.filename
|
59
|
+
my_file
|
60
|
+
end
|
61
|
+
{:saikuro => {:files => files,
|
62
|
+
:classes => @classes.map {|c| c.to_h},
|
63
|
+
:methods => @meths.map {|m| m.to_h}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def saikuro_results
|
69
|
+
Dir.glob("#{metric_directory}/**/*.html")
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
class Saikuro::SFile
|
78
|
+
|
79
|
+
attr_reader :elements
|
80
|
+
|
81
|
+
def initialize(path)
|
82
|
+
@path = path
|
83
|
+
@file_handle = File.open(@path, "r")
|
84
|
+
@elements = []
|
85
|
+
get_elements
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.is_valid_text_file?(path)
|
89
|
+
File.open(path, "r") do |f|
|
90
|
+
unless f.readline.match /--/
|
91
|
+
return false
|
92
|
+
else
|
93
|
+
return true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def filename
|
99
|
+
File.basename(@path, '_cyclo.html')
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_h
|
103
|
+
merge_classes
|
104
|
+
{:classes => @elements}
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_elements
|
108
|
+
begin
|
109
|
+
while ( line = @file_handle.readline) do
|
110
|
+
if line.match /START/
|
111
|
+
line = @file_handle.readline
|
112
|
+
element = Saikuro::ParsingElement.new(line)
|
113
|
+
elsif line.match /END/
|
114
|
+
@elements << element
|
115
|
+
element = nil
|
116
|
+
else
|
117
|
+
element << line
|
118
|
+
end
|
119
|
+
end
|
120
|
+
rescue EOFError
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def merge_classes
|
127
|
+
new_elements = []
|
128
|
+
get_class_names.each do |target_class|
|
129
|
+
elements = @elements.find_all {|el| el.name == target_class }
|
130
|
+
complexity = 0
|
131
|
+
lines = 0
|
132
|
+
defns = []
|
133
|
+
elements.each do |el|
|
134
|
+
complexity += el.complexity.to_i
|
135
|
+
lines += el.lines.to_i
|
136
|
+
defns << el.defs
|
137
|
+
end
|
138
|
+
|
139
|
+
new_element = {:class_name => target_class,
|
140
|
+
:complexity => complexity,
|
141
|
+
:lines => lines,
|
142
|
+
:methods => defns.flatten.map {|d| d.to_h}}
|
143
|
+
new_element[:methods] = new_element[:methods].
|
144
|
+
sort_by {|x| x[:complexity] }.
|
145
|
+
reverse
|
146
|
+
|
147
|
+
new_elements << new_element
|
148
|
+
end
|
149
|
+
@elements = new_elements if new_elements
|
150
|
+
end
|
151
|
+
|
152
|
+
def get_class_names
|
153
|
+
class_names = []
|
154
|
+
@elements.each do |element|
|
155
|
+
unless class_names.include?(element.name)
|
156
|
+
class_names << element.name
|
157
|
+
end
|
158
|
+
end
|
159
|
+
class_names
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
class Saikuro::ParsingElement
|
165
|
+
TYPE_REGEX=/Type:(.*) Name/
|
166
|
+
NAME_REGEX=/Name:(.*) Complexity/
|
167
|
+
COMPLEXITY_REGEX=/Complexity:(.*) Lines/
|
168
|
+
LINES_REGEX=/Lines:(.*)/
|
169
|
+
|
170
|
+
attr_reader :complexity, :lines, :defs, :element_type
|
171
|
+
attr_accessor :name
|
172
|
+
|
173
|
+
def initialize(line)
|
174
|
+
@line = line
|
175
|
+
@element_type = line.match(TYPE_REGEX)[1].strip
|
176
|
+
@name = line.match(NAME_REGEX)[1].strip
|
177
|
+
@complexity = line.match(COMPLEXITY_REGEX)[1].strip
|
178
|
+
@lines = line.match(LINES_REGEX)[1].strip
|
179
|
+
@defs = []
|
180
|
+
end
|
181
|
+
|
182
|
+
def <<(line)
|
183
|
+
@defs << Saikuro::ParsingElement.new(line)
|
184
|
+
end
|
185
|
+
|
186
|
+
def to_h
|
187
|
+
base = {:name => @name, :complexity => @complexity.to_i, :lines => @lines.to_i}
|
188
|
+
unless @defs.empty?
|
189
|
+
defs = @defs.map do |my_def|
|
190
|
+
my_def = my_def.to_h
|
191
|
+
my_def.delete(:defs)
|
192
|
+
my_def
|
193
|
+
end
|
194
|
+
base[:defs] = defs
|
195
|
+
end
|
196
|
+
return base
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
end
|