reek 3.8.3 → 3.9.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +16 -6
  5. data/README.md +1 -0
  6. data/features/command_line_interface/smells_count.feature +1 -1
  7. data/features/command_line_interface/stdin.feature +1 -1
  8. data/features/configuration_loading.feature +2 -2
  9. data/features/programmatic_access.feature +2 -2
  10. data/features/rake_task/rake_task.feature +4 -4
  11. data/features/reports/json.feature +2 -2
  12. data/features/reports/reports.feature +6 -6
  13. data/features/reports/yaml.feature +2 -2
  14. data/features/samples.feature +19 -19
  15. data/features/step_definitions/sample_file_steps.rb +1 -1
  16. data/lib/reek/ast/node.rb +12 -1
  17. data/lib/reek/ast/sexp_extensions.rb +1 -0
  18. data/lib/reek/ast/sexp_extensions/constant.rb +9 -0
  19. data/lib/reek/ast/sexp_extensions/methods.rb +1 -20
  20. data/lib/reek/ast/sexp_extensions/module.rb +2 -2
  21. data/lib/reek/ast/sexp_extensions/self.rb +12 -0
  22. data/lib/reek/ast/sexp_extensions/send.rb +4 -9
  23. data/lib/reek/ast/sexp_extensions/super.rb +1 -1
  24. data/lib/reek/ast/sexp_extensions/variables.rb +5 -0
  25. data/lib/reek/context/attribute_context.rb +12 -0
  26. data/lib/reek/context/code_context.rb +28 -26
  27. data/lib/reek/context/ghost_context.rb +54 -0
  28. data/lib/reek/context/method_context.rb +28 -1
  29. data/lib/reek/context/module_context.rb +55 -1
  30. data/lib/reek/context/root_context.rb +8 -0
  31. data/lib/reek/context/singleton_attribute_context.rb +15 -0
  32. data/lib/reek/context/singleton_method_context.rb +20 -0
  33. data/lib/reek/context/visibility_tracker.rb +25 -16
  34. data/lib/reek/context_builder.rb +61 -31
  35. data/lib/reek/examiner.rb +0 -6
  36. data/lib/reek/smells/control_parameter.rb +1 -1
  37. data/lib/reek/smells/nested_iterators.rb +1 -1
  38. data/lib/reek/smells/nil_check.rb +2 -2
  39. data/lib/reek/smells/utility_function.rb +1 -2
  40. data/lib/reek/version.rb +1 -1
  41. data/reek.gemspec +1 -12
  42. data/spec/reek/ast/node_spec.rb +51 -3
  43. data/spec/reek/ast/sexp_extensions_spec.rb +16 -3
  44. data/spec/reek/context/code_context_spec.rb +12 -31
  45. data/spec/reek/context/ghost_context_spec.rb +60 -0
  46. data/spec/reek/context/module_context_spec.rb +22 -2
  47. data/spec/reek/context_builder_spec.rb +225 -2
  48. data/spec/reek/examiner_spec.rb +1 -1
  49. data/spec/reek/smells/attribute_spec.rb +35 -0
  50. data/spec/reek/smells/duplicate_method_call_spec.rb +1 -1
  51. data/spec/reek/tree_dresser_spec.rb +0 -1
  52. data/spec/samples/checkstyle.xml +2 -2
  53. metadata +8 -152
  54. data/lib/reek/ast/sexp_formatter.rb +0 -31
  55. data/spec/reek/ast/sexp_formatter_spec.rb +0 -35
@@ -6,6 +6,26 @@ module Reek
6
6
  # A context wrapper for any singleton method definition found in a syntax tree.
7
7
  #
8
8
  class SingletonMethodContext < MethodContext
9
+ def singleton_method?
10
+ true
11
+ end
12
+
13
+ def instance_method?
14
+ false
15
+ end
16
+
17
+ def module_function?
18
+ false
19
+ end
20
+
21
+ # Was this singleton method defined with an instance method-like syntax?
22
+ def defined_as_instance_method?
23
+ type == :def
24
+ end
25
+
26
+ def apply_current_visibility(current_visibility)
27
+ super if defined_as_instance_method?
28
+ end
9
29
  end
10
30
  end
11
31
  end
@@ -3,13 +3,14 @@ require 'private_attr/everywhere'
3
3
  module Reek
4
4
  module Context
5
5
  # Responsible for tracking visibilities in regards to CodeContexts.
6
- # :reek:Attribute
7
6
  class VisibilityTracker
