threedaymonk-roodi 1.3.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/README.txt +96 -0
  2. data/bin/roodi +21 -0
  3. data/bin/roodi-describe +7 -0
  4. data/lib/roodi.rb +6 -0
  5. data/lib/roodi/checks.rb +16 -0
  6. data/lib/roodi/checks/abc_metric_method_check.rb +77 -0
  7. data/lib/roodi/checks/assignment_in_conditional_check.rb +32 -0
  8. data/lib/roodi/checks/case_missing_else_check.rb +20 -0
  9. data/lib/roodi/checks/check.rb +30 -0
  10. data/lib/roodi/checks/class_line_count_check.rb +18 -0
  11. data/lib/roodi/checks/class_name_check.rb +21 -0
  12. data/lib/roodi/checks/class_variable_check.rb +24 -0
  13. data/lib/roodi/checks/control_coupling_check.rb +20 -0
  14. data/lib/roodi/checks/cyclomatic_complexity_block_check.rb +32 -0
  15. data/lib/roodi/checks/cyclomatic_complexity_check.rb +29 -0
  16. data/lib/roodi/checks/cyclomatic_complexity_method_check.rb +32 -0
  17. data/lib/roodi/checks/empty_rescue_body_check.rb +33 -0
  18. data/lib/roodi/checks/for_loop_check.rb +20 -0
  19. data/lib/roodi/checks/line_count_check.rb +29 -0
  20. data/lib/roodi/checks/method_line_count_check.rb +19 -0
  21. data/lib/roodi/checks/method_name_check.rb +21 -0
  22. data/lib/roodi/checks/module_line_count_check.rb +18 -0
  23. data/lib/roodi/checks/module_name_check.rb +21 -0
  24. data/lib/roodi/checks/name_check.rb +23 -0
  25. data/lib/roodi/checks/parameter_number_check.rb +30 -0
  26. data/lib/roodi/core.rb +1 -0
  27. data/lib/roodi/core/checking_visitor.rb +23 -0
  28. data/lib/roodi/core/error.rb +17 -0
  29. data/lib/roodi/core/iterator_visitor.rb +19 -0
  30. data/lib/roodi/core/parser.rb +35 -0
  31. data/lib/roodi/core/runner.rb +78 -0
  32. data/lib/roodi/core/visitable_sexp.rb +20 -0
  33. data/lib/roodi_task.rb +35 -0
  34. data/roodi.yml +15 -0
  35. data/spec/roodi/checks/abc_metric_method_check_spec.rb +89 -0
  36. data/spec/roodi/checks/assignment_in_conditional_check_spec.rb +64 -0
  37. data/spec/roodi/checks/case_missing_else_check_spec.rb +32 -0
  38. data/spec/roodi/checks/class_line_count_check_spec.rb +39 -0
  39. data/spec/roodi/checks/class_name_check_spec.rb +39 -0
  40. data/spec/roodi/checks/class_variable_check_spec.rb +17 -0
  41. data/spec/roodi/checks/control_coupling_check_spec.rb +23 -0
  42. data/spec/roodi/checks/cyclomatic_complexity_block_check_spec.rb +36 -0
  43. data/spec/roodi/checks/cyclomatic_complexity_method_check_spec.rb +183 -0
  44. data/spec/roodi/checks/empty_rescue_body_check_spec.rb +114 -0
  45. data/spec/roodi/checks/for_loop_check_spec.rb +18 -0
  46. data/spec/roodi/checks/method_line_count_check_spec.rb +39 -0
  47. data/spec/roodi/checks/method_name_check_spec.rb +76 -0
  48. data/spec/roodi/checks/module_line_count_check_spec.rb +39 -0
  49. data/spec/roodi/checks/module_name_check_spec.rb +27 -0
  50. data/spec/roodi/checks/parameter_number_check_spec.rb +47 -0
  51. data/spec/spec_helper.rb +3 -0
  52. metadata +129 -0
