reek 1.4.0 → 1.5.0

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 (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