metric_fu 1.5.1 → 2.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.
Files changed (40) hide show
  1. data/HISTORY +5 -0
  2. data/MIT-LICENSE +1 -1
  3. data/README +8 -6
  4. data/Rakefile +5 -3
  5. data/TODO +0 -5
  6. data/lib/base/base_template.rb +16 -0
  7. data/lib/base/churn_analyzer.rb +52 -0
  8. data/lib/base/code_issue.rb +97 -0
  9. data/lib/base/configuration.rb +4 -2
  10. data/lib/base/flay_analyzer.rb +50 -0
  11. data/lib/base/flog_analyzer.rb +43 -0
  12. data/lib/base/line_numbers.rb +65 -0
  13. data/lib/base/location.rb +83 -0
  14. data/lib/base/metric_analyzer.rb +404 -0
  15. data/lib/base/ranking.rb +33 -0
  16. data/lib/base/rcov_analyzer.rb +43 -0
  17. data/lib/base/reek_analyzer.rb +114 -0
  18. data/lib/base/roodi_analyzer.rb +37 -0
  19. data/lib/base/saikuro_analyzer.rb +48 -0
  20. data/lib/base/scoring_strategies.rb +29 -0
  21. data/lib/base/stats_analyzer.rb +37 -0
  22. data/lib/base/table.rb +102 -0
  23. data/lib/generators/hotspots.rb +52 -0
  24. data/lib/generators/rcov.rb +41 -0
  25. data/lib/metric_fu.rb +5 -3
  26. data/lib/templates/awesome/hotspots.html.erb +54 -0
  27. data/lib/templates/awesome/index.html.erb +3 -0
  28. data/lib/templates/standard/hotspots.html.erb +54 -0
  29. data/spec/base/line_numbers_spec.rb +62 -0
  30. data/spec/generators/rails_best_practices_spec.rb +52 -0
  31. data/spec/generators/rcov_spec.rb +180 -0
  32. data/spec/generators/roodi_spec.rb +24 -0
  33. data/spec/graphs/rails_best_practices_grapher_spec.rb +61 -0
  34. data/spec/graphs/stats_grapher_spec.rb +68 -0
  35. data/spec/resources/line_numbers/foo.rb +33 -0
  36. data/spec/resources/line_numbers/module.rb +11 -0
  37. data/spec/resources/line_numbers/module_surrounds_class.rb +15 -0
  38. data/spec/resources/line_numbers/two_classes.rb +11 -0
  39. data/spec/resources/yml/metric_missing.yml +1 -0
  40. metadata +51 -11
data/HISTORY CHANGED
@@ -1,3 +1,8 @@
1
+ === MetricFu 2.0.0 / 2010-11-10
2
+
3
+ * Hotspots - Dan Mayer, Ben Brinckerhoff, Jake Scruggs
4
+ * Rcov integration with Hotspots - Jake Scruggs, Tony Castiglione, Rob Meyer
5
+
1
6
  === MetricFu 1.5.1 / 2010-7-28
2
7
 
3
8
  * Patch that allows graphers to skip dates that didn't generate metrics for that graph (GitHub Issue #20). - Chris Griego
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2008,2009 Jake Scruggs
1
+ Copyright (c) 2008,2009,2010 Jake Scruggs
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
data/README CHANGED
@@ -3,12 +3,13 @@ See http://metric-fu.rubyforge.org/ for documentation, or the HISTORY file for a
3
3
  How to contribute:
4
4
 
5
5
  1. Fork metric_fu on github.
6
- 2. 'gem install metric_fu --development' #to get development dependencies
7
- 3. Run the tests ('rake')
8
- 4. Run metric_fu on itself ('rake metrics:all')
9
- 5. Make the changes you want and back them up with tests.
10
- 6. Make sure two important rake tests still run ('rake' and 'rake metrics:all')
11
- 7. Commit and send me a pull request with details as to what has been changed.
6
+ 2. 'gem install metric_fu' #to get gem dependencies
7
+ 3. Install the gems RSpec 1.3.0, test-construct, and googlecharts
8
+ 4. Run the tests ('rake')
9
+ 5. Run metric_fu on itself ('rake metrics:all')
10
+ 6. Make the changes you want and back them up with tests.
11
+ 7. Make sure two important rake tests still run ('rake' and 'rake metrics:all')
12
+ 8. Commit and send me a pull request with details as to what has been changed.
12
13
 
13
14
  Extra Credit:
14
15
  1. Make sure your changes work in 1.8.7, Ruby Enterprise Edition, and 1.9.1 (Hint use 'rvm' to help install multiple rubies)
