reek 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/History.txt +44 -4
  2. data/License.txt +20 -0
  3. data/README.rdoc +83 -0
  4. data/Rakefile +0 -1
  5. data/bin/reek +3 -11
  6. data/config/defaults.reek +20 -1
  7. data/features/masking_smells.feature +111 -0
  8. data/features/options.feature +49 -0
  9. data/features/reports.feature +90 -0
  10. data/features/samples.feature +284 -0
  11. data/features/stdin.feature +43 -0
  12. data/features/step_definitions/reek_steps.rb +35 -0
  13. data/features/support/env.rb +38 -0
  14. data/lib/reek.rb +1 -1
  15. data/lib/reek/adapters/application.rb +47 -0
  16. data/lib/reek/adapters/config_file.rb +31 -0
  17. data/lib/reek/adapters/core_extras.rb +72 -0
  18. data/lib/reek/{object_source.rb → adapters/object_source.rb} +15 -19
  19. data/lib/reek/{rake_task.rb → adapters/rake_task.rb} +2 -2
  20. data/lib/reek/adapters/report.rb +91 -0
  21. data/lib/reek/adapters/source.rb +53 -0
  22. data/lib/reek/{spec.rb → adapters/spec.rb} +45 -60
  23. data/lib/reek/block_context.rb +1 -1
  24. data/lib/reek/class_context.rb +26 -6
  25. data/lib/reek/code_context.rb +8 -0
  26. data/lib/reek/code_parser.rb +82 -39
  27. data/lib/reek/command_line.rb +85 -0
  28. data/lib/reek/configuration.rb +51 -0
  29. data/lib/reek/detector_stack.rb +39 -0
  30. data/lib/reek/exceptions.reek +8 -1
  31. data/lib/reek/method_context.rb +53 -11
  32. data/lib/reek/module_context.rb +1 -2
  33. data/lib/reek/name.rb +8 -2
  34. data/lib/reek/sexp_formatter.rb +2 -0
  35. data/lib/reek/smell_warning.rb +26 -8
  36. data/lib/reek/smells/control_couple.rb +8 -4
  37. data/lib/reek/smells/data_clump.rb +88 -0
  38. data/lib/reek/smells/duplication.rb +11 -9
  39. data/lib/reek/smells/feature_envy.rb +3 -4
  40. data/lib/reek/smells/large_class.rb +17 -17
  41. data/lib/reek/smells/long_method.rb +10 -8
  42. data/lib/reek/smells/long_parameter_list.rb +16 -10
  43. data/lib/reek/smells/long_yield_list.rb +1 -1
  44. data/lib/reek/smells/nested_iterators.rb +3 -3
  45. data/lib/reek/smells/simulated_polymorphism.rb +58 -0
  46. data/lib/reek/smells/smell_detector.rb +94 -27
  47. data/lib/reek/smells/uncommunicative_name.rb +23 -23
  48. data/lib/reek/smells/utility_function.rb +27 -11
  49. data/lib/reek/sniffer.rb +183 -0
  50. data/reek.gemspec +5 -5
  51. data/spec/quality/reek_source_spec.rb +15 -0
  52. data/spec/reek/adapters/report_spec.rb +49 -0
  53. data/spec/reek/adapters/should_reek_of_spec.rb +108 -0
  54. data/spec/reek/adapters/should_reek_only_of_spec.rb +87 -0
  55. data/spec/reek/adapters/should_reek_spec.rb +92 -0
  56. data/spec/reek/block_context_spec.rb +7 -1
  57. data/spec/reek/class_context_spec.rb +39 -16
  58. data/spec/reek/code_context_spec.rb +7 -7
  59. data/spec/reek/code_parser_spec.rb +6 -1
  60. data/spec/reek/config_spec.rb +3 -3
  61. data/spec/reek/configuration_spec.rb +12 -0
  62. data/spec/reek/method_context_spec.rb +2 -2
  63. data/spec/reek/name_spec.rb +24 -0
  64. data/spec/reek/object_source_spec.rb +23 -0
  65. data/spec/reek/singleton_method_context_spec.rb +2 -2
  66. data/spec/reek/smell_warning_spec.rb +53 -0
  67. data/spec/reek/smells/data_clump_spec.rb +87 -0
  68. data/spec/reek/smells/duplication_spec.rb +13 -17
  69. data/spec/reek/smells/feature_envy_spec.rb +23 -28
  70. data/spec/reek/smells/large_class_spec.rb +109 -34
  71. data/spec/reek/smells/long_method_spec.rb +140 -3
  72. data/spec/reek/smells/long_parameter_list_spec.rb +1 -2
  73. data/spec/reek/smells/simulated_polymorphism_spec.rb +50 -0
  74. data/spec/reek/smells/smell_detector_spec.rb +53 -0
  75. data/spec/reek/smells/uncommunicative_name_spec.rb +20 -7
  76. data/spec/reek/smells/utility_function_spec.rb +76 -67
  77. data/spec/reek/sniffer_spec.rb +10 -0
  78. data/spec/samples/all_but_one_masked/clean_one.rb +6 -0
  79. data/spec/samples/all_but_one_masked/dirty.rb +7 -0
  80. data/spec/samples/all_but_one_masked/masked.reek +5 -0
  81. data/spec/samples/clean_due_to_masking/clean_one.rb +6 -0
  82. data/spec/samples/clean_due_to_masking/clean_three.rb +6 -0
  83. data/spec/samples/clean_due_to_masking/clean_two.rb +6 -0
  84. data/spec/samples/clean_due_to_masking/dirty_one.rb +7 -0
  85. data/spec/samples/clean_due_to_masking/dirty_two.rb +7 -0
  86. data/spec/samples/clean_due_to_masking/masked.reek +7 -0
  87. data/spec/samples/corrupt_config_file/corrupt.reek +1 -0
  88. data/spec/samples/corrupt_config_file/dirty.rb +7 -0
  89. data/spec/samples/empty_config_file/dirty.rb +7 -0
  90. data/spec/samples/empty_config_file/empty.reek +0 -0
  91. data/spec/samples/exceptions.reek +4 -0
  92. data/spec/{slow/samples → samples}/inline.rb +0 -0
  93. data/spec/samples/masked/dirty.rb +7 -0
  94. data/spec/samples/masked/masked.reek +3 -0
  95. data/spec/samples/mixed_results/clean_one.rb +6 -0
  96. data/spec/samples/mixed_results/clean_three.rb +6 -0
  97. data/spec/samples/mixed_results/clean_two.rb +6 -0
  98. data/spec/samples/mixed_results/dirty_one.rb +7 -0
  99. data/spec/samples/mixed_results/dirty_two.rb +7 -0
  100. data/spec/samples/not_quite_masked/dirty.rb +8 -0
  101. data/spec/samples/not_quite_masked/masked.reek +5 -0
  102. data/spec/{slow/samples → samples}/optparse.rb +0 -0
  103. data/spec/samples/overrides/masked/dirty.rb +7 -0
  104. data/spec/samples/overrides/masked/lower.reek +5 -0
  105. data/spec/samples/overrides/upper.reek +5 -0
  106. data/spec/{slow/samples → samples}/redcloth.rb +0 -0
  107. data/spec/samples/three_clean_files/clean_one.rb +6 -0
  108. data/spec/samples/three_clean_files/clean_three.rb +6 -0
  109. data/spec/samples/three_clean_files/clean_two.rb +6 -0
  110. data/spec/samples/two_smelly_files/dirty_one.rb +7 -0
  111. data/spec/samples/two_smelly_files/dirty_two.rb +7 -0
  112. data/spec/spec.opts +1 -1
  113. data/spec/spec_helper.rb +4 -4
  114. data/tasks/reek.rake +8 -5
  115. data/tasks/test.rake +51 -0
  116. metadata +75 -25
  117. data/README.txt +0 -6
  118. data/lib/reek/options.rb +0 -92
  119. data/lib/reek/report.rb +0 -81
  120. data/lib/reek/smells/smells.rb +0 -81
  121. data/lib/reek/source.rb +0 -127
  122. data/spec/reek/options_spec.rb +0 -13
  123. data/spec/reek/report_spec.rb +0 -48
  124. data/spec/reek/smells/smell_spec.rb +0 -24
  125. data/spec/slow/inline_spec.rb +0 -43
  126. data/spec/slow/optparse_spec.rb +0 -108
  127. data/spec/slow/redcloth_spec.rb +0 -101
  128. data/spec/slow/reek_source_spec.rb +0 -20
  129. data/spec/slow/script_spec.rb +0 -55
  130. data/spec/slow/source_list_spec.rb +0 -40
  131. data/tasks/rspec.rake +0 -21
