reek 1.2.6 → 1.2.8

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 (209) hide show
  1. data/.yardopts +10 -0
  2. data/History.txt +62 -0
  3. data/README.md +74 -0
  4. data/bin/reek +2 -2
  5. data/config/defaults.reek +44 -5
  6. data/features/api.feature +20 -0
  7. data/features/masking_smells.feature +60 -42
  8. data/features/options.feature +7 -2
  9. data/features/rake_task.feature +29 -22
  10. data/features/reports.feature +9 -41
  11. data/features/samples.feature +184 -196
  12. data/features/stdin.feature +5 -8
  13. data/features/step_definitions/reek_steps.rb +8 -4
  14. data/features/support/env.rb +2 -3
  15. data/features/yaml.feature +85 -0
  16. data/lib/reek/cli/application.rb +46 -0
  17. data/lib/reek/cli/command_line.rb +108 -0
  18. data/lib/reek/cli/help_command.rb +18 -0
  19. data/lib/reek/cli/reek_command.rb +37 -0
  20. data/lib/reek/cli/report.rb +58 -0
  21. data/lib/reek/cli/version_command.rb +19 -0
  22. data/lib/reek/cli/yaml_command.rb +32 -0
  23. data/lib/reek/core/code_context.rb +64 -0
  24. data/lib/reek/core/code_parser.rb +166 -0
  25. data/lib/reek/core/method_context.rb +84 -0
  26. data/lib/reek/core/module_context.rb +20 -0
  27. data/lib/reek/core/object_refs.rb +51 -0
  28. data/lib/reek/core/singleton_method_context.rb +20 -0
  29. data/lib/reek/core/smell_configuration.rb +62 -0
  30. data/lib/reek/core/sniffer.rb +105 -0
  31. data/lib/reek/core/stop_context.rb +30 -0
  32. data/lib/reek/core/warning_collector.rb +27 -0
  33. data/lib/reek/examiner.rb +104 -0
  34. data/lib/reek/rake/task.rb +142 -0
  35. data/lib/reek/smell_warning.rb +73 -23
  36. data/lib/reek/smells/attribute.rb +26 -21
  37. data/lib/reek/smells/boolean_parameter.rb +38 -0
  38. data/lib/reek/smells/class_variable.rb +22 -9
  39. data/lib/reek/smells/control_couple.rb +40 -16
  40. data/lib/reek/smells/data_clump.rb +114 -23
  41. data/lib/reek/smells/duplication.rb +55 -15
  42. data/lib/reek/smells/feature_envy.rb +16 -7
  43. data/lib/reek/smells/irresponsible_module.rb +37 -0
  44. data/lib/reek/smells/large_class.rb +37 -19
  45. data/lib/reek/smells/long_method.rb +20 -9
  46. data/lib/reek/smells/long_parameter_list.rb +22 -10
  47. data/lib/reek/smells/long_yield_list.rb +43 -7
  48. data/lib/reek/smells/nested_iterators.rb +68 -8
  49. data/lib/reek/smells/simulated_polymorphism.rb +26 -15
  50. data/lib/reek/smells/smell_detector.rb +29 -51
  51. data/lib/reek/smells/uncommunicative_method_name.rb +74 -0
  52. data/lib/reek/smells/uncommunicative_module_name.rb +74 -0
  53. data/lib/reek/smells/uncommunicative_parameter_name.rb +79 -0
  54. data/lib/reek/smells/uncommunicative_variable_name.rb +89 -0
  55. data/lib/reek/smells/utility_function.rb +51 -12
  56. data/lib/reek/smells.rb +29 -0
  57. data/lib/reek/source/code_comment.rb +37 -0
  58. data/lib/reek/source/config_file.rb +72 -0
  59. data/lib/reek/source/core_extras.rb +46 -0
  60. data/lib/reek/source/reference_collector.rb +28 -0
  61. data/lib/reek/source/sexp_formatter.rb +17 -0
  62. data/lib/reek/source/source_code.rb +44 -0
  63. data/lib/reek/source/source_file.rb +32 -0
  64. data/lib/reek/source/source_locator.rb +42 -0
  65. data/lib/reek/source/tree_dresser.rb +204 -0
  66. data/lib/reek/source.rb +18 -0
  67. data/lib/reek/spec/should_reek.rb +31 -0
  68. data/lib/reek/spec/should_reek_of.rb +37 -0
  69. data/lib/reek/spec/should_reek_only_of.rb +37 -0
  70. data/lib/reek/spec.rb +51 -0
  71. data/lib/reek.rb +8 -4
  72. data/reek.gemspec +9 -7
  73. data/spec/matchers/smell_of_matcher.rb +58 -0
  74. data/spec/reek/{help_command_spec.rb → cli/help_command_spec.rb} +3 -4
  75. data/spec/reek/cli/reek_command_spec.rb +46 -0
  76. data/spec/reek/cli/report_spec.rb +30 -0
  77. data/spec/reek/{version_command_spec.rb → cli/version_command_spec.rb} +3 -3
  78. data/spec/reek/cli/yaml_command_spec.rb +47 -0
  79. data/spec/reek/core/code_context_spec.rb +145 -0
  80. data/spec/reek/{code_parser_spec.rb → core/code_parser_spec.rb} +7 -6
  81. data/spec/reek/{config_spec.rb → core/config_spec.rb} +2 -6
  82. data/spec/reek/{method_context_spec.rb → core/method_context_spec.rb} +16 -36
  83. data/spec/reek/core/module_context_spec.rb +27 -0
  84. data/spec/reek/{object_refs_spec.rb → core/object_refs_spec.rb} +5 -6
  85. data/spec/reek/core/singleton_method_context_spec.rb +9 -0
  86. data/spec/reek/core/smell_configuration_spec.rb +11 -0
  87. data/spec/reek/core/stop_context_spec.rb +17 -0
  88. data/spec/reek/core/warning_collector_spec.rb +27 -0
  89. data/spec/reek/examiner_spec.rb +103 -0
  90. data/spec/reek/smell_warning_spec.rb +74 -82
  91. data/spec/reek/smells/attribute_spec.rb +42 -23
  92. data/spec/reek/smells/behaves_like_variable_detector.rb +2 -2
  93. data/spec/reek/smells/boolean_parameter_spec.rb +66 -0
  94. data/spec/reek/smells/class_variable_spec.rb +60 -75
  95. data/spec/reek/smells/control_couple_spec.rb +37 -32
  96. data/spec/reek/smells/data_clump_spec.rb +63 -24
  97. data/spec/reek/smells/duplication_spec.rb +106 -57
  98. data/spec/reek/smells/feature_envy_spec.rb +113 -92
  99. data/spec/reek/smells/irresponsible_module_spec.rb +58 -0
  100. data/spec/reek/smells/large_class_spec.rb +67 -57
  101. data/spec/reek/smells/long_method_spec.rb +40 -10
  102. data/spec/reek/smells/long_parameter_list_spec.rb +50 -28
  103. data/spec/reek/smells/long_yield_list_spec.rb +57 -0
  104. data/spec/reek/smells/nested_iterators_spec.rb +115 -7
  105. data/spec/reek/smells/simulated_polymorphism_spec.rb +56 -20
  106. data/spec/reek/smells/smell_detector_shared.rb +42 -0
  107. data/spec/reek/smells/uncommunicative_method_name_spec.rb +42 -0
  108. data/spec/reek/smells/uncommunicative_module_name_spec.rb +66 -0
  109. data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +71 -0
  110. data/spec/reek/smells/uncommunicative_variable_name_spec.rb +120 -0
  111. data/spec/reek/smells/utility_function_spec.rb +50 -4
  112. data/spec/reek/source/code_comment_spec.rb +82 -0
  113. data/spec/reek/source/object_source_spec.rb +20 -0
  114. data/spec/reek/source/reference_collector_spec.rb +53 -0
  115. data/spec/reek/{adapters/source_spec.rb → source/source_code_spec.rb} +7 -8
  116. data/spec/reek/source/tree_dresser_spec.rb +270 -0
  117. data/spec/reek/spec/should_reek_of_spec.rb +76 -0
  118. data/spec/reek/spec/should_reek_only_of_spec.rb +89 -0
  119. data/spec/reek/{adapters → spec}/should_reek_spec.rb +8 -32
  120. data/spec/samples/all_but_one_masked/clean_one.rb +1 -0
  121. data/spec/samples/all_but_one_masked/dirty.rb +1 -0
  122. data/spec/samples/all_but_one_masked/masked.reek +5 -1
  123. data/spec/samples/clean_due_to_masking/clean_one.rb +1 -0
  124. data/spec/samples/clean_due_to_masking/clean_three.rb +1 -0
  125. data/spec/samples/clean_due_to_masking/clean_two.rb +1 -0
  126. data/spec/samples/clean_due_to_masking/dirty_one.rb +1 -1
  127. data/spec/samples/clean_due_to_masking/dirty_two.rb +1 -1
  128. data/spec/samples/clean_due_to_masking/masked.reek +5 -1
  129. data/spec/samples/config/allow_duplication.reek +3 -0
  130. data/spec/samples/config/deeper_nested_iterators.reek +3 -0
  131. data/spec/samples/corrupt_config_file/dirty.rb +1 -1
  132. data/spec/samples/demo/demo.rb +8 -0
  133. data/spec/samples/empty_config_file/dirty.rb +2 -1
  134. data/spec/samples/exceptions.reek +1 -1
  135. data/spec/samples/inline_config/dirty.rb +16 -0
  136. data/spec/samples/inline_config/masked.reek +7 -0
  137. data/spec/samples/mask_some/dirty.rb +8 -0
  138. data/spec/samples/mask_some/some.reek +8 -0
  139. data/spec/samples/masked/dirty.rb +2 -1
  140. data/spec/samples/masked/masked.reek +3 -1
  141. data/spec/samples/mixed_results/clean_one.rb +1 -0
  142. data/spec/samples/mixed_results/clean_three.rb +1 -0
  143. data/spec/samples/mixed_results/clean_two.rb +1 -0
  144. data/spec/samples/mixed_results/dirty_one.rb +1 -0
  145. data/spec/samples/mixed_results/dirty_two.rb +1 -0
  146. data/spec/samples/not_quite_masked/dirty.rb +2 -1
  147. data/spec/samples/not_quite_masked/masked.reek +1 -1
  148. data/spec/samples/overrides/masked/dirty.rb +2 -1
  149. data/spec/samples/overrides/masked/lower.reek +3 -1
  150. data/spec/samples/three_clean_files/clean_one.rb +1 -0
  151. data/spec/samples/three_clean_files/clean_three.rb +1 -0
  152. data/spec/samples/three_clean_files/clean_two.rb +1 -0
  153. data/spec/samples/two_smelly_files/dirty_one.rb +2 -1
  154. data/spec/samples/two_smelly_files/dirty_two.rb +2 -1
  155. data/spec/spec_helper.rb +10 -2
  156. data/tasks/reek.rake +2 -2
  157. data/tasks/test.rake +12 -3
  158. metadata +121 -79
  159. data/README.rdoc +0 -84
  160. data/features/profile.feature +0 -34
  161. data/lib/reek/adapters/application.rb +0 -46
  162. data/lib/reek/adapters/command_line.rb +0 -77
  163. data/lib/reek/adapters/config_file.rb +0 -31
  164. data/lib/reek/adapters/core_extras.rb +0 -64
  165. data/lib/reek/adapters/rake_task.rb +0 -121
  166. data/lib/reek/adapters/report.rb +0 -86
  167. data/lib/reek/adapters/source.rb +0 -72
  168. data/lib/reek/adapters/spec.rb +0 -133
  169. data/lib/reek/block_context.rb +0 -62
  170. data/lib/reek/class_context.rb +0 -41
  171. data/lib/reek/code_context.rb +0 -68
  172. data/lib/reek/code_parser.rb +0 -203
  173. data/lib/reek/configuration.rb +0 -57
  174. data/lib/reek/detector_stack.rb +0 -37
  175. data/lib/reek/help_command.rb +0 -14
  176. data/lib/reek/if_context.rb +0 -18
  177. data/lib/reek/masking_collection.rb +0 -33
  178. data/lib/reek/method_context.rb +0 -138
  179. data/lib/reek/module_context.rb +0 -49
  180. data/lib/reek/name.rb +0 -57
  181. data/lib/reek/object_refs.rb +0 -49
  182. data/lib/reek/reek_command.rb +0 -28
  183. data/lib/reek/sexp_formatter.rb +0 -10
  184. data/lib/reek/singleton_method_context.rb +0 -26
  185. data/lib/reek/smells/uncommunicative_name.rb +0 -84
  186. data/lib/reek/sniffer.rb +0 -177
  187. data/lib/reek/stop_context.rb +0 -35
  188. data/lib/reek/tree_dresser.rb +0 -82
  189. data/lib/reek/version_command.rb +0 -14
  190. data/lib/reek/yield_call_context.rb +0 -12
  191. data/spec/reek/adapters/report_spec.rb +0 -31
  192. data/spec/reek/adapters/should_reek_of_spec.rb +0 -138
  193. data/spec/reek/adapters/should_reek_only_of_spec.rb +0 -87
  194. data/spec/reek/block_context_spec.rb +0 -65
  195. data/spec/reek/class_context_spec.rb +0 -161
  196. data/spec/reek/code_context_spec.rb +0 -182
  197. data/spec/reek/configuration_spec.rb +0 -12
  198. data/spec/reek/if_context_spec.rb +0 -17
  199. data/spec/reek/masking_collection_spec.rb +0 -236
  200. data/spec/reek/module_context_spec.rb +0 -46
  201. data/spec/reek/name_spec.rb +0 -37
  202. data/spec/reek/object_source_spec.rb +0 -23
  203. data/spec/reek/reek_command_spec.rb +0 -45
  204. data/spec/reek/singleton_method_context_spec.rb +0 -16
  205. data/spec/reek/smells/smell_detector_spec.rb +0 -36
  206. data/spec/reek/smells/uncommunicative_name_spec.rb +0 -146
  207. data/spec/reek/sniffer_spec.rb +0 -11
  208. data/spec/reek/stop_context_spec.rb +0 -33
  209. data/spec/reek/tree_dresser_spec.rb +0 -20
