reek 1.3.6 → 1.3.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +6 -0
  3. data/README.md +11 -1
  4. data/config/defaults.reek +1 -0
  5. data/features/command_line_interface/options.feature +1 -0
  6. data/features/rake_task/rake_task.feature +3 -0
  7. data/features/ruby_api/api.feature +1 -3
  8. data/features/samples.feature +27 -20
  9. data/features/support/env.rb +2 -2
  10. data/lib/reek/cli/application.rb +0 -4
  11. data/lib/reek/cli/command_line.rb +10 -12
  12. data/lib/reek/cli/reek_command.rb +1 -1
  13. data/lib/reek/cli/report.rb +36 -8
  14. data/lib/reek/config_file_exception.rb +3 -0
  15. data/lib/reek/core/code_context.rb +18 -8
  16. data/lib/reek/core/code_parser.rb +65 -61
  17. data/lib/reek/core/method_context.rb +4 -0
  18. data/lib/reek/core/module_context.rb +2 -2
  19. data/lib/reek/core/smell_repository.rb +3 -0
  20. data/lib/reek/core/sniffer.rb +0 -1
  21. data/lib/reek/core/stop_context.rb +1 -1
  22. data/lib/reek/smells/attribute.rb +1 -1
  23. data/lib/reek/smells/control_parameter.rb +79 -45
  24. data/lib/reek/smells/data_clump.rb +1 -1
  25. data/lib/reek/smells/duplicate_method_call.rb +1 -1
  26. data/lib/reek/smells/long_parameter_list.rb +1 -1
  27. data/lib/reek/smells/long_yield_list.rb +1 -1
  28. data/lib/reek/smells/nested_iterators.rb +1 -1
  29. data/lib/reek/smells/nil_check.rb +10 -5
  30. data/lib/reek/smells/repeated_conditional.rb +1 -1
  31. data/lib/reek/smells/smell_detector.rb +2 -3
  32. data/lib/reek/smells/too_many_instance_variables.rb +1 -1
  33. data/lib/reek/smells/too_many_methods.rb +1 -1
  34. data/lib/reek/smells/too_many_statements.rb +1 -1
  35. data/lib/reek/smells/uncommunicative_method_name.rb +4 -4
  36. data/lib/reek/smells/uncommunicative_module_name.rb +4 -4
  37. data/lib/reek/smells/uncommunicative_parameter_name.rb +9 -9
  38. data/lib/reek/smells/uncommunicative_variable_name.rb +1 -1
  39. data/lib/reek/smells/unused_parameters.rb +2 -6
  40. data/lib/reek/smells/utility_function.rb +1 -1
  41. data/lib/reek/source/code_comment.rb +1 -1
  42. data/lib/reek/source/config_file.rb +9 -8
  43. data/lib/reek/source/sexp_extensions.rb +2 -2
  44. data/lib/reek/source/sexp_node.rb +8 -5
  45. data/lib/reek/source/source_repository.rb +5 -0
  46. data/lib/reek/version.rb +1 -1
  47. data/reek.gemspec +3 -2
  48. data/spec/reek/cli/report_spec.rb +38 -8
  49. data/spec/reek/core/code_context_spec.rb +35 -3
  50. data/spec/reek/core/module_context_spec.rb +1 -1
  51. data/spec/reek/smells/repeated_conditional_spec.rb +1 -1
  52. data/spec/reek/smells/smell_detector_shared.rb +1 -2
  53. data/spec/reek/smells/too_many_statements_spec.rb +39 -25
  54. data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +44 -30
  55. data/spec/reek/smells/unused_parameters_spec.rb +15 -11
  56. data/spec/reek/source/sexp_extensions_spec.rb +2 -2
  57. data/spec/reek/source/sexp_node_spec.rb +0 -1
  58. data/spec/samples/ruby20_syntax.rb +1 -5
  59. metadata +172 -162
  60. data/lib/reek/cli/yaml_command.rb +0 -32
  61. data/lib/reek/core/hash_extensions.rb +0 -29
  62. data/spec/reek/cli/yaml_command_spec.rb +0 -47
  63. data/spec/reek/core/config_spec.rb +0 -38
