reek 1.2.3 → 1.2.4

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 (47) hide show
  1. data/History.txt +9 -1
  2. data/features/options.feature +1 -9
  3. data/features/samples.feature +7 -2
  4. data/features/step_definitions/reek_steps.rb +3 -3
  5. data/lib/reek.rb +1 -1
  6. data/lib/reek/adapters/application.rb +22 -27
  7. data/lib/reek/adapters/command_line.rb +41 -42
  8. data/lib/reek/adapters/report.rb +30 -23
  9. data/lib/reek/adapters/spec.rb +1 -1
  10. data/lib/reek/code_context.rb +6 -2
  11. data/lib/reek/code_parser.rb +3 -7
  12. data/lib/reek/detector_stack.rb +2 -4
  13. data/lib/reek/help_command.rb +14 -0
  14. data/lib/reek/masking_collection.rb +33 -0
  15. data/lib/reek/method_context.rb +18 -6
  16. data/lib/reek/module_context.rb +0 -13
  17. data/lib/reek/reek_command.rb +28 -0
  18. data/lib/reek/singleton_method_context.rb +1 -1
  19. data/lib/reek/smell_warning.rb +5 -3
  20. data/lib/reek/smells/attribute.rb +17 -1
  21. data/lib/reek/smells/class_variable.rb +1 -1
  22. data/lib/reek/smells/control_couple.rb +13 -10
  23. data/lib/reek/smells/large_class.rb +1 -1
  24. data/lib/reek/smells/long_method.rb +0 -2
  25. data/lib/reek/smells/simulated_polymorphism.rb +2 -2
  26. data/lib/reek/sniffer.rb +1 -3
  27. data/lib/reek/tree_dresser.rb +35 -23
  28. data/lib/reek/version_command.rb +14 -0
  29. data/reek.gemspec +3 -3
  30. data/spec/reek/adapters/report_spec.rb +8 -8
  31. data/spec/reek/adapters/should_reek_of_spec.rb +1 -1
  32. data/spec/reek/adapters/should_reek_only_of_spec.rb +2 -2
  33. data/spec/reek/adapters/should_reek_spec.rb +3 -3
  34. data/spec/reek/code_context_spec.rb +11 -11
  35. data/spec/reek/code_parser_spec.rb +0 -88
  36. data/spec/reek/help_command_spec.rb +24 -0
  37. data/spec/reek/masking_collection_spec.rb +236 -0
  38. data/spec/reek/method_context_spec.rb +43 -1
  39. data/spec/reek/reek_command_spec.rb +45 -0
  40. data/spec/reek/smell_warning_spec.rb +12 -4
  41. data/spec/reek/smells/attribute_spec.rb +79 -7
  42. data/spec/reek/smells/control_couple_spec.rb +40 -11
  43. data/spec/reek/smells/long_parameter_list_spec.rb +1 -1
  44. data/spec/reek/smells/smell_detector_spec.rb +0 -17
  45. data/spec/reek/tree_dresser_spec.rb +20 -0
  46. data/spec/reek/version_command_spec.rb +29 -0
  47. metadata +11 -2
@@ -91,7 +91,6 @@ module Reek
91
91
 
92
92
  def process_call(exp)
93
93
  @element.record_call_to(exp)
94
- @element.check_for_attribute_declaration(exp)
95
94
  process_default(exp)
96
95
  end
97
96
 
@@ -120,20 +119,17 @@ module Reek
120
119
 
121
120
  def process_until(exp)
122
121
  count_clause(exp[2])
123
- process_default(exp)
124
- @element.count_statements(-1)
122
+ process_case(exp)
125
123
  end
126
124
 
127
125
  def process_for(exp)
128
126
  count_clause(exp[3])
129
- process_default(exp)
130
- @element.count_statements(-1)
127
+ process_case(exp)
131
128
  end
132
129
 
133
130
  def process_rescue(exp)
134
131
  count_clause(exp[1])
135
- process_default(exp)
136
- @element.count_statements(-1)
132
+ process_case(exp)
137
133
  end
138
134
 
139
135
  def process_resbody(exp)
@@ -20,9 +20,7 @@ module Reek
20
20
  end
21
21
 
22
22
  def num_smells
23
- total = 0
24
- @detectors.each { |det| total += det.num_smells }
25
- total
23
+ @detectors.inject(0) { |total, detector| total += detector.num_smells }
26
24
  end
27
25
 
28
26
  def has_smell?(patterns)
@@ -31,7 +29,7 @@ module Reek
31
29
  end
