reek 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +5 -0
  3. data/README.md +70 -92
  4. data/config/defaults.reek +3 -0
  5. data/features/samples.feature +24 -20
  6. data/features/step_definitions/reek_steps.rb +1 -1
  7. data/features/support/env.rb +7 -7
  8. data/lib/reek/core/code_context.rb +1 -1
  9. data/lib/reek/core/code_parser.rb +19 -18
  10. data/lib/reek/core/method_context.rb +8 -7
  11. data/lib/reek/core/module_context.rb +1 -1
  12. data/lib/reek/core/smell_repository.rb +1 -0
  13. data/lib/reek/core/sniffer.rb +3 -1
  14. data/lib/reek/rake/task.rb +1 -5
  15. data/lib/reek/smell_description.rb +26 -0
  16. data/lib/reek/smell_warning.rb +35 -49
  17. data/lib/reek/smells.rb +1 -0
  18. data/lib/reek/smells/attribute.rb +1 -1
  19. data/lib/reek/smells/control_parameter.rb +14 -7
  20. data/lib/reek/smells/data_clump.rb +1 -1
  21. data/lib/reek/smells/duplicate_method_call.rb +2 -9
  22. data/lib/reek/smells/module_initialize.rb +38 -0
  23. data/lib/reek/smells/nested_iterators.rb +1 -1
  24. data/lib/reek/smells/nil_check.rb +3 -3
  25. data/lib/reek/smells/repeated_conditional.rb +3 -2
  26. data/lib/reek/smells/smell_detector.rb +1 -1
  27. data/lib/reek/smells/too_many_instance_variables.rb +1 -1
  28. data/lib/reek/smells/too_many_methods.rb +1 -1
  29. data/lib/reek/smells/uncommunicative_method_name.rb +0 -4
  30. data/lib/reek/smells/uncommunicative_parameter_name.rb +0 -4
  31. data/lib/reek/smells/uncommunicative_variable_name.rb +11 -9
  32. data/lib/reek/smells/utility_function.rb +2 -2
  33. data/lib/reek/source/ast_node.rb +40 -0
  34. data/lib/reek/source/ast_node_class_map.rb +37 -0
  35. data/lib/reek/source/reference_collector.rb +3 -3
  36. data/lib/reek/source/sexp_extensions.rb +133 -59
  37. data/lib/reek/source/sexp_formatter.rb +10 -4
  38. data/lib/reek/source/sexp_node.rb +25 -17
  39. data/lib/reek/source/source_code.rb +21 -9
  40. data/lib/reek/source/tree_dresser.rb +10 -33
  41. data/lib/reek/version.rb +1 -1
  42. data/reek.gemspec +2 -4
  43. data/spec/matchers/smell_of_matcher.rb +9 -1
  44. data/spec/quality/reek_source_spec.rb +0 -35
  45. data/spec/reek/core/code_context_spec.rb +22 -8
  46. data/spec/reek/core/method_context_spec.rb +10 -10
  47. data/spec/reek/smell_description_spec.rb +43 -0
  48. data/spec/reek/smell_warning_spec.rb +0 -3
  49. data/spec/reek/smells/control_parameter_spec.rb +24 -0
  50. data/spec/reek/smells/feature_envy_spec.rb +50 -17
  51. data/spec/reek/smells/irresponsible_module_spec.rb +25 -17
  52. data/spec/reek/smells/module_initialize_spec.rb +20 -0
  53. data/spec/reek/smells/prima_donna_method_spec.rb +2 -2
  54. data/spec/reek/smells/repeated_conditional_spec.rb +10 -4
  55. data/spec/reek/smells/too_many_instance_variables_spec.rb +47 -21
  56. data/spec/reek/smells/too_many_statements_spec.rb +11 -1
  57. data/spec/reek/smells/uncommunicative_variable_name_spec.rb +1 -1
  58. data/spec/reek/smells/utility_function_spec.rb +26 -25
  59. data/spec/reek/source/sexp_extensions_spec.rb +164 -91
  60. data/spec/reek/source/sexp_formatter_spec.rb +13 -1
  61. data/spec/reek/source/sexp_node_spec.rb +5 -5
  62. data/spec/reek/source/source_code_spec.rb +18 -6
  63. data/spec/reek/source/tree_dresser_spec.rb +5 -5
  64. data/spec/spec_helper.rb +8 -4
  65. metadata +16 -50
