metric_fu-roodi 2.2.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 (66) hide show
  1. data/.gitignore +3 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +6 -0
  4. data/History.txt +93 -0
  5. data/Manifest.txt +56 -0
  6. data/README.txt +98 -0
  7. data/Rakefile +35 -0
  8. data/bin/metric_fu-roodi +21 -0
  9. data/bin/metric_fu-roodi-describe +7 -0
  10. data/lib/roodi.rb +3 -0
  11. data/lib/roodi/checks.rb +18 -0
  12. data/lib/roodi/checks/abc_metric_method_check.rb +79 -0
  13. data/lib/roodi/checks/assignment_in_conditional_check.rb +32 -0
  14. data/lib/roodi/checks/case_missing_else_check.rb +20 -0
  15. data/lib/roodi/checks/check.rb +76 -0
  16. data/lib/roodi/checks/class_line_count_check.rb +28 -0
  17. data/lib/roodi/checks/class_name_check.rb +31 -0
  18. data/lib/roodi/checks/class_variable_check.rb +24 -0
  19. data/lib/roodi/checks/control_coupling_check.rb +20 -0
  20. data/lib/roodi/checks/cyclomatic_complexity_block_check.rb +41 -0
  21. data/lib/roodi/checks/cyclomatic_complexity_check.rb +50 -0
  22. data/lib/roodi/checks/cyclomatic_complexity_method_check.rb +42 -0
  23. data/lib/roodi/checks/empty_rescue_body_check.rb +32 -0
  24. data/lib/roodi/checks/for_loop_check.rb +20 -0
  25. data/lib/roodi/checks/line_count_check.rb +22 -0
  26. data/lib/roodi/checks/method_line_count_check.rb +29 -0
  27. data/lib/roodi/checks/method_name_check.rb +31 -0
  28. data/lib/roodi/checks/missing_foreign_key_index_check.rb +99 -0
  29. data/lib/roodi/checks/module_line_count_check.rb +28 -0
  30. data/lib/roodi/checks/module_name_check.rb +31 -0
  31. data/lib/roodi/checks/name_check.rb +16 -0
  32. data/lib/roodi/checks/npath_complexity_check.rb +75 -0
  33. data/lib/roodi/checks/npath_complexity_method_check.rb +29 -0
  34. data/lib/roodi/checks/parameter_number_check.rb +34 -0
  35. data/lib/roodi/core.rb +1 -0
  36. data/lib/roodi/core/checking_visitor.rb +26 -0
  37. data/lib/roodi/core/error.rb +17 -0
  38. data/lib/roodi/core/parser.rb +30 -0
  39. data/lib/roodi/core/runner.rb +81 -0
  40. data/lib/roodi/core/visitable_sexp.rb +25 -0
  41. data/lib/roodi/version.rb +3 -0
  42. data/lib/roodi_task.rb +35 -0
  43. data/roodi.gemspec +26 -0
  44. data/roodi.yml +25 -0
  45. data/spec/roodi/checks/abc_metric_method_check_spec.rb +89 -0
  46. data/spec/roodi/checks/assignment_in_conditional_check_spec.rb +105 -0
  47. data/spec/roodi/checks/case_missing_else_check_spec.rb +32 -0
  48. data/spec/roodi/checks/class_line_count_check_spec.rb +39 -0
  49. data/spec/roodi/checks/class_name_check_spec.rb +39 -0
  50. data/spec/roodi/checks/class_variable_check_spec.rb +17 -0
  51. data/spec/roodi/checks/control_coupling_check_spec.rb +23 -0
  52. data/spec/roodi/checks/cyclomatic_complexity_block_check_spec.rb +67 -0
  53. data/spec/roodi/checks/cyclomatic_complexity_method_check_spec.rb +200 -0
  54. data/spec/roodi/checks/empty_rescue_body_check_spec.rb +140 -0
  55. data/spec/roodi/checks/for_loop_check_spec.rb +18 -0
  56. data/spec/roodi/checks/method_line_count_check_spec.rb +56 -0
  57. data/spec/roodi/checks/method_name_check_spec.rb +76 -0
  58. data/spec/roodi/checks/missing_foreign_key_index_check_spec.rb +33 -0
  59. data/spec/roodi/checks/module_line_count_check_spec.rb +39 -0
  60. data/spec/roodi/checks/module_name_check_spec.rb +27 -0
  61. data/spec/roodi/checks/npath_complexity_method_check_spec.rb +53 -0
  62. data/spec/roodi/checks/parameter_number_check_spec.rb +47 -0
  63. data/spec/roodi/core/runner_spec.rb +25 -0
  64. data/spec/roodi/roodi.yml +2 -0
  65. data/spec/spec_helper.rb +3 -0
  66. metadata +149 -0
