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,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
@@ -35,14 +34,24 @@ module Reek
35
34
  class FeatureEnvy < SmellDetector
36
35
  include ExcludeInitialize
37
36
 
37
+ SMELL_CLASS = 'LowCohesion'
38
+ SMELL_SUBCLASS = self.name.split(/::/)[-1]
39
+
40
+ RECEIVER_KEY = 'receiver'
41
+ REFERENCES_KEY = 'references'
42
+
38
43
  #
39
44
  # Checks whether the given +context+ includes any code fragment that
40
45
  # might "belong" on another class.
41
- # Remembers any smells found.
42
46
  #
43
- def examine_context(context)
44
- context.envious_receivers.each do |ref|
45
- found(context, "refers to #{SexpFormatter.format(ref)} more than self")
47
+ # @return [Array<SmellWarning>]
48
+ #
49
+ def examine_context(method_ctx)
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})
46
55
  end
47
56
  end
48
57
  end
@@ -0,0 +1,37 @@
1
+ require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
2
+ require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'source')
3
+
4
+ module Reek
5
+ module Smells
6
+
7
+ #
8
+ # It is considered good practice to annotate every class and module
9
+ # with a brief comment outlining its responsibilities.
10
+ #
11
+ class IrresponsibleModule < SmellDetector
12
+
13
+ SMELL_CLASS = self.name.split(/::/)[-1]
14
+ SMELL_SUBCLASS = SMELL_CLASS
15
+
16
+ MODULE_NAME_KEY = 'module_name'
17
+
18
+ def self.contexts # :nodoc:
19
+ [:class]
20
+ end
21
+
22
+ #
23
+ # Checks the given class or module for a descriptive comment.
24
+ #
25
+ # @return [Array<SmellWarning>]
26
+ #
27
+ def examine_context(ctx)
28
+ comment = Source::CodeComment.new(ctx.exp.comments)
29
+ return [] if comment.is_descriptive?
30
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, [ctx.exp.line],
31
+ 'has no descriptive comment',
32
+ @source, SMELL_SUBCLASS, {MODULE_NAME_KEY => ctx.exp.text_name})
33
+ [smell]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
- require 'reek/smells/smell_detector'
2
- require 'reek/smell_warning'
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
3
 
4
4
  module Reek
5
5
  module Smells
@@ -16,6 +16,12 @@ module Reek
16
16
  #
17
17
  class LargeClass < SmellDetector
18
18
 
19
+ SMELL_CLASS = self.name.split(/::/)[-1]
20
+ SUBCLASS_TOO_MANY_METHODS = 'TooManyMethods'
21
+ SUBCLASS_TOO_MANY_IVARS = 'TooManyInstanceVariables'
22
+ METHOD_COUNT_KEY = 'method_count'
23
+ IVAR_COUNT_KEY = 'ivar_count'
24
+
19
25
  # The name of the config field that sets the maximum number of methods
20
26
  # permitted in a class.
21
27
  MAX_ALLOWED_METHODS_KEY = 'max_methods'
@@ -40,29 +46,41 @@ module Reek
40
46
  )
41
47
  end
42
48
 
43
- def initialize(config = LargeClass.default_config)
44
- super(config)
49
+ def initialize(source, config = LargeClass.default_config)
50
+ super(source, config)
45
51
  end
46
52
 
47
- def check_num_methods(klass) # :nodoc:
48
- actual = klass.local_nodes(:defn).length
49
- return if actual <= value(MAX_ALLOWED_METHODS_KEY, klass, DEFAULT_MAX_METHODS)
50
- found(klass, "has at least #{actual} methods")
53
+ #
54
+ # Checks +klass+ for too many methods or too many instance variables.
55
+ #
56
+ # @return [Array<SmellWarning>]
57
+ #
58
+ def examine_context(ctx)
59
+ @max_allowed_ivars = value(MAX_ALLOWED_IVARS_KEY, ctx, DEFAULT_MAX_IVARS)
60
+ @max_allowed_methods = value(MAX_ALLOWED_METHODS_KEY, ctx, DEFAULT_MAX_METHODS)
61
+ check_num_methods(ctx) + check_num_ivars(ctx)
51
62
  end