@@ -1,20 +1,78 @@
1
+
1
2
  module Reek
2
3
 
3
4
  #
4
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.
5
7
  #
6
8
  class SmellWarning
9
+
7
10
  include Comparable
8
11
 
9
- def initialize(detector_class, context, line, message, masked)
10
- @smell = detector_class.class.name.split(/::/)[-1]
11
- @context = context
12
- @line = line
13
- @message = message
14
- @is_masked = masked
12
+ MESSAGE_KEY = 'message'
13
+ SUBCLASS_KEY = 'subclass'
14
+ CLASS_KEY = 'class'
15
+
16
+ CONTEXT_KEY = 'context'
17
+ LINES_KEY = 'lines'
18
+ SOURCE_KEY = 'source'
19
+
20
+ ACTIVE_KEY = 'is_active'
21
+
22
+ def initialize(class_name, context, lines, message,
23
+ source = '', subclass_name = '', parameters = {})
24
+ @smell = {
25
+ CLASS_KEY => class_name,
26
+ SUBCLASS_KEY => subclass_name,
27
+ MESSAGE_KEY => message,
28
+ }
29
+ @smell.merge!(parameters)
30
+ @status = {
31
+ ACTIVE_KEY => true
32
+ }
33
+ @location = {
34
+ CONTEXT_KEY => context.to_s,
35
+ LINES_KEY => lines,
36
+ SOURCE_KEY => source
37
+ }
15
38
  end