@@ -1,4 +1,7 @@
1
1
  module Reek
2
+ #
3
+ # An exception that occured when loading the configuration file.
4
+ #
2
5
  class ConfigFileException < RuntimeError
3
6
 
4
7
  end
@@ -10,12 +10,11 @@ module Reek
10
10
  #
11
11
  class CodeContext
12
12
 
13
- attr_reader :exp, :config
13
+ attr_reader :exp
14
14
 
15
15
  def initialize(outer, exp)
16
16
  @outer = outer
17
17
  @exp = exp
18
- @config = local_config
19
18
  end
20
19
 
21
20
  def name
@@ -56,12 +55,23 @@ module Reek
56
55
  exp.full_name(outer)
57
56
  end
58
57
 
59
- def local_config
60
- return Hash.new if @exp.nil?
61
- config = Source::CodeComment.new(@exp.comments || '').config
62
- return config unless @outer
63
- @outer.config.deep_copy.adopt!(config)
64
- # no tests for this -----^
58
+ def config_for(detector_class)
59
+ outer_config_for(detector_class).merge(
60
+ config[detector_class.smell_class_name] || {})
61
+ end
62
+
63
+ private
64
+
65
+ def config
66
+ @config ||= if @exp
67
+ Source::CodeComment.new(@exp.comments || '').config
68
+ else
69
+ {}
70
+ end
71
+ end
72
+
73
+ def outer_config_for(detector_class)
74
+ @outer ? @outer.config_for(detector_class) : {}
65
75
  end
66
76
  end
67
77
  end
@@ -29,124 +29,128 @@ module Reek
29
29
  end
30
30
 
31
31
  def process_module(exp)
32
- name = Source::SexpFormatter.format(exp[1])
33
- scope = ModuleContext.new(@element, name, exp)
34
- push(scope) do
35
- process_default(exp) unless exp.superclass == [:const, :Struct]
36
- check_smells(exp[0])
32
+ inside_new_context(ModuleContext, exp) do
33
+ process_default(exp)
37
34
  end
38
- scope
39
35
  end
40
36
 
41
- def process_class(exp)
42
- process_module(exp)
43
- end
37
+ alias process_class process_module
44
38
 
45
39
  def process_defn(exp)
46
- handle_context(MethodContext, exp[0], exp)
40
+ inside_new_context(MethodContext, exp) do
41
+ count_statement_list(exp.body)
42
+ process_default(exp)
43
+ end
47
44
  end
48
45
 
49
46
  def process_defs(exp)
50
- handle_context(SingletonMethodContext, exp[0], exp)
47
+ inside_new_context(SingletonMethodContext, exp) do
48
+ count_statement_list(exp.body)
49
+ process_default(exp)
50
+ end
51
51
  end
52
52
 
53
- def process_args(exp) end
53
+ def process_args(_) end
54
54
 
55
- def process_zsuper(exp)
56
- @element.record_use_of_self
57
- end
55
+ #
56
+ # Recording of calls to methods and self
57
+ #
58
58
 
59
- def process_block(exp)
60
- @element.count_statements(CodeParser.count_statements(exp[1..-1]))
59
+ def process_call(exp)
60
+ @element.record_call_to(exp)
61
61
  process_default(exp)
62
62
  end
63
63
 
64
- def process_call(exp)
65
- @element.record_call_to(exp)
64
+ alias process_attrasgn process_call
65
+ alias process_op_asgn1 process_call
66
+
67
+ def process_ivar(exp)
68
+ @element.record_use_of_self
66
69
  process_default(exp)
67
70
  end
68
71
 
69
- def process_attrasgn(exp)
70
- process_call(exp)
72
+ alias process_iasgn process_ivar
73
+
74
+ def process_self(_)
75
+ @element.record_use_of_self
76
+ end
77
+
78
+ alias process_zsuper process_self
79
+
80
+ #
81
+ # Statement counting
82
+ #
83
+
84
+ def process_iter(exp)
85
+ count_clause(exp[3])
86
+ process_default(exp)
71
87
  end