52
63
 
53
- def check_num_ivars(klass) # :nodoc:
54
- count = klass.variable_names.length
55
- return if count <= value(MAX_ALLOWED_IVARS_KEY, klass, DEFAULT_MAX_IVARS)
56
- found(klass, "has at least #{count} instance variables")
64
+ private
65
+
66
+ def check_num_methods(ctx) # :nodoc:
67
+ actual = ctx.local_nodes(:defn).length
68
+ return [] if actual <= @max_allowed_methods
69
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, [ctx.exp.line],
70
+ "has at least #{actual} methods",
71
+ @source, SUBCLASS_TOO_MANY_METHODS,
72
+ {METHOD_COUNT_KEY => actual})
73
+ [smell]
57
74
  end
58
75
 
59
- #
60
- # Checks +klass+ for too many methods or too many instance variables.
61
- # Remembers any smells found.
62
- #
63
- def examine_context(klass)
64
- check_num_methods(klass)
65
- check_num_ivars(klass)
76
+ def check_num_ivars(ctx) # :nodoc:
77
+ count = ctx.local_nodes(:iasgn).map {|iasgn| iasgn[1]}.uniq.length
78
+ return [] if count <= @max_allowed_ivars
79
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, [ctx.exp.line],
80
+ "has at least #{count} instance variables",
81
+ @source, SUBCLASS_TOO_MANY_IVARS,
82
+ {IVAR_COUNT_KEY => count})
83
+ [smell]
66
84
  end
67
85
  end
68
86
  end
@@ -1,5 +1,5 @@
1
- require 'reek/smells/smell_detector'
2
- require 'reek/smell_warning'
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
3
 
4
4
  module Reek
5
5
  module Smells
@@ -12,6 +12,11 @@ module Reek
12
12
  #
13
13
  class LongMethod < SmellDetector
14
14
 
15
+ SMELL_CLASS = self.name.split(/::/)[-1]
16
+ SUBCLASS_TOO_MANY_STATEMENTS = 'TooManyStatements'
17
+
18
+ STATEMENT_COUNT_KEY = 'statement_count'
19
+
15
20
  # The name of the config field that sets the maximum number of
16
21
  # statements permitted in any method.
17
22
  MAX_ALLOWED_STATEMENTS_KEY = 'max_statements'
@@ -25,18 +30,24 @@ module Reek
25
30
  )
26
31
  end
27
32
 
28
- def initialize(config = LongMethod.default_config)
29
- super(config)
33
+ def initialize(source, config = LongMethod.default_config)
34
+ super(source, config)
30
35
  end
31
36
 
32
37
  #
33
38
  # Checks the length of the given +method+.
34
- # Remembers any smells found.
35
39
  #
36
- def examine_context(method)
37
- num = method.num_statements
38
- return false if num <= value(MAX_ALLOWED_STATEMENTS_KEY, method, DEFAULT_MAX_STATEMENTS)
39
- found(method, "has approx #{num} statements")
40
+ # @return [Array<SmellWarning>]
41
+ #
42
+ def examine_context(ctx)
43
+ @max_allowed_statements = value(MAX_ALLOWED_STATEMENTS_KEY, ctx, DEFAULT_MAX_STATEMENTS)
44
+ num = ctx.num_statements
45
+ return [] if num <= @max_allowed_statements
46
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, [ctx.exp.line],
47
+ "has approx #{num} statements",
48
+ @source, SUBCLASS_TOO_MANY_STATEMENTS,
49
+ {STATEMENT_COUNT_KEY => num})
50
+ [smell]
40
51
  end
41
52
  end
42
53
  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
@@ -13,6 +15,11 @@ module Reek
13
15
  #
14
16
  class LongParameterList < SmellDetector
15
17
 