16
39
 
17
- def hash # :nodoc:
40
+ #
41
+ # Details of the smell found, including its class ({CLASS_KEY}),
42
+ # subclass ({SUBCLASS_KEY}) and summary message ({MESSAGE_KEY})
43
+ #
44
+ # @return [Hash{String => String}]
45
+ #
46
+ attr_reader :smell
47
+
48
+ def smell_class() @smell[CLASS_KEY] end
49
+ def subclass() @smell[SUBCLASS_KEY] end
50
+ def message() @smell[MESSAGE_KEY] end
51
+
52
+ #
53
+ # Details of the smell's location, including its context ({CONTEXT_KEY}),
54
+ # the line numbers on which it occurs ({LINES_KEY}) and the source
55
+ # file ({SOURCE_KEY})
56
+ #
57
+ # @return [Hash{String => String, Array<Number>}]
58
+ #
59
+ attr_reader :location
60
+
61
+ def context() @location[CONTEXT_KEY] end
62
+ def lines() @location[LINES_KEY] end
63
+ def source() @location[SOURCE_KEY] end
64
+
65
+ #
66
+ # Details of the smell's status, including whether it is active ({ACTIVE_KEY})
67
+ # (as opposed to being masked by a config file)
68
+ #
69
+ # @return [Hash{String => Boolean}]
70
+ #
71
+ attr_reader :status
72
+
73
+ def is_active() @status[ACTIVE_KEY] end
74
+
75
+ def hash
18
76
  sort_key.hash