72
88
 
73
- def process_op_asgn1(exp)
74
- process_call(exp)
89
+ def process_block(exp)
90
+ count_statement_list(exp[1..-1])
91
+ @element.count_statements(-1)
92
+ process_default(exp)
75
93
  end
76
94
 
77
95
  def process_if(exp)
78
96
  count_clause(exp[2])
79
97
  count_clause(exp[3])
80
- process_default(exp)
81
98
  @element.count_statements(-1)
99
+ process_default(exp)
82
100
  end
83
101
 
84
102
  def process_while(exp)
85
- process_until(exp)
86
- end
87
-
88
- def process_until(exp)
89
103
  count_clause(exp[2])
90
- process_case(exp)
104
+ @element.count_statements(-1)
105
+ process_default(exp)
91
106
  end
92
107
 
108
+ alias process_until process_while
109
+
93
110
  def process_for(exp)
94
111
  count_clause(exp[3])
95
- process_case(exp)
112
+ @element.count_statements(-1)
113
+ process_default(exp)
96
114
  end
97
115
 
98
116
  def process_rescue(exp)
99
117
  count_clause(exp[1])
100
- process_case(exp)
118
+ @element.count_statements(-1)
119
+ process_default(exp)
101
120
  end
102
121
 
103
122
  def process_resbody(exp)
104
- process_when(exp)
123
+ count_statement_list(exp[2..-1].compact)
124
+ process_default(exp)
105
125
  end
106
126
 
107
127
  def process_case(exp)
108
- process_default(exp)
128
+ count_statement_list(exp[2..-1].compact)
109
129
  @element.count_statements(-1)
110
- end
111
-
112
- def process_when(exp)
113
- @element.count_statements(CodeParser.count_statements(exp[2..-1].compact))
114
130
  process_default(exp)
115
131
  end
116
132
 
117
- def process_ivar(exp)
118
- process_iasgn(exp)
119
- end
120
-
121
- def process_iasgn(exp)
122
- @element.record_use_of_self
133
+ def process_when(exp)
134
+ count_statement_list(exp[2..-1].compact)
135
+ @element.count_statements(-1)
123
136
  process_default(exp)
124
137
  end
125
138
 
126
- def process_self(exp)
127
- @element.record_use_of_self
128
- end
139
+ private
129
140
 
130
141
  def count_clause(sexp)
131
- if sexp and !sexp.has_type?(:block)
132
- @element.count_statements(1)
133
- end
142
+ @element.count_statements(1) if sexp
134
143
  end
135
144
 
136
- def self.count_statements(stmts)
137
- ignore = 0
138
- ignore += 1 if stmts[1] == s(:nil)
139
- stmts.length - ignore
145
+ def count_statement_list(statement_list)
146
+ @element.count_statements statement_list.length
140
147
  end
141
148
 
142
- private
143
-
144
- def handle_context(klass, type, exp)
149
+ def inside_new_context(klass, exp)
145
150
  scope = klass.new(@element, exp)
146
151
  push(scope) do
147
- @element.count_statements(CodeParser.count_statements(exp.body))
148
- process_default(exp)
149
- check_smells(type)
152
+ yield
153
+ check_smells(exp[0])
150
154
  end
151
155
  scope
152
156
  end
@@ -155,9 +159,9 @@ module Reek
155
159
  @sniffer.examine(@element, type)
156
160
  end
157
161
 
158
- def push(context)
162
+ def push(scope)
159
163
  orig = @element
160
- @element = context
164
+ @element = scope
161
165
  yield
162
166
  @element = orig
163
167
  end
@@ -57,6 +57,10 @@ module Reek
57
57
  return [] if @refs.self_is_max?
58
58
  @refs.max_keys
59
59
  end
60
+
61
+ def uses_param?(param)
62
+ local_nodes(:lvar).include?(Sexp.new(:lvar, param.to_sym))
63
+ end
60
64
  end
61
65
  end
62
66
  end
@@ -11,9 +11,9 @@ module Reek
11
11
  #
