reek 1.2.7.3 → 1.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/History.txt +17 -0
  2. data/README.md +32 -48
  3. data/config/defaults.reek +7 -1
  4. data/features/api.feature +20 -0
  5. data/features/masking_smells.feature +41 -0
  6. data/features/options.feature +4 -0
  7. data/features/rake_task.feature +14 -0
  8. data/features/yaml.feature +8 -8
  9. data/lib/reek.rb +1 -1
  10. data/lib/reek/cli/command_line.rb +9 -2
  11. data/lib/reek/cli/reek_command.rb +5 -4
  12. data/lib/reek/cli/yaml_command.rb +2 -2
  13. data/lib/reek/core/code_context.rb +10 -1
  14. data/lib/reek/core/method_context.rb +2 -2
  15. data/lib/reek/core/sniffer.rb +3 -1
  16. data/lib/reek/core/stop_context.rb +4 -0
  17. data/lib/reek/examiner.rb +2 -2
  18. data/lib/reek/rake/task.rb +16 -0
  19. data/lib/reek/smell_warning.rb +3 -2
  20. data/lib/reek/smells/attribute.rb +13 -9
  21. data/lib/reek/smells/boolean_parameter.rb +14 -9
  22. data/lib/reek/smells/class_variable.rb +16 -5
  23. data/lib/reek/smells/control_couple.rb +11 -6
  24. data/lib/reek/smells/data_clump.rb +33 -30
  25. data/lib/reek/smells/duplication.rb +39 -8
  26. data/lib/reek/smells/feature_envy.rb +7 -8
  27. data/lib/reek/smells/irresponsible_module.rb +12 -3
  28. data/lib/reek/smells/large_class.rb +31 -15
  29. data/lib/reek/smells/long_method.rb +15 -5
  30. data/lib/reek/smells/long_parameter_list.rb +14 -7
  31. data/lib/reek/smells/long_yield_list.rb +12 -9
  32. data/lib/reek/smells/nested_iterators.rb +46 -11
  33. data/lib/reek/smells/simulated_polymorphism.rb +16 -8
  34. data/lib/reek/smells/smell_detector.rb +13 -13
  35. data/lib/reek/smells/uncommunicative_method_name.rb +12 -20
  36. data/lib/reek/smells/uncommunicative_module_name.rb +17 -19
  37. data/lib/reek/smells/uncommunicative_parameter_name.rb +22 -15
  38. data/lib/reek/smells/uncommunicative_variable_name.rb +24 -18
  39. data/lib/reek/smells/utility_function.rb +6 -6
  40. data/lib/reek/source/code_comment.rb +19 -1
  41. data/lib/reek/source/tree_dresser.rb +40 -22
  42. data/reek.gemspec +6 -4
  43. data/spec/matchers/smell_of_matcher.rb +58 -0
  44. data/spec/reek/core/code_context_spec.rb +4 -2
  45. data/spec/reek/core/code_parser_spec.rb +2 -1
  46. data/spec/reek/core/method_context_spec.rb +5 -5
  47. data/spec/reek/smells/attribute_spec.rb +2 -4
  48. data/spec/reek/smells/boolean_parameter_spec.rb +32 -42
  49. data/spec/reek/smells/class_variable_spec.rb +22 -6
  50. data/spec/reek/smells/control_couple_spec.rb +15 -14
  51. data/spec/reek/smells/data_clump_spec.rb +29 -111
  52. data/spec/reek/smells/duplication_spec.rb +79 -49
  53. data/spec/reek/smells/feature_envy_spec.rb +1 -2
  54. data/spec/reek/smells/irresponsible_module_spec.rb +43 -22
  55. data/spec/reek/smells/large_class_spec.rb +34 -59
  56. data/spec/reek/smells/long_method_spec.rb +15 -10
  57. data/spec/reek/smells/long_parameter_list_spec.rb +24 -24
  58. data/spec/reek/smells/long_yield_list_spec.rb +13 -14
  59. data/spec/reek/smells/nested_iterators_spec.rb +93 -76
  60. data/spec/reek/smells/smell_detector_shared.rb +4 -2
  61. data/spec/reek/smells/uncommunicative_method_name_spec.rb +10 -27
  62. data/spec/reek/smells/uncommunicative_module_name_spec.rb +22 -23
  63. data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +36 -26
  64. data/spec/reek/smells/uncommunicative_variable_name_spec.rb +45 -48
  65. data/spec/reek/smells/utility_function_spec.rb +14 -13
  66. data/spec/reek/source/code_comment_spec.rb +61 -3
  67. data/spec/reek/source/tree_dresser_spec.rb +96 -1
  68. data/spec/samples/config/allow_duplication.reek +3 -0
  69. data/spec/samples/config/deeper_nested_iterators.reek +3 -0
  70. data/spec/samples/demo/demo.rb +8 -0
  71. data/spec/samples/inline_config/dirty.rb +16 -0
  72. data/spec/samples/inline_config/masked.reek +7 -0
  73. data/spec/samples/mask_some/dirty.rb +8 -0
  74. data/spec/samples/mask_some/some.reek +8 -0
  75. data/spec/spec_helper.rb +2 -0
  76. metadata +15 -5
