reek 1.2.7.2 → 1.2.7.3

Sign up to get free protection for your applications and to get access to all the features.
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