8
- attr_accessor :visibility
9
7
  private_attr_accessor :tracked_visibility
10
8
 
11
- def initialize(visibility = :public)
12
- @visibility = visibility
9
+ VISIBILITY_MODIFIERS = [:private, :public, :protected, :module_function]
10
+ VISIBILITY_MAP = { public_class_method: :public, private_class_method: :private }
11
+
12
+ def initialize
13
+ @tracked_visibility = :public
13
14
  end
14
15
 
15
16
  # Handle the effects of a visibility modifier.
@@ -22,6 +23,7 @@ module Reek
22
23
  # @param names [Array<Symbol>]
23
24
  #
24
25
  def track_visibility(children: raise, visibility: raise, names: raise)
26
+ return unless VISIBILITY_MODIFIERS.include? visibility
25
27
  if names.any?
26
28
  children.each do |child|
27
29
  child.visibility = visibility if names.include?(child.name)
@@ -31,23 +33,30 @@ module Reek
31
33
  end
32
34
  end
33
35
 
36
+ # Handle the effects of a singleton visibility modifier. These can only
37
+ # be used to modify existing children.
38
+ #
39
+ # @example
40
+ # track_singleton_visibility children, :private_class_method,
41
+ # [:hide_me, :implementation_detail]
42
+ #
43
+ # @param children [Array<CodeContext>]
44
+ # @param visibility [Symbol]
45
+ # @param names [Array<Symbol>]
46
+ #
47
+ def track_singleton_visibility(children: raise, visibility: raise, names: raise)
48
+ return if names.empty?
49
+ visibility = VISIBILITY_MAP[visibility]
50
+ return unless visibility
51
+ track_visibility children: children, visibility: visibility, names: names
52
+ end
53
+
34
54
  # Sets the visibility of a child CodeContext to the tracked visibility.
35
55
  #
36
56
  # @param child [CodeContext]
37
57
  #
38
58
  def set_child_visibility(child)
39
- child.visibility = tracked_visibility
40
- end
41
-
42
- # @return [Boolean] If the visibility is public or not.
43
- def non_public_visibility?
44
- visibility != :public
45
- end
46
-
47
- private
48
-
49
- def tracked_visibility
50
- @tracked_visibility ||= :public
59
+ child.apply_current_visibility tracked_visibility
51
60
  end
52
61
  end
53
62
  end
@@ -1,10 +1,12 @@
1
+ require_relative 'context/attribute_context'
2
+ require_relative 'context/class_context'
3
+ require_relative 'context/ghost_context'
1
4
  require_relative 'context/method_context'
2
5
  require_relative 'context/module_context'
3
6
  require_relative 'context/root_context'
4
- require_relative 'context/singleton_method_context'
5
- require_relative 'context/attribute_context'
6
7
  require_relative 'context/send_context'
7
- require_relative 'context/class_context'
8
+ require_relative 'context/singleton_attribute_context'
9
+ require_relative 'context/singleton_method_context'
8
10
  require_relative 'ast/node'
9
11
 
10
12
  module Reek
@@ -16,16 +18,16 @@ module Reek
16
18
  # counting. Ideally `ContextBuilder` would only build up the context tree and leave the
17
19
  # statement and reference counting to the contexts.
18
20
  #
19
- # :reek:TooManyMethods: { max_methods: 27 }
21
+ # :reek:TooManyMethods: { max_methods: 30 }
20
22
  # :reek:UnusedPrivateMethod: { exclude: [ !ruby/regexp /process_/ ] }
21
23
  class ContextBuilder
22
24
  attr_reader :context_tree
23
- private_attr_accessor :element
25
+ private_attr_accessor :current_context
24
26
  private_attr_reader :exp
25
27
 
26
28
  def initialize(syntax_tree)
27
29
  @exp = syntax_tree
28
- @element = Context::RootContext.new(exp)
30
+ @current_context = Context::RootContext.new(exp)
29
31
  @context_tree = build(exp)
30
32
  end
31
33
 
@@ -60,7 +62,7 @@ module Reek
60
62
  else
61
63
  process exp
62
64
  end
63
- element
65
+ current_context
64
66
  end
65
67
 
66
68
  # Handles every node for which we have no context_processor.
@@ -79,6 +81,19 @@ module Reek
79
81
 
80
82
  alias_method :process_class, :process_module
81
83
 