@@ -27,7 +27,7 @@ module Reek
27
27
  # and if it is an Array, it is assumed to be a list of file paths,
28
28
  # each of which is opened and parsed for source code.
29
29
  #
30
- def initialize(source)
30
+ def initialize(source, config_files = [])
31
31
  sources = case source
32
32
  when Array
33
33
  @description = 'dir'
@@ -41,7 +41,7 @@ module Reek
41
41
  [src]
42
42
  end
43
43
  collector = Core::WarningCollector.new
44
- sources.each { |src| Core::Sniffer.new(src).report_on(collector) }
44
+ sources.each { |src| Core::Sniffer.new(src, config_files).report_on(collector) }
45
45
  @smells = collector.warnings
46
46
  end
47
47
 
@@ -7,6 +7,8 @@ module Reek
7
7
 
8
8
  #
9
9
  # Defines a task library for running reek.
10
+ # (Classes here will be configured via the Rakefile, and therefore will
11
+ # possess a :reek:attribute or two.)
10
12
  #
11
13
  module Rake
12
14
 
@@ -40,6 +42,11 @@ module Reek
40
42
  # Defaults to ['<the absolute path to reek's lib directory>']
41
43
  attr_accessor :libs
42
44
 
45
+ # Glob pattern to match config files.
46
+ # Setting the REEK_CFG environment variable overrides this.
47
+ # Defaults to 'config/**/*.reek'.
48
+ attr_accessor :config_files
49
+
43
50
  # Glob pattern to match source files.
44
51
  # Setting the REEK_SRC environment variable overrides this.
45
52
  # Defaults to 'lib/**/*.rb'.
@@ -65,6 +72,7 @@ module Reek
65
72
  def initialize(name = :reek)
66
73
  @name = name
67
74
  @libs = [File.expand_path(File.dirname(__FILE__) + '/../../../lib')]
75
+ @config_files = nil
68
76
  @source_files = nil
69
77
  @ruby_opts = []
70
78
  @reek_opts = ''
@@ -72,6 +80,7 @@ module Reek
72
80
  @sort = nil
73
81
 
74
82
  yield self if block_given?
83
+ @config_files ||= 'config/**/*.reek'
75
84
  @source_files ||= 'lib/**/*.rb'
76
85
  define
77
86
  end
@@ -104,9 +113,16 @@ module Reek
104
113
  ruby_options +
105
114
  [ %Q|"#{Task.reek_script}"| ] +
106
115
  [sort_option] +
116
+ config_file_list.collect { |fn| ['-c', %["#{fn}"]] }.flatten +
107
117
  source_file_list.collect { |fn| %["#{fn}"] }
108
118
  end
109
119
 
120
+ def config_file_list
121
+ files = ENV['REEK_CFG'] || @config_files
122
+ return [] unless files
123
+ return FileList[files]
124
+ end
125
+
110
126
  def ruby_options
111
127
  lib_path = @libs.join(File::PATH_SEPARATOR)
112
128
  @ruby_opts.clone << "-I\"#{lib_path}\""
@@ -3,6 +3,7 @@ module Reek
3
3
 
4
4
  #
5
5
  # Reports a warning that a smell has been found.
6
+ # This object is essentially a DTO, and therefore contains a :reek:attribute or two.
6
7
  #
7
8
  class SmellWarning
8
9
 
@@ -71,7 +72,7 @@ module Reek
71
72
 