@@ -24,4 +25,5 @@ Resources:
24
25
  Homepage: http://metric-fu.rubyforge.org/
25
26
  Github: http://github.com/jscruggs/metric_fu
26
27
  Google Group: http://groups.google.com/group/metric_fu
28
+ Issue Tracker: http://github.com/jscruggs/metric_fu/issues
27
29
  My Blog: http://jakescruggs.blogspot.com/
data/Rakefile CHANGED
@@ -8,9 +8,11 @@ desc "Run all specs in spec directory"
8
8
  Spec::Rake::SpecTask.new(:spec) do |t|
9
9
  t.spec_files = FileList['spec/**/*_spec.rb']
10
10
  end
11
-
11
+
12
12
  MetricFu::Configuration.run do |config|
13
- config.roodi = config.roodi.merge(:roodi_config => 'config/roodi_config.yml')
13
+ config.roodi = config.roodi.merge(:roodi_config => 'config/roodi_config.yml')
14
+ config.churn = { :start_date => "1 year ago", :minimum_churn_count => 10}
15
+ config.hotspots = { :start_date => "1 year ago", :minimum_churn_count => 10}
14
16
  end
15
-
17
+
16
18
  task :default => :spec
data/TODO CHANGED
@@ -1,9 +1,4 @@
1
1
  == TODO list
2
2
 
3
3
  * Color code flog results with scale from: http://jakescruggs.blogspot.com/2008/08/whats-good-flog-score.html
4
- * Integrate Flog, Saikuro, and Coverage into one report so you can see methods that have high complexity and low coverage (this is a big one)
5
- * Update flog specs so that they actually run flog
6
- * Add flay specs that run flay
7
- * Convert readme to markdown and rename to README.mkdn so github will render it
8
- * Saikuro.rb falls over if tries to parse an empty file. Fair enough. We shouldn't feed it empty files
9
4
  * Make running metric_fu on metric_fu less embarrassing
@@ -117,6 +117,22 @@ module MetricFu
117
117
  name
118
118
  end
119
119
  end
120
+
121
+ def display_location(location, stat)
122
+ file_path, class_name, method_name = location.file_path, location.class_name, location.method_name
123
+ str = ""
124
+ str += link_to_filename(file_path)
125
+ str += " : " if method_name || class_name
126
+ if(method_name)
127
+ str += "#{method_name}"
128
+ else
129
+ #TODO HOTSPOTS BUG ONLY exists on move over to metric_fu
130
+ if class_name.is_a?(String)
131
+ str+= "#{class_name}"
132
+ end
133
+ end
134
+ str
135
+ end
120
136
 
121
137
  def file_url(name, line) # :nodoc:
122
138
  return '' unless name