84
+ # Handles `sclass` nodes
85
+ #
86
+ # An input example that would trigger this method would be:
87
+ #
88
+ # class << self
89
+ # end
90
+ #
91
+ def process_sclass(exp)
92
+ inside_new_context(Context::GhostContext, exp) do
93
+ process(exp)
94
+ end
95
+ end
96
+
82
97
  # Handles `casgn` ("class assign") nodes.
83
98
  #
84
99
  # An input example that would trigger this method would be:
@@ -102,7 +117,7 @@ module Reek
102
117
  # Given the above example we would count 2 statements overall.
103
118
  #
104
119
  def process_def(exp)
105
- inside_new_context(Context::MethodContext, exp) do
120
+ inside_new_context(current_context.method_context_class, exp) do
106
121
  increase_statement_count_by(exp.body)
107
122
  process(exp)
108
123
  end
@@ -133,20 +148,13 @@ module Reek
133
148
  # we also record to what the method call is referring to
134
149
  # which we later use for smell detectors like FeatureEnvy.
135
150
  #
136
- # :reek:TooManyStatements: { max_statements: 7 }
137
- # :reek:FeatureEnvy
138
151
  def process_send(exp)
139
- method_name = exp.method_name
140
- if exp.visibility_modifier?
141
- element.track_visibility(method_name, exp.arg_names)
142
- elsif exp.attribute_writer?
143
- exp.args.each do |arg|
144
- append_new_context(Context::AttributeContext, arg, exp)
145
- end
146
- else
147
- append_new_context(Context::SendContext, exp, method_name)
152
+ case current_context
153
+ when Context::ModuleContext
154
+ handle_send_for_modules exp
155
+ when Context::MethodContext
156
+ handle_send_for_methods exp
148
157
  end
149
- element.record_call_to(exp)
150
158
  process(exp)
151
159
  end
152
160
 
@@ -163,7 +171,7 @@ module Reek
163
171
  # We record one reference to `x` given the example above.
164
172
  #
165
173
  def process_op_asgn(exp)
166
- element.record_call_to(exp)
174
+ current_context.record_call_to(exp)
167
175
  process(exp)
168
176
  end
169
177
 
@@ -182,7 +190,7 @@ module Reek
182
190
  # We record one reference to `self`.
183
191
  #
184
192
  def process_ivar(exp)
185
- element.record_use_of_self
193
+ current_context.record_use_of_self
186
194
  process(exp)
187
195
  end
188
196
 
@@ -195,7 +203,7 @@ module Reek
195
203
  # def self.foo; end
196
204
  #
197
205
  def process_self(_)
198
- element.record_use_of_self
206
+ current_context.record_use_of_self
199
207
  end
200
208
 
201
209
  # Handles `zsuper` nodes a.k.a. calls to `super` without any arguments but a block possibly.
@@ -215,7 +223,7 @@ module Reek
215
223
  # We record one reference to `self`.
216
224
  #
217
225
  def process_zsuper(_)
218
- element.record_use_of_self
226
+ current_context.record_use_of_self
219
227
  end
220
228
 
221
229
  # Handles `block` nodes.
@@ -435,11 +443,11 @@ module Reek
435
443
 
436
444
  # :reek:ControlParameter
437
445
  def increase_statement_count_by(sexp)
438
- element.statement_counter.increase_by sexp
446
+ current_context.statement_counter.increase_by sexp
439
447
  end
440
448
 
441
449
  def decrease_statement_count
442
- element.statement_counter.decrease_by 1
450
+ current_context.statement_counter.decrease_by 1
443
451
  end
444
452
 
445
453
  # Stores a reference to the current context, creates a nested new one,
@@ -452,12 +460,13 @@ module Reek
452
460
  def inside_new_context(klass, exp)
453
461
  new_context = append_new_context(klass, exp)
454
462
 
455
- orig, self.element = element, new_context
463
+ orig, self.current_context = current_context, new_context
456
464
  yield
457
- self.element = orig
465
+ self.current_context = orig
458
466
  end
459
467
 
460
- # Append a new child context to the current element.
468
+ # Append a new child context to the current context but does not change the
469
+ # current context.
461
470
  #
462
471
  # @param klass [Context::*Context] - context class
463
472
  # @param args - arguments for the class initializer
@@ -465,8 +474,29 @@ module Reek
465
474
  # @return [Context::*Context] - the context that was appended
466
475
  #
467
476
  def append_new_context(klass, *args)