72
73
  def is_active() @status[ACTIVE_KEY] end
73
74
 
74
- def hash # :nodoc:
75
+ def hash
75
76
  sort_key.hash
76
77
  end
77
78
 
@@ -85,7 +86,7 @@ module Reek
85
86
 
86
87
  def contains_all?(patterns)
87
88
  rpt = sort_key.to_s
88
- return patterns.all? {|patt| patt === rpt}
89
+ return patterns.all? {|pattern| pattern === rpt}
89
90
  end
90
91
 
91
92
  def matches?(klass, patterns)
@@ -21,6 +21,8 @@ module Reek
21
21
  class Attribute < SmellDetector
22
22
 
23
23
  SMELL_CLASS = self.name.split(/::/)[-1]
24
+ SMELL_SUBCLASS = SMELL_CLASS
25
+
24
26
  ATTRIBUTE_KEY = 'attribute'
25
27
 
26
28
  def self.contexts # :nodoc:
@@ -37,19 +39,21 @@ module Reek
37
39
 
38
40
  #
39
41
  # Checks whether the given class declares any attributes.
40
- # Remembers any smells found.
41
42
  #
42
- def examine_context(module_ctx)
43
- attributes_in(module_ctx).each do |attr, line|
44
- found(module_ctx, "declares the attribute #{attr}", '',
45
- {'attribute' => attr.to_s}, [line])
43
+ # @return [Array<SmellWarning>]
44
+ #
45
+ def examine_context(ctx)
46
+ attributes_in(ctx).map do |attr, line|
47
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, [line],
48
+ "declares the attribute #{attr}",
49
+ @source, SMELL_SUBCLASS,
50
+ {ATTRIBUTE_KEY => attr.to_s})
51
+ smell
46
52
  end
47
53
  end
48
54
 
49
- #
50
- # Collects the names of the class variables declared and/or used
51
- # in the given module.
52
- #
55
+ private
56
+
53
57
  def attributes_in(module_ctx)
54
58
  result = Set.new
55
59
  attr_defn_methods = [:attr, :attr_reader, :attr_writer, :attr_accessor]
@@ -14,18 +14,23 @@ module Reek
14
14
  #
15
15
  class BooleanParameter < SmellDetector
16
16
 
17
+ SMELL_CLASS = 'ControlCouple'
18
+ SMELL_SUBCLASS = self.name.split(/::/)[-1]
19
+
20
+ PARAMETER_KEY = 'parameter'
21
+
22
+ #
23
+ # Checks whether the given method has any Boolean parameters.
17
24
  #
18
- # Checks whether the given method has a Boolean parameter.
19
- # Remembers any smells found.
25
+ # @return [Array<SmellWarning>]
20
26
  #
21
27
  def examine_context(method_ctx)
22
- method_ctx.parameters.default_assignments.each do |param, value|
23
- next unless [:true, :false].include?(value[0])
24
- smell = SmellWarning.new('ControlCouple', method_ctx.full_name, [method_ctx.exp.line],
25
- "has boolean parameter '#{param.to_s}'",
26
- @source, 'BooleanParameter', {'parameter' => param.to_s})
27
- @smells_found << smell
28
- #SMELL: serious duplication
28
+ method_ctx.parameters.default_assignments.select do |param, value|
29
+ [:true, :false].include?(value[0])
30
+ end.map do |param, value|
31
+ SmellWarning.new(SMELL_CLASS, method_ctx.full_name, [method_ctx.exp.line],
32
+ "has boolean parameter '#{param.to_s}'",
33
+ @source, SMELL_SUBCLASS, {PARAMETER_KEY => param.to_s})
29
34
  end
30
35
  end
31
36
  end
@@ -15,17 +15,26 @@ module Reek
15
15
  #
16
16
  class ClassVariable < SmellDetector
17
17
 
18
+ SMELL_CLASS = self.name.split(/::/)[-1]
19
+ SMELL_SUBCLASS = SMELL_CLASS
20
+ VARIABLE_KEY = 'variable'
21
+
18
22
  def self.contexts # :nodoc:
19
23
  [:class, :module]
20
24
  end
21
25
 
22
26
  #
23
27
  # Checks whether the given class or module declares any class variables.
24
- # Remembers any smells found.
28
+ #
29
+ # @return [Array<SmellWarning>]
25
30
  #