18
+ SMELL_CLASS = self.name.split(/::/)[-1]
19
+ SMELL_SUBCLASS = 'LongParameterList'
20
+
21
+ PARAMETER_COUNT_KEY = 'parameter_count'
22
+
16
23
  # The name of the config field that sets the maximum number of
17
24
  # parameters permitted in any method or block.
18
25
  MAX_ALLOWED_PARAMS_KEY = 'max_params'
@@ -24,25 +31,30 @@ module Reek
24
31
  def self.default_config
25
32
  super.adopt(
26
33
  MAX_ALLOWED_PARAMS_KEY => DEFAULT_MAX_ALLOWED_PARAMS,
27
- SmellConfiguration::OVERRIDES_KEY => {
34
+ Core::SmellConfiguration::OVERRIDES_KEY => {
28
35
  "initialize" => {MAX_ALLOWED_PARAMS_KEY => 5}
29
36
  }
30
37
  )
31
38
  end
32
39
 
33
- def initialize(config = LongParameterList.default_config)
34
- super(config)
35
- @action = 'has'
40
+ def initialize(source, config = LongParameterList.default_config)
41
+ super(source, config)
36
42
  end
37
43
 
38
44
  #
39
- # Checks the number of parameters in the given scope.
40
- # Remembers any smells found.
45
+ # Checks the number of parameters in the given method.
46
+ #
47
+ # @return [Array<SmellWarning>]
41
48
  #
42
49
  def examine_context(ctx)
43
- num_params = ctx.parameters.length
44
- return false if num_params <= value(MAX_ALLOWED_PARAMS_KEY, ctx, DEFAULT_MAX_ALLOWED_PARAMS)
45
- found(ctx, "#{@action} #{num_params} parameters")
50
+ @max_allowed_params = value(MAX_ALLOWED_PARAMS_KEY, ctx, DEFAULT_MAX_ALLOWED_PARAMS)
51
+ num_params = ctx.exp.arg_names.length
52
+ return [] if num_params <= @max_allowed_params
53
+ smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, [ctx.exp.line],
54
+ "has #{num_params} parameters",
55
+ @source, SMELL_SUBCLASS,
56
+ {PARAMETER_COUNT_KEY => num_params})
57
+ [smell]
46
58
  end
47
59
  end
48
60
  end
@@ -1,17 +1,53 @@
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')
2
3
 
3
4
  module Reek
4
5
  module Smells
5
6
 
6
- class LongYieldList < LongParameterList
7
+ #
8
+ # A variant on LongParameterList that checks the number of items
9
+ # passed to a block by a +yield+ call.
10
+ #
11
+ class LongYieldList < SmellDetector
7
12
 
8
- def self.contexts # :nodoc:
9
- [:yield]
13
+ SMELL_SUBCLASS = self.name.split(/::/)[-1]
14
+ SMELL_CLASS = 'LongParameterList'
15
+
16
+ # The name of the config field that sets the maximum number of
17
+ # parameters permitted in any method or block.
18
+ MAX_ALLOWED_PARAMS_KEY = 'max_params'
19
+
20
+ # The default value of the +MAX_ALLOWED_PARAMS_KEY+ configuration
21
+ # value.
22
+ DEFAULT_MAX_ALLOWED_PARAMS = 3
23
+
24
+ PARAMETER_COUNT_KEY = 'parameter_count'
25
+
26
+ def self.default_config
27
+ super.adopt(
28
+ MAX_ALLOWED_PARAMS_KEY => DEFAULT_MAX_ALLOWED_PARAMS
29
+ )
30
+ end
31
+
32
+ def initialize(source, config = LongYieldList.default_config)
33
+ super(source, config)
10
34
  end
11
35
 