19
77
  end
20
78
 
@@ -28,29 +86,21 @@ module Reek
28
86
 
29
87
  def contains_all?(patterns)
30
88
  rpt = sort_key.to_s
31
- return patterns.all? {|exp| exp === rpt}
89
+ return patterns.all? {|pattern| pattern === rpt}
32
90
  end
33
91
 
34
- def sort_key
35
- [@context, @message, smell_name]
92
+ def matches?(klass, patterns)
93
+ @smell.values.include?(klass.to_s) and contains_all?(patterns)
36
94
  end
37
95
 
38
- protected :sort_key
39
-
40
- def report(format)
41
- format.gsub(/\%s/, smell_name).gsub(/\%c/, @context).gsub(/\%w/, @message).gsub(/\%m/, @is_masked ? '(masked) ' : '')
96
+ def report_on(listener)
97
+ listener.found_smell(self)
42
98
  end
43
99
 
44
- def report_on(report)
45
- if @is_masked
46
- report.found_masked_smell(self)
47
- else
48
- report.found_smell(self)
49
- end
50
- end
100
+ protected
51
101
 
52
- def smell_name
53
- @smell.gsub(/([a-z])([A-Z])/) { |sub| "#{$1} #{$2}"}.split.join(' ')
102
+ def sort_key
103
+ [@location[CONTEXT_KEY], @smell[MESSAGE_KEY], @smell[CLASS_KEY]]
54
104
  end