@@ -11,7 +11,7 @@ module Reek
11
11
  case slice(0)
12
12
  when :masgn
13
13
  @names = arg[1..-1].map {|lasgn| Name.new(lasgn[1]) }
14
- when :lasgn
14
+ when :lasgn, :iasgn
15
15
  @names = [Name.new(arg)]
16
16
  end
17
17
  end
@@ -2,8 +2,11 @@ require 'set'
2
2
  require 'reek/code_context'
3
3
 
4
4
  class Class
5
- def is_overriding_method?(sym)
6
- instance_methods(false).include?(sym) and superclass.instance_methods(true).include?(sym)
5
+ def is_overriding_method?(name)
6
+ sym = name.to_sym
7
+ mine = instance_methods(false)
8
+ dads = superclass.instance_methods(true)
9
+ (mine.include?(sym) and dads.include?(sym)) or (mine.include?(name) and dads.include?(name))
7
10
  end
8
11
  end
9
12
 
@@ -15,12 +18,22 @@ module Reek
15
18
  ClassContext.new(res[0], res[1], exp[2])
16
19
  end
17
20
 
21
+ def ClassContext.from_s(src)
22
+ source = src.to_reek_source
23
+ sniffer = Sniffer.new(source)
24
+ CodeParser.new(sniffer).process_class(source.syntax_tree)
25
+ end
26
+
27
+ attr_reader :conditionals, :parsed_methods
28
+
29
+ # SMELL: inconsistent with other contexts (not linked to the sexp)
18
30
  def initialize(outer, name, superclass = nil)