26
31
  def examine_context(ctx)
27
- class_variables_in(ctx.exp).each do |cvar_name|
28
- found(ctx, "declares the class variable #{cvar_name}", '', {'variable' => cvar_name.to_s})
32
+ class_variables_in(ctx.exp).map do |attr_name, lines|
33
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, lines,
34
+ "declares the class variable #{attr_name.to_s}",
35
+ @source, SMELL_SUBCLASS,
36
+ {VARIABLE_KEY => attr_name.to_s})
37
+ smell
29
38
  end
30
39
  end
31
40
 
@@ -34,8 +43,10 @@ module Reek
34
43
  # in the given module.
35
44
  #
36
45
  def class_variables_in(ast)
37
- result = Set.new
38
- collector = proc { |cvar_node| result << cvar_node.name }
46
+ result = Hash.new {|hash,key| hash[key] = []}
47
+ collector = proc do |cvar_node|
48
+ result[cvar_node.name].push(cvar_node.line)
49
+ end
39
50
  [:cvar, :cvasgn, :cvdecl].each do |stmt_type|
40
51
  ast.each_node(stmt_type, [:class, :module], &collector)
41
52
  end
@@ -45,18 +45,23 @@ module Reek
45
45
 
46
46
  SMELL_CLASS = self.name.split(/::/)[-1]
47
47
  SMELL_SUBCLASS = 'ControlParameter'
48
+ PARAMETER_KEY = 'parameter'
48
49
 
49
50
  #
50
51
  # Checks whether the given method chooses its execution path
51
52
  # by testing the value of one of its parameters.
52
- # Remembers any smells found.
53
53
  #
54
- def examine_context(method_ctx)
55
- control_parameters(method_ctx).each do |cond, occurs|
56
- param = cond.format
54
+ # @return [Array<SmellWarning>]
55
+ #
56
+ def examine_context(ctx)
57
+ control_parameters(ctx).map do |cond, occurs|
58
+ param = cond.format_ruby
57
59
  lines = occurs.map {|exp| exp.line}
58
- found(method_ctx, "is controlled by argument #{param}",
59
- SMELL_SUBCLASS, {'parameter' => param}, lines)
60
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, lines,
61
+ "is controlled by argument #{param}",
62
+ @source, SMELL_SUBCLASS,
63
+ {PARAMETER_KEY => param})
64
+ smell
60
65
  end
61
66
  end
62
67
 
@@ -34,23 +34,32 @@ module Reek
34
34
  OCCURRENCES_KEY = 'occurrences'
35
35
  PARAMETERS_KEY = 'parameters'
36
36
 
37
- def self.contexts # :nodoc:
37
+ # @private
38
+ def self.contexts
38
39
  [:class, :module]
39
40
  end
40
41
 
42
+ #
41
43
  # The name of the config field that sets the maximum allowed
42
- # copies of any clump.
44
+ # copies of any clump. No group of common parameters will be
45
+ # reported as a DataClump unless there are more than this many
46
+ # methods containing those parameters.
47
+ #
43
48
  MAX_COPIES_KEY = 'max_copies'
44
-
45
49
  DEFAULT_MAX_COPIES = 2
46
50
 
51
+ #
52
+ # The name of the config field that sets the minimum clump
53
+ # size. No group of common parameters will be reported as
54
+ # a DataClump unless it contains at least this many parameters.
55
+ #
47
56
  MIN_CLUMP_SIZE_KEY = 'min_clump_size'
48
57
  DEFAULT_MIN_CLUMP_SIZE = 2
49
58
 
50
59
  def self.default_config
51
60
  super.adopt(
52
- MAX_COPIES_KEY => DEFAULT_MAX_COPIES,
53
- MIN_CLUMP_SIZE_KEY => DEFAULT_MIN_CLUMP_SIZE
61
+ MAX_COPIES_KEY => DEFAULT_MAX_COPIES,
62
+ MIN_CLUMP_SIZE_KEY => DEFAULT_MIN_CLUMP_SIZE
54
63
  )
55
64
  end
56
65
 
@@ -60,26 +69,25 @@ module Reek
60
69
 
61
70
  #
62
71
  # Checks the given class or module for multiple identical parameter sets.