55
105
  end
56
106
  end
@@ -1,4 +1,6 @@
1
- require 'reek/smells/smell_detector'
1
+ require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
2
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
3
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'core', 'smell_configuration')
2
4
 
3
5
  module Reek
4
6
  module Smells
@@ -18,43 +20,46 @@ module Reek
18
20
  #
19
21
  class Attribute < SmellDetector
20
22
 
21
- ATTRIBUTE_METHODS = [:attr, :attr_reader, :attr_writer, :attr_accessor]
23
+ SMELL_CLASS = self.name.split(/::/)[-1]
24
+ SMELL_SUBCLASS = SMELL_CLASS
25
+
26
+ ATTRIBUTE_KEY = 'attribute'
22
27
 
23
28
  def self.contexts # :nodoc:
24
29
  [:class, :module]
25
30
  end
26
31
 
27
32
  def self.default_config
28
- super.adopt(SmellConfiguration::ENABLED_KEY => false)
33
+ super.adopt(Core::SmellConfiguration::ENABLED_KEY => false)
29
34
  end
30
35
 
31
- def initialize(config = Attribute.default_config)
32
- super(config)
36
+ def initialize(source, config = Attribute.default_config)
37
+ super(source, config)
33
38
  end
34
39
 
35
40
  #
36
41
  # Checks whether the given class declares any attributes.
37
- # Remembers any smells found.
38
42
  #
39
- def examine_context(mod)
40
- # SMELL: Duplication
41
- # MethodContext, ClassContext and ModuleContext all know which
42
- # calls constitute attribute declarations. Need a method on
43
- # ModuleContext: each_public_call.select [names] {...}
44
- attributes_in(mod).each do |attr|
45
- found(mod, "declares the attribute #{attr}")
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
- #
53
- def attributes_in(mod)
55
+ private
56
+
57
+ def attributes_in(module_ctx)
54
58
  result = Set.new
55
- mod.local_nodes(:call) do |call_node|
56
- if ATTRIBUTE_METHODS.include?(call_node.method_name)
57
- call_node.arg_names.each {|arg| result << arg }
59
+ attr_defn_methods = [:attr, :attr_reader, :attr_writer, :attr_accessor]
60
+ module_ctx.local_nodes(:call) do |call_node|
61
+ if attr_defn_methods.include?(call_node.method_name)
62
+ call_node.arg_names.each {|arg| result << [arg, call_node.line] }
58
63
  end
59
64
  end
60
65
  result
@@ -0,0 +1,38 @@
1
+ require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
2
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
3
+
4
+ module Reek
5
+ module Smells
6
+
7
+ #
8
+ # A Boolean parameter effectively permits a method's caller
9
+ # to decide which execution path to take. The
10
+ # offending parameter is a kind of Control Couple.
11
+ #
12
+ # Currently Reek can only detect a Boolean parameter when it has a
13
+ # default initializer.
14
+ #
15
+ class BooleanParameter < SmellDetector
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.
24
+ #
25
+ # @return [Array<SmellWarning>]
26
+ #
27
+ def examine_context(method_ctx)
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})
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,4 +1,6 @@
1
- require 'reek/smells/smell_detector'
1
+ require 'set'
2
+ require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
3
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
2
4
 
