roodi1.9 2.0.1
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.
- data/.gitignore +2 -0
- data/Gemfile +0 -0
- data/History.txt +88 -0
- data/Manifest.txt +56 -0
- data/README.md +123 -0
- data/Rakefile +40 -0
- data/bin/roodi1.9 +21 -0
- data/bin/roodi1.9-describe +7 -0
- data/lib/roodi/checks/abc_metric_method_check.rb +77 -0
- data/lib/roodi/checks/assignment_in_conditional_check.rb +34 -0
- data/lib/roodi/checks/case_missing_else_check.rb +20 -0
- data/lib/roodi/checks/check.rb +61 -0
- data/lib/roodi/checks/class_line_count_check.rb +18 -0
- data/lib/roodi/checks/class_name_check.rb +21 -0
- data/lib/roodi/checks/class_variable_check.rb +24 -0
- data/lib/roodi/checks/control_coupling_check.rb +20 -0
- data/lib/roodi/checks/cyclomatic_complexity_block_check.rb +39 -0
- data/lib/roodi/checks/cyclomatic_complexity_check.rb +47 -0
- data/lib/roodi/checks/cyclomatic_complexity_method_check.rb +40 -0
- data/lib/roodi/checks/empty_rescue_body_check.rb +32 -0
- data/lib/roodi/checks/for_loop_check.rb +20 -0
- data/lib/roodi/checks/line_count_check.rb +29 -0
- data/lib/roodi/checks/method_line_count_check.rb +19 -0
- data/lib/roodi/checks/method_name_check.rb +21 -0
- data/lib/roodi/checks/missing_foreign_key_index_check.rb +98 -0
- data/lib/roodi/checks/module_line_count_check.rb +18 -0
- data/lib/roodi/checks/module_name_check.rb +21 -0
- data/lib/roodi/checks/name_check.rb +23 -0
- data/lib/roodi/checks/npath_complexity_check.rb +73 -0
- data/lib/roodi/checks/npath_complexity_method_check.rb +28 -0
- data/lib/roodi/checks/parameter_number_check.rb +30 -0
- data/lib/roodi/checks.rb +18 -0
- data/lib/roodi/core/checking_visitor.rb +26 -0
- data/lib/roodi/core/error.rb +17 -0
- data/lib/roodi/core/parser.rb +30 -0
- data/lib/roodi/core/runner.rb +80 -0
- data/lib/roodi/core/visitable_sexp.rb +25 -0
- data/lib/roodi/core.rb +1 -0
- data/lib/roodi.rb +6 -0
- data/lib/roodi_task.rb +35 -0
- data/roodi.yml +19 -0
- data/roodi1.9.gemspec +17 -0
- data/spec/roodi/checks/abc_metric_method_check_spec.rb +89 -0
- data/spec/roodi/checks/assignment_in_conditional_check_spec.rb +105 -0
- data/spec/roodi/checks/case_missing_else_check_spec.rb +32 -0
- data/spec/roodi/checks/class_line_count_check_spec.rb +39 -0
- data/spec/roodi/checks/class_name_check_spec.rb +39 -0
- data/spec/roodi/checks/class_variable_check_spec.rb +17 -0
- data/spec/roodi/checks/control_coupling_check_spec.rb +23 -0
- data/spec/roodi/checks/cyclomatic_complexity_block_check_spec.rb +67 -0
- data/spec/roodi/checks/cyclomatic_complexity_method_check_spec.rb +200 -0
- data/spec/roodi/checks/empty_rescue_body_check_spec.rb +140 -0
- data/spec/roodi/checks/for_loop_check_spec.rb +18 -0
- data/spec/roodi/checks/method_line_count_check_spec.rb +56 -0
- data/spec/roodi/checks/method_name_check_spec.rb +76 -0
- data/spec/roodi/checks/missing_foreign_key_index_check_spec.rb +33 -0
- data/spec/roodi/checks/module_line_count_check_spec.rb +39 -0
- data/spec/roodi/checks/module_name_check_spec.rb +27 -0
- data/spec/roodi/checks/npath_complexity_method_check_spec.rb +53 -0
- data/spec/roodi/checks/parameter_number_check_spec.rb +47 -0
- data/spec/roodi/core/runner_spec.rb +25 -0
- data/spec/roodi/roodi.yml +1 -0
- data/spec/spec_helper.rb +3 -0
- metadata +112 -0
@@ -0,0 +1,47 @@
|
|
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
|
+
@count = 0
|
12
|
+
@counting = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
COMPLEXITY_NODE_TYPES.each do |type|
|
16
|
+
define_method "evaluate_start_#{type}" do |node|
|
17
|
+
@count = @count + 1 if counting?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def count_complexity(node)
|
24
|
+
count_branches(node) + 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def increase_depth
|
28
|
+
@count = 1 unless counting?
|
29
|
+
@counting = @counting + 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def decrease_depth
|
33
|
+
@counting = @counting - 1
|
34
|
+
if @counting <= 0
|
35
|
+
@counting = 0
|
36
|
+
evaluate_matching_end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def counting?
|
43
|
+
@counting > 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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] + COMPLEXITY_NODE_TYPES
|
24
|
+
end
|
25
|
+
|
26
|
+
def evaluate_start_defn(node)
|
27
|
+
@method_name = @node[1]
|
28
|
+
increase_depth
|
29
|
+
end
|
30
|
+
|
31
|
+
def evaluate_end_defn(node)
|
32
|
+
decrease_depth
|
33
|
+
end
|
34
|
+
|
35
|
+
def evaluate_matching_end
|
36
|
+
add_error "Method name \"#{@method_name}\" cyclomatic complexity is #{@count}. It should be #{@complexity} or less." unless @count <= @complexity
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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, :lasgn, :true, :false]
|
11
|
+
|
12
|
+
def interesting_nodes
|
13
|
+
[:resbody]
|
14
|
+
end
|
15
|
+
|
16
|
+
def evaluate_start(node)
|
17
|
+
add_error("Rescue block should not be empty.") unless has_statement?(node.children[1])
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def has_statement?(node)
|
23
|
+
false unless node
|
24
|
+
has_local_statement?(node) or node.children.any? { |child| has_statement?(child) } if node
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_local_statement?(node)
|
28
|
+
STATEMENT_NODES.include?(node.node_type)
|
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 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_start(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_start(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 - node.line - 1
|
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,98 @@
|
|
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
|
+
@foreign_keys = {}
|
14
|
+
@indexes = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def interesting_nodes
|
18
|
+
[:call]
|
19
|
+
end
|
20
|
+
|
21
|
+
def evaluate_start_call(node)
|
22
|
+
if analyzing_schema(node)
|
23
|
+
if creating_table(node)
|
24
|
+
@current_table = create_table_name(node)
|
25
|
+
end
|
26
|
+
|
27
|
+
if creating_foreign_key(node)
|
28
|
+
@foreign_keys[@current_table] ||= []
|
29
|
+
@foreign_keys[@current_table] << foreign_key_column_name(node)
|
30
|
+
end
|
31
|
+
|
32
|
+
if adding_index(node)
|
33
|
+
@indexes[index_table_name(node)] ||= []
|
34
|
+
@indexes[index_table_name(node)] << index_column_name(node)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def evaluate_end_call(node)
|
40
|
+
#ignored
|
41
|
+
end
|
42
|
+
|
43
|
+
def analyzing_schema(node)
|
44
|
+
pathname = Pathname.new(node.file)
|
45
|
+
@analyzing_schema ||= ("schema.rb" == pathname.basename.to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
def creating_table(node)
|
49
|
+
:create_table == node[2]
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_table_name(node)
|
53
|
+
# Get table name out of this:
|
54
|
+
# s(:call, nil, :create_table, s(:arglist, s(:str, "duplicate_blocks"), s(:hash, s(:lit, :force), s(:true))))
|
55
|
+
node[3][1][1]
|
56
|
+
end
|
57
|
+
|
58
|
+
def creating_foreign_key(node)
|
59
|
+
#s(:call, s(:lvar, :t), :integer, s(:arglist, s(:str, "duplicate_set_id"), s(:hash, s(:lit, :null), s(:false))))
|
60
|
+
column_type = node[2]
|
61
|
+
column_name = node[3][1][1]
|
62
|
+
:integer == column_type && "_id" == column_name[-3,3]
|
63
|
+
end
|
64
|
+
|
65
|
+
def foreign_key_column_name(node)
|
66
|
+
#s(:call, s(:lvar, :t), :integer, s(:arglist, s(:str, "duplicate_set_id"), s(:hash, s(:lit, :null), s(:false))))
|
67
|
+
column_name = node[3][1][1]
|
68
|
+
end
|
69
|
+
|
70
|
+
def adding_index(node)
|
71
|
+
:add_index == node[2]
|
72
|
+
end
|
73
|
+
|
74
|
+
def index_table_name(node)
|
75
|
+
# Get table name out of this:
|
76
|
+
# 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"))))
|
77
|
+
node[3][1][1]
|
78
|
+
end
|
79
|
+
|
80
|
+
def index_column_name(node)
|
81
|
+
# Get index column name out of this:
|
82
|
+
# 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"))))
|
83
|
+
node[3][2][1][1]
|
84
|
+
end
|
85
|
+
|
86
|
+
def end_file(filename)
|
87
|
+
@foreign_keys.keys.each do |table|
|
88
|
+
foreign_keys = @foreign_keys[table] || []
|
89
|
+
indexes = @indexes[table] || []
|
90
|
+
missing_indexes = foreign_keys - indexes
|
91
|
+
missing_indexes.each do |fkey|
|
92
|
+
add_error("Table '#{table}' is missing an index on the foreign key '#{fkey}'", filename, 1)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
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_start(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,73 @@
|
|
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
|
+
def initialize(complexity)
|
12
|
+
super()
|
13
|
+
@complexity = complexity
|
14
|
+
@value_stack = []
|
15
|
+
@current_value = 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def evalute_start_if(node)
|
19
|
+
push_value
|
20
|
+
end
|
21
|
+
|
22
|
+
def evalute_start_while(node)
|
23
|
+
push_value
|
24
|
+
end
|
25
|
+
|
26
|
+
def evalute_start_until(node)
|
27
|
+
push_value
|
28
|
+
end
|
29
|
+
|
30
|
+
def evalute_start_for(node)
|
31
|
+
push_value
|
32
|
+
end
|
33
|
+
|
34
|
+
def evalute_start_case(node)
|
35
|
+
push_value
|
36
|
+
end
|
37
|
+
|
38
|
+
def evalute_start_rescue(node)
|
39
|
+
push_value
|
40
|
+
end
|
41
|
+
|
42
|
+
MULTIPLYING_NODE_TYPES.each do |type|
|
43
|
+
define_method "evaluate_end_#{type}" do |node|
|
44
|
+
leave_multiplying_conditional
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
ADDING_NODE_TYPES.each do |type|
|
49
|
+
define_method "evaluate_end_#{type}" do |node|
|
50
|
+
leave_multiplying_conditional
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def push_value
|
57
|
+
@value_stack.push @current_value
|
58
|
+
@current_value = 1
|
59
|
+
end
|
60
|
+
|
61
|
+
def leave_multiplying_conditional
|
62
|
+
pop = @value_stack.pop
|
63
|
+
@current_value = (@current_value + 1) * pop
|
64
|
+
end
|
65
|
+
|
66
|
+
def leave_adding_conditional
|
67
|
+
pop = @value_stack.pop
|
68
|
+
puts "#{type}, so adding #{pop}"
|
69
|
+
@current_value = @current_value - 1 + pop
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,28 @@
|
|
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
|
+
DEFAULT_COMPLEXITY = 8
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
complexity = options['complexity'] || DEFAULT_COMPLEXITY
|
11
|
+
super(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
|
+
end
|
27
|
+
end
|
28
|
+
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_start(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/checks.rb
ADDED
@@ -0,0 +1,18 @@
|
|
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/missing_foreign_key_index_check'
|
15
|
+
require 'roodi/checks/module_line_count_check'
|
16
|
+
require 'roodi/checks/module_name_check'
|
17
|
+
require 'roodi/checks/npath_complexity_method_check'
|
18
|
+
require 'roodi/checks/parameter_number_check'
|
@@ -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,80 @@
|
|
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
|
+
@parser = Parser.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def check(filename, content)
|
22
|
+
@checks ||= load_checks
|
23
|
+
@checker ||= CheckingVisitor.new(@checks)
|
24
|
+
@checks.each {|check| check.start_file(filename)}
|
25
|
+
node = parse(filename, content)
|
26
|
+
node.accept(@checker) if node
|
27
|
+
@checks.each {|check| check.end_file(filename)}
|
28
|
+
end
|
29
|
+
|
30
|
+
def check_content(content, filename = "dummy-file.rb")
|
31
|
+
check(filename, content)
|
32
|
+
end
|
33
|
+
|
34
|
+
def check_file(filename)
|
35
|
+
check(filename, File.read(filename))
|
36
|
+
end
|
37
|
+
|
38
|
+
def print(filename, content)
|
39
|
+
node = @parser.parse(content, filename)
|
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.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|
|
73
|
+
klass = eval("Roodi::Checks::#{check[0]}")
|
74
|
+
check_objects << (check[1].empty? ? klass.new : klass.new(check[1]))
|
75
|
+
end
|
76
|
+
check_objects
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|