63
- # Remembers any smells found.
72
+ #
73
+ # @return [Array<SmellWarning>]
64
74
  #
65
75
  def examine_context(ctx)
66
- max_copies = value(MAX_COPIES_KEY, ctx, DEFAULT_MAX_COPIES)
67
- min_clump_size = value(MIN_CLUMP_SIZE_KEY, ctx, DEFAULT_MIN_CLUMP_SIZE)
68
- MethodGroup.new(ctx, min_clump_size, max_copies).clumps.each do |clump, methods|
69
- smell = SmellWarning.new('DataClump', ctx.full_name,
70
- methods.map {|meth| meth.line},
71
- "takes parameters #{DataClump.print_clump(clump)} to #{methods.length} methods",
72
- @source, 'DataClump', {
73
- PARAMETERS_KEY => clump.map {|name| name.to_s},
74
- OCCURRENCES_KEY => methods.length,
75
- METHODS_KEY => methods.map {|meth| meth.name}
76
- })
77
- @smells_found << smell
78
- #SMELL: serious duplication
79
- # SMELL: name.to_s is becoming a nuisance
76
+ @max_copies = value(MAX_COPIES_KEY, ctx, DEFAULT_MAX_COPIES)
77
+ @min_clump_size = value(MIN_CLUMP_SIZE_KEY, ctx, DEFAULT_MIN_CLUMP_SIZE)
78
+ MethodGroup.new(ctx, @min_clump_size, @max_copies).clumps.map do |clump, methods|
79
+ SmellWarning.new('DataClump', ctx.full_name,
80
+ methods.map {|meth| meth.line},
81
+ "takes parameters #{DataClump.print_clump(clump)} to #{methods.length} methods",
82
+ @source, 'DataClump', {
83
+ PARAMETERS_KEY => clump.map {|name| name.to_s},
84
+ OCCURRENCES_KEY => methods.length,
85
+ METHODS_KEY => methods.map {|meth| meth.name}
86
+ })
80
87
  end
81
88
  end
82
89
 
90
+ # @private
83
91
  def self.print_clump(clump)
84
92
  "[#{clump.map {|name| name.to_s}.join(', ')}]"
85
93
  end
@@ -108,14 +116,12 @@ module Reek
108
116
  methods.each do |other_method|
109
117
  clump = [method.arg_names, other_method.arg_names].intersection
110
118
  if clump.length >= @min_clump_size
111
- others = methods.select do |other| # BUG: early ones have already been eliminated
112
- clump - other.arg_names == []
113
- end
119
+ others = methods.select { |other| clump - other.arg_names == [] }
114
120
  results[clump] += [method] + others
115
121
  end
116
122
  end
117
123
  end
118
-
124
+
119
125
  def collect_clumps_in(methods, results)
120
126
  return if methods.length <= @max_copies
121
127
  tail = methods[1..-1]
@@ -126,9 +132,7 @@ module Reek
126
132
  def clumps
127
133
  results = Hash.new([])
128
134
  collect_clumps_in(@candidate_methods, results)
129
- results.each_key do |key|
130
- results[key].uniq!
131
- end
135
+ results.each_key { |key| results[key].uniq! }
132
136
  results
133
137
  end
134
138
 
@@ -148,13 +152,12 @@ module Reek
148
152
  end
149
153
  end
150
154
 
151
- #
152
155
  # A method definition and a copy of its parameters
153
- #
156
+ # @private
154
157
  class CandidateMethod
155
158
  def initialize(defn_node)
156
159
  @defn = defn_node
157
- @params = defn_node.arg_names.clone.sort {|a,b| a.to_s <=> b.to_s}
160
+ @params = defn_node.arg_names.clone.sort {|first, second| first.to_s <=> second.to_s}
158
161
  end
159
162
 
160
163
  def arg_names
@@ -19,32 +19,59 @@ module Reek
19
19
  #
20
20
  class Duplication < SmellDetector
21
21
 
22
+ SMELL_CLASS = self.name.split(/::/)[-1]
23
+ SMELL_SUBCLASS = 'DuplicateMethodCall'
24
+ CALL_KEY = 'call'
25
+ OCCURRENCES_KEY = 'occurrences'
26
+
22
27
  # The name of the config field that sets the maximum number of
23
28
  # identical calls to be permitted within any single method.
24
29
  MAX_ALLOWED_CALLS_KEY = 'max_calls'