19
31
  super(outer, nil)
20
32
  @name = name
21
33
  @superclass = superclass
22
34
  @parsed_methods = []
23
35
  @instance_variables = Set.new
36
+ @conditionals = []
24
37
  end
25
38
 
26
39
  def myself
@@ -28,9 +41,8 @@ module Reek
28
41
  end
29
42
 
30
43
  def find_module(modname)
31
- sym = modname.to_s
32
44
  return nil unless myself
33
- @myself.const_defined?(sym) ? @myself.const_get(sym) : nil
45
+ @myself.const_or_nil(modname.to_s)
34
46
  end
35
47
 
36
48
  def is_overriding_method?(name)
@@ -50,8 +62,8 @@ module Reek
50
62
  @instance_variables << Name.new(sym)
51
63
  end
52
64
 
53
- def record_method(name)
54
- @parsed_methods << name.to_s
65
+ def record_method(meth)
66
+ @parsed_methods << meth
55
67
  end
56
68
 
57
69
  def outer_name
@@ -65,5 +77,13 @@ module Reek
65
77
  def variable_names
66
78
  @instance_variables
67
79
  end
80
+
81
+ def record_conditional(exp)
82
+ @conditionals << exp
83
+ end
84
+
85
+ def parameterized_methods(min_clump_size)
86
+ parsed_methods.select {|meth| meth.parameters.length >= min_clump_size }
87
+ end
68
88
  end
69
89
  end