32
30
 
33
31
  def smelly?
34
- # SMELL: Duplication: look at al those loops!
32
+ # SMELL: Duplication: look at all those loops!
35
33
  @detectors.each { |det| return true if det.smelly? }
36
34
  false
37
35
  end
@@ -0,0 +1,14 @@
1
+
2
+ module Reek
3
+
4
+ class HelpCommand
5
+ def initialize(parser)
6
+ @parser = parser
7
+ end
8
+ def execute(view)
9
+ view.output(@parser.to_s)
10
+ view.report_success
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,33 @@
1
+ require 'set'
2
+
3
+ #
4
+ # A set of items that can be "masked" or "visible". If an item is added
5
+ # more than once, only the "visible" copy is retained.
6
+ #
7
+ class MaskingCollection
8
+ def initialize
9
+ @visible_items = SortedSet.new
10
+ @masked_items = SortedSet.new
11
+ end
12
+ def add(item)
13
+ @visible_items.add(item)
14
+ @masked_items.delete(item) if @masked_items.include?(item)
15
+ end
16
+ def add_masked(item)
17
+ @masked_items.add(item) unless @visible_items.include?(item)
18
+ end
19
+ def num_visible_items
20
+ @visible_items.length
21
+ end
22
+ def num_masked_items
23
+ @masked_items.length
24
+ end
25
+ def each_item(&blk)
26
+ all = SortedSet.new(@visible_items)
27
+ all.merge(@masked_items)
28
+ all.each(&blk)
29
+ end
30
+ def each_visible_item(&blk)
31
+ @visible_items.each(&blk)
32
+ end
33
+ end
@@ -28,14 +28,26 @@ end
28
28
  module Reek
29
29
 
30
30
  module MethodParameters
31
- def self.is_arg?(param)
32
- return false if (Array === param and param[0] == :block)
31
+ def default_assignments
32
+ assignments = self[-1]
33
+ result = {}
34
+ return result unless is_assignment_block?(assignments)
35
+ assignments[1..-1].each do |exp|
36
+ result[exp[1]] = exp[2] if exp[0] == :lasgn
37
+ end
38
+ result
39
+ end
40
+ def is_arg?(param)
41
+ return false if is_assignment_block?(param)
33
42
  return !(param.to_s =~ /^\&/)
34
43
  end
44
+ def is_assignment_block?(param)
45
+ Array === param and param[0] == :block
46
+ end
35
47
 
36
48
  def names
37
49
  return @names if @names
38
- @names = self[1..-1].select {|arg| MethodParameters.is_arg?(arg)}.map {|arg| Name.new(arg)}
50
+ @names = self[1..-1].select {|arg| is_arg?(arg)}.map {|arg| Name.new(arg)}
39
51
  end
40
52
 
41
53
  def length
@@ -57,7 +69,8 @@ module Reek
57
69
  attr_reader :refs
58
70
  attr_reader :num_statements
59
71
 
60
- def initialize(outer, exp, record = true)
72
+ def initialize(outer, exp)
73
+ # SMELL: Unused Parameter
61
74
  super(outer, exp)
62
75
  @parameters = exp[exp[0] == :defn ? 2 : 3] # SMELL: SimulatedPolymorphism
63
76
  @parameters ||= []
@@ -67,7 +80,7 @@ module Reek
67
80
  @calls = Hash.new(0)
68
81
  @depends_on_self = false
69
82
  @refs = ObjectRefs.new
70
- @outer.record_method(self)
83
+ @outer.record_method(self) # SMELL: these could be found by tree walking
71
84
  end
72
85
 
73
86
  def count_statements(num)
@@ -85,7 +98,6 @@ module Reek
85
98
  def record_call_to(exp)
86
99
  @calls[exp] += 1
87
100
  record_receiver(exp)
88
- check_for_attribute_declaration(exp)
89
101
  end
90
102
 
91
103
  def record_receiver(exp)
@@ -18,12 +18,9 @@ module Reek
18
18
  end
19
19
  end
20
20
 
21
- attr_reader :attributes
22
-
23
21
  def initialize(outer, name, exp)
24
22
  super(outer, exp)
25
23
  @name = name
26
- @attributes = Set.new
27
24
  @parsed_methods = []
28
25
  end
29
26
 
@@ -40,20 +37,10 @@ module Reek
40
37
  @parsed_methods.select {|meth| meth.parameters.length >= min_clump_size }
41
38
  end
42
39
 