@@ -0,0 +1,32 @@
1
+ require 'roodi/checks/cyclomatic_complexity_check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks cyclomatic complexity of a method against a specified limit.
6
+ #
7
+ # The cyclomatic complexity is measured by the number of "if", "unless", "elsif", "?:",
8
+ # "while", "until", "for", "rescue", "case", "when", "&&", "and", "||" and "or"
9
+ # statements (plus one) in the body of the member. It is a measure of the minimum
10
+ # number of possible paths through the source and therefore the number of required tests.
11
+ #
12
+ # Generally, for a method, 1-4 is considered good, 5-8 ok, 9-10 consider re-factoring, and
13
+ # 11+ re-factor now!
14
+ class CyclomaticComplexityMethodCheck < CyclomaticComplexityCheck
15
+ DEFAULT_COMPLEXITY = 8
16
+
17
+ def initialize(options = {})
18
+ complexity = options['complexity'] || DEFAULT_COMPLEXITY
19
+ super(complexity)
20
+ end
21
+
22
+ def interesting_nodes
23
+ [:defn]
24
+ end
25
+
26
+ def evaluate(node)
27
+ complexity = count_complexity(node)
28
+ add_error "Method name \"#{node[1]}\" cyclomatic complexity is #{complexity}. It should be #{@complexity} or less." unless complexity <= @complexity
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks the body of a rescue block to make sure it's not empty..
6
+ #
7
+ # When the body of a rescue block is empty, exceptions can get caught and swallowed without
8
+ # any feedback to the user.
9
+ class EmptyRescueBodyCheck < Check
10
+ STATEMENT_NODES = [:fcall, :return, :attrasgn, :vcall, :nil, :call]
11
+
12
+ def interesting_nodes
13
+ [:resbody]
14
+ end
15
+
16
+ def evaluate(node)
17
+ add_error("Rescue block should not be empty.") unless has_statement?(node)
18
+ end
19
+
20
+ private
21
+
22
+ def has_statement?(node)
23
+ return true if STATEMENT_NODES.include?(node.node_type)
24
+ return true if assigning_other_than_exception_to_local_variable?(node)
25
+ return true if node.children.any? { |child| has_statement?(child) }
26
+ end
27
+
28
+ def assigning_other_than_exception_to_local_variable?(node)
29
+ node.node_type == :lasgn && node[2].to_a != [:gvar, :$!]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks to make sure for loops are not being used..
6
+ #
7
+ # Using a for loop is not idiomatic use of Ruby, and is usually a sign that someone with
8
+ # more experience in a different programming language is trying out Ruby. Use
9
+ # Enumerable.each with a block instead.
10
+ class ForLoopCheck < Check
11
+ def interesting_nodes
12
+ [:for]
13
+ end
14
+
15
+ def evaluate(node)
16
+ add_error "Don't use 'for' loops. Use Enumerable.each instead."
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ class LineCountCheck < Check
6
+ def initialize(interesting_nodes, line_count, message_prefix)
7
+ super()
8
+ @interesting_nodes = interesting_nodes
9
+ @line_count = line_count
10
+ @message_prefix = message_prefix
11
+ end
12
+
13
+ def interesting_nodes
14
+ @interesting_nodes
15
+ end
16
+
17
+ def evaluate(node)
18
+ line_count = count_lines(node)
19
+ add_error "#{@message_prefix} \"#{node[1]}\" has #{line_count} lines. It should have #{@line_count} or less." unless line_count <= @line_count
20
+ end
21
+
22
+ protected
23
+
24
+ def count_lines(node)
25
+ node.last.line - 2
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ require 'roodi/checks/line_count_check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks a method to make sure the number of lines it has is under the specified limit.
6
+ #
7
+ # A method getting too large is a code smell that indicates it might be doing more than one
8
+ # thing and becoming hard to test. It should probably be refactored into multiple methods
9
+ # that each do a single thing well.
10
+ class MethodLineCountCheck < LineCountCheck
11
+ DEFAULT_LINE_COUNT = 20
12
+
13
+ def initialize(options = {})
14
+ line_count = options['line_count'] || DEFAULT_LINE_COUNT
15
+ super([:defn], line_count, 'Method')
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
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
+ DEFAULT_PATTERN = /^[_a-z<>=\[|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/
10
+
11
+ def initialize(options = {})
12
+ pattern = options['pattern'] || DEFAULT_PATTERN
13
+ super([:defn], pattern, 'Method')
14
+ end
15
+
16
+ def find_name(node)
17
+ node[1]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
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
+ DEFAULT_LINE_COUNT = 300
11
+
12
+ def initialize(options = {})
13
+ line_count = options['line_count'] || DEFAULT_LINE_COUNT
14
+ super([:module], line_count, 'Module')
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
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
+ DEFAULT_PATTERN = /^[A-Z][a-zA-Z0-9]*$/
10
+
11
+ def initialize(options = {})
12
+ pattern = options['pattern'] || DEFAULT_PATTERN
13
+ super([:module], pattern, 'Module')
14
+ end
15
+
16
+ def find_name(node)
17
+ node[1].class == Symbol ? node[1] : node[1].last
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ class NameCheck < Check
6
+ def initialize(interesting_nodes, pattern, message_prefix)
7
+ super()
8
+ @interesting_nodes = interesting_nodes
9
+ @pattern = pattern
10
+ @message_prefix = message_prefix
11
+ end
12
+
13
+ def interesting_nodes
14
+ @interesting_nodes
15
+ end
16
+
17
+ def evaluate(node)
18
+ name = find_name(node)
19
+ add_error "#{@message_prefix} name \"#{name}\" should match pattern #{@pattern.inspect}" unless name.to_s =~ @pattern
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
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
+ DEFAULT_PARAMETER_COUNT = 5
12
+
13
+ def initialize(options = {})
14
+ super()
15
+ @parameter_count = options['parameter_count'] || DEFAULT_PARAMETER_COUNT
16
+ end
17
+
18
+ def interesting_nodes
19
+ [:defn]
20
+ end
21
+
22
+ def evaluate(node)
23
+ method_name = node[1]
24
+ arguments = node[2]
25
+ parameter_count = arguments.inject(-1) { |count, each| count = count + (each.class == Symbol ? 1 : 0) }
26
+ add_error "Method name \"#{method_name}\" has #{parameter_count} parameters. It should have #{@parameter_count} or less." unless parameter_count <= @parameter_count
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/roodi/core.rb ADDED
@@ -0,0 +1 @@
1
+ require 'roodi/core/runner'
@@ -0,0 +1,23 @@
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(node)} unless checks.nil?
19
+ nil
20
+ end
21
+ end
22
+ end
23
+ 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,19 @@
1
+ module Roodi
2
+ module Core
3
+ class IteratorVisitor
4
+ def initialize(payload)
5
+ @payload = payload
6
+ end
7
+
8
+ def visit(visited)
9
+ visited.accept(@payload)
10
+ visitable_nodes = visited.is_language_node? ? visited.sexp_body : visited
11
+ visitable_nodes.each do |child|
12
+ if child.class == Sexp then
13
+ child.accept(self)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ require 'ruby_parser'
3
+
4
+ module Roodi
5
+ module Core
6
+ class Parser
7
+
8
+ def silence_stream(*streams)
9
+ on_hold = streams.collect{ |stream| stream.dup }
10
+ streams.each do |stream|
11
+ stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
12
+ stream.sync = true
13
+ end
14
+ yield
15
+ ensure
16
+ streams.each_with_index do |stream, i|
17
+ stream.reopen(on_hold[i])
18
+ end
19
+ end
20
+
21
+ def parse(content, filename)
22
+ silence_stream(STDERR) do
23
+ return silent_parse(content, filename)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def silent_parse(content, filename)
30
+ @parser ||= RubyParser.new
31
+ @parser.parse(content, filename)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,78 @@
1
+ require 'pp'
2
+ require 'yaml'
3
+
4
+ require 'roodi/core/checking_visitor'
5
+ require 'roodi/core/iterator_visitor'
6
+ require 'roodi/core/parser'
7
+ require 'roodi/core/visitable_sexp'
8
+
9
+ module Roodi
10
+ module Core
11
+ class Runner
12
+ DEFAULT_CONFIG = File.join(File.dirname(__FILE__), "..", "..", "..", "roodi.yml")
13
+
14
+ attr_writer :config
15
+
16
+ def initialize(*checks)
17
+ @config = DEFAULT_CONFIG
18
+ @checks = checks unless checks.empty?
19
+ @parser = Parser.new
20
+ end
21
+
22
+ def check(filename, content)
23
+ @checks ||= load_checks
24
+ node = parse(filename, content)
25
+ node.accept(IteratorVisitor.new(CheckingVisitor.new(@checks))) if node
26
+ end
27
+
28
+ def check_content(content)
29
+ check("dummy-file.rb", content)
30
+ end
31
+
32
+ def check_file(filename)
33
+ check(filename, File.read(filename))
34
+ end
35
+
36
+ def print(filename, content)
37
+ node = @parser.parse(content, filename)
38
+ puts "Line: #{node.line}"
39
+ pp node
40
+ end
41
+
42
+ def print_content(content)
43
+ print("dummy-file.rb", content)
44
+ end
45
+
46
+ def print_file(filename)
47
+ print(filename, File.read(filename))
48
+ end
49
+
50
+ def errors
51
+ @checks ||= []
52
+ all_errors = @checks.collect {|check| check.errors}
53
+ all_errors.flatten
54
+ end
55
+
56
+ private
57
+
58
+ def parse(filename, content)
59
+ begin
60
+ @parser.parse(content, filename)
61
+ rescue Exception => e
62
+ puts "#{filename} looks like it's not a valid Ruby file. Skipping..." if ENV["ROODI_DEBUG"]
63
+ nil
64
+ end
65
+ end
66
+
67
+ def load_checks
68
+ check_objects = []
69
+ checks = YAML.load_file @config
70
+ checks.each do |check|
71
+ klass = eval("Roodi::Checks::#{check[0]}")
72
+ check_objects << (check[1].empty? ? klass.new : klass.new(check[1]))
73
+ end
74
+ check_objects
75
+ end
76
+ end
77
+ end
78
+ end