@@ -1,4 +1,3 @@
1
- require 'sexp'
2
1
  require 'reek/core/method_context'
3
2
  require 'reek/core/module_context'
4
3
  require 'reek/core/stop_context'
@@ -12,6 +11,7 @@ module Reek
12
11
  #
13
12
  # SMELL: This class is responsible for counting statements and for feeding
14
13
  # each context to the smell repository.
14
+ # SMELL: This class has a name that doesn't match its responsibility.
15
15
  class CodeParser
16
16
  def initialize(smell_repository, ctx = StopContext.new)
17
17
  @smell_repository = smell_repository
@@ -19,14 +19,14 @@ module Reek
19
19
  end
20
20
 
21
21
  def process(exp)
22
- meth = "process_#{exp[0]}"
22
+ meth = "process_#{exp.type}"
23
23
  meth = :process_default unless self.respond_to?(meth)
24
24
  send(meth, exp)
25
25
  @element
26
26
  end
27
27
 
28
28
  def process_default(exp)
29
- exp.each { |sub| process(sub) if sub.is_a? Array }
29
+ exp.children.each { |sub| process(sub) if sub.is_a? AST::Node }
30
30
  end
31
31
 
32
32
  def process_module(exp)
@@ -37,16 +37,16 @@ module Reek
37
37
 
38
38
  alias_method :process_class, :process_module
39
39
 
40
- def process_defn(exp)
40
+ def process_def(exp)
41
41
  inside_new_context(MethodContext, exp) do
42
- count_statement_list(exp.body)
42
+ count_clause(exp.body)
43
43
  process_default(exp)
44
44
  end
45
45
  end
46
46
 
47
47
  def process_defs(exp)
48
48
  inside_new_context(SingletonMethodContext, exp) do
49
- count_statement_list(exp.body)
49
+ count_clause(exp.body)
50
50
  process_default(exp)
51
51
  end
52
52
  end
@@ -57,20 +57,20 @@ module Reek
57
57
  # Recording of calls to methods and self
58
58
  #
59
59
 
60
- def process_call(exp)
60
+ def process_send(exp)
61
61
  @element.record_call_to(exp)
62
62
  process_default(exp)
63
63
  end
64
64
 
65
- alias_method :process_attrasgn, :process_call
66
- alias_method :process_op_asgn1, :process_call
65
+ alias_method :process_attrasgn, :process_send
66
+ alias_method :process_op_asgn, :process_send
67
67
 
68
68
  def process_ivar(exp)
69
69
  @element.record_use_of_self
70
70
  process_default(exp)
71
71
  end
72
72
 
73
- alias_method :process_iasgn, :process_ivar
73
+ alias_method :process_ivasgn, :process_ivar
74
74
 
75
75
  def process_self(_)
76
76
  @element.record_use_of_self
@@ -82,17 +82,19 @@ module Reek
82
82
  # Statement counting
83
83
  #
84
84
 
85
- def process_iter(exp)
86
- count_clause(exp[3])
85
+ def process_block(exp)
86
+ count_clause(exp.block)
87
87
  process_default(exp)
88
88
  end
89
89
 
90
- def process_block(exp)
91
- count_statement_list(exp[1..-1])
90
+ def process_begin(exp)
91
+ count_statement_list(exp.children)
92
92
  @element.count_statements(-1)
93
93
  process_default(exp)
94
94
  end
95
95
 
96
+ alias_method :process_kwbegin, :process_begin
97
+
96
98
  def process_if(exp)
97
99
  count_clause(exp[2])
98
100
  count_clause(exp[3])
@@ -126,14 +128,13 @@ module Reek
126
128
  end
127
129
 
128
130
  def process_case(exp)
129
- count_statement_list(exp[2..-1].compact)
131
+ count_clause(exp.else_body)
130
132
  @element.count_statements(-1)
131
133
  process_default(exp)
132
134
  end
133
135
 
134
136
  def process_when(exp)
135
- count_statement_list(exp[2..-1].compact)
136
- @element.count_statements(-1)
137
+ count_clause(exp.body)
137
138
  process_default(exp)
138
139
  end
139
140
 
@@ -151,7 +152,7 @@ module Reek
151
152
  scope = klass.new(@element, exp)
152
153
  push(scope) do
153
154
  yield
154
- check_smells(exp[0])
155
+ check_smells(exp.type)
155
156
  end
156
157
  scope
157
158
  end
@@ -9,8 +9,8 @@ module Reek
9
9
  module MethodParameters
