metric_fu-roodi 2.2.0

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