3
5
  module Reek
4
6
  module Smells
@@ -13,17 +15,26 @@ module Reek
13
15
  #
14
16
  class ClassVariable < SmellDetector
15
17
 
18
+ SMELL_CLASS = self.name.split(/::/)[-1]
19
+ SMELL_SUBCLASS = SMELL_CLASS
20
+ VARIABLE_KEY = 'variable'
21
+
16
22
  def self.contexts # :nodoc:
17
23
  [:class, :module]
18
24
  end
19
25
 
20
26
  #
21
27
  # Checks whether the given class or module declares any class variables.
22
- # Remembers any smells found.
23
28
  #
24
- def examine_context(mod)
25
- class_variables_in(mod).each do |cvar_name|
26
- found(mod, "declares the class variable #{cvar_name}")
29
+ # @return [Array<SmellWarning>]
30
+ #
31
+ def examine_context(ctx)
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
27
38
  end
28
39
  end
29
40
 
@@ -31,11 +42,13 @@ module Reek
31
42
  # Collects the names of the class variables declared and/or used
32
43
  # in the given module.
33
44
  #
34
- def class_variables_in(mod)
35
- result = Set.new
36
- collector = proc { |cvar_node| result << cvar_node.name }
45
+ def class_variables_in(ast)
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
37
50
  [:cvar, :cvasgn, :cvdecl].each do |stmt_type|
38
- mod.local_nodes(stmt_type, &collector)
51
+ ast.each_node(stmt_type, [:class, :module], &collector)
39
52
  end
40
53
  result
41
54
  end
@@ -1,6 +1,5 @@
1
- require 'reek/smells/smell_detector'
2
- require 'reek/smell_warning'
3
- require 'reek/sexp_formatter'
1
+ require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
2
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
4
3
 
5
4
  module Reek
6
5
  module Smells
@@ -33,27 +32,52 @@ module Reek
33
32
  # method probably has more than one responsibility,
34
33
  # because it includes at least two different code paths.
35
34
  #
35
+ # One possible solution is to use the Strategy Pattern
36
+ # to pass into the callee what must be done. This is
37
+ # not considered to be control coupling because the
38
+ # callee will do the same thing with the strategy,
39
+ # whatever it happens to be. Sometimes in Ruby the
40
+ # strategy may actually just be a block passed in, and
41
+ # that remains next to where the caller invokes it in
42
+ # the source code.
43
+ #
36
44
  class ControlCouple < SmellDetector
37
45
 
38
- def self.contexts # :nodoc:
39
- [:if, :defn, :defs]
40
- end
46
+ SMELL_CLASS = self.name.split(/::/)[-1]
47
+ SMELL_SUBCLASS = 'ControlParameter'
48
+ PARAMETER_KEY = 'parameter'
41
49
 
42
50
  #
43
- # Checks whether the given conditional statement relies on a control couple.
44
- # Remembers any smells found.
51
+ # Checks whether the given method chooses its execution path
52
+ # by testing the value of one of its parameters.
53
+ #
54
+ # @return [Array<SmellWarning>]
45
55
  #
46
56
  def examine_context(ctx)
47
- case ctx
48
- when IfContext
49
- return unless ctx.tests_a_parameter?
50
- found(ctx, "is controlled by argument #{SexpFormatter.format(ctx.if_expr)}")
51
- else
52
- ctx.parameters.default_assignments.each do |param, value|
53
- next unless [:true, :false].include?(value[0])
54
- found(ctx, "is controlled by argument #{param.to_s}")
57
+ control_parameters(ctx).map do |cond, occurs|
58
+ param = cond.format_ruby
59
+ lines = occurs.map {|exp| exp.line}
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
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def control_parameters(method_ctx)
71
+ params = method_ctx.exp.parameter_names
72
+ result = Hash.new {|hash,key| hash[key] = []}
73
+ return result if params.empty?
74
+ method_ctx.local_nodes(:if) do |if_node|
75
+ cond = if_node[1]
76
+ if cond[0] == :lvar and params.include?(cond[1])
77
+ result[cond].push(cond)
55
78
  end
56
79
  end
80
+ result
57
81
  end
58
82
  end
59
83
  end
@@ -1,6 +1,15 @@
1
- require 'reek/smells/smell_detector'
2
- require 'reek/smell_warning'
3
- require 'reek/sexp_formatter'
1
+ require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
2
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
3
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'source')
4
+
5
+ #
6
+ # Extensions to +Array+ needed by Reek.
7
+ #
8
+ class Array
9
+ def intersection
10
+ self.inject { |res, elem| elem & res }
11
+ end
12
+ end
4
13
 