12
- def initialize(config = LongYieldList.default_config)
13
- super
14
- @action = 'yields'
36
+ #
37
+ # Checks the number of parameters in the given scope.
38
+ #
39
+ # @return [Array<SmellWarning>]
40
+ #
41
+ def examine_context(method_ctx)
42
+ @max_allowed_params = value(MAX_ALLOWED_PARAMS_KEY, method_ctx, DEFAULT_MAX_ALLOWED_PARAMS)
43
+ method_ctx.local_nodes(:yield).select do |yield_node|
44
+ yield_node.args.length > @max_allowed_params
45
+ end.map do |yield_node|
46
+ num_params = yield_node.args.length
47
+ SmellWarning.new(SMELL_CLASS, method_ctx.full_name, [yield_node.line],
48
+ "yields #{num_params} parameters",
49
+ @source, SMELL_SUBCLASS, {PARAMETER_COUNT_KEY => num_params})
50
+ end
15
51
  end
16
52
  end
17
53
  end
@@ -1,5 +1,5 @@
1
- require 'reek/smells/smell_detector'
2
- require 'reek/smell_warning'
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
3
 
4
4
  module Reek
5
5
  module Smells
@@ -11,17 +11,77 @@ module Reek
11
11
  #
12
12
  class NestedIterators < SmellDetector
13
13
 
14
- def self.contexts # :nodoc:
15
- [:iter]
14
+ SMELL_CLASS = self.name.split(/::/)[-1]
15
+ SMELL_SUBCLASS = SMELL_CLASS
16
+ # SMELL: should be a subclass of UnnecessaryComplexity
17
+ NESTING_DEPTH_KEY = 'depth'
18
+
19
+ # The name of the config field that sets the maximum depth
20
+ # of nested iterators to be permitted within any single method.
21
+ MAX_ALLOWED_NESTING_KEY = 'max_allowed_nesting'
22
+
23
+ DEFAULT_MAX_ALLOWED_NESTING = 1
24
+
25
+ # The name of the config field that sets the names of any
26
+ # methods for which nesting should not be considered
27
+ IGNORE_ITERATORS_KEY = 'ignore_iterators'
28
+
29
+ DEFAULT_IGNORE_ITERATORS = []
30
+
31
+ def self.default_config
32
+ super.adopt(
33
+ MAX_ALLOWED_NESTING_KEY => DEFAULT_MAX_ALLOWED_NESTING,
34
+ IGNORE_ITERATORS_KEY => DEFAULT_IGNORE_ITERATORS
35
+ )
36
+ end
37
+
38
+ def initialize(source, config = NestedIterators.default_config)
39
+ super(source, config)
16
40
  end
17
41
 
18
42
  #
19
43
  # Checks whether the given +block+ is inside another.
20
- # Remembers any smells found.
21
44
  #
