metric_fu 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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