468
- klass.new(element, *args).tap do |new_context|
469
- element.append_child_context new_context
477
+ klass.new(current_context, *args).tap do |new_context|
478
+ new_context.register_with_parent(current_context)
479
+ end
480
+ end
481
+
482
+ def handle_send_for_modules(exp)
483
+ method_name = exp.name
484
+ arg_names = exp.arg_names
485
+ current_context.track_visibility(method_name, arg_names)
486
+ current_context.track_singleton_visibility(method_name, arg_names)
487
+ register_attributes(exp)
488
+ end
489
+
490
+ def handle_send_for_methods(exp)
491
+ append_new_context(Context::SendContext, exp, exp.name)
492
+ current_context.record_call_to(exp)
493
+ end
494
+
495
+ def register_attributes(exp)
496
+ return unless exp.attribute_writer?
497
+ klass = current_context.attribute_context_class
498
+ exp.args.each do |arg|
499
+ append_new_context(klass, arg, exp)
470
500
  end
471
501
  end
472
502
  end
@@ -1,9 +1,3 @@
1
- # NOTE: context_builder is required first to ensure unparser is required before
2
- # parser. This prevents a potentially incompatible version of parser from being
3
- # loaded first. This is only relevant when running bin/reek straight from a
4
- # checkout directory without using Bundler.
5
- #
6
- # See also https://github.com/troessner/reek/pull/468
7
1
  require_relative 'context_builder'
8
2
  require_relative 'source/source_code'
9
3
  require_relative 'cli/warning_collector'
@@ -149,7 +149,7 @@ module Reek
149
149
  end
150
150
 
151
151
  def comparison_call?(call_node)
152
- comparison_method_names.include? call_node.method_name
152
+ comparison_method_names.include? call_node.name
153
153
  end
154
154
 
155
155
  def comparison_method_names
@@ -120,7 +120,7 @@ module Reek
120
120
 
121
121
  # :reek:FeatureEnvy
122
122
  def ignored_iterator?(exp)
