reek 1.2.7.2 → 1.2.7.3

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 (67) hide show
  1. data/History.txt +9 -1
  2. data/config/defaults.reek +4 -4
  3. data/features/masking_smells.feature +14 -57
  4. data/features/options.feature +1 -2
  5. data/features/rake_task.feature +6 -6
  6. data/features/reports.feature +8 -38
  7. data/features/samples.feature +181 -181
  8. data/features/stdin.feature +3 -3
  9. data/lib/reek.rb +1 -1
  10. data/lib/reek/cli/command_line.rb +2 -7
  11. data/lib/reek/cli/reek_command.rb +6 -6
  12. data/lib/reek/cli/report.rb +27 -49
  13. data/lib/reek/core/code_parser.rb +6 -15
  14. data/lib/reek/core/method_context.rb +1 -1
  15. data/lib/reek/core/module_context.rb +0 -18
  16. data/lib/reek/core/smell_configuration.rb +0 -4
  17. data/lib/reek/core/sniffer.rb +11 -19
  18. data/lib/reek/core/warning_collector.rb +27 -0
  19. data/lib/reek/examiner.rb +43 -35
  20. data/lib/reek/rake/task.rb +2 -0
  21. data/lib/reek/smell_warning.rb +10 -25
  22. data/lib/reek/smells/boolean_parameter.rb +1 -1
  23. data/lib/reek/smells/control_couple.rb +4 -1
  24. data/lib/reek/smells/data_clump.rb +40 -33
  25. data/lib/reek/smells/feature_envy.rb +1 -1
  26. data/lib/reek/smells/long_parameter_list.rb +4 -1
  27. data/lib/reek/smells/long_yield_list.rb +6 -3
  28. data/lib/reek/smells/simulated_polymorphism.rb +1 -1
  29. data/lib/reek/smells/smell_detector.rb +4 -32
  30. data/lib/reek/smells/uncommunicative_method_name.rb +1 -1
  31. data/lib/reek/smells/uncommunicative_module_name.rb +1 -1
  32. data/lib/reek/smells/uncommunicative_parameter_name.rb +2 -2
  33. data/lib/reek/smells/uncommunicative_variable_name.rb +11 -18
  34. data/lib/reek/smells/utility_function.rb +7 -4
  35. data/lib/reek/source/reference_collector.rb +9 -2
  36. data/lib/reek/source/source_locator.rb +6 -0
  37. data/lib/reek/spec/should_reek.rb +3 -6
  38. data/lib/reek/spec/should_reek_only_of.rb +4 -3
  39. data/reek.gemspec +4 -4
  40. data/spec/reek/cli/reek_command_spec.rb +3 -4
  41. data/spec/reek/cli/report_spec.rb +10 -6
  42. data/spec/reek/cli/yaml_command_spec.rb +1 -1
  43. data/spec/reek/core/code_context_spec.rb +1 -3
  44. data/spec/reek/core/module_context_spec.rb +1 -1
  45. data/spec/reek/core/warning_collector_spec.rb +27 -0
  46. data/spec/reek/examiner_spec.rb +80 -19
  47. data/spec/reek/smell_warning_spec.rb +4 -61
  48. data/spec/reek/smells/attribute_spec.rb +4 -7
  49. data/spec/reek/smells/behaves_like_variable_detector.rb +2 -2
  50. data/spec/reek/smells/class_variable_spec.rb +0 -1
  51. data/spec/reek/smells/control_couple_spec.rb +8 -15
  52. data/spec/reek/smells/data_clump_spec.rb +85 -1
  53. data/spec/reek/smells/duplication_spec.rb +7 -8
  54. data/spec/reek/smells/feature_envy_spec.rb +2 -32
  55. data/spec/reek/smells/long_parameter_list_spec.rb +9 -16
  56. data/spec/reek/smells/long_yield_list_spec.rb +8 -15
  57. data/spec/reek/smells/smell_detector_shared.rb +12 -0
  58. data/spec/reek/smells/uncommunicative_variable_name_spec.rb +9 -10
  59. data/spec/reek/smells/utility_function_spec.rb +11 -15
  60. data/spec/reek/spec/should_reek_only_of_spec.rb +6 -6
  61. data/spec/reek/spec/should_reek_spec.rb +3 -3
  62. metadata +36 -22
  63. data/lib/reek/core/class_context.rb +0 -22
  64. data/lib/reek/core/detector_stack.rb +0 -33
  65. data/lib/reek/core/masking_collection.rb +0 -52
  66. data/spec/reek/core/class_context_spec.rb +0 -53
  67. data/spec/reek/core/masking_collection_spec.rb +0 -235
