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
data/README.txt ADDED
@@ -0,0 +1,96 @@
1
+ = roodi
2
+
3
+ * http://roodi.rubyforge.org
4
+
5
+ == DESCRIPTION:
6
+
7
+ Roodi stands for Ruby Object Oriented Design Inferometer. It parses your Ruby code and warns you about design issues you have based on the checks that is has configured.
8
+
9
+ == INSTALL:
10
+
11
+ * sudo gem install roodi
12
+
13
+ == SYNOPSIS:
14
+
15
+ To check one or more files using the default configuration that comes with Roodi, use:
16
+ roodi [-config=file] [pattern ...]
17
+
18
+ === EXAMPLE USAGE
19
+
20
+ Check all ruby files in a rails app:
21
+ roodi "rails_app/**/*.rb"
22
+
23
+ Check one controller and one model file in a rails app:
24
+ roodi app/controller/sample_controller.rb app/models/sample.rb
25
+
26
+ Check one controller and all model files in a rails app:
27
+ roodi app/controller/sample_controller.rb "app/models/*.rb"
28
+
29
+ Check all ruby files in a rails app with a custom configuration file:
30
+ roodi -config=my_roodi_config.yml "rails_app/**/*.rb"
31
+
32
+ If you're writing a check, it is useful to see the structure of a file the way that Roodi tokenizes it (via ruby_parser). Use:
33
+ roodi-describe [filename]
34
+
35
+ == CUSTOM CONFIGURATION
36
+
37
+ To change the set of checks included, or to change the default values of the checks, you can provide your own config file. The config file is a YAML file that lists the checks to be included. Each check can optionally include a hash of options that are passed to the check to configure it. For example, the default config file looks like this:
38
+
39
+ AssignmentInConditionalCheck: { }
40
+ CaseMissingElseCheck: { }
41
+ ClassLineCountCheck: { line_count: 300 }
42
+ ClassNameCheck: { pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ }
43
+ CyclomaticComplexityBlockCheck: { complexity: 4 }
44
+ CyclomaticComplexityMethodCheck: { complexity: 8 }
45
+ EmptyRescueBodyCheck: { }
46
+ ForLoopCheck: { }
47
+ MethodLineCountCheck: { line_count: 20 }
48
+ MethodNameCheck: { pattern: !ruby/regexp /^[_a-z<>=\[\]|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/ }
49
+ ModuleLineCountCheck: { line_count: 300 }
50
+ ModuleNameCheck: { pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ }
51
+ ParameterNumberCheck: { parameter_count: 5 }
52
+
53
+ == SUPPORTED CHECKS:
54
+
55
+ * AssignmentInConditionalCheck - Check for an assignment inside a conditional. It's probably a mistaken equality comparison.
56
+ * CaseMissingElseCheck - Check that case statements have an else statement so that all cases are covered.
57
+ * ClassLineCountCheck - Check that the number of lines in a class is below the threshold.
58
+ * ClassNameCheck - Check that class names match convention.
59
+ * CyclomaticComplexityBlockCheck - Check that the cyclomatic complexity of all blocks is below the threshold.
60
+ * CyclomaticComplexityMethodCheck - Check that the cyclomatic complexity of all methods is below the threshold.
61
+ * EmptyRescueBodyCheck - Check that there are no empty rescue blocks.
62
+ * ForLoopCheck - Check that for loops aren't used (Use Enumerable.each instead)
63
+ * MethodLineCountCheck - Check that the number of lines in a method is below the threshold.
64
+ * MethodNameCheck - Check that method names match convention.
65
+ * ModuleLineCountCheck - Check that the number of lines in a module is below the threshold.
66
+ * ModuleNameCheck - Check that module names match convention.
67
+ * ParameterNumberCheck - Check that the number of parameters on a method is below the threshold.
68
+
69
+ == SUGGESTED CHECKS:
70
+
71
+ * BlockVariableShadowCheck - Check that a block variable does not have the same name as a method parameter or local variable. It may be mistakenly referenced within the block.
72
+
73
+ == LICENSE:
74
+
75
+ (The MIT License)
76
+
77
+ Copyright (c) 2008 Marty Andrews
78
+
79
+ Permission is hereby granted, free of charge, to any person obtaining
80
+ a copy of this software and associated documentation files (the
81
+ 'Software'), to deal in the Software without restriction, including
82
+ without limitation the rights to use, copy, modify, merge, publish,
83
+ distribute, sublicense, and/or sell copies of the Software, and to
84
+ permit persons to whom the Software is furnished to do so, subject to
85
+ the following conditions:
86
+
87
+ The above copyright notice and this permission notice shall be
88
+ included in all copies or substantial portions of the Software.
89
+
90
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
91
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
92
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
93
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
94
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
95
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
96
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/roodi ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
4
+
5
+ require 'roodi'
6
+
7
+ runner = Roodi::Core::Runner.new
8
+
9
+ config_param = ARGV.detect {|arg| arg =~ /-config=.*/}
10
+ runner.config = config_param.split("=")[1] if config_param
11
+ ARGV.delete config_param
12
+
13
+ ARGV.each do |arg|
14
+ Dir.glob(arg).each { |file| runner.check_file(file) }
15
+ end
16
+
17
+ runner.errors.each {|error| puts error}
18
+
19
+ puts "\nFound #{runner.errors.size} errors."
20
+
21
+ exit runner.errors.size
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
4
+ require 'roodi'
5
+
6
+ roodi = Roodi::Core::Runner.new
7
+ roodi.print_file(ARGV[0])
data/lib/roodi.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'roodi/checks'
2
+ require 'roodi/core'
3
+
4
+ module Roodi
5
+ VERSION = '1.3.8'
6
+ end
@@ -0,0 +1,16 @@
1
+ require 'roodi/checks/abc_metric_method_check'
2
+ require 'roodi/checks/assignment_in_conditional_check'
3
+ require 'roodi/checks/case_missing_else_check'
4
+ require 'roodi/checks/class_line_count_check'
5
+ require 'roodi/checks/class_name_check'
6
+ require 'roodi/checks/class_variable_check'
7
+ require 'roodi/checks/control_coupling_check'
8
+ require 'roodi/checks/cyclomatic_complexity_block_check'
9
+ require 'roodi/checks/cyclomatic_complexity_method_check'
10
+ require 'roodi/checks/empty_rescue_body_check'
11
+ require 'roodi/checks/for_loop_check'
12
+ require 'roodi/checks/method_line_count_check'
13
+ require 'roodi/checks/method_name_check'
14
+ require 'roodi/checks/module_line_count_check'
15
+ require 'roodi/checks/module_name_check'
16
+ require 'roodi/checks/parameter_number_check'
@@ -0,0 +1,77 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # TODO: Add summary
6
+ #
7
+ # TODO: Add detail
8
+ class AbcMetricMethodCheck < Check
9
+ # ASSIGNMENTS = [:attrasgn, :attrset, :dasgn_curr, :iasgn, :lasgn, :masgn]
10
+ ASSIGNMENTS = [:lasgn]
11
+ # BRANCHES = [:if, :else, :while, :until, :for, :rescue, :case, :when, :and, :or]
12
+ BRANCHES = [:vcall, :call]
13
+ # CONDITIONS = [:and, :or]
14
+ CONDITIONS = [:==, :<=, :>=, :<, :>]
15
+ # = *= /= %= += <<= >>= &= |= ^=
16
+ OPERATORS = [:*, :/, :%, :+, :<<, :>>, :&, :|, :^]
17
+ DEFAULT_SCORE = 10
18
+
19
+ def initialize(options = {})
20
+ super()
21
+ @score = options['score'] || DEFAULT_SCORE
22
+ end
23
+
24
+ def interesting_nodes
25
+ [:defn]
26
+ end
27
+
28
+ def evaluate(node)
29
+ method_name = node[1]
30
+ a = count_assignments(node)
31
+ b = count_branches(node)
32
+ c = count_conditionals(node)
33
+ score = Math.sqrt(a*a + b*b + c*c)
34
+ add_error "Method name \"#{method_name}\" has an ABC metric score of <#{a},#{b},#{c}> = #{score}. It should be #{@score} or less." unless score <= @score
35
+ end
36
+
37
+ private
38
+
39
+ def count_assignments(node)
40
+ count = 0
41
+ count = count + 1 if assignment?(node)
42
+ node.children.each {|node| count += count_assignments(node)}
43
+ count
44
+ end
45
+
46
+ def count_branches(node)
47
+ count = 0
48
+ count = count + 1 if branch?(node)
49
+ node.children.each {|node| count += count_branches(node)}
50
+ count
51
+ end
52
+
53
+ def count_conditionals(node)
54
+ count = 0
55
+ count = count + 1 if conditional?(node)
56
+ node.children.each {|node| count += count_conditionals(node)}
57
+ count
58
+ end
59
+
60
+ def assignment?(node)
61
+ ASSIGNMENTS.include?(node.node_type)
62
+ end
63
+
64
+ def branch?(node)
65
+ BRANCHES.include?(node.node_type) && !conditional?(node) && !operator?(node)
66
+ end
67
+
68
+ def conditional?(node)
69
+ (:call == node.node_type) && CONDITIONS.include?(node[2])
70
+ end
71
+
72
+ def operator?(node)
73
+ (:call == node.node_type) && OPERATORS.include?(node[2])
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,32 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks a conditional to see if it contains an assignment.
6
+ #
7
+ # A conditional containing an assignment is likely to be a mistyped equality check. You
8
+ # should either fix the typo or factor out the assignment so that the code is clearer.
9
+ class AssignmentInConditionalCheck < Check
10
+ def initialize(options = {})
11
+ super()
12
+ end
13
+
14
+ def interesting_nodes
15
+ [:if, :while]
16
+ end
17
+
18
+ def evaluate(node)
19
+ add_error("Found = in conditional. It should probably be an ==") if has_assignment?(node[1])
20
+ end
21
+
22
+ private
23
+
24
+ def has_assignment?(node)
25
+ found_assignment = false
26
+ found_assignment = found_assignment || node.node_type == :lasgn
27
+ node.children.each { |child| found_assignment = found_assignment || has_assignment?(child) }
28
+ found_assignment
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks a case statement to make sure it has an 'else' clause.
6
+ #
7
+ # It's usually a good idea to have an else clause in every case statement. Even if the
8
+ # developer is sure that all currently possible cases are covered, this should be
9
+ # expressed in the else clause. This way the code is protected aginst later changes,
10
+ class CaseMissingElseCheck < Check
11
+ def interesting_nodes
12
+ [:case]
13
+ end
14
+
15
+ def evaluate(node)
16
+ add_error "Case statement is missing an else clause." unless node.last
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ require 'roodi/core/error'
2
+
3
+ module Roodi
4
+ module Checks
5
+ class Check
6
+ def initialize
7
+ @errors = []
8
+ end
9
+
10
+ def position(offset = 0)
11
+ "#{@line[2]}:#{@line[1] + offset}"
12
+ end
13
+
14
+ def evaluate_node(node)
15
+ @node = node
16
+ eval_method = "evaluate_#{node.node_type}"
17
+ self.send(eval_method, node) if self.respond_to? eval_method
18
+ evaluate(node) if self.respond_to? :evaluate
19
+ end
20
+
21
+ def add_error(error)
22
+ @errors << Roodi::Core::Error.new("#{@node.file}", "#{@node.line}", error)
23
+ end
24
+
25
+ def errors
26
+ @errors
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ require 'roodi/checks/line_count_check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks a class to make sure the number of lines it has is under the specified limit.
6
+ #
7
+ # A class 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 classes.
9
+ class ClassLineCountCheck < LineCountCheck
10
+ DEFAULT_LINE_COUNT = 300
11
+
12
+ def initialize(options = {})
13
+ line_count = options['line_count'] || DEFAULT_LINE_COUNT
14
+ super([:class], line_count, 'Class')
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 class name to make sure it matches the specified pattern.
6
+ #
7
+ # Keeping to a consistent naming convention makes your code easier to read.
8
+ class ClassNameCheck < NameCheck
9
+ DEFAULT_PATTERN = /^[A-Z][a-zA-Z0-9]*$/
10
+
11
+ def initialize(options = {})
12
+ pattern = options['pattern'] || DEFAULT_PATTERN
13
+ super([:class], pattern, 'Class')
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,24 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks to make sure class variables are not being used..
6
+ #
7
+ # Class variables in Ruby have a complicated inheritance policy, and their use
8
+ # can lead to mistakes. Often an alternate design can be used to solve the
9
+ # problem instead.
10
+ #
11
+ # This check is looking for a code smell rather than a definite error. If you're
12
+ # sure that you're doing the right thing, try turning this check off in your
13
+ # config file.
14
+ class ClassVariableCheck < Check
15
+ def interesting_nodes
16
+ [:cvar]
17
+ end
18
+
19
+ def evaluate(node)
20
+ add_error "Don't use class variables. You might want to try a different design."
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ class ControlCouplingCheck < Check
6
+ def interesting_nodes
7
+ [:defn, :lvar]
8
+ end
9
+
10
+ def evaluate_defn(node)
11
+ @method_name = node[1]
12
+ @arguments = node[2][1..-1]
13
+ end
14
+
15
+ def evaluate_lvar(node)
16
+ add_error "Method \"#{@method_name}\" uses the argument \"#{node[1]}\" for internal control." if @arguments.detect {|each| each == node[1]}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ require 'roodi/checks/cyclomatic_complexity_check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ # Checks cyclomatic complexity of a block 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", "&amp;&amp;", "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 block, 1-2 is considered good, 3-4 ok, 5-8 consider re-factoring, and 8+
13
+ # re-factor now!
14
+ class CyclomaticComplexityBlockCheck < CyclomaticComplexityCheck
15
+ DEFAULT_COMPLEXITY = 4
16
+
17
+ def initialize(options = {})
18
+ complexity = options['complexity'] || DEFAULT_COMPLEXITY
19
+ super(complexity)
20
+ end
21
+
22
+ def interesting_nodes
23
+ [:iter]
24
+ end
25
+
26
+ def evaluate(node)
27
+ complexity = count_complexity(node)
28
+ add_error "Block cyclomatic complexity is #{complexity}. It should be #{@complexity} or less." unless complexity <= @complexity
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ require 'roodi/checks/check'
2
+
3
+ module Roodi
4
+ module Checks
5
+ class CyclomaticComplexityCheck < Check
6
+ COMPLEXITY_NODE_TYPES = [:if, :while, :until, :for, :rescue, :case, :when, :and, :or]
7
+
8
+ def initialize(complexity)
9
+ super()
10
+ @complexity = complexity
11
+ end
12
+
13
+ protected
14
+
15
+ def count_complexity(node)
16
+ count_branches(node) + 1
17
+ end
18
+
19
+ private
20
+
21
+ def count_branches(node)
22
+ count = 0
23
+ count = count + 1 if COMPLEXITY_NODE_TYPES.include? node.node_type
24
+ node.children.each {|node| count += count_branches(node)}
25
+ count
26
+ end
27
+ end
28
+ end
29
+ end