43
- def record_attribute(attr)
44
- @attributes << Name.new(attr)
45
- end
46
-
47
40
  def record_method(meth)
48
41
  @parsed_methods << meth
49
42
  end
50
43
 
51
- def check_for_attribute_declaration(exp)
52
- if [:attr, :attr_reader, :attr_writer, :attr_accessor].include? exp[2]
53
- exp[3][1..-1].each {|arg| record_attribute(arg[1])}
54
- end
55
- end
56
-
57
44
  def outer_name
58
45
  "#{@outer.outer_name}#{@name}::"
59
46
  end
@@ -0,0 +1,28 @@
1
+ require 'reek/sniffer'
2
+
3
+ module Reek
4
+
5
+ class ReekCommand
6
+
7
+ def initialize(sources, report_class, show_all)
8
+ @sniffer = sources.length > 0 ? sources.sniff : sniff_stdin
9
+ @report_class = report_class
10
+ @show_all = show_all
11
+ end
12
+
13
+ def sniff_stdin
14
+ Reek::Sniffer.new($stdin.to_reek_source('$stdin'))
15
+ end
16
+
17
+ def execute(view)
18
+ rpt = @report_class.new(@sniffer.sniffers, @show_all)
19
+ view.output(rpt.report)
20
+ if @sniffer.smelly?
21
+ view.report_smells
22
+ else
23
+ view.report_success
24
+ end
25
+ end
26
+ end
27
+
28
+ end
@@ -6,7 +6,7 @@ module Reek
6
6
  class SingletonMethodContext < MethodContext
7
7
 
8
8
  def initialize(outer, exp)
9
- super(outer, exp, false)
9
+ super(outer, exp)
10
10
  @name = Name.new(exp[2])
11
11
  @receiver = SexpFormatter.format(exp[1])
12
12
  record_depends_on_self
@@ -21,7 +21,9 @@ module Reek
21
21
  sort_key <=> other.sort_key
22
22
  end
23
23
 
24
- alias eql? <=> # :nodoc:
24
+ def eql?(other)
25
+ (self <=> other) == 0
26
+ end
25
27
 
26
28
  #
27
29
  # Returns +true+ only if this is a warning about an instance of
@@ -49,9 +51,9 @@ module Reek
49
51
 
50
52
  def report_on(report)
51
53
  if @is_masked
52
- report.record_masked_smell(self)
54
+ report.found_masked_smell(self)
53
55
  else
54
- report << self
56
+ report.found_smell(self)
55
57
  end
56
58
  end
57
59
  end
@@ -18,6 +18,8 @@ module Reek
18
18
  #
19
19
  class Attribute < SmellDetector
20
20
 
21
+ ATTRIBUTE_METHODS = [:attr, :attr_reader, :attr_writer, :attr_accessor]
22
+
21
23
  def self.contexts # :nodoc:
22
24
  [:class, :module]
23
25
  end
@@ -39,10 +41,24 @@ module Reek
39
41
  # MethodContext, ClassContext and ModuleContext all know which
40
42
  # calls constitute attribute declarations. Need a method on
41
43
  # ModuleContext: each_public_call.select [names] {...}
42
- mod.attributes.each do |attr|
44
+ attributes_in(mod).each do |attr|
43
45
  found(mod, "declares the attribute #{attr}")
44
46
  end
45
47
  end
48
+
49
+ #
50
+ # Collects the names of the class variables declared and/or used
51
+ # in the given module.
52
+ #
53
+ def attributes_in(mod)
54
+ 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 }
58
+ end
59
+ end
60
+ result
61
+ end
46
62
  end
47
63
  end
48
64
  end
@@ -35,7 +35,7 @@ module Reek
35
35
  result = Set.new
36
36
  collector = proc { |cvar_node| result << cvar_node.name }
37
37
  [:cvar, :cvasgn, :cvdecl].each do |stmt_type|
38
- mod.each(stmt_type, [:class, :module], &collector)
38
+ mod.local_nodes(stmt_type, &collector)
39
39
  end
40
40
  result
41
41
  end
@@ -34,24 +34,27 @@ module Reek
34
34
  # because it includes at least two different code paths.
35
35
  #
36
36
  class ControlCouple < SmellDetector
37
- include ExcludeInitialize
38
37
 
39
38
  def self.contexts # :nodoc:
40
- [:if]
39
+ [:if, :defn, :defs]
41
40
  end
42
41
 
43
42
  #
44
43
  # Checks whether the given conditional statement relies on a control couple.
45
44
  # Remembers any smells found.
46
45
  #