@@ -1,3 +1,10 @@
1
+ class Module
2
+
3
+ def const_or_nil(sym)
4
+ const_defined?(sym) ? const_get(sym) : nil
5
+ end
6
+ end
7
+
1
8
  module Reek
2
9
 
3
10
  #
@@ -16,6 +23,7 @@ module Reek
16
23
  @myself = nil
17
24
  end
18
25
 
26
+ # SMELL: Temporary Field -- @name isn't always initialized
19
27
  def matches?(strings)
20
28
  me = @name.to_s
21
29
  strings.any? do |str|
@@ -9,15 +9,33 @@ require 'reek/method_context'
9
9
  require 'reek/singleton_method_context'
10
10
  require 'reek/yield_call_context'
11
11
 
12
+ #
13
+ # Extensions to +Sexp+ to allow +CodeParser+ to navigate the abstract
14
+ # syntax tree more easily.
15
+ #
16
+ class Sexp
17
+ def children
18
+ find_all { |item| Sexp === item }
19
+ end
20
+
21
+ def is_language_node?
22
+ first.class == Symbol
23
+ end
24
+
25
+ def has_type?(type)
26
+ is_language_node? and first == type
27
+ end
28
+ end
29
+
12
30
  module Reek
13
31
 
14
32
  class CodeParser
15
33
 
16
- # Creates a new Ruby code checker. Any smells discovered
17
- # will be stored in +report+.
18
- def initialize(report, smells, ctx = StopContext.new)
19
- @report = report
20
- @smells = smells
34
+ #
35
+ # Creates a new Ruby code checker.
36
+ #
37
+ def initialize(sniffer, ctx = StopContext.new)
38
+ @sniffer = sniffer
21
39
  @element = ctx
22
40
  end
23
41
 
@@ -25,6 +43,7 @@ module Reek
25
43
  meth = "process_#{exp[0]}"
26
44
  meth = :process_default unless self.respond_to?(meth)
27
45
  self.send(meth, exp)
46
+ @element
28
47
  end
29
48
 
30
49
  def process_default(exp)
@@ -39,10 +58,12 @@ module Reek
39
58
  end
40
59
 
41
60
  def process_class(exp)
42
- push(ClassContext.create(@element, exp)) do
61
+ scope = ClassContext.create(@element, exp)
62
+ push(scope) do
43
63
  process_default(exp) unless @element.is_struct?
44
64
  check_smells(:class)
45
65
  end
66
+ scope
46
67
  end
47
68
 
48
69
  def process_defn(exp)
@@ -53,14 +74,16 @@ module Reek
53
74
  handle_context(SingletonMethodContext, :defs, exp)
54
75
  end
55
76
 
56
- def process_args(exp)
57
- exp[1..-1].each {|sym| @element.record_parameter(sym) }
58
- end
77
+ def process_args(exp) end
59
78
 
60
79
  def process_attrset(exp)
61
80
  @element.record_depends_on_self if /^@/ === exp[1].to_s
62
81
  end
63
82
 
83
+ def process_zsuper(exp)
84
+ @element.record_use_of_self
85
+ end
86
+
64
87
  def process_lit(exp)
65
88
  val = exp[1]
66
89
  @element.record_depends_on_self if val == :self
@@ -70,11 +93,6 @@ module Reek
70
93
  process(exp[1])
71
94
  handle_context(BlockContext, :iter, exp[2..-1])
72
95
  end
73
-
74
- def process_dasgn_curr(exp)
75
- @element.record_parameter(exp[1])
76
- process_default(exp)
77
- end
78
96
 
79
97
  def process_block(exp)
80
98
  @element.count_statements(CodeParser.count_statements(exp))
@@ -90,19 +108,10 @@ module Reek
90
108
  process_default(exp)
91
109
  end
92
110
 
93
- def process_fcall(exp)
94
- @element.record_use_of_self
95
- process_default(exp)
96
- end
97
-
98
111
  def process_cfunc(exp)
99
112
  @element.record_depends_on_self