10
10
  def default_assignments
11
11
  result = []
12
- self[1..-1].each do |exp|
13
- result << exp[1..2] if exp.is_a?(Sexp) && exp[0] == :lasgn
12
+ each do |exp|
13
+ result << exp[1..2] if exp.optional_argument?
14
14
  end
15
15
  result
16
16
  end
@@ -26,9 +26,8 @@ module Reek
26
26
 
27
27
  def initialize(outer, exp)
28
28
  super(outer, exp)
29
- @parameters = exp[exp[0] == :defn ? 2 : 3] # SMELL: SimulatedPolymorphism
30
- @parameters ||= []
31
- @parameters.extend(MethodParameters)
29
+ @parameters = exp.parameters.dup
30
+ @parameters.extend MethodParameters
32
31
  @num_statements = 0
33
32
  @refs = ObjectRefs.new
34
33
  end
@@ -41,6 +40,8 @@ module Reek
41
40
  receiver, meth = exp[1..2]
42
41
  receiver ||= [:self]
43
42
  case receiver[0]
43
+ when :lvasgn
44
+ @refs.record_reference_to(receiver.updated(:lvar))
44
45
  when :lvar
45
46
  @refs.record_reference_to(receiver) unless meth == :new
46
47
  when :self
@@ -58,7 +59,7 @@ module Reek
58
59
  end
59
60
 
60
61
  def uses_param?(param)
61
- local_nodes(:lvar).include?(Sexp.new(:lvar, param.to_sym))
62
+ local_nodes(:lvar).find { |node| node.var_name == param.to_sym }
62
63
  end
63
64
 
64
65
  def unused_params
@@ -70,7 +71,7 @@ module Reek
70
71
  end
71
72
 
72
73
  def uses_super_with_implicit_arguments?
73
- exp.body.contains_nested_node? :zsuper
74
+ (body = exp.body) && body.contains_nested_node?(:zsuper)
74
75
  end
75
76
  end
76
77
  end
@@ -9,7 +9,7 @@ module Reek
9
9
  class ModuleContext < CodeContext
10
10
  def initialize(outer, exp)
11
11
  super(outer, exp)
12
- @name = Source::SexpFormatter.format(exp[1])
12
+ @name = Source::SexpFormatter.format(exp.children.first)
13
13
  end
14
14
  end
15
15
  end
@@ -19,6 +19,7 @@ module Reek
19
19
  Smells::IrresponsibleModule,
20
20
  Smells::LongParameterList,
21
21
  Smells::LongYieldList,
22
+ Smells::ModuleInitialize,
22
23
  Smells::NestedIterators,
23
24
  Smells::NilCheck,
24
25
  Smells::PrimaDonnaMethod,
@@ -8,7 +8,9 @@ module Reek
8
8
  # Configures all available smell detectors and applies them to a source.
9
9
  #
10
10
  class Sniffer
11
- def initialize(src, extra_config_files = [], smell_repository = Core::SmellRepository.new(src.desc))
11
+ def initialize(src,
12
+ extra_config_files = [],
13
+ smell_repository = Core::SmellRepository.new(src.desc))
12
14
  @smell_repository = smell_repository
13
15
  @source = src
14
16
 
@@ -97,10 +97,6 @@ module Reek
97
97
  raise('Smells found!') if !system(cmd) && fail_on_error
98
98
  end
99
99
 
100
- def self.reek_script
101
- File.expand_path(File.dirname(__FILE__) + '/../../../bin/reek')
102
- end
103
-
104
100
  def self.ruby_exe
105
101
  File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
106
102
  end
@@ -108,7 +104,7 @@ module Reek
108
104
  def cmd_words
109
105
  [Task.ruby_exe] +
110
106
  ruby_options +
111
- [%("#{Task.reek_script}")] +
107
+ [%(reek)] +
112
108
  [sort_option] +
113
109
  config_file_list.map { |fn| ['-c', %("#{fn}")] }.flatten +
114
110
  source_file_list.map { |fn| %("#{fn}") }