@@ -0,0 +1,31 @@
1
+ require 'roodi/checks/name_check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks a method name to make sure it matches the specified pattern.
6
+ #
7
+ # Keeping to a consistent nameing convention makes your code easier to read.
8
+ class MethodNameCheck < NameCheck
9
+
10
+ DEFAULT_PATTERN = /^[_a-z<>=\[|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/
11
+
12
+ def initialize
13
+ super()
14
+ self.pattern = DEFAULT_PATTERN
15
+ end
16
+
17
+ def interesting_nodes
18
+ [:defn]
19
+ end
20
+
21
+ def message_prefix
22
+ 'Method'
23
+ end
24
+
25
+ def find_name(node)
26
+ node[1]
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,99 @@
1
+ require 'roodi/checks/check'
2
+ require 'pathname'
3
+
4
+ module Roodi
5
+ module Checks
6
+ # Checks to make sure for loops are not being used..
7
+ #
8
+ # Using a for loop is not idiomatic use of Ruby, and is usually a sign that someone with
9
+ # more experience in a different programming language is trying out Ruby. Use
10
+ # Enumerable.each with a block instead.
11
+ class MissingForeignKeyIndexCheck < Check
12
+ def initialize
13
+ super()
14
+ @foreign_keys = {}
15
+ @indexes = {}
16
+ end
17
+
18
+ def interesting_nodes
19
+ [:call]
20
+ end
21
+
22
+ def evaluate_start_call(node)
23
+ if analyzing_schema(node)
24
+ if creating_table(node)
25
+ @current_table = create_table_name(node)
26
+ end
27
+
28
+ if creating_foreign_key(node)
29
+ @foreign_keys[@current_table] ||= []
30
+ @foreign_keys[@current_table] << foreign_key_column_name(node)
31
+ end
32
+
33
+ if adding_index(node)
34
+ @indexes[index_table_name(node)] ||= []
35
+ @indexes[index_table_name(node)] << index_column_name(node)
36
+ end
37
+ end
38
+ end
39
+
40
+ def evaluate_end_call(node)
41
+ #ignored
42
+ end
43
+
44
+ def analyzing_schema(node)
45
+ pathname = Pathname.new(node.file)
46
+ @analyzing_schema ||= ("schema.rb" == pathname.basename.to_s)
47
+ end
48
+
49
+ def creating_table(node)
50
+ :create_table == node[2]
51
+ end
52
+
53
+ def create_table_name(node)
54
+ # Get table name out of this:
55
+ # s(:call, nil, :create_table, s(:arglist, s(:str, "duplicate_blocks"), s(:hash, s(:lit, :force), s(:true))))
56
+ node[3][1][1]
57
+ end
58
+
59
+ def creating_foreign_key(node)
60
+ #s(:call, s(:lvar, :t), :integer, s(:arglist, s(:str, "duplicate_set_id"), s(:hash, s(:lit, :null), s(:false))))
61
+ column_type = node[2]
62
+ column_name = node[3][1][1]
63
+ :integer == column_type && "_id" == column_name[-3,3]
64
+ end
65
+
66
+ def foreign_key_column_name(node)
67
+ #s(:call, s(:lvar, :t), :integer, s(:arglist, s(:str, "duplicate_set_id"), s(:hash, s(:lit, :null), s(:false))))
68
+ column_name = node[3][1][1]
69
+ end
70
+
71
+ def adding_index(node)
72
+ :add_index == node[2]
73
+ end
74
+
75
+ def index_table_name(node)
76
+ # Get table name out of this:
77
+ # s(:call, nil, :add_index, s(:arglist, s(:str, "duplicate_blocks"), s(:array, s(:str, "duplicate_set_id")), s(:hash, s(:lit, :name), s(:str, "index_duplicate_blocks_on_duplicate_set_id"))))
78
+ node[3][1][1]
79
+ end
80
+
81
+ def index_column_name(node)
82
+ # Get index column name out of this:
83
+ # s(:call, nil, :add_index, s(:arglist, s(:str, "duplicate_blocks"), s(:array, s(:str, "duplicate_set_id")), s(:hash, s(:lit, :name), s(:str, "index_duplicate_blocks_on_duplicate_set_id"))))
84
+ node[3][2][1][1]
85
+ end
86
+
87
+ def end_file(filename)
88
+ @foreign_keys.keys.each do |table|
89
+ foreign_keys = @foreign_keys[table] || []
90
+ indexes = @indexes[table] || []
91
+ missing_indexes = foreign_keys - indexes
92
+ missing_indexes.each do |fkey|
93
+ add_error("Table '#{table}' is missing an index on the foreign key '#{fkey}'", filename, 1)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,28 @@
1
+ require 'roodi/checks/line_count_check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks a module to make sure the number of lines it has is under the specified limit.
6
+ #
7
+ # A module getting too large is a code smell that indicates it might be taking on too many
8
+ # responsibilities. It should probably be refactored into multiple smaller modules.
9
+ class ModuleLineCountCheck < LineCountCheck
10
+
11
+ DEFAULT_LINE_COUNT = 300
12
+
13
+ def initialize
14
+ super()
15
+ self.line_count = DEFAULT_LINE_COUNT
16
+ end
17
+
18
+ def interesting_nodes
19
+ [:module]
20
+ end
21
+
22
+ def message_prefix
23
+ 'Module'
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ require 'roodi/checks/name_check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks a module name to make sure it matches the specified pattern.
6
+ #
7
+ # Keeping to a consistent nameing convention makes your code easier to read.
8
+ class ModuleNameCheck < NameCheck
9
+
10
+ DEFAULT_PATTERN = /^[A-Z][a-zA-Z0-9]*$/
11
+
12
+ def initialize
13
+ super()
14
+ self.pattern = DEFAULT_PATTERN
15
+ end
16
+
17
+ def interesting_nodes
18
+ [:module]
19
+ end
20
+
21
+ def message_prefix
22
+ 'Module'
23
+ end
24
+
25
+ def find_name(node)
26
+ node[1].class == Symbol ? node[1] : node[1].last
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ class NameCheck < Check
6
+
7
+ attr_accessor :pattern
8
+
9
+ def evaluate_start(node)
10
+ name = find_name(node)
11
+ add_error "#{message_prefix} name \"#{name}\" should match pattern #{@pattern.inspect}" unless name.to_s =~ @pattern
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,75 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ class NpathComplexityCheck < Check
6
+ # , :when, :and, :or
7
+ MULTIPLYING_NODE_TYPES = [:if, :while, :until, :for, :case]
8
+ ADDING_NODE_TYPES = [:rescue]
9
+ COMPLEXITY_NODE_TYPES = MULTIPLYING_NODE_TYPES + ADDING_NODE_TYPES
10
+
11
+ attr_accessor :complexity
12
+
13
+ def initialize(complexity)
14
+ super()
15
+ @complexity = complexity
16
+ @value_stack = []
17
+ @current_value = 1
18
+ end
19
+
20
+ def evalute_start_if(node)
21
+ push_value
22
+ end
23
+
24
+ def evalute_start_while(node)
25
+ push_value
26
+ end
27
+
28
+ def evalute_start_until(node)
29
+ push_value
30
+ end
31
+
32
+ def evalute_start_for(node)
33
+ push_value
34
+ end
35
+
36
+ def evalute_start_case(node)
37
+ push_value
38
+ end
39
+
40
+ def evalute_start_rescue(node)
41
+ push_value
42
+ end
43
+
44
+ MULTIPLYING_NODE_TYPES.each do |type|
45
+ define_method "evaluate_end_#{type}" do |node|
46
+ leave_multiplying_conditional
47
+ end
48
+ end
49
+
50
+ ADDING_NODE_TYPES.each do |type|
51
+ define_method "evaluate_end_#{type}" do |node|
52
+ leave_multiplying_conditional
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def push_value
59
+ @value_stack.push @current_value
60
+ @current_value = 1
61
+ end
62
+
63
+ def leave_multiplying_conditional
64
+ pop = @value_stack.pop
65
+ @current_value = (@current_value + 1) * pop
66
+ end
67
+
68
+ def leave_adding_conditional
69
+ pop = @value_stack.pop
70
+ puts "#{type}, so adding #{pop}"
71
+ @current_value = @current_value - 1 + pop
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,29 @@
1
+ require 'roodi/checks/npath_complexity_check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks Npath complexity of a method against a specified limit.
6
+ class NpathComplexityMethodCheck < NpathComplexityCheck
7
+
8
+ DEFAULT_COMPLEXITY = 8
9
+
10
+ def initialize
11
+ super(DEFAULT_COMPLEXITY)
12
+ end
13
+
14
+ def interesting_nodes
15
+ [:defn] + COMPLEXITY_NODE_TYPES
16
+ end
17
+
18
+ def evaluate_start_defn(node)
19
+ @method_name = @node[1]
20
+ push_value
21
+ end
22
+
23
+ def evaluate_end_defn(node)
24
+ add_error "Method name \"#{@method_name}\" n-path complexity is #{@current_value}. It should be #{@complexity} or less." unless @current_value <= @complexity
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks a method to make sure the number of parameters it has is under the specified limit.
6
+ #
7
+ # A method taking too many parameters is a code smell that indicates it might be doing too
8
+ # much, or that the parameters should be grouped into one or more objects of their own. It
9
+ # probably needs some refactoring.
10
+ class ParameterNumberCheck < Check
11
+
12
+ DEFAULT_PARAMETER_COUNT = 5
13
+
14
+ attr_accessor :parameter_count
15
+
16
+ def initialize
17
+ super()
18
+ self.parameter_count = DEFAULT_PARAMETER_COUNT
19
+ end
20
+
21
+ def interesting_nodes
22
+ [:defn]
23
+ end
24
+
25
+ def evaluate_start(node)
26
+ method_name = node[1]
27
+ arguments = node[2]
28
+ actual_parameter_count = arguments.inject(-1) { |count, each| count = count + (each.class == Symbol ? 1 : 0) }
29
+ add_error "Method name \"#{method_name}\" has #{actual_parameter_count} parameters. It should have #{@parameter_count} or less." unless actual_parameter_count <= @parameter_count
30
+ end
31
+
32
+ end
33
+ end
34
+ end
data/lib/roodi/core.rb ADDED
@@ -0,0 +1 @@
1
+ require 'roodi/core/runner'
@@ -0,0 +1,26 @@
1
+ module Roodi
2
+ module Core
3
+ class CheckingVisitor
4
+ def initialize(*checks)
5
+ @checks ||= {}
6
+ checks.first.each do |check|
7
+ nodes = check.interesting_nodes
8
+ nodes.each do |node|
9
+ @checks[node] ||= []
10
+ @checks[node] << check
11
+ @checks[node].uniq!
12
+ end
13
+ end
14
+ end
15
+
16
+ def visit(node)
17
+ checks = @checks[node.node_type]
18
+ checks.each {|check| check.evaluate_node_start(node)} unless checks.nil?
19
+
20
+ node.visitable_children.each {|sexp| sexp.accept(self)}
21
+
22
+ checks.each {|check| check.evaluate_node_end(node)} unless checks.nil?
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ module Roodi
2
+ module Core
3
+ class Error
4
+ attr_reader :filename, :line_number, :message
5
+
6
+ def initialize(filename, line_number, message)
7
+ @filename = filename
8
+ @line_number = line_number
9
+ @message = message
10
+ end
11
+
12
+ def to_s
13
+ "#{@filename}:#{@line_number} - #{@message}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'ruby_parser'
3
+
4
+ module Roodi
5
+ module Core
6
+ class Parser
7
+ def parse(content, filename)
8
+ silence_stream(STDERR) do
9
+ return silent_parse(content, filename)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def silence_stream(stream)
16
+ old_stream = stream.dup
17
+ stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
18
+ stream.sync = true
19
+ yield
20
+ ensure
21
+ stream.reopen(old_stream)
22
+ end
23
+
24
+ def silent_parse(content, filename)
25
+ @parser ||= RubyParser.new
26
+ @parser.parse(content, filename)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,81 @@
1
+ require 'pp'
2
+ require 'yaml'
3
+
4
+ require 'roodi/core/checking_visitor'
5
+ require 'roodi/core/parser'
6
+ require 'roodi/core/visitable_sexp'
7
+
8
+ module Roodi
9
+ module Core
10
+ class Runner
11
+ DEFAULT_CONFIG = File.join(File.dirname(__FILE__), "..", "..", "..", "roodi.yml")
12
+
13
+ attr_writer :config
14
+
15
+ def initialize(*checks)
16
+ @config = DEFAULT_CONFIG
17
+ @checks = checks unless checks.empty?
18
+ end
19
+
20
+ def check(filename, content)
21
+ @checks ||= load_checks
22
+ @checker ||= CheckingVisitor.new(@checks)
23
+ @checks.each {|check| check.start_file(filename)}
24
+ node = parse(filename, content)
25
+ node.accept(@checker) if node
26
+ @checks.each {|check| check.end_file(filename)}
27
+ end
28
+
29
+ def check_content(content, filename = "dummy-file.rb")
30
+ check(filename, content)
31
+ end
32
+
33
+ def check_file(filename)
34
+ return unless File.exists?(filename)
35
+ check(filename, File.read(filename))
36
+ end
37
+
38
+ def print(filename, content)
39
+ node = parse(filename, content)
40
+ puts "Line: #{node.line}"
41
+ pp node
42
+ end
43
+
44
+ def print_content(content)
45
+ print("dummy-file.rb", content)
46
+ end
47
+
48
+ def print_file(filename)
49
+ print(filename, File.read(filename))
50
+ end
51
+
52
+ def errors
53
+ @checks ||= []
54
+ all_errors = @checks.collect {|check| check.errors}
55
+ all_errors.flatten
56
+ end
57
+
58
+ private
59
+
60
+ def parse(filename, content)
61
+ begin
62
+ Parser.new.parse(content, filename)
63
+ rescue Exception => e
64
+ puts "#{filename} looks like it's not a valid Ruby file. Skipping..." if ENV["ROODI_DEBUG"]
65
+ nil
66
+ end
67
+ end
68
+
69
+ def load_checks
70
+ check_objects = []
71
+ checks = YAML.load_file @config
72
+ checks.each do |check_class_name, options|
73
+ check_class = Roodi::Checks.const_get(check_class_name)
74
+ check_objects << check_class.make(options || {})
75
+ end
76
+ check_objects
77
+ end
78
+
79
+ end
80
+ end
81
+ end