100
113
  end
101
114
 
102
- def process_vcall(exp)
103
- @element.record_use_of_self
104
- end
105
-
106
115
  def process_attrasgn(exp)
107
116
  process_call(exp)
108
117
  end
@@ -112,7 +121,46 @@ module Reek
112
121
  end
113
122
 
114
123
  def process_if(exp)
124
+ @element.record_conditional(exp[1])
125
+ count_clause(exp[2])
126
+ count_clause(exp[3])
115
127
  handle_context(IfContext, :if, exp)
128
+ @element.count_statements(-1)
129
+ end
130
+
131
+ def process_while(exp)
132
+ process_until(exp)
133
+ end
134
+
135
+ def process_until(exp)
136
+ count_clause(exp[2])
137
+ process_default(exp)
138
+ @element.count_statements(-1)
139
+ end
140
+
141
+ def process_for(exp)
142
+ count_clause(exp[3])
143
+ process_case(exp)
144
+ end
145
+
146
+ def process_rescue(exp)
147
+ count_clause(exp[1])
148
+ process_case(exp)
149
+ end
150
+
151
+ def process_resbody(exp)
152
+ process_when(exp)
153
+ end
154
+
155
+ def process_case(exp)
156
+ @element.record_conditional(exp[1])
157
+ process_default(exp)
158
+ @element.count_statements(-1)
159
+ end
160
+
161
+ def process_when(exp)
162
+ count_clause(exp[2])
163
+ process_default(exp)
116
164
  end
117
165
 
118
166
  def process_ivar(exp)
@@ -131,36 +179,35 @@ module Reek
131
179
  end
132
180
 
133
181
  def process_self(exp)
134
- @element.record_depends_on_self
182
+ @element.record_use_of_self
183
+ end
184
+
185
+ def count_clause(sexp)
186
+ if sexp and !sexp.has_type?(:block)
187
+ @element.count_statements(1)
188
+ end
135
189
  end
136
190
 
137
191
  def self.count_statements(exp)
138
192
  stmts = exp[1..-1]
139
193
  ignore = 0
140
- ignore = 1 if is_expr?(stmts[0], :args)
141
194
  ignore += 1 if stmts[1] == s(:nil)
142
195
  stmts.length - ignore
143
196
  end
144
197
 
145
198
  private
146
199
 
147
- def self.is_expr?(exp, type)
148
- Array === exp and exp[0] == type
149
- end
150
-
151
- def self.is_global_variable?(exp)
152
- is_expr?(exp, :gvar)
153
- end
154
-
155
200
  def handle_context(klass, type, exp)
156
- push(klass.new(@element, exp)) do
201
+ scope = klass.new(@element, exp)
202
+ push(scope) do
157
203
  process_default(exp)
158
204
  check_smells(type)
159
205
  end
206
+ scope
160
207
  end
161
208
 
162
209
  def check_smells(type)
163
- @smells[type].each {|smell| smell.examine(@element, @report) }
210
+ @sniffer.examine(@element, type)
164
211
  end
165
212
 
166
213
  def push(context)
@@ -169,9 +216,5 @@ module Reek
169
216
  yield
170
217
  @element = orig
171
218
  end
172
-
173
- def pop(exp)
174
- @element = @element.outer
175
- end
176
219
  end
177
220
  end