5
14
  module Reek
6
15
  module Smells
@@ -19,42 +28,66 @@ module Reek
19
28
  #
20
29
  class DataClump < SmellDetector
21
30
 
22
- def self.contexts # :nodoc:
31
+ SMELL_CLASS = self.name.split(/::/)[-1]
32
+
33
+ METHODS_KEY = 'methods'
34
+ OCCURRENCES_KEY = 'occurrences'
35
+ PARAMETERS_KEY = 'parameters'
36
+
37
+ # @private
38
+ def self.contexts
23
39
  [:class, :module]
24
40
  end
25
41
 
42
+ #
26
43
  # The name of the config field that sets the maximum allowed
27
- # 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
+ #
28
48
  MAX_COPIES_KEY = 'max_copies'
29
-
30
49
  DEFAULT_MAX_COPIES = 2
31
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
+ #
32
56
  MIN_CLUMP_SIZE_KEY = 'min_clump_size'
33
57
  DEFAULT_MIN_CLUMP_SIZE = 2
34
58
 
35
59
  def self.default_config
36
60
  super.adopt(
37
- MAX_COPIES_KEY => DEFAULT_MAX_COPIES,
38
- MIN_CLUMP_SIZE_KEY => DEFAULT_MIN_CLUMP_SIZE
61
+ MAX_COPIES_KEY => DEFAULT_MAX_COPIES,
62
+ MIN_CLUMP_SIZE_KEY => DEFAULT_MIN_CLUMP_SIZE
39
63
  )
40
64
  end
41
65
 
42
- def initialize(config = DataClump.default_config)
43
- super(config)
66
+ def initialize(source, config = DataClump.default_config)
67
+ super(source, config)
44
68
  end
45
69
 
46
70
  #
47
71
  # Checks the given class or module for multiple identical parameter sets.
48
- # Remembers any smells found.
72
+ #
73
+ # @return [Array<SmellWarning>]
49
74
  #
50
75
  def examine_context(ctx)
51
- max_copies = value(MAX_COPIES_KEY, ctx, DEFAULT_MAX_COPIES)
52
- min_clump_size = value(MIN_CLUMP_SIZE_KEY, ctx, DEFAULT_MIN_CLUMP_SIZE)
53
- MethodGroup.new(ctx, min_clump_size, max_copies).clumps.each do |clump, occurs|
54
- found(ctx, "takes parameters #{DataClump.print_clump(clump)} to #{occurs} methods")
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
+ })
55
87
  end
56
88
  end
57
89
 
90
+ # @private
58
91
  def self.print_clump(clump)
59
92
  "[#{clump.map {|name| name.to_s}.join(', ')}]"
60
93
  end
@@ -62,27 +95,85 @@ module Reek
62
95
  end
63
96
 
64
97
  # Represents a group of methods
65
- class MethodGroup # :nodoc:
98
+ # @private
99
+ class MethodGroup
66
100
 
67
101
  def self.intersection_of_parameters_of(methods)
68
- methods.map {|meth| meth.parameters.names.sort}.intersection
102
+ methods.map {|meth| meth.arg_names}.intersection
69
103
  end
70
104
 
71
105
  def initialize(ctx, min_clump_size, max_copies)
72
- @ctx = ctx
73
106
  @min_clump_size = min_clump_size
74
107
  @max_copies = max_copies
108
+ @candidate_methods = ctx.local_nodes(:defn).select do |meth|
109
+ meth.arg_names.length >= @min_clump_size
110
+ end.map {|defn_node| CandidateMethod.new(defn_node)}
111
+ delete_infrequent_parameters
112
+ delete_small_methods
75
113
  end
76
114
 
77
- def clumps
78
- results = Hash.new(0)
79
- @ctx.parameterized_methods(@min_clump_size).bounded_power_set(@max_copies).each do |methods|
80
- clump = MethodGroup.intersection_of_parameters_of(methods)
115
+ def clumps_containing(method, methods, results)
116
+ methods.each do |other_method|
117
+ clump = [method.arg_names, other_method.arg_names].intersection
81
118
  if clump.length >= @min_clump_size
82
- results[clump] = [methods.length, results[clump]].max
119
+ others = methods.select { |other| clump - other.arg_names == [] }
120
+ results[clump] += [method] + others
83
121
  end