@@ -0,0 +1,26 @@
1
+ module Reek
2
+ class SmellDescription
3
+ attr_reader :smell_class, :smell_subclass, :message, :details
4
+
5
+ def initialize(smell_class, smell_subclass, message, details)
6
+ @smell_class = smell_class
7
+ @smell_subclass = smell_subclass
8
+ @message = message
9
+ @details = details
10
+ end
11
+
12
+ def [](key)
13
+ @details[key]
14
+ end
15
+
16
+ def encode_with coder
17
+ coder.tag = nil
18
+ coder['class'] = @smell_class
19
+ coder['subclass'] = @smell_subclass
20
+ coder['message'] = @message
21
+ @details.each do |k, v|
22
+ coder[k] = v
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,71 +1,45 @@
1
+ require 'reek/smell_description'
2
+
1
3
  module Reek
2
4
  #
3
5
  # Reports a warning that a smell has been found.
4
- # This object is essentially a DTO, and therefore contains a :reek:attribute or two.
5
6
  #
6
7
  class SmellWarning
7
8
  include Comparable
8
9
 
9
- MESSAGE_KEY = 'message'
10
- SUBCLASS_KEY = 'subclass'
11
- CLASS_KEY = 'class'
12
-
13
- CONTEXT_KEY = 'context'
14
- LINES_KEY = 'lines'
15
- SOURCE_KEY = 'source'
16
-
17
- ACTIVE_KEY = 'is_active'
18
-
19
10
  def initialize(class_name, context, lines, message,
20
11
  source = '', subclass_name = '', parameters = {})
21
- @smell = {
22
- CLASS_KEY => class_name,
23
- SUBCLASS_KEY => subclass_name,
24
- MESSAGE_KEY => message
25
- }
26
- @smell.merge!(parameters)
27
- @status = {
28
- ACTIVE_KEY => true
29
- }
12
+ @smell = SmellDescription.new(class_name, subclass_name, message, parameters)
30
13
  @location = {
31
- CONTEXT_KEY => context.to_s,
32
- LINES_KEY => lines,
33
- SOURCE_KEY => source
14
+ 'context' => context.to_s,
15
+ 'lines' => lines,
16
+ 'source' => source
34
17
  }
35
18
  end
36
19
 
37
20
  #
38
- # Details of the smell found, including its class ({CLASS_KEY}),
39
- # subclass ({SUBCLASS_KEY}) and summary message ({MESSAGE_KEY})
21
+ # Details of the smell found, including its class, subclass and summary message.
40
22
  #
41
23
  # @return [Hash{String => String}]
42
24
  #
43
25
  attr_reader :smell
44
26
 
45
- def smell_class() @smell[CLASS_KEY] end
46
- def subclass() @smell[SUBCLASS_KEY] end
47
- def message() @smell[MESSAGE_KEY] end
27
+ def smell_classes() [smell_class, subclass] end
28
+ def smell_class() @smell.smell_class end
29
+ def subclass() @smell.smell_subclass end
30
+ def message() @smell.message end
48
31
 
49
32
  #
50
- # Details of the smell's location, including its context ({CONTEXT_KEY}),
51
- # the line numbers on which it occurs ({LINES_KEY}) and the source
52
- # file ({SOURCE_KEY})
33
+ # Details of the smell's location, including its context,
34
+ # the line numbers on which it occurs and the source file
53
35
  #
54
36
  # @return [Hash{String => String, Array<Number>}]
55
37
  #
56
38
  attr_reader :location
57
39
 
58
- def context() @location[CONTEXT_KEY] end
59
- def lines() @location[LINES_KEY] end
60
- def source() @location[SOURCE_KEY] end
61
-
62
- #
63
- # Details of the smell's status, including whether it is active ({ACTIVE_KEY})
64
- # (as opposed to being masked by a config file)
65
- #
66
- # @return [Hash{String => Boolean}]
67
- #
68
- attr_reader :status
40
+ def context() @location.fetch('context') end
41
+ def lines() @location.fetch('lines') end
42
+ def source() @location.fetch('source') end
69
43
 
70
44
  def hash
71
45
  sort_key.hash
@@ -79,23 +53,35 @@ module Reek
79
53
  (self <=> other) == 0
80
54
  end
81
55
 
82
- def contains_all?(patterns)
83
- rpt = sort_key.to_s
84
- patterns.all? { |pattern| pattern =~ rpt }
85
- end
86
-
87
56
  def matches?(klass, patterns)
88
- @smell.values.include?(klass.to_s) && contains_all?(patterns)
57
+ smell_classes.include?(klass.to_s) && contains_all?(patterns)
89
58
  end
90
59
 
91
60
  def report_on(listener)
92
61
  listener.found_smell(self)
93
62
  end
94
63
 