@@ -0,0 +1,85 @@
1
+ require 'optparse'
2
+ require 'reek'
3
+
4
+ module Reek
5
+
6
+ # SMELL: Greedy Module
7
+ # This creates the command-line parser AND invokes it. And for the
8
+ # -v and -h options it also executes them. And it holds the config
9
+ # options for the rest of the application.
10
+ class Options
11
+
12
+ CTX_SORT = '%m%c %w (%s)'
13
+ SMELL_SORT = '%m[%s] %c %w'
14
+
15
+ def self.default_options
16
+ {
17
+ :format => CTX_SORT,
18
+ :show_all => false,
19
+ :quiet => false
20
+ }
21
+ end
22
+
23
+ # SMELL: Global Variable
24
+ @@opts = default_options
25
+
26
+ def self.[](key)
27
+ @@opts[key]
28
+ end
29
+
30
+ def initialize(argv)
31
+ @argv = argv
32
+ @parser = OptionParser.new
33
+ set_options
34
+ end
35
+
36
+ def parse
37
+ @parser.parse!(@argv)
38
+ @argv
39
+ end
40
+
41
+ def set_options
42
+ @parser.banner = <<EOB
43
+ Usage: #{@parser.program_name} [options] [files]
44
+
45
+ Examples:
46
+
47
+ #{@parser.program_name} lib/*.rb
48
+ #{@parser.program_name} -q -a lib
49
+ cat my_class.rb | #{@parser.program_name}
50
+
51
+ See http://wiki.github.com/kevinrutherford/reek for detailed help.
52
+
53
+ EOB
54
+
55
+ @parser.separator "Common options:"
56
+
57
+ @parser.on("-h", "--help", "Show this message") do
58
+ puts @parser
59
+ exit(0)
60
+ end
61
+ @parser.on("-v", "--version", "Show version") do
62
+ puts "#{@parser.program_name} #{Reek::VERSION}"
63
+ exit(0)
64
+ end
65
+
66
+ @parser.separator "\nReport formatting:"
67
+
68
+ @parser.on("-a", "--[no-]show-all", "Show all smells, including those masked by config settings") do |opt|
69
+ @@opts[:show_all] = opt
70
+ end
71
+ @parser.on("-q", "--quiet", "Suppress headings for smell-free source files") do
72
+ @@opts[:quiet] = true
73
+ end
74
+ @parser.on('-f', "--format FORMAT", 'Specify the format of smell warnings') do |arg|
75
+ @@opts[:format] = arg unless arg.nil?
76
+ end
77
+ @parser.on('-c', '--context-first', "Sort by context; sets the format string to \"#{CTX_SORT}\"") do
78
+ @@opts[:format] = CTX_SORT
79
+ end
80
+ @parser.on('-s', '--smell-first', "Sort by smell; sets the format string to \"#{SMELL_SORT}\"") do
81
+ @@opts[:format] = SMELL_SORT
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,51 @@
1
+ module Reek
2
+
3
+ #
4
+ # Represents a single set of configuration options for a smell detector
5
+ #
6
+ class SmellConfiguration
7
+
8
+ # The name of the config field that specifies whether a smell is
9
+ # enabled. Set to +true+ or +false+.
10
+ ENABLED_KEY = 'enabled'
11
+
12
+ # The name of the config field that sets scope-specific overrides
13
+ # for other values in the current smell detector's configuration.
14
+ OVERRIDES_KEY = 'overrides'
15
+
16
+ attr_reader :hash
17
+
18
+ def initialize(hash)
19
+ @hash = hash
20
+ end
21
+
22
+ # SMELL: Getter
23
+ def enabled?
24
+ @hash[ENABLED_KEY]
25
+ end
26
+
27
+ def overrides_for(context)
28
+ Overrides.new(@hash.fetch(OVERRIDES_KEY, {})).for_context(context)
29
+ end
30
+
31
+ # Retrieves the value, if any, for the given +key+.
32
+ #
33
+ # Returns +fall_back+ if this config has no value for the key.
34
+ #
35
+ def value(key, context, fall_back)
36
+ overrides_for(context).each { |conf| return conf[key] if conf.has_key?(key) }
37
+ return @hash.fetch(key, fall_back)
38
+ end
39
+ end
40
+
41
+ class Overrides
42
+ def initialize(hash)
43
+ @hash = hash
44
+ end
45
+
46
+ def for_context(context)
47
+ contexts = @hash.keys.select {|ckey| context.matches?([ckey])}
48
+ contexts.map { |exc| @hash[exc] }
49
+ end
50
+ end
51
+ end