12
12
  class ModuleContext < CodeContext
13
13
 
14
- def initialize(outer, name, exp)
14
+ def initialize(outer, exp)
15
15
  super(outer, exp)
16
- @name = name
16
+ @name = Source::SexpFormatter.format(exp[1])
17
17
  end
18
18
  end
19
19
  end
@@ -2,6 +2,9 @@ require 'reek/smells'
2
2
 
3
3
  module Reek
4
4
  module Core
5
+ #
6
+ # Contains all the existing smells and exposes operations on them.
7
+ #
5
8
  class SmellRepository
6
9
 
7
10
  def self.smell_classes
@@ -2,7 +2,6 @@ require 'reek/core/code_parser'
2
2
  require 'reek/core/smell_repository'
3
3
  require 'reek/source/config_file'
4
4
  require 'yaml'
5
- require 'reek/core/hash_extensions'
6
5
 
7
6
  module Reek
8
7
  module Core
@@ -14,7 +14,7 @@ module Reek
14
14
  nil
15
15
  end
16
16
 
17
- def config
17
+ def config_for(_)
18
18
  {}
19
19
  end
20
20
 
@@ -30,7 +30,7 @@ module Reek
30
30
  end
31
31
 
32
32
  def self.default_config
33
- super.adopt(Core::SmellConfiguration::ENABLED_KEY => false)
33
+ super.merge(Core::SmellConfiguration::ENABLED_KEY => false)
34
34
  end
35
35
 
36
36
  #
@@ -8,10 +8,10 @@ module Reek
8
8
  # Control Coupling occurs when a method or block checks the value of
9
9
  # a parameter in order to decide which execution path to take. The
10
10
  # offending parameter is often called a Control Couple.
11
- #
11
+ #
12
12
  # A simple example would be the <tt>quoted</tt> parameter
13
13
  # in the following method:
14
- #
14
+ #
15
15
  # def write(quoted)
16
16
  # if quoted
17
17
  # write_quoted(@value)
@@ -19,15 +19,15 @@ module Reek
19
19
  # puts @value
20
20
  # end
21
21
  # end
22
- #
22
+ #
23
23
  # Control Coupling is a kind of duplication, because the calling method
24
24
  # already knows which path should be taken.
25
- #
25
+ #
26
26
  # Control Coupling reduces the code's flexibility by creating a
27
27
  # dependency between the caller and callee:
28
28
  # any change to the possible values of the controlling parameter must
29
29
  # be reflected on both sides of the call.
30
- #
30
+ #
31
31
  # A Control Couple also reveals a loss of simplicity: the called
32
32
  # method probably has more than one responsibility,
33
33
  # because it includes at least two different code paths.
@@ -55,60 +55,94 @@ module Reek
55
55
  # @return [Array<SmellWarning>]
56
56
  #
57
57
  def examine_context(ctx)
58
- control_parameters(ctx).map do |lvars, occurs|
59
- param = lvars.format_ruby
60
- lines = occurs.map {|exp| exp.line}
61
- smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, lines,
62
- "is controlled by argument #{param}",
63
- @source, SMELL_SUBCLASS,
64
- {PARAMETER_KEY => param})
65
- smell
58
+ ControlParameterCollector.new(ctx).control_parameters.map do |control_parameter|
59
+ SmellWarning.new(SMELL_CLASS, ctx.full_name, control_parameter.lines,
60
+ control_parameter.smell_message,
61
+ @source, SMELL_SUBCLASS,
62
+ {PARAMETER_KEY => control_parameter.name})
66
63
  end
67
64
  end
68
65
 
69
- private
66
+ #
67
+ # Collects information about a single control parameter.
68
+ #
69
+ class FoundControlParameter
70
+ def initialize(param)
71
+ @param = param
72
+ @occurences = []
73
+ end
70
74
 