@@ -14,6 +14,8 @@ module Reek
14
14
  #
15
15
  # Example:
16
16
  #
17
+ # require 'reek/rake/task'
18
+ #
17
19
  # Reek::Rake::Task.new do |t|
18
20
  # t.fail_on_error = false
19
21
  # end
@@ -18,7 +18,7 @@ module Reek
18
18
 
19
19
  ACTIVE_KEY = 'is_active'
20
20
 
21
- def initialize(class_name, context, lines, message, masked,
21
+ def initialize(class_name, context, lines, message,
22
22
  source = '', subclass_name = '', parameters = {})
23
23
  @smell = {
24
24
  CLASS_KEY => class_name,
@@ -27,10 +27,10 @@ module Reek
27
27
  }
28
28
  @smell.merge!(parameters)
29
29
  @status = {
30
- ACTIVE_KEY => !masked
30
+ ACTIVE_KEY => true
31
31
  }
32
32
  @location = {
33
- CONTEXT_KEY => context,
33
+ CONTEXT_KEY => context.to_s,
34
34
  LINES_KEY => lines,
35
35
  SOURCE_KEY => source
36
36
  }
@@ -69,6 +69,8 @@ module Reek
69
69
  #
70
70
  attr_reader :status
71
71
 
72
+ def is_active() @status[ACTIVE_KEY] end
73
+
72
74
  def hash # :nodoc:
73
75
  sort_key.hash
74
76
  end
@@ -90,31 +92,14 @@ module Reek
90
92
  @smell.values.include?(klass.to_s) and contains_all?(patterns)
91
93
  end
92
94
 
93
- def sort_key
94
- [@location[CONTEXT_KEY], @smell[MESSAGE_KEY], smell_name]
95
- end
96
-
97
- protected :sort_key
98
-
99
- def report(format)
100
- format.gsub(/\%s/, smell_name).
101
- gsub(/\%c/, @location[CONTEXT_KEY]).
102
- gsub(/\%w/, @smell[MESSAGE_KEY]).
103
- gsub(/\%m/, @status[ACTIVE_KEY] ? '' : '(masked) ')
104
- end
105
-
106
- def report_on(report)
107
- if @status[ACTIVE_KEY]
108
- report.found_smell(self)
109
- else
110
- report.found_masked_smell(self)
111
- end
95
+ def report_on(listener)
96
+ listener.found_smell(self)
112
97
  end
113
98
 
114
- private
99
+ protected
115
100
 
116
- def smell_name
117
- @smell[CLASS_KEY].gsub(/([a-z])([A-Z])/) { |sub| "#{$1} #{$2}"}.split.join(' ')
101
+ def sort_key
102
+ [@location[CONTEXT_KEY], @smell[MESSAGE_KEY], @smell[CLASS_KEY]]
118
103
  end
119
104
  end
120
105
  end
@@ -22,7 +22,7 @@ module Reek
22
22
  method_ctx.parameters.default_assignments.each do |param, value|
23
23
  next unless [:true, :false].include?(value[0])
24
24
  smell = SmellWarning.new('ControlCouple', method_ctx.full_name, [method_ctx.exp.line],
25
- "has boolean parameter '#{param.to_s}'", @masked,
25
+ "has boolean parameter '#{param.to_s}'",
26
26
  @source, 'BooleanParameter', {'parameter' => param.to_s})
27
27
  @smells_found << smell
28
28
  #SMELL: serious duplication
@@ -43,6 +43,9 @@ module Reek
43
43
  #
44
44
  class ControlCouple < SmellDetector
45
45
 
46
+ SMELL_CLASS = self.name.split(/::/)[-1]
47
+ SMELL_SUBCLASS = 'ControlParameter'
48
+
46
49
  #
47
50
  # Checks whether the given method chooses its execution path
48
51
  # by testing the value of one of its parameters.
@@ -53,7 +56,7 @@ module Reek
53
56
  param = cond.format
54
57
  lines = occurs.map {|exp| exp.line}
55
58
  found(method_ctx, "is controlled by argument #{param}",
56
- 'ControlParameter', {'parameter' => param}, lines)
59
+ SMELL_SUBCLASS, {'parameter' => param}, lines)
57
60
  end