25
30
 
26
31
  DEFAULT_MAX_CALLS = 1
27
32
 
33
+ # The name of the config field that sets the names of any
34
+ # methods for which identical calls should be to be permitted
35
+ # within any single method.
36
+ ALLOW_CALLS_KEY = 'allow_calls'
37
+
38
+ DEFAULT_ALLOW_CALLS = []
39
+
28
40
  def self.default_config
29
- super.adopt(MAX_ALLOWED_CALLS_KEY => DEFAULT_MAX_CALLS)
41
+ super.adopt(
42
+ MAX_ALLOWED_CALLS_KEY => DEFAULT_MAX_CALLS,
43
+ ALLOW_CALLS_KEY => DEFAULT_ALLOW_CALLS
44
+ )
30
45
  end
31
46
 
32
47
  def initialize(source, config = Duplication.default_config)
33
48
  super(source, config)
34
49
  end
35
50
 
36
- def examine_context(method_ctx)
37
- calls(method_ctx).each do |call_exp, copies|
51
+ #
52
+ # Looks for duplicate calls within the body of the method +ctx+.
53
+ #
54
+ # @return [Array<SmellWarning>]
55
+ #
56
+ def examine_context(ctx)
57
+ @max_allowed_calls = value(MAX_ALLOWED_CALLS_KEY, ctx, DEFAULT_MAX_CALLS)
58
+ @allow_calls = value(ALLOW_CALLS_KEY, ctx, DEFAULT_ALLOW_CALLS)
59
+ calls(ctx).select do |call_exp, copies|
60
+ copies.length > @max_allowed_calls and not allow_calls?(call_exp.format_ruby)
61
+ end.map do |call_exp, copies|
38
62
  occurs = copies.length
39
- next if occurs <= value(MAX_ALLOWED_CALLS_KEY, method_ctx, DEFAULT_MAX_CALLS)
40
- call = call_exp.format
63
+ call = call_exp.format_ruby
41
64
  multiple = occurs == 2 ? 'twice' : "#{occurs} times"
42
- found(method_ctx, "calls #{call} #{multiple}",
43
- 'DuplicateMethodCall', {'call' => call, 'occurrences' => occurs},
44
- copies.map {|exp| exp.line})
65
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, copies.map {|exp| exp.line},
66
+ "calls #{call} #{multiple}",
67
+ @source, SMELL_SUBCLASS,
68
+ {CALL_KEY => call, OCCURRENCES_KEY => occurs})
69
+ smell
45
70
  end
46
71
  end
47
72
 
73
+ private
74
+
48
75
  def calls(method_ctx)
49
76
  result = Hash.new {|hash,key| hash[key] = []}
50
77
  method_ctx.local_nodes(:call) do |call_node|
@@ -56,6 +83,10 @@ module Reek
56
83
  end
57
84
  result
58
85
  end
86
+
87
+ def allow_calls?(method)
88
+ @allow_calls.any? { |allow| /#{allow}/ === method }
89
+ end
59
90
  end
60
91
  end
61
92
  end
@@ -43,16 +43,15 @@ module Reek
43
43
  #
44
44
  # Checks whether the given +context+ includes any code fragment that
45
45
  # might "belong" on another class.
46
- # Remembers any smells found.
46
+ #
47
+ # @return [Array<SmellWarning>]
47
48
  #
48
49
  def examine_context(method_ctx)
49
- method_ctx.envious_receivers.each do |ref, occurs|
50
- target = ref.format
51
- smell = SmellWarning.new(SMELL_CLASS, method_ctx.full_name, [method_ctx.exp.line],
52
- "refers to #{target} more than self",
53
- @source, SMELL_SUBCLASS, {RECEIVER_KEY => target, REFERENCES_KEY => occurs})
54
- @smells_found << smell
55
- #SMELL: serious duplication
50
+ method_ctx.envious_receivers.map do |ref, occurs|
51
+ target = ref.format_ruby
52
+ SmellWarning.new(SMELL_CLASS, method_ctx.full_name, [method_ctx.exp.line],
53
+ "refers to #{target} more than self",
54
+ @source, SMELL_SUBCLASS, {RECEIVER_KEY => target, REFERENCES_KEY => occurs})
56
55
  end
57
56
  end
58
57
  end