@@ -0,0 +1,52 @@
1
+ class ChurnAnalyzer
2
+ include ScoringStrategies
3
+
4
+ COLUMNS = %w{times_changed}
5
+
6
+ def columns
7
+ COLUMNS
8
+ end
9
+
10
+ def name
11
+ :churn
12
+ end
13
+
14
+ def map(row)
15
+ ScoringStrategies.present(row)
16
+ end
17
+
18
+ def reduce(scores)
19
+ ScoringStrategies.sum(scores)
20
+ end
21
+
22
+ def score(metric_ranking, item)
23
+ flat_churn_score = 0.50
24
+ metric_ranking.scored?(item) ? flat_churn_score : 0
25
+ end
26
+
27
+ def generate_records(data, table)
28
+ return if data==nil
29
+ Array(data[:changes]).each do |change|
30
+ table << {
31
+ "metric" => :churn,
32
+ "times_changed" => change[:times_changed],
33
+ "file_path" => change[:file_path]
34
+ }
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def self.update_changes(total, changed)
41
+ changed.each do |change|
42
+ #should work as has_key(change), but hash == doesn't work on 1.8.6 here for some reason it never matches
43
+ if total.has_key?(change.to_a.sort)
44
+ total[change.to_a.sort] += 1
45
+ else
46
+ total[change.to_a.sort] = 1
47
+ end
48
+ end
49
+ total
50
+ end
51
+
52
+ end
@@ -0,0 +1,97 @@
1
+ require 'delegate'
2
+ require MetricFu::LIB_ROOT + '/base/metric_analyzer'
3
+ require MetricFu::LIB_ROOT + '/base/flog_analyzer'
4
+ require MetricFu::LIB_ROOT + '/base/saikuro_analyzer'
5
+ require MetricFu::LIB_ROOT + '/base/churn_analyzer'
6
+ require MetricFu::LIB_ROOT + '/base/reek_analyzer'
7
+ require MetricFu::LIB_ROOT + '/base/flay_analyzer'
8
+
9
+ module CarefulArray
10
+
11
+ def carefully_remove(elements)
12
+ missing_elements = elements - self
13
+ raise "Cannot delete missing elements: #{missing_elements.inspect}" unless missing_elements.empty?
14
+ (self - elements).extend(CarefulArray)
15
+ end
16
+
17
+ end
18
+
19
+ class CodeIssue < DelegateClass(Record) #DelegateClass(Ruport::Data::Record)
20
+ include Comparable
21
+
22
+ # TODO: Yuck! 'stat_value' is a column for StatAnalyzer
23
+ EXCLUDED_COLUMNS = FlogAnalyzer::COLUMNS + SaikuroAnalyzer::COLUMNS + ['stat_value'] + ChurnAnalyzer::COLUMNS + ReekAnalyzer.new.columns.extend(CarefulArray).carefully_remove(['reek__type_name', 'reek__comparable_message']) + FlayAnalyzer.new.columns.extend(CarefulArray).carefully_remove(['flay_matching_reason'])
24
+
25
+ def <=>(other)
26
+ spaceship_for_columns(self.attributes, other)
27
+ end
28
+
29
+ def ===(other)
30
+ self.hash_for(included_columns_hash, included_columns) == other.hash_for(included_columns_hash, included_columns)
31
+ end
32
+
33
+ def spaceship_for_columns(columns, other)
34
+ columns.each do |column|
35
+ equality = self[column].to_s <=> other[column].to_s
36
+ return equality if equality!=0
37
+ end
38
+ return 0
39
+ end
40
+
41
+ def hash_for(column_hash, columns)
42
+ @hashes ||= {}
43
+ # fetch would be cleaner, but slower
44
+ if @hashes.has_key?(column_hash)
45
+ @hashes[column_hash]
46
+ else
47
+ values = columns.map {|column| self[column]}
48
+ hash_for_columns = values.join('').hash
49
+ @hashes[column_hash]=hash_for_columns
50
+ hash_for_columns
51
+ end
52
+ end
53
+
54
+ def included_columns_hash
55
+ @included_columns_hash ||= included_columns.hash
56
+ end
57
+
58
+ def included_columns
59
+ @included_columns ||= self.attributes.extend(CarefulArray).carefully_remove(EXCLUDED_COLUMNS)
60
+ end
61
+
62
+ def find_counterpart_index_in(collection)
63
+ # each_with_index is cleaner, but it is slower and we
64
+ # spend a lot of time in this loop
65
+ index = 0
66
+ collection.each do |issue|
67
+ return index if self === issue
68
+ index += 1
69
+ end
70
+ return nil
71
+ end
72
+
73
+ def modifies?(other)
74
+ case self.metric
75
+ when :reek
76
+ #return false unless ["Large Class", "Long Method", "Long Parameter List"].include?(self.reek__type_name)
77
+ return false if self.reek__type_name != other.reek__type_name
78
+ self.reek__value != other.reek__value
79
+ when :flog
80
+ self.score != other.score
81
+ when :saikuro
82
+ self.complexity != other.complexity
83
+ when :stats
84
+ self.stat_value != other.stat_value
85
+ when :churn
86
+ self.times_changed != other.times_changed
87
+ when :flay
88
+ #self.flay_reason != other.flay_reason
89
+ # do nothing for now
90
+ when :roodi, :stats
91
+ # do nothing
92
+ else
93
+ raise ArgumentError, "Invalid metric type #{self.metric}"
94
+ end
95
+ end
96
+
97
+ end
@@ -6,7 +6,8 @@ module MetricFu
6
6
  # course, in order to use these metrics, their respective gems must
7
7
  # be installed on the system.
8
8
  AVAILABLE_METRICS = [:churn, :flog, :flay, :reek,
9
- :roodi, :saikuro, :rcov]
9
+ :roodi, :saikuro, :rcov,
10
+ :hotspots]
10
11
 
11
12
  AVAILABLE_GRAPHS = [:flog, :flay, :reek, :roodi, :rcov, :rails_best_practices]
12
13
  AVAILABLE_GRAPH_ENGINES = [:gchart, :bluff]
@@ -139,6 +140,7 @@ module MetricFu
139
140
  :external => nil
140
141
  }
141
142
  @rails_best_practices = {}
143
+ @hotspots = {}
142
144
  @file_globs_to_ignore = []
143
145
 
144
146
  @graph_engine = :bluff # can be :bluff or :gchart
@@ -159,7 +161,7 @@ module MetricFu
159
161
  @metrics = MetricFu::AVAILABLE_METRICS + [:stats, :rails_best_practices]