58
61
  end
59
62
 
@@ -6,23 +6,6 @@ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'sourc
6
6
  # Extensions to +Array+ needed by Reek.
7
7
  #
8
8
  class Array
9
- def power_set
10
- self.inject([[]]) { |cum, element| cum.cross(element) }
11
- end
12
-
13
- def bounded_power_set(lower_bound)
14
- power_set.select {|ps| ps.length > lower_bound}
15
- end
16
-
17
- def cross(element)
18
- result = []
19
- self.each do |set|
20
- result << set
21
- result << (set + [element])
22
- end
23
- result
24
- end
25
-
26
9
  def intersection
27
10
  self.inject { |res, elem| elem & res }
28
11
  end
@@ -85,7 +68,7 @@ module Reek
85
68
  MethodGroup.new(ctx, min_clump_size, max_copies).clumps.each do |clump, methods|
86
69
  smell = SmellWarning.new('DataClump', ctx.full_name,
87
70
  methods.map {|meth| meth.line},
88
- "takes parameters #{DataClump.print_clump(clump)} to #{methods.length} methods", @masked,
71
+ "takes parameters #{DataClump.print_clump(clump)} to #{methods.length} methods",
89
72
  @source, 'DataClump', {
90
73
  PARAMETERS_KEY => clump.map {|name| name.to_s},
91
74
  OCCURRENCES_KEY => methods.length,
@@ -105,10 +88,10 @@ module Reek
105
88
 
106
89
  # Represents a group of methods
107
90
  # @private
108
- class MethodGroup # :nodoc:
91
+ class MethodGroup
109
92
 
110
93
  def self.intersection_of_parameters_of(methods)
111
- methods.map {|meth| meth.arg_names.sort {|a,b| a.to_s <=> b.to_s}}.intersection
94
+ methods.map {|meth| meth.arg_names}.intersection
112
95
  end
113
96
 
114
97
  def initialize(ctx, min_clump_size, max_copies)
@@ -117,37 +100,61 @@ module Reek
117
100
  @candidate_methods = ctx.local_nodes(:defn).select do |meth|
118
101
  meth.arg_names.length >= @min_clump_size
119
102
  end.map {|defn_node| CandidateMethod.new(defn_node)}
120
- prune_candidates
103
+ delete_infrequent_parameters
104
+ delete_small_methods
121
105
  end
122
106
 
123
- def clumps
124
- results = Hash.new([])
125
- @candidate_methods.bounded_power_set(@max_copies).each do |methods|
126
- clump = MethodGroup.intersection_of_parameters_of(methods)
107
+ def clumps_containing(method, methods, results)
108
+ methods.each do |other_method|
109
+ clump = [method.arg_names, other_method.arg_names].intersection
127
110
  if clump.length >= @min_clump_size
128
- results[clump] = methods if methods.length > results[clump].length
111
+ others = methods.select do |other| # BUG: early ones have already been eliminated
112
+ clump - other.arg_names == []
113
+ end
114
+ results[clump] += [method] + others
129
115
  end
130
116
  end
117
+ end
118
+
119
+ def collect_clumps_in(methods, results)
120
+ return if methods.length <= @max_copies
121
+ tail = methods[1..-1]
122
+ clumps_containing(methods[0], tail, results)
123
+ collect_clumps_in(tail, results)
124
+ end
125
+
126
+ def clumps
127
+ results = Hash.new([])
128
+ collect_clumps_in(@candidate_methods, results)
129
+ results.each_key do |key|
130
+ results[key].uniq!
131
+ end
131
132
  results
132
133
  end
133
134
 
134
- def prune_candidates
135
+ def delete_small_methods
136
+ @candidate_methods = @candidate_methods.select do |meth|
137
+ meth.arg_names.length >= @min_clump_size
138
+ end
139
+ end
140
+
141
+ def delete_infrequent_parameters
135
142
  @candidate_methods.each do |meth|
136
143
  meth.arg_names.each do |param|
137
- count = @candidate_methods.select {|cm| cm.arg_names.include?(param)}.length
138
- meth.delete(param) if count <= @max_copies
144
+ occurs = @candidate_methods.inject(0) {|sum, cm| cm.arg_names.include?(param) ? sum+1 : sum}
145
+ meth.delete(param) if occurs <= @max_copies
139
146
  end
140
147
  end
141
- @candidate_methods = @candidate_methods.select do |meth|
142
- meth.arg_names.length >= @min_clump_size
143
- end
144
148
  end
145
149
  end
146
150
 
151
+ #
152
+ # A method definition and a copy of its parameters
153
+ #
147
154
  class CandidateMethod
148
155
  def initialize(defn_node)
149
156
  @defn = defn_node
150
- @params = defn_node.arg_names.clone
157
+ @params = defn_node.arg_names.clone.sort {|a,b| a.to_s <=> b.to_s}
151
158
  end
152
159
 
153
160
  def arg_names
@@ -49,7 +49,7 @@ module Reek
49
49
  method_ctx.envious_receivers.each do |ref, occurs|
50
50
  target = ref.format
51
51
  smell = SmellWarning.new(SMELL_CLASS, method_ctx.full_name, [method_ctx.exp.line],
52
- "refers to #{target} more than self", @masked,
52
+ "refers to #{target} more than self",
53
53
  @source, SMELL_SUBCLASS, {RECEIVER_KEY => target, REFERENCES_KEY => occurs})
54
54
  @smells_found << smell
55
55
  #SMELL: serious duplication
@@ -15,6 +15,9 @@ module Reek
15
15
  #
16
16
  class LongParameterList < SmellDetector
17
17
 
18
+ SMELL_CLASS = self.name.split(/::/)[-1]
19
+ SMELL_SUBCLASS = 'LongParameterList'
20
+
18
21
  # The name of the config field that sets the maximum number of
19
22
  # parameters permitted in any method or block.
20
23
  MAX_ALLOWED_PARAMS_KEY = 'max_params'
@@ -44,7 +47,7 @@ module Reek
44
47
  num_params = method_ctx.parameters.length
45
48
  return false if num_params <= value(MAX_ALLOWED_PARAMS_KEY, method_ctx, DEFAULT_MAX_ALLOWED_PARAMS)
46
49
  found(method_ctx, "has #{num_params} parameters",
47
- 'LongParameterList', {'parameter_count' => num_params})
50
+ SMELL_SUBCLASS, {'parameter_count' => num_params})
48
51
  end
49
52
  end
50
53
  end
@@ -10,6 +10,9 @@ module Reek
10
10
  #
11
11
  class LongYieldList < SmellDetector
12
12
 
13
+ SMELL_SUBCLASS = self.name.split(/::/)[-1]
14
+ SMELL_CLASS = 'LongParameterList'
15
+
13
16
  # The name of the config field that sets the maximum number of
14
17
  # parameters permitted in any method or block.
15
18
  MAX_ALLOWED_PARAMS_KEY = 'max_params'
@@ -36,9 +39,9 @@ module Reek
36
39
  method_ctx.local_nodes(:yield).each do |yield_node|
37
40
  num_params = yield_node.args.length
38
41
  next if num_params <= value(MAX_ALLOWED_PARAMS_KEY, method_ctx, DEFAULT_MAX_ALLOWED_PARAMS)
39
- smell = SmellWarning.new('LongParameterList', method_ctx.full_name, [yield_node.line],
40
- "yields #{num_params} parameters", @masked,
41
- @source, 'LongYieldList', {'parameter_count' => num_params})
42
+ smell = SmellWarning.new(SMELL_CLASS, method_ctx.full_name, [yield_node.line],
43
+ "yields #{num_params} parameters",
44
+ @source, SMELL_SUBCLASS, {'parameter_count' => num_params})
42
45
  @smells_found << smell
43
46
  #SMELL: serious duplication
44
47
  end
@@ -42,7 +42,7 @@ module Reek
42
42
  end
43
43
 
44
44
  #
45
- # Checks the given ClassContext for multiple identical conditional tests.
45
+ # Checks the given class for multiple identical conditional tests.
46
46
  # Remembers any smells found.
47
47
  #
48
48
  def examine_context(klass)
@@ -47,10 +47,10 @@ module Reek
47
47
  @source = source
48
48
  @config = Core::SmellConfiguration.new(config)
49
49
  @smells_found = Set.new
50
- @masked = false
51
50
  end
52
51
 
53
- def listen_to(hooks)
52
+ def register(hooks)
53
+ return unless @config.enabled?
54
54
  self.class.contexts.each { |ctx| hooks[ctx] << self }
55
55
  end
56
56
 
@@ -63,17 +63,6 @@ module Reek
63
63
  @config.adopt!(config)
64
64
  end
65
65
 
66
- def copy
67
- self.class.new(@source, @config.deep_copy)
68
- end
69
-
70
- def supersede_with(config)
71
- clone = self.copy
72
- @masked = true
73
- clone.configure_with(config)
74
- clone
75
- end
76
-
77
66
  def examine(context)
78
67
  examine_context(context) if @config.enabled? and !exception?(context)
79
68
  end
@@ -87,34 +76,17 @@ module Reek
87
76
 
88
77
  def found(context, message, subclass = '', parameters = {}, lines = nil)
89
78
  lines ||= [context.exp.line] # SMELL: nil?!?!?! Yuk
90
- smell = SmellWarning.new(self.class.name.split(/::/)[-1], context.full_name, lines, message, @masked,
79
+ smell = SmellWarning.new(self.class.name.split(/::/)[-1], context.full_name,
80
+ lines, message,
91
81
  @source, subclass, parameters)
92
82
  @smells_found << smell
93
83
  smell
94
84
  end
95
85
 
96
- def has_smell?(patterns)
97
- return false if @masked
98
- @smells_found.each { |warning| return true if warning.contains_all?(patterns) }
99
- false
100
- end
101
-
102
- def smell_type
103
- self.class.name.split(/::/)[-1]
104
- end
105
-
106
86
  def report_on(report)
107
87
  @smells_found.each { |smell| smell.report_on(report) }
108
88
  end
109
89
 
110
- def num_smells
111
- @masked ? 0 : @smells_found.length
112
- end
113
-
114
- def smelly?
115
- (not @masked) and (@smells_found.length > 0)
116
- end
117
-
118
90
  def value(key, ctx, fall_back)
119
91
  @config.value(key, ctx, fall_back)
120
92
  end
@@ -60,7 +60,7 @@ module Reek
60
60
  return false if accept?(method_ctx)
61
61
  return false unless is_bad_name?(name, method_ctx)
62
62
  smell = SmellWarning.new('UncommunicativeName', method_ctx.full_name, [method_ctx.exp.line],
63
- "has the name '#{name}'", @masked,
63
+ "has the name '#{name}'",
64
64
  @source, 'UncommunicativeMethodName', {METHOD_NAME_KEY => name.to_s})
65
65
  @smells_found << smell
66
66
  #SMELL: serious duplication
@@ -56,7 +56,7 @@ module Reek
56
56
  return false if accept?(module_ctx)
57
57
  return false unless is_bad_name?(name, module_ctx)
58
58
  smell = SmellWarning.new('UncommunicativeName', module_ctx.full_name, [module_ctx.exp.line],
59
- "has the name '#{name}'", @masked,
59
+ "has the name '#{name}'",
60
60
  @source, 'UncommunicativeModuleName', {'module_name' => name.to_s})
61
61
  @smells_found << smell
62
62
  #SMELL: serious duplication
@@ -30,7 +30,7 @@ module Reek
30
30
  # uncommunicative.
31
31
  ACCEPT_KEY = 'accept'
32
32
 
33
- DEFAULT_ACCEPT_SET = ['Inline::C']
33
+ DEFAULT_ACCEPT_SET = []
34
34
 
35
35
  def self.default_config
36
36
  super.adopt(
@@ -55,7 +55,7 @@ module Reek
55
55
  context.exp.parameter_names.each do |name|
56
56
  next unless is_bad_name?(name, context)
57
57
  smell = SmellWarning.new('UncommunicativeName', context.full_name, [context.exp.line],
58
- "has the parameter name '#{name}'", @masked,
58
+ "has the parameter name '#{name}'",
59
59
  @source, 'UncommunicativeParameterName', {'parameter_name' => name.to_s})
60
60
  @smells_found << smell
61
61
  #SMELL: serious duplication
@@ -30,7 +30,7 @@ module Reek
30
30
  # uncommunicative.
31
31
  ACCEPT_KEY = 'accept'
32
32
 
33
- DEFAULT_ACCEPT_SET = ['Inline::C']
33
+ DEFAULT_ACCEPT_SET = []
34
34
 
35
35
  def self.default_config
36
36
  super.adopt(
@@ -52,10 +52,10 @@ module Reek
52
52
  # Remembers any smells found.
53
53
  #
54
54
  def examine_context(context)
55
- variable_names(context).each do |name, lines|
55
+ variable_names(context.exp).each do |name, lines|
56
56
  next unless is_bad_name?(name, context)
57
57
  smell = SmellWarning.new('UncommunicativeName', context.full_name, lines,
58
- "has the variable name '#{name}'", @masked,
58
+ "has the variable name '#{name}'",
59
59
  @source, 'UncommunicativeVariableName', {'variable_name' => name.to_s})
60
60
  @smells_found << smell
61
61
  #SMELL: serious duplication
@@ -64,25 +64,18 @@ module Reek
64
64
 
65
65
  def is_bad_name?(name, context) # :nodoc:
66
66
  var = name.to_s.gsub(/^[@\*\&]*/, '')
67
- return false if var == '*' or value(ACCEPT_KEY, context, DEFAULT_ACCEPT_SET).include?(var)
67
+ return false if value(ACCEPT_KEY, context, DEFAULT_ACCEPT_SET).include?(var)
68
68
  value(REJECT_KEY, context, DEFAULT_REJECT_SET).detect {|patt| patt === var}
69
69
  end
70
70
 
71
- def variable_names(context)
72
- result = Hash.new {|hash,key| hash[key] = []}
73
- case context
74
- when Core::MethodContext
75
- context.local_nodes(:lasgn).each do |asgn|
76
- result[asgn[1]].push(asgn.line)
77
- end
78
- else
79
- context.local_nodes(:iasgn).each do |asgn|
80
- result[asgn[1]].push(asgn.line)
81
- end
82
- context.each_node(:lasgn, [:class, :module, :defs, :defn]).each do |asgn|
83
- result[asgn[1]].push(asgn.line)
84
- end
71
+ def variable_names(exp)
72
+ assignment_nodes = exp.each_node(:lasgn, [:class, :module, :defs, :defn])
73
+ case exp.first
74
+ when :class, :module
75
+ assignment_nodes += exp.each_node(:iasgn, [:class, :module])
85
76
  end
77
+ result = Hash.new {|hash,key| hash[key] = []}
78
+ assignment_nodes.each {|asgn| result[asgn[1]].push(asgn.line) }
86
79
  result
87
80
  end
88
81
  end