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
@@ -0,0 +1,114 @@
1
+ class ReekAnalyzer
2
+ include ScoringStrategies
3
+
4
+ REEK_ISSUE_INFO = {'Uncommunicative Name' =>
5
+ {'link' => 'http://wiki.github.com/kevinrutherford/reek/uncommunicative-name', 'info' => 'An Uncommunicative Name is a name that doesn’t communicate its intent well enough.'},
6
+ 'Class Variable' =>
7
+ {'link' => 'http://wiki.github.com/kevinrutherford/reek/class-variable', 'info' => 'Class variables form part of the global runtime state, and as such make it easy for one part of the system to accidentally or inadvertently depend on another part of the system.'},
8
+ 'Duplication' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/duplication', 'info' => 'Duplication occurs when two fragments of code look nearly identical, or when two fragments of code have nearly identical effects at some conceptual level.'},
9
+ 'Low Cohesion' => {'link' => 'http://en.wikipedia.org/wiki/Cohesion_(computer_science)', 'info' => 'Low cohesion is associated with undesirable traits such as being difficult to maintain, difficult to test, difficult to reuse, and even difficult to understand.'},
10
+ 'Nested Iterators' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/nested-iterators', 'info' => 'Nested Iterator occurs when a block contains another block.'},
11
+ 'Control Couple' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/control-couple', 'info' => 'Control coupling occurs when a method or block checks the value of a parameter in order to decide which execution path to take. The offending parameter is often called a “Control Couple”.'},
12
+ 'Irresponsible Module' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/irresponsible-module', 'info' => 'Classes and modules are the units of reuse and release. It is therefore considered good practice to annotate every class and module with a brief comment outlining its responsibilities.'},
13
+ 'Long Parameter List' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/long-parameter-list', 'info' => 'A Long Parameter List occurs when a method has more than one or two parameters, or when a method yields more than one or two objects to an associated block.'},
14
+ 'Data Clump' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/data-clump', 'info' => 'In general, a Data Clump occurs when the same two or three items frequently appear together in classes and parameter lists, or when a group of instance variable names start or end with similar substrings.'},
15
+ 'Simulated Polymorphism' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/simulated-polymorphism', 'info' => 'Simulated Polymorphism occurs when, code uses a case statement (especially on a type field) or code uses instance_of?, kind_of?, is_a?, or === to decide what code to execute'},
16
+ 'Large Class' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/large-class', 'info' => 'A Large Class is a class or module that has a large number of instance variables, methods or lines of code in any one piece of its specification.'},
17
+ 'Long Method' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/long-method', 'info' => 'Long methods can be hard to read and understand. They often are harder to test and maintain as well, which can lead to buggier code.'},
18
+ 'Feature Envy' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/feature-envy', 'info' => 'Feature Envy occurs when a code fragment references another object more often than it references itself, or when several clients do the same series of manipulations on a particular type of object.'},
19
+ 'Utility Function' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/utility-function', 'info' => 'A Utility Function is any instance method that has no dependency on the state of the instance. It reduces the code’s ability to communicate intent. Code that “belongs” on one class but which is located in another can be hard to find.'},
20
+ 'Attribute' => {'link' => 'http://wiki.github.com/kevinrutherford/reek/attribute', 'info' => 'A class that publishes a getter or setter for an instance variable invites client classes to become too intimate with its inner workings, and in particular with its representation of state.'}
21
+ }
22
+
23
+ # Note that in practice, the prefix reek__ is appended to each one
24
+ # This was a partially implemented idea to avoid column name collisions
25
+ # but it is only done in the ReekAnalyzer
26
+ COLUMNS = %w{type_name message value value_description comparable_message}
27
+
28
+ def self.issue_link(issue)
29
+ REEK_ISSUE_INFO[issue]
30
+ end
31
+
32
+ def columns
33
+ COLUMNS.map{|column| "#{name}__#{column}"}
34
+ end
35
+
36
+ def name
37
+ :reek
38
+ end
39
+
40
+ def map(row)
41
+ ScoringStrategies.present(row)
42
+ end
43
+
44
+ def reduce(scores)
45
+ ScoringStrategies.sum(scores)
46
+ end
47
+
48
+ def score(metric_ranking, item)
49
+ ScoringStrategies.percentile(metric_ranking, item)
50
+ end
51
+
52
+ def generate_records(data, table)
53
+ return if data==nil
54
+ data[:matches].each do |match|
55
+ file_path = match[:file_path]
56
+ match[:code_smells].each do |smell|
57
+ location = Location.for(smell[:method])
58
+ smell_type = smell[:type]
59
+ message = smell[:message]
60
+ table << {
61
+ "metric" => name,
62
+ "file_path" => file_path,
63
+ # NOTE: ReekAnalyzer is currently different than other analyzers with regard
64
+ # to column name. Note the COLUMNS constant and #columns method
65
+ "reek__message" => message,
66
+ "reek__type_name" => smell_type,
67
+ "reek__value" => parse_value(message),
68
+ "reek__value_description" => build_value_description(smell_type, message),
69
+ "reek__comparable_message" => comparable_message(smell_type, message),
70
+ "class_name" => location.class_name,
71
+ "method_name" => location.method_name,
72
+ }
73
+ end
74
+ end
75
+ end
76
+
77
+ def self.numeric_smell?(type)
78
+ ["Large Class", "Long Method", "Long Parameter List"].include?(type)
79
+ end
80
+
81
+ private
82
+
83
+ def comparable_message(type_name, message)
84
+ if self.class.numeric_smell?(type_name)
85
+ match = message.match(/\d+/)
86
+ if(match)
87
+ match.pre_match + match.post_match
88
+ else
89
+ message
90
+ end
91
+ else
92
+ message
93
+ end
94
+ end
95
+
96
+ def build_value_description(type_name, message)
97
+ item_type = message.match(/\d+ (.*)$/)
98
+ if(item_type)
99
+ "number of #{item_type[1]} in #{type_name.downcase}"
100
+ else
101
+ nil
102
+ end
103
+ end
104
+
105
+ def parse_value(message)
106
+ match = message.match(/\d+/)
107
+ if(match)
108
+ match[0].to_i
109
+ else
110
+ nil
111
+ end
112
+ end
113
+
114
+ end
@@ -0,0 +1,37 @@
1
+ class RoodiAnalyzer
2
+ include ScoringStrategies
3
+
4
+ COLUMNS = %w{problems}
5
+
6
+ def columns
7
+ COLUMNS
8
+ end
9
+
10
+ def name
11
+ :roodi
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[:problems]).each do |problem|
29
+ table << {
30
+ "metric" => name,
31
+ "problems" => problem[:problem],
32
+ "file_path" => problem[:file]
33
+ }
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,48 @@
1
+ class SaikuroAnalyzer
2
+ include ScoringStrategies
3
+
4
+ COLUMNS = %w{lines complexity}
5
+
6
+ def columns
7
+ COLUMNS
8
+ end
9
+
10
+ def name
11
+ :saikuro
12
+ end
13
+
14
+ def map(row)
15
+ row.complexity
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
+ data[:files].each do |file|
29
+ file_name = file[:filename]
30
+ file[:classes].each do |klass|
31
+ location = Location.for(klass[:class_name])
32
+ offending_class = location.class_name
33
+ klass[:methods].each do |match|
34
+ offending_method = Location.for(match[:name]).method_name
35
+ table << {
36
+ "metric" => name,
37
+ "lines" => match[:lines],
38
+ "complexity" => match[:complexity],
39
+ "class_name" => offending_class,
40
+ "method_name" => offending_method,
41
+ "file_path" => file_name,
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,29 @@
1
+ module ScoringStrategies
2
+
3
+ def percentile(ranking, item)
4
+ ranking.percentile(item)
5
+ end
6
+
7
+ def identity(ranking, item)
8
+ ranking[item]
9
+ end
10
+
11
+ def present(row)
12
+ 1
13
+ end
14
+
15
+ def sum(scores)
16
+ scores.inject(0) {|s,x| s+x}
17
+ end
18
+
19
+ def average(scores)
20
+ # remove dependency on statarray
21
+ # scores.to_statarray.mean
22
+ score_length = scores.length
23
+ sum = 0
24
+ sum = scores.inject( nil ) { |sum,x| sum ? sum+x : x }
25
+ (sum.to_f / score_length.to_f)
26
+ end
27
+
28
+ extend self
29
+ end
@@ -0,0 +1,37 @@
1
+ class StatsAnalyzer
2
+
3
+ COLUMNS = %w{stat_name stat_value}
4
+
5
+ def columns
6
+ COLUMNS
7
+ end
8
+
9
+ def name
10
+ :stats
11
+ end
12
+
13
+ def map(row)
14
+ 0
15
+ end
16
+
17
+ def reduce(scores)
18
+ 0
19
+ end
20
+
21
+ def score(metric_ranking, item)
22
+ 0
23
+ end
24
+
25
+ def generate_records(data, table)
26
+ return if data == nil
27
+ data.each do |key, value|
28
+ next if value.kind_of?(Array)
29
+ table << {
30
+ "metric" => name,
31
+ "stat_name" => key,
32
+ "stat_value" => value
33
+ }
34
+ end
35
+ end
36
+
37
+ end
data/lib/base/table.rb ADDED
@@ -0,0 +1,102 @@
1
+ class Table
2
+
3
+ def initialize(opts = {})
4
+ @rows = []
5
+ @columns = opts.fetch(:column_names)
6
+
7
+ @make_index = opts.fetch(:make_index) {true}
8
+ @metric_index = {}
9
+ end
10
+
11
+ def <<(row)
12
+ record = nil
13
+ if row.is_a?(Record) || row.is_a?(CodeIssue)
14
+ record = row
15
+ else
16
+ record = Record.new(row, @columns)
17
+ end
18
+ @rows << record
19
+ updated_key_index(record) if @make_index
20
+ end
21
+
22
+ def each
23
+ @rows.each do |row|
24
+ yield row
25
+ end
26
+ end
27
+
28
+ def size
29
+ length
30
+ end
31
+
32
+ def length
33
+ @rows.length
34
+ end
35
+
36
+ def [](index)
37
+ @rows[index]
38
+ end
39
+
40
+ def column(column_name)
41
+ arr = []
42
+ @rows.each do |row|
43
+ arr << row[column_name]
44
+ end
45
+ arr
46
+ end
47
+
48
+ def group_by_metric
49
+ @metric_index.to_a
50
+ end
51
+
52
+ def rows_with(conditions)
53
+ if optimized_conditions?(conditions)
54
+ optimized_select(conditions)
55
+ else
56
+ slow_select(conditions)
57
+ end
58
+ end
59
+
60
+ def delete_at(index)
61
+ @rows.delete_at(index)
62
+ end
63
+
64
+ def to_a
65
+ @rows
66
+ end
67
+
68
+ def map
69
+ new_table = Table.new(:column_names => @columns)
70
+ @rows.map do |row|
71
+ new_table << (yield row)
72
+ end
73
+ new_table
74
+ end
75
+
76
+ private
77
+
78
+ def optimized_conditions?(conditions)
79
+ conditions.keys.length == 1 && conditions.keys.first.to_sym == :metric
80
+ end
81
+
82
+ def optimized_select(conditions)
83
+ metric = (conditions['metric'] || conditions[:metric]).to_s
84
+ @metric_index[metric].to_a.clone
85
+ end
86
+
87
+ def slow_select(conditions)
88
+ @rows.select do |row|
89
+ conditions.all? do |key, value|
90
+ row.has_key?(key.to_s) && row[key.to_s] == value
91
+ end
92
+ end
93
+ end
94
+
95
+ def updated_key_index(record)
96
+ if record.has_key?('metric')
97
+ @metric_index[record.metric] ||= Table.new(:column_names => @columns, :make_index => false)
98
+ @metric_index[record.metric] << record
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,52 @@
1
+ module MetricFu
2
+
3
+ class Hotspots < Generator
4
+
5
+ def initialize(options={})
6
+ super
7
+ end
8
+
9
+ def self.verify_dependencies!
10
+ true
11
+ end
12
+
13
+ def emit
14
+ @analyzer = MetricAnalyzer.new(MetricFu.report.report_hash)
15
+ end
16
+
17
+ def analyze
18
+ num = nil
19
+ worst_items = {}
20
+ if @analyzer
21
+ worst_items[:files] =
22
+ @analyzer.worst_files(num).inject([]) do |array, worst_file|
23
+ array <<
24
+ {:location => @analyzer.location(:file, worst_file),
25
+ :details => @analyzer.problems_with(:file, worst_file)}
26
+ array
27
+ end
28
+ worst_items[:classes] = @analyzer.worst_classes(num).inject([]) do |array, class_name|
29
+ location = @analyzer.location(:class, class_name)
30
+ array <<
31
+ {:location => location,
32
+ :details => @analyzer.problems_with(:class, class_name)}
33
+ array
34
+ end
35
+ worst_items[:methods] = @analyzer.worst_methods(num).inject([]) do |array, method_name|
36
+ location = @analyzer.location(:method, method_name)
37
+ array <<
38
+ {:location => location,
39
+ :details => @analyzer.problems_with(:method, method_name)}
40
+ array
41
+ end
42
+ end
43
+
44
+ @hotspots = worst_items
45
+ end
46
+
47
+ def to_h
48
+ {:hotspots => @hotspots}
49
+ end
50
+ end
51
+
52
+ end
@@ -48,11 +48,52 @@ module MetricFu
48
48
 
49
49
  def to_h
50
50
  global_percent_run = ((@global_total_lines_run.to_f / @global_total_lines.to_f) * 100)
51
+ add_method_data
51
52
  {:rcov => @rcov.merge({:global_percent_run => round_to_tenths(global_percent_run) })}
52
53
  end
53
54
 
54
55
  private
55
56
 
57
+ def add_method_data
58
+ @rcov.each_pair do |file_path, info|
59
+ file_contents = ""
60
+ coverage = []
61
+
62
+ info[:lines].each_with_index do |line, index|
63
+ file_contents << "#{line[:content]}\n"
64
+ coverage << line[:was_run]
65
+ end
66
+
67
+ begin
68
+ line_numbers = LineNumbers.new(file_contents)
69
+ rescue StandardError => e
70
+ raise e unless e.message =~ /you shouldn't be able to get here/
71
+ puts "ruby_parser blew up while trying to parse #{file_path}. You won't have method level Rcov information for this file."
72
+ next
73
+ end
74
+
75
+ method_coverage_map = {}
76
+ coverage.each_with_index do |covered, index|
77
+ line_number = index + 1
78
+ if line_numbers.in_method?(line_number)
79
+ method_name = line_numbers.method_at_line(line_number)
80
+ method_coverage_map[method_name] ||= {}
81
+ method_coverage_map[method_name][:total] ||= 0
82
+ method_coverage_map[method_name][:total] += 1
83
+ method_coverage_map[method_name][:uncovered] ||= 0
84
+ method_coverage_map[method_name][:uncovered] += 1 if !covered
85
+ end
86
+ end
87
+
88
+ @rcov[file_path][:methods] = {}
89
+
90
+ method_coverage_map.each do |method_name, coverage_data|
91
+ @rcov[file_path][:methods][method_name] = (coverage_data[:uncovered] / coverage_data[:total].to_f) * 100.0
92
+ end
93
+
94
+ end
95
+ end
96
+
56
97
  def assemble_files(output)
57
98
  files = {}
58
99
  output.each_slice(2) {|out| files[out.first.strip] = out.last}