160
162
  else
161
163
  @metrics = MetricFu::AVAILABLE_METRICS
162
- end
164
+ end
163
165
  end
164
166
 
165
167
  def set_graphs
@@ -0,0 +1,50 @@
1
+ class FlayAnalyzer
2
+ include ScoringStrategies
3
+
4
+ COLUMNS = %w{flay_reason flay_matching_reason}
5
+
6
+ def columns
7
+ COLUMNS
8
+ end
9
+
10
+ def name
11
+ :flay
12
+ end
13
+
14
+ def map(row)
15
+ ScoringStrategies.present(row)
16
+ end
17
+
18
+ def reduce(scores)
19
+ ScoringStrategies.sum(scores)
20
+ end
21
+
22
+ def score(metric_ranking, item)
23
+ ScoringStrategies.percentile(metric_ranking, item)
24
+ end
25
+
26
+ def generate_records(data, table)
27
+ return if data==nil
28
+ Array(data[:matches]).each do |match|
29
+ problems = match[:reason]
30
+ matching_reason = problems.gsub(/^[0-9]+\) /,'').gsub(/\:[0-9]+/,'')
31
+ files = []
32
+ locations = []
33
+ match[:matches].each do |file_match|
34
+ file_path = file_match[:name].sub(%r{^/},'')
35
+ locations << "#{file_path}:#{file_match[:line]}"
36
+ files << file_path
37
+ end
38
+ files = files.uniq
39
+ files.each do |file|
40
+ table << {
41
+ "metric" => self.name,
42
+ "file_path" => file,
43
+ "flay_reason" => problems+" files: #{locations.join(', ')}",
44
+ "flay_matching_reason" => matching_reason
45
+ }
46
+ end
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,43 @@
1
+ class FlogAnalyzer
2
+ include ScoringStrategies
3
+
4
+ COLUMNS = %w{score}
5
+
6
+ def columns
7
+ COLUMNS
8
+ end
9
+
10
+ def name
11
+ :flog
12
+ end
13
+
14
+ def map(row)
15
+ row.score
16
+ end
17
+
18
+ def reduce(scores)
19
+ ScoringStrategies.average(scores)
20
+ end
21
+
22
+ def score(metric_ranking, item)
23
+ ScoringStrategies.identity(metric_ranking, item)
24
+ end
25
+
26
+ def generate_records(data, table)
27
+ return if data==nil
28
+ Array(data[:method_containers]).each do |method_container|
29
+ Array(method_container[:methods]).each do |entry|
30
+ file_path = entry[1][:path].sub(%r{^/},'') if entry[1][:path]
31
+ location = Location.for(entry.first)
32
+ table << {
33
+ "metric" => name,
34
+ "score" => entry[1][:score],
35
+ "file_path" => file_path,
36
+ "class_name" => location.class_name,
37
+ "method_name" => location.method_name
38
+ }
39
+ end
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,65 @@
1
+ require 'ruby_parser'
2
+
3
+ class LineNumbers
4
+
5
+ def initialize(contents)
6
+ rp = RubyParser.new
7
+ file_sexp = rp.parse(contents)
8
+ @locations = {}
9
+ case file_sexp[0]
10
+ when :class
11
+ process_class(file_sexp)
12
+ when :module
13
+ process_module(file_sexp)
14
+ when :block
15
+ file_sexp.each_of_type(:class) { |sexp| process_class(sexp) }
16
+ else
17
+ end
18
+ end
19
+
20
+ def in_method? line_number
21
+ !!@locations.detect do |method_name, line_number_range|
22
+ line_number_range.include?(line_number)
23
+ end
24
+ end
25
+
26
+ def method_at_line line_number
27
+ found_method_and_range = @locations.detect do |method_name, line_number_range|
28
+ line_number_range.include?(line_number)
29
+ end
30
+ return nil unless found_method_and_range
31
+ found_method_and_range.first
32
+ end
33
+
34
+ private
35
+
36
+ def process_module(sexp)
37
+ module_name = sexp[1]
38
+ sexp.each_of_type(:class) do |sexp|
39
+ process_class(sexp, module_name)
40
+ hide_methods_from_next_round(sexp)
41
+ end
42
+ process_class(sexp)
43
+ end
44
+
45
+ def process_class(sexp, module_name=nil)
46
+ class_name = sexp[1]
47
+ process_class_self_blocks(sexp, class_name)
48
+ module_name_string = module_name ? "#{module_name}::" : nil
49
+ sexp.each_of_type(:defn) { |s| @locations["#{module_name_string}#{class_name}##{s[1]}"] = (s.line)..(s.last.line) }
50
+ sexp.each_of_type(:defs) { |s| @locations["#{module_name_string}#{class_name}::#{s[2]}"] = (s.line)..(s.last.line) }
51
+ end
52
+
53
+ def process_class_self_blocks(sexp, class_name)
54
+ sexp.each_of_type(:sclass) do |sexp_in_class_self_block|
55
+ sexp_in_class_self_block.each_of_type(:defn) { |s| @locations["#{class_name}::#{s[1]}"] = (s.line)..(s.last.line) }
56
+ hide_methods_from_next_round(sexp_in_class_self_block)
57
+ end
58
+ end
59
+
60
+ def hide_methods_from_next_round(sexp)
61
+ sexp.find_and_replace_all(:defn, :ignore_me)
62
+ sexp.find_and_replace_all(:defs, :ignore_me)
63
+ end
64
+
65
+ end
@@ -0,0 +1,83 @@
1
+ class Location
2
+ include Comparable
3
+
4
+ attr_accessor :class_name, :method_name, :file_path, :simple_method_name, :hash
5
+
6
+ def self.get(file_path, class_name, method_name)
7
+ # This could be more 'confident' using Maybe, but we want it to be as fast as possible
8
+ file_path_copy = file_path == nil ? nil : file_path.clone
9
+ class_name_copy = class_name == nil ? nil : class_name.clone
10
+ method_name_copy = method_name == nil ? nil : method_name.clone
11
+ key = [file_path_copy, class_name_copy, method_name_copy]
12
+ @@locations ||= {}
13
+ if @@locations.has_key?(key)
14
+ @@locations[key]
15
+ else
16
+ location = self.new(file_path_copy, class_name_copy, method_name_copy)
17
+ @@locations[key] = location
18
+ location.freeze # we cache a lot of method call results, so we want location to be immutable
19
+ location
20
+ end
21
+ end
22
+
23
+ def initialize(file_path, class_name, method_name)
24
+ @file_path = file_path
25
+ @class_name = class_name
26
+ @method_name = method_name
27
+ @simple_method_name = @method_name.sub(@class_name,'') unless @method_name == nil
28
+ @hash = [@file_path, @class_name, @method_name].hash
29
+ end
30
+
31
+ # TODO - we need this method (and hash accessor above) as a temporary hack where we're using Location as a hash key
32
+ def eql?(other)
33
+ [self.file_path.to_s, self.class_name.to_s, self.method_name.to_s] == [other.file_path.to_s, other.class_name.to_s, other.method_name.to_s]
34
+ end
35
+
36
+ # END we need these methods as a temporary hack where we're using Location as a hash key
37
+
38
+ def self.for(class_or_method_name)
39
+ class_or_method_name = strip_modules(class_or_method_name)
40
+ if(class_or_method_name)
41
+ begin
42
+ match = class_or_method_name.match(/(.*)((\.|\#|\:\:[a-z])(.+))/)
43
+ rescue => error
44
+ #new error during port to metric_fu occasionally a unintialized
45
+ #MatchData object shows up here. Not expected.
46
+ match = nil
47
+ end
48
+
49
+ # reek reports the method with :: not # on modules like
50
+ # module ApplicationHelper \n def signed_in?, convert it so it records correctly
51
+ # but classes have to start with a capital letter... HACK for REEK bug, reported underlying issue to REEK
52
+ if(match)
53
+ class_name = strip_modules(match[1])
54
+ method_name = class_or_method_name.gsub(/\:\:/,"#")
55
+ else
56
+ class_name = strip_modules(class_or_method_name)
57
+ method_name = nil
58
+ end
59
+ else
60
+ class_name = nil
61
+ method_name = nil
62
+ end
63
+ self.get(nil, class_name, method_name)
64
+ end
65
+
66
+ def <=>(other)
67
+ [self.file_path.to_s, self.class_name.to_s, self.method_name.to_s] <=> [other.file_path.to_s, other.class_name.to_s, other.method_name.to_s]
68
+ end
69
+
70
+ private
71
+
72
+ def self.strip_modules(class_or_method_name)
73
+ # reek reports the method with :: not # on modules like
74
+ # module ApplicationHelper \n def signed_in?, convert it so it records correctly
75
+ # but classes have to start with a capital letter... HACK for REEK bug, reported underlying issue to REEK
76
+ if(class_or_method_name=~/\:\:[A-Z]/)
77
+ class_or_method_name.split("::").last
78
+ else
79
+ class_or_method_name
80
+ end
81
+ end
82
+
83
+ end