71
- def control_parameters(method_ctx)
72
- result = Hash.new {|hash, key| hash[key] = []}
73
- method_ctx.exp.parameter_names.each do |param|
74
- next if used_outside_conditional?(method_ctx, param)
75
- find_matchs(method_ctx, param).each {|match| result[match].push(match)}
75
+ def record(occurences)
76
+ @occurences.concat occurences
77
+ end
78
+
79
+ def smell_message
80
+ "is controlled by argument #{name}"
81
+ end
82
+
83
+ def lines
84
+ @occurences.map(&:line)
76
85
  end
77
- result
78
- end
79
86
 
80
- # Returns wether the parameter is used outside of the conditional statement.
81
- def used_outside_conditional?(method_ctx, param)
82
- method_ctx.exp.each_node(:lvar, [:if, :case, :and, :or, :args]) do |node|
83
- return true if node.value == param
87
+ def name
88
+ @param.to_s
84
89
  end
85
- false
86
90
  end
87
91
 
88
- # Find the use of the param that match the definition of a control parameter.
89
- def find_matchs(method_ctx, param)
90
- matchs = []
91
- [:if, :case, :and, :or].each do |keyword|
92
- method_ctx.local_nodes(keyword).each do |node|
93
- return [] if used_besides_in_condition?(node, param)
94
- node.each_node(:lvar, []) {|inner| matchs.push(inner) if inner.value == param}
92
+ #
93
+ # Collects all control parameters in a given context.
94
+ #
95
+ class ControlParameterCollector
96
+ def initialize(context)
97
+ @context = context
98
+ end
99
+
100
+ def control_parameters
101
+ result = Hash.new {|hash, key| hash[key] = FoundControlParameter.new(key)}
102
+ potential_parameters.each do |param|
103
+ matches = find_matches(param)
104
+ result[param].record(matches) if matches.any?
95
105
  end
106
+ result.values
107
+ end
108
+
109
+ private
110
+
111
+ # Returns parameters that aren't used outside of a conditional statements and that
112
+ # could be good candidates for being a control parameter.
113
+ def potential_parameters
114
+ @context.exp.parameter_names.select {|param| !used_outside_conditional?(param)}
115
+ end
116
+
117
+ # Returns wether the parameter is used outside of the conditional statement.
118
+ def used_outside_conditional?(param)
119
+ nodes = @context.exp.each_node(:lvar, [:if, :case, :and, :or, :args])
120
+ nodes.any? {|node| node.value == param}
121
+ end
122
+
123
+ # Find the use of the param that match the definition of a control parameter.
124
+ def find_matches(param)
125
+ matches = []
126
+ [:if, :case, :and, :or].each do |keyword|
127
+ @context.local_nodes(keyword).each do |node|
128
+ return [] if used_besides_in_condition?(node, param)
129
+ node.each_node(:lvar, []) {|inner| matches.push(inner) if inner.value == param}
130
+ end
131
+ end
132
+ matches
96
133
  end
97
- matchs
98
- end
99
134
 
100
- # Returns wether the parameter is used somewhere besides in the condition of the
101
- # conditional statement.
102
- def used_besides_in_condition?(node, param)
103
- times_in_conditional, times_total = 0, 0
104
- node.each_node(:lvar, [:if, :case]) {|inner| times_total +=1 if inner[VALUE_POSITION] == param}
105
- if node.condition
106
- times_in_conditional += 1 if node.condition[VALUE_POSITION] == param
107
- node.condition.each do |inner|
108
- times_in_conditional += 1 if inner.class == Sexp && inner[VALUE_POSITION] == param
135
+ # Returns wether the parameter is used somewhere besides in the condition of the
136
+ # conditional statement.
137
+ def used_besides_in_condition?(node, param)
138
+ times_in_conditional, times_total = 0, 0
139
+ node.each_node(:lvar, [:if, :case]) {|lvar| times_total +=1 if lvar.value == param}
140
+ if node.condition
141
+ times_in_conditional += 1 if node.condition[VALUE_POSITION] == param
142
+ times_in_conditional += node.condition.count {|inner| inner.class == Sexp && inner[VALUE_POSITION] == param}
109
143
  end
144
+ return times_total > times_in_conditional
110
145
  end
111
- return times_total > times_in_conditional
112
146
  end
113
147
  end
114
148
  end