84
122
  end
123
+ end
124
+
125
+ def collect_clumps_in(methods, results)
126
+ return if methods.length <= @max_copies
127
+ tail = methods[1..-1]
128
+ clumps_containing(methods[0], tail, results)
129
+ collect_clumps_in(tail, results)
130
+ end
131
+
132
+ def clumps
133
+ results = Hash.new([])
134
+ collect_clumps_in(@candidate_methods, results)
135
+ results.each_key { |key| results[key].uniq! }
85
136
  results
86
137
  end
138
+
139
+ def delete_small_methods
140
+ @candidate_methods = @candidate_methods.select do |meth|
141
+ meth.arg_names.length >= @min_clump_size
142
+ end
143
+ end
144
+
145
+ def delete_infrequent_parameters
146
+ @candidate_methods.each do |meth|
147
+ meth.arg_names.each do |param|
148
+ occurs = @candidate_methods.inject(0) {|sum, cm| cm.arg_names.include?(param) ? sum+1 : sum}
149
+ meth.delete(param) if occurs <= @max_copies
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ # A method definition and a copy of its parameters
156
+ # @private
157
+ class CandidateMethod
158
+ def initialize(defn_node)
159
+ @defn = defn_node
160
+ @params = defn_node.arg_names.clone.sort {|first, second| first.to_s <=> second.to_s}
161
+ end
162
+
163
+ def arg_names
164
+ @params
165
+ end
166
+
167
+ def delete(param)
168
+ @params.delete(param)
169
+ end
170
+
171
+ def line
172
+ @defn.line
173
+ end
174
+
175
+ def name
176
+ @defn.name.to_s # BUG: should report the symbols!
177
+ end
87
178
  end
88
179
  end
@@ -1,6 +1,5 @@
1
- require 'reek/smells/smell_detector'
2
- require 'reek/smell_warning'
3
- require 'reek/sexp_formatter'
1
+ require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
2
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
4
3
 
5
4
  module Reek
6
5
  module Smells
@@ -20,32 +19,73 @@ module Reek
20
19
  #
21
20
  class Duplication < SmellDetector
22
21
 
22
+ SMELL_CLASS = self.name.split(/::/)[-1]
23
+ SMELL_SUBCLASS = 'DuplicateMethodCall'
24
+ CALL_KEY = 'call'
25
+ OCCURRENCES_KEY = 'occurrences'
26
+
23
27
  # The name of the config field that sets the maximum number of
24
28
  # identical calls to be permitted within any single method.
25
29
  MAX_ALLOWED_CALLS_KEY = 'max_calls'
26
30
 
27
31
  DEFAULT_MAX_CALLS = 1
28
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
+
29
40
  def self.default_config
30
- 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
+ )
31
45
  end
32
46
 
33
- def initialize(config = Duplication.default_config)
34
- super(config)
47
+ def initialize(source, config = Duplication.default_config)
48
+ super(source, config)
35
49
  end
36
50
 
37
- def examine_context(method)
38
- smelly_calls(method).each do |call_data|
39
- num = call_data[1]
40
- multiple = num == 2 ? 'twice' : "#{num} times"
41
- found(method, "calls #{SexpFormatter.format(call_data[0])} #{multiple}")
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|
62
+ occurs = copies.length
63
+ call = call_exp.format_ruby
64
+ multiple = occurs == 2 ? 'twice' : "#{occurs} times"
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
42
70
  end
43
71
  end
44
-
45
- def smelly_calls(method) # :nodoc:
46
- method.calls.select do |key,val|
47
- val > value(MAX_ALLOWED_CALLS_KEY, method, DEFAULT_MAX_CALLS) and key[2] != :new
72
+
73
+ private
74
+
75
+ def calls(method_ctx)
76
+ result = Hash.new {|hash,key| hash[key] = []}
77
+ method_ctx.local_nodes(:call) do |call_node|
78
+ next if call_node.method_name == :new
79
+ result[call_node].push(call_node)
80
+ end
81
+ method_ctx.local_nodes(:attrasgn) do |asgn_node|
82
+ result[asgn_node].push(asgn_node) unless asgn_node.args.length < 2
48
83
  end
84
+ result
85
+ end
86
+
87
+ def allow_calls?(method)
88
+ @allow_calls.any? { |allow| /#{allow}/ === method }
49
89
  end
50
90
  end
51
91
  end