123
- ignore_iterators.any? { |pattern| /#{pattern}/ =~ exp.call.method_name } ||
123
+ ignore_iterators.any? { |pattern| /#{pattern}/ =~ exp.call.name } ||
124
124
  exp.without_block_arguments?
125
125
  end
126
126
  end
@@ -54,7 +54,7 @@ module Reek
54
54
  end
55
55
 
56
56
  def nil_query?(call)
57
- call.method_name == :nil?
57
+ call.name == :nil?
58
58
  end
59
59
 
60
60
  def nil_comparison?(call)
@@ -62,7 +62,7 @@ module Reek
62
62
  end
63
63
 
64
64
  def comparison_call?(call)
65
- comparison_methods.include? call.method_name
65
+ comparison_methods.include? call.name
66
66
  end
67
67
 
68
68
  def involves_nil?(call)
@@ -58,8 +58,7 @@ module Reek
58
58
  # :reek:FeatureEnvy
59
59
  # :reek:TooManyStatements: { max_statements: 6 }
60
60
  def inspect(ctx)
61
- return [] if ctx.singleton_method?
62
- return [] if ctx.number_of_statements == 0
61
+ return [] if ctx.singleton_method? || ctx.module_function?
63
62
  return [] if ctx.references_self?
64
63
  return [] if num_helper_methods(ctx).zero?
65
64
  return [] if ignore_method?(ctx)
@@ -6,6 +6,6 @@ module Reek
6
6
  # @public
7
7
  module Version
8
8
  # @public
9
- STRING = '3.8.3'
9
+ STRING = '3.9.0'
10
10
  end
11
11
  end
@@ -23,18 +23,7 @@ Gem::Specification.new do |s|
23
23
  s.summary = 'Code smell detector for Ruby'
24
24
 
25
25
  s.add_runtime_dependency 'codeclimate-engine-rb', '~> 0.1.0'
26
- s.add_runtime_dependency 'parser', '~> 2.2', '>= 2.2.2.5'
26
+ s.add_runtime_dependency 'parser', '~> 2.3'
27
27
  s.add_runtime_dependency 'private_attr', '~> 1.1'
28
28
  s.add_runtime_dependency 'rainbow', '~> 2.0'
29
- s.add_runtime_dependency 'unparser', '~> 0.2.2'
30
-
31
- s.add_development_dependency 'activesupport', '~> 4.2'
32
- s.add_development_dependency 'aruba', '~> 0.10.0'
33
- s.add_development_dependency 'ataru', '~> 0.2.0'
34
- s.add_development_dependency 'bundler', '~> 1.1'
35
- s.add_development_dependency 'cucumber', '~> 2.0'
36
- s.add_development_dependency 'factory_girl', '~> 4.0'
37
- s.add_development_dependency 'rake', '~> 10.0'
38
- s.add_development_dependency 'rspec', '~> 3.0'
39
- s.add_development_dependency 'rubocop', '~> 0.34.0'
40
29
  end
@@ -2,9 +2,57 @@ require_relative '../../spec_helper'
2
2
  require_lib 'reek/ast/node'
3
3
 
4
4
  RSpec.describe Reek::AST::Node do
5
- context 'format' do
6
- it 'formats self' do
7
- expect(sexp(:self).format_to_ruby).to eq('self')
5
+ describe '#format_to_ruby' do
6
+ it 'returns #to_s if location is not present' do
7
+ ast = sexp(:self)
8
+ expect(ast.format_to_ruby).to eq ast.to_s
9
+ end
10
+
11
+ it 'gives the right result for self' do
12
+ ast = Reek::Source::SourceCode.from('self').syntax_tree
13
+ expect(ast.format_to_ruby).to eq 'self'
14
+ end
15
+
16
+ it 'gives the right result for a simple expression' do
17
+ ast = Reek::Source::SourceCode.from('foo').syntax_tree
18
+ expect(ast.format_to_ruby).to eq 'foo'
19
+ end
20
+
21
+ it 'gives the right result for a more complex expression' do
22
+ ast = Reek::Source::SourceCode.from('foo(bar)').syntax_tree
23
+ result = ast.format_to_ruby
24
+ expect(result).to eq 'foo(bar)'
25
+ end
26
+
27
+ it 'gives te right result for send with a receiver' do
28
+ ast = Reek::Source::SourceCode.from('baz.foo(bar)').syntax_tree
29
+ expect(ast.format_to_ruby).to eq 'baz.foo(bar)'
30
+ end
31
+
32
+ it 'gives the right result for if' do
33
+ source = <<-SRC
34
+ if foo
35
+ bar
36
+ else
37
+ baz
38
+ qux
39
+ end
40
+ SRC
41
+
42
+ ast = Reek::Source::SourceCode.from(source).syntax_tree
43
+ expect(ast.format_to_ruby).to eq 'if foo ... end'
44
+ end
45
+
46
+ it 'gives the right result for def with no body' do
47
+ source = "def my_method\nend"
48
+ ast = Reek::Source::SourceCode.from(source).syntax_tree
49
+ expect(ast.format_to_ruby).to eq 'def my_method; end'
50
+ end
51
+
52
+ it 'gives the right result for ivar' do
53
+ source = '@foo'
54
+ ast = Reek::Source::SourceCode.from(source).syntax_tree
55
+ expect(ast.format_to_ruby).to eq '@foo'
8
56
  end
9
57
  end
10
58
 
@@ -319,8 +319,7 @@ end
319
319
  RSpec.describe Reek::AST::SexpExtensions::ModuleNode do
320
320
  context 'with a simple name' do
321
321
  subject do
322
- mod = sexp(:module, :Fred, nil)
323
- mod
322
+ Reek::Source::SourceCode.from('module Fred; end').syntax_tree
324
323
  end
325
324
 
326
325
  it 'has the correct #name' do
@@ -342,7 +341,7 @@ RSpec.describe Reek::AST::SexpExtensions::ModuleNode do
342
341
 
343
342
  context 'with a scoped name' do
344
343
  subject do
345
- sexp(:module, sexp(:const, sexp(:const, nil, :Foo), :Bar), nil)
344
+ Reek::Source::SourceCode.from('module Foo::Bar; end').syntax_tree
346
345
  end
347
346
 
348
347
  it 'has the correct #name' do
@@ -361,6 +360,20 @@ RSpec.describe Reek::AST::SexpExtensions::ModuleNode do
361
360
  expect(subject.full_name('Blimey::O::Reilly')).to eq 'Blimey::O::Reilly::Foo::Bar'
362
361
  end
363
362
  end
363
+
364
+ context 'with a name scoped in a namespace that is not a constant' do
365
+ subject do
366
+ Reek::Source::SourceCode.from('module foo::Bar; end').syntax_tree
367
+ end
368
+
369
+ it 'has the correct #name' do
370
+ expect(subject.name).to eq 'foo::Bar'
371
+ end
372
+
373
+ it 'has the correct #simple_name' do
374
+ expect(subject.simple_name).to eq 'Bar'
375
+ end
376
+ end
364
377
  end
365
378
 
366
379
  RSpec.describe Reek::AST::SexpExtensions::CasgnNode do