64
+ def init_with(coder)
65
+ @location = coder['location']
66
+ smell_attributes = coder['smell']
67
+ smell_class = smell_attributes.delete('class')
68
+ smell_subclass = smell_attributes.delete('subclass')
69
+ smell_message = smell_attributes.delete('message')
70
+ @smell = SmellDescription.new(smell_class,
71
+ smell_subclass,
72
+ smell_message,
73
+ smell_attributes)
74
+ end
75
+
95
76
  protected
96
77
 
78
+ def contains_all?(patterns)
79
+ rpt = sort_key.to_s
80
+ patterns.all? { |pattern| pattern =~ rpt }
81
+ end
82
+
97
83
  def sort_key
98
- [@location[CONTEXT_KEY], @smell[MESSAGE_KEY], @smell[CLASS_KEY]]
84
+ [context, message, smell_class]
99
85
  end
100
86
  end
101
87
  end
data/lib/reek/smells.rb CHANGED
@@ -8,6 +8,7 @@ require 'reek/smells/feature_envy'
8
8
  require 'reek/smells/irresponsible_module'
9
9
  require 'reek/smells/long_parameter_list'
10
10
  require 'reek/smells/long_yield_list'
11
+ require 'reek/smells/module_initialize'
11
12
  require 'reek/smells/nested_iterators'
12
13
  require 'reek/smells/nil_check'
13
14
  require 'reek/smells/prima_donna_method'
@@ -50,7 +50,7 @@ module Reek
50
50
  def attributes_in(module_ctx)
51
51
  result = Set.new
52
52
  attr_defn_methods = [:attr, :attr_reader, :attr_writer, :attr_accessor]
53
- module_ctx.local_nodes(:call) do |call_node|
53
+ module_ctx.local_nodes(:send) do |call_node|
54
54
  if attr_defn_methods.include?(call_node.method_name)
55
55
  call_node.arg_names.each { |arg| result << [arg, call_node.line] }
56
56
  end
@@ -88,6 +88,8 @@ module Reek
88
88
 
89
89
  # Finds cases of ControlParameter in a particular node for a particular parameter
90
90
  class ControlParameterFinder
91
+ CONDITIONAL_NODE_TYPES = [:if, :case, :and, :or]
92
+
91
93
  def initialize(node, param)
92
94
  @node = node
93
95
  @param = param
@@ -108,7 +110,7 @@ module Reek
108
110
  private
109
111
 
110
112
  def conditional_nodes
111
- @node.body.unnested_nodes([:if, :case, :and, :or])
113
+ @node.body_nodes(CONDITIONAL_NODE_TYPES)
112
114
  end
113
115
 
114
116
  def nested_finders
@@ -118,8 +120,8 @@ module Reek
118
120
  end
119
121
 
120
122
  def uses_param_in_call_in_condition?
121
- return false unless (condition = @node.condition)
122
- condition.each_node(:call) do |inner|
123
+ return unless condition
124
+ condition.each_node(:send) do |inner|
123
125
  next unless regular_call_involving_param? inner
124
126
  return true
125
127
  end
@@ -127,10 +129,15 @@ module Reek
127
129
  end
128
130
 
129
131
  def uses_of_param_in_condition
130
- return [] unless (condition = @node.condition)
132
+ return [] unless condition
131
133
  condition.each_node(:lvar).select { |inner| inner.var_name == @param }
132
134
  end
133
135
 
136
+ def condition
137
+ return nil unless CONDITIONAL_NODE_TYPES.include? @node.type
138
+ @node.condition
139
+ end
140
+
134
141
  def regular_call_involving_param?(call_node)
135
142
  call_involving_param?(call_node) && !comparison_call?(call_node)
136
143
  end
@@ -140,15 +147,15 @@ module Reek
140
147
  end
141
148
 
142
149
  def comparison_method_names
143
- [:==, :!=]
150
+ [:==, :!=, :=~]
144
151
  end
145
152
 
146
153
  def call_involving_param?(call_node)
147
- call_node.participants.any? { |it| it.var_name == @param }
154
+ call_node.each_node(:lvar).any? { |it| it.var_name == @param }
148
155
  end
149
156
 
150
157
  def uses_param_in_body?
151
- nodes = @node.body.each_node(:lvar, [:if, :case, :and, :or])
158
+ nodes = @node.body_nodes([:lvar], [:if, :case, :and, :or])
152
159
  nodes.any? { |lvar_node| lvar_node.var_name == @param }
153
160
  end
154
161
  end