reek 1.3.6 → 1.3.7

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.
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