22
- def examine_context(block)
23
- return false unless block.nested_block?
24
- found(block, 'is nested')
45
+ # @return [Array<SmellWarning>]
46
+ #
47
+ def examine_context(ctx)
48
+ @ignore_iterators = value(IGNORE_ITERATORS_KEY, ctx, DEFAULT_IGNORE_ITERATORS)
49
+ @max_allowed_nesting = value(MAX_ALLOWED_NESTING_KEY, ctx, DEFAULT_MAX_ALLOWED_NESTING)
50
+ find_deepest_iterators(ctx).map do |iter|
51
+ depth = iter[1]
52
+ SmellWarning.new(SMELL_CLASS, ctx.full_name, [iter[0].line],
53
+ "contains iterators nested #{depth} deep",
54
+ @source, SMELL_SUBCLASS,
55
+ {NESTING_DEPTH_KEY => depth})
56
+ end
57
+ # BUG: no longer reports nesting outside methods (eg. in Optparse)
58
+ end
59
+
60
+ private
61
+
62
+ def find_deepest_iterators(ctx)
63
+ result = []
64
+ find_iters(ctx.exp, 1, result)
65
+ result.select {|item| item[1] > @max_allowed_nesting}
66
+ end
67
+
68
+ def find_iters(exp, depth, result)
69
+ exp.each do |elem|
70
+ next unless Sexp === elem
71
+ case elem.first
72
+ when :iter
73
+ find_iters([elem.call], depth, result)
74
+ current = result.length
75
+ call = Source::SexpFormatter.format(elem.call)
76
+ ignored = @ignore_iterators.any? { |ignore| /#{ignore}/ === call }
77
+ find_iters([elem.block], depth + (ignored ? 0 : 1), result)
78
+ result << [elem, depth] if result.length == current unless ignored
79
+ when :class, :defn, :defs, :module
80
+ next
81
+ else
82
+ find_iters(elem, depth, result)
83
+ end
84
+ end
25
85
  end
26
86
  end
27
87
  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
@@ -24,6 +23,9 @@ module Reek
24
23
  #
25
24
  class SimulatedPolymorphism < SmellDetector
26
25
 
26
+ SMELL_CLASS = self.name.split(/::/)[-1]
27
+ SMELL_SUBCLASS = 'RepeatedConditional'
28
+
27
29
  def self.contexts # :nodoc:
28
30
  [:class]
29
31
  end
@@ -38,17 +40,26 @@ module Reek
38
40
  super.adopt(MAX_IDENTICAL_IFS_KEY => DEFAULT_MAX_IFS)
39
41
  end
40
42
 
41
- def initialize(config = SimulatedPolymorphism.default_config)
42
- super(config)
43
+ def initialize(source, config = SimulatedPolymorphism.default_config)
44
+ super(source, config)
43
45
  end
44
46
 
45
47
  #
46
- # Checks the given ClassContext for multiple identical conditional tests.
47
- # Remembers any smells found.
48
+ # Checks the given class for multiple identical conditional tests.
49
+ #
50
+ # @return [Array<SmellWarning>]
48
51
  #
49
- def examine_context(klass)
50
- conditional_counts(klass).each do |key, val|
51
- found(klass, "tests #{SexpFormatter.format(key)} at least #{val} times") if val > value(MAX_IDENTICAL_IFS_KEY, klass, DEFAULT_MAX_IFS)
52
+ def examine_context(ctx)
53
+ @max_identical_ifs = value(MAX_IDENTICAL_IFS_KEY, ctx, DEFAULT_MAX_IFS)
54
+ conditional_counts(ctx).select do |key, lines|
55
+ lines.length > @max_identical_ifs
56
+ end.map do |key, lines|
57
+ occurs = lines.length
58
+ expr = key.format_ruby
59
+ SmellWarning.new(SMELL_CLASS, ctx.full_name, lines,
60
+ "tests #{expr} at least #{occurs} times",
61
+ @source, SMELL_SUBCLASS,
62
+ {'expression' => expr, 'occurrences' => occurs})
52
63
  end
53
64
  end
54
65
 
@@ -57,14 +68,14 @@ module Reek
57
68
  # the given syntax tree together with the number of times each
58
69
  # occurs. Ignores nested classes and modules.
59
70
  #
60
- def conditional_counts(klass)
61
- result = Hash.new(0)
71
+ def conditional_counts(sexp)
72
+ result = Hash.new {|hash, key| hash[key] = []}
62
73
  collector = proc { |node|
63
74
  condition = node.condition
64
- result[condition] += 1 unless condition == s(:call, nil, :block_given?, s(:arglist))
75
+ next if condition.nil? or condition == s(:call, nil, :block_given?, s(:arglist))
76
+ result[condition].push(condition.line)
65
77
  }
66
- klass.local_nodes(:if, &collector)
67
- klass.local_nodes(:case, &collector)
78
+ [:if, :case].each {|stmt| sexp.local_nodes(stmt, &collector) }
68
79
  result
69
80
  end
70
81
  end
@@ -1,4 +1,6 @@
1
- require 'reek/configuration'
1
+ require 'set'
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
@@ -7,11 +9,14 @@ module Reek
7
9
  def self.default_config
8
10
  super.adopt(EXCLUDE_KEY => ['initialize'])
9
11
  end
10
- def initialize(config = self.class.default_config)
11
- super
12
+ def initialize(source, config = self.class.default_config)
13
+ super(source, config)
12
14
  end
13
15
  end
14
16
 
17
+ #
18
+ # Shared responsibilities of all smell detectors.
19
+ #
15
20
  class SmellDetector
16
21
 
17
22
  # The name of the config field that lists the names of code contexts
@@ -24,25 +29,28 @@ module Reek
24
29
  DEFAULT_EXCLUDE_SET = []
25
30
 
26
31
  class << self
27
- def contexts # :nodoc:
32
+ def contexts
28
33
  [:defn, :defs]
29
34
  end
30
35
 
31
36
  def default_config
32
37
  {
33
- SmellConfiguration::ENABLED_KEY => true,
38
+ Core::SmellConfiguration::ENABLED_KEY => true,
34
39
  EXCLUDE_KEY => DEFAULT_EXCLUDE_SET
35
40
  }
36
41
  end
37
42
  end
38
43
 
39
- def initialize(config = self.class.default_config)
40
- @config = SmellConfiguration.new(config)
41
- @smells_found = Set.new
42
- @masked = false
44
+ attr_reader :smells_found # SMELL: only published for tests
45
+
46
+ def initialize(source, config = self.class.default_config)
47
+ @source = source
48
+ @config = Core::SmellConfiguration.new(config)
49
+ @smells_found = []
43
50
  end
44
51
 
45
- def listen_to(hooks)
52
+ def register(hooks)
53
+ return unless @config.enabled?
46
54
  self.class.contexts.each { |ctx| hooks[ctx] << self }
47
55
  end
48
56
 
@@ -51,66 +59,36 @@ module Reek
51
59
  @config.enabled?
52
60
  end
53
61
 
54
- def configure(config)
55
- my_part = config[self.class.name.split(/::/)[-1]]
56
- return unless my_part
57
- configure_with(my_part)
58
- end
59
-
60
62
  def configure_with(config)
61
63
  @config.adopt!(config)
62
64
  end
63
65
 
64
- def copy
65
- self.class.new(@config.deep_copy)
66
- end
67
-
68
- def supersede_with(config)
69
- clone = self.copy
70
- @masked = true
71
- clone.configure_with(config)
72
- clone
73
- end
74
-
75
66
  def examine(context)
76
- before = @smells_found.size
77
- examine_context(context) if @config.enabled? and !exception?(context)
78
- @smells_found.length > before
67
+ enabled = @config.enabled? && config_for(context)[Core::SmellConfiguration::ENABLED_KEY] != false
68
+ if enabled && !exception?(context)
69
+ sm = examine_context(context)
70
+ @smells_found += sm
71
+ end
79
72
  end
80
73
 
81
74
  def examine_context(context)
82
75
  end
83
-
76
+
84
77
  def exception?(context)
85
78
  context.matches?(value(EXCLUDE_KEY, context, DEFAULT_EXCLUDE_SET))
86
79
  end
87
80
 
88
- def found(context, message)
89
- smell = SmellWarning.new(self, context.full_name, context.exp.line, message, @masked)
90
- @smells_found << smell
91
- smell
92
- end
93
-
94
- def has_smell?(patterns)
95
- return false if @masked
96
- @smells_found.each { |warning| return true if warning.contains_all?(patterns) }
97
- false
98
- end
99
-
100
81
  def report_on(report)
101
82
  @smells_found.each { |smell| smell.report_on(report) }
102
83
  end
103
84
 
104
- def num_smells
105
- @masked ? 0 : @smells_found.length
106
- end
107
-
108
- def smelly?
109
- (not @masked) and (@smells_found.length > 0)
85
+ def value(key, ctx, fall_back)
86
+ config_for(ctx)[key] || @config.value(key, ctx, fall_back)
110
87
  end
111
88
 
112
- def value(key, ctx, fall_back)
113
- @config.value(key, ctx, fall_back)
89
+ def config_for(ctx)
90
+ ctx.config[self.class.name.split(/::/)[-1]] || {}
91
+ # BUG: needs to consider smell class AND subclass
114
92
  end
115
93
  end
116
94
  end