47
- def examine_context(cond)
48
- return unless cond.tests_a_parameter?
49
- # SMELL: Duplication
50
- # This smell is reported once for each conditional that tests the
51
- # same parameter. Which means that the same smell can recur within
52
- # a single detector. Which in turn means that SmellDetector must
53
- # use a Set to hold smells found.
54
- found(cond, "is controlled by argument #{SexpFormatter.format(cond.if_expr)}")
46
+ 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
+ when MethodContext
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}")
55
+ end
56
+ else
57
+ end
55
58
  end
56
59
  end
57
60
  end
@@ -45,7 +45,7 @@ module Reek
45
45
  end
46
46
 
47
47
  def check_num_methods(klass) # :nodoc:
48
- actual = klass.each(:defn, [:class, :module]).length
48
+ actual = klass.local_nodes(:defn).length
49
49
  return if actual <= value(MAX_ALLOWED_METHODS_KEY, klass, DEFAULT_MAX_METHODS)
50
50
  found(klass, "has at least #{actual} methods")
51
51
  end
@@ -25,8 +25,6 @@ module Reek
25
25
  )
26
26
  end
27
27
 
28
- attr_reader :max_statements
29
-
30
28
  def initialize(config = LongMethod.default_config)
31
29
  super(config)
32
30
  end
@@ -63,8 +63,8 @@ module Reek
63
63
  condition = node.condition
64
64
  result[condition] += 1 unless condition == s(:call, nil, :block_given?, s(:arglist))
65
65
  }
66
- klass.each(:if, [:class, :module], &collector)
67
- klass.each(:case, [:class, :module], &collector)
66
+ klass.local_nodes(:if, &collector)
67
+ klass.local_nodes(:case, &collector)
68
68
  result
69
69
  end
70
70
  end
data/lib/reek/sniffer.rb CHANGED
@@ -166,9 +166,7 @@ private
166
166
  end
167
167
 
168
168
  def num_smells
169
- total = 0
170
- @sniffers.each {|sniffer| total += sniffer.num_smells}
171
- total
169
+ @sniffers.inject(0) {|total, sniffer| total += sniffer.num_smells}
172
170
  end
173
171
 
174
172
  def sniff
@@ -32,43 +32,55 @@ module Reek
32
32
  end
33
33
  end
34
34
 
35
- module CvarNode
36
- def name
37
- self[1]
35
+ module SexpExtensions
36
+ module CaseNode
37
+ def condition
38
+ self[1]
39
+ end
40
+ end
41
+
42
+ module CallNode
43
+ def receiver() self[1] end
44
+ def method_name() self[2] end
45
+ def args() self[3] end
46
+ def arg_names
47
+ args[1..-1].map {|arg| arg[1]}
48
+ end
38
49
  end
39
- end
40
50
 
41
- module IfNode
42
- def condition
43
- self[1]
51
+ module CvarNode
52
+ def name() self[1] end
44
53
  end
45
- end
46
54
 
47
- module CaseNode
48
- def condition
49
- self[1]
55
+ CvasgnNode = CvarNode
56
+ CvdeclNode = CvarNode
57
+
58
+ module DefnNode
59
+ def method_name() self[1] end
60
+ def parameters() self[2] end
61
+ end
62
+
63
+ module IfNode
64
+ def condition
65
+ self[1]
66
+ end
50
67
  end
51
68
  end
52
69
 
53
70
  class TreeDresser
54
- # SMELL: Duplication
55
- # Put these into a new module and build the mapping automagically
56
- # based on the node type
57
- EXTENSIONS = {
58
- :cvar => CvarNode,
59
- :cvasgn => CvarNode,
60
- :cvdecl => CvarNode,
61
- :if => IfNode,
62
- :case => CaseNode,
63
- }
64
71
 
65
72
  def dress(sexp)
66
73
  sexp.extend(SexpNode)
67
- if EXTENSIONS.has_key?(sexp[0])
68
- sexp.extend(EXTENSIONS[sexp[0]])
74
+ module_name = extensions_for(sexp.sexp_type)
75
+ if Reek::SexpExtensions.const_defined?(module_name)
76
+ sexp.extend(Reek::SexpExtensions.const_get(module_name))
69
77
  end
70
78
  sexp[0..-1].each { |sub| dress(sub) if Array === sub }
71
79
  sexp
72
80
  end
81
+
82
+ def extensions_for(node_type)
83
+ "#{node_type.to_s.capitalize}Node"
84
+ end
73
85
  end
74
86
  end