reek 3.0.4 → 3.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +6 -0
  3. data/README.md +41 -20
  4. data/features/configuration_files/directory_specific_directives.feature +280 -0
  5. data/features/configuration_files/masking_smells.feature +0 -14
  6. data/features/step_definitions/sample_file_steps.rb +2 -2
  7. data/lib/reek/ast/sexp_extensions.rb +72 -60
  8. data/lib/reek/cli/application.rb +2 -5
  9. data/lib/reek/cli/reek_command.rb +1 -1
  10. data/lib/reek/configuration/app_configuration.rb +115 -61
  11. data/lib/reek/configuration/configuration_file_finder.rb +19 -0
  12. data/lib/reek/context/code_context.rb +102 -29
  13. data/lib/reek/context/method_context.rb +11 -48
  14. data/lib/reek/context/module_context.rb +2 -6
  15. data/lib/reek/context/root_context.rb +7 -14
  16. data/lib/reek/examiner.rb +15 -10
  17. data/lib/reek/report/report.rb +5 -4
  18. data/lib/reek/smells/boolean_parameter.rb +1 -1
  19. data/lib/reek/smells/duplicate_method_call.rb +1 -5
  20. data/lib/reek/smells/irresponsible_module.rb +2 -2
  21. data/lib/reek/smells/smell_repository.rb +30 -16
  22. data/lib/reek/smells/utility_function.rb +1 -1
  23. data/lib/reek/source/source_code.rb +10 -6
  24. data/lib/reek/source/source_locator.rb +27 -26
  25. data/lib/reek/spec.rb +8 -6
  26. data/lib/reek/spec/should_reek.rb +6 -2
  27. data/lib/reek/spec/should_reek_of.rb +5 -2
  28. data/lib/reek/spec/should_reek_only_of.rb +1 -1
  29. data/lib/reek/tree_walker.rb +49 -21
  30. data/lib/reek/version.rb +1 -1
  31. data/spec/reek/ast/sexp_extensions_spec.rb +4 -12
  32. data/spec/reek/configuration/app_configuration_spec.rb +80 -52
  33. data/spec/reek/configuration/configuration_file_finder_spec.rb +27 -15
  34. data/spec/reek/context/code_context_spec.rb +66 -17
  35. data/spec/reek/context/method_context_spec.rb +66 -64
  36. data/spec/reek/context/root_context_spec.rb +3 -1
  37. data/spec/reek/context/singleton_method_context_spec.rb +1 -2
  38. data/spec/reek/examiner_spec.rb +5 -8
  39. data/spec/reek/report/xml_report_spec.rb +3 -2
  40. data/spec/reek/smells/attribute_spec.rb +12 -17
  41. data/spec/reek/smells/duplicate_method_call_spec.rb +17 -25
  42. data/spec/reek/smells/feature_envy_spec.rb +4 -5
  43. data/spec/reek/smells/irresponsible_module_spec.rb +43 -0
  44. data/spec/reek/smells/nested_iterators_spec.rb +12 -26
  45. data/spec/reek/smells/too_many_statements_spec.rb +2 -210
  46. data/spec/reek/smells/utility_function_spec.rb +49 -5
  47. data/spec/reek/source/source_locator_spec.rb +39 -43
  48. data/spec/reek/spec/should_reek_of_spec.rb +3 -2
  49. data/spec/reek/spec/should_reek_spec.rb +25 -17
  50. data/spec/reek/tree_walker_spec.rb +214 -23
  51. data/spec/samples/configuration/full_configuration.reek +9 -0
  52. data/spec/spec_helper.rb +10 -10
  53. metadata +4 -3
  54. data/features/configuration_files/overrides_defaults.feature +0 -15
data/lib/reek/spec.rb CHANGED
@@ -85,8 +85,10 @@ module Reek
85
85
  #
86
86
  # expect(src).to reek_of(Reek::Smells::DuplicateMethodCall, name: '@other.thing')
87
87
  #
88
- def reek_of(smell_category, smell_details = {})
89
- ShouldReekOf.new(smell_category, smell_details)
88
+ def reek_of(smell_category,
89
+ smell_details = {},
90
+ configuration = Configuration::AppConfiguration.new)
91
+ ShouldReekOf.new(smell_category, smell_details, configuration)
90
92
  end
91
93
 
92
94
  #
@@ -96,15 +98,15 @@ module Reek
96
98
  # 1.) "reek_of" doesn't mind if there are other smells of a different category.
97
99
  # "reek_only_of" will fail in that case.
98
100
  # 2.) "reek_only_of" doesn't support the additional smell_details hash.
99
- def reek_only_of(smell_category)
100
- ShouldReekOnlyOf.new(smell_category)
101
+ def reek_only_of(smell_category, configuration = Configuration::AppConfiguration.new)
102
+ ShouldReekOnlyOf.new(smell_category, configuration)
101
103
  end
102
104
 
103
105
  #
104
106
  # Returns +true+ if and only if the target source code contains smells.
105
107
  #
106
- def reek
107
- ShouldReek.new
108
+ def reek(configuration = Configuration::AppConfiguration.new)
109
+ ShouldReek.new configuration
108
110
  end
109
111
  end
110
112
  end
@@ -7,9 +7,13 @@ module Reek
7
7
  # An rspec matcher that matches when the +actual+ has code smells.
8
8
  #
9
9
  # @api private
10
- class ShouldReek # :nodoc:
10
+ class ShouldReek
11
+ def initialize(configuration = Configuration::AppConfiguration.new)
12
+ @configuration = configuration
13
+ end
14
+
11
15
  def matches?(actual)
12
- @examiner = Examiner.new(actual)
16
+ @examiner = Examiner.new(actual, configuration: @configuration)
13
17
  @examiner.smelly?
14
18
  end
15
19
 
@@ -8,13 +8,16 @@ module Reek
8
8
  #
9
9
  # @api private
10
10
  class ShouldReekOf
11
- def initialize(smell_category, smell_details = {})
11
+ def initialize(smell_category,
12
+ smell_details = {},
13
+ configuration = Configuration::AppConfiguration.new)
12
14
  @smell_category = normalize smell_category
13
15
  @smell_details = smell_details
16
+ @configuration = configuration
14
17
  end
15
18
 
16
19
  def matches?(actual)
17
- @examiner = Examiner.new(actual)
20
+ @examiner = Examiner.new(actual, configuration: @configuration)
18
21
  @all_smells = @examiner.smells
19
22
  @all_smells.any? { |warning| warning.matches?(@smell_category, @smell_details) }
20
23
  end
@@ -10,7 +10,7 @@ module Reek
10
10
  # @api private
11
11
  class ShouldReekOnlyOf < ShouldReekOf
12
12
  def matches?(actual)
13
- matches_examiner?(Examiner.new(actual))
13
+ matches_examiner?(Examiner.new(actual, configuration: @configuration))
14
14
  end
15
15
 
16
16
  def matches_examiner?(examiner)
@@ -2,24 +2,36 @@ require_relative 'context/method_context'
2
2
  require_relative 'context/module_context'
3
3
  require_relative 'context/root_context'
4
4
  require_relative 'context/singleton_method_context'
5
- require_relative 'smells/smell_repository'
6
5
  require_relative 'ast/node'
7
6
 
8
7
  module Reek
9
8
  #
10
- # Traverses a Sexp abstract syntax tree and fires events whenever
11
- # it encounters specific node types.
9
+ # Traverses an abstract syntax tree and fires events whenever it encounters
10
+ # specific node types.
12
11
  #
13
12
  # SMELL: This class is responsible for counting statements and for feeding
14
13
  # each context to the smell repository.
15
14
  #
15
+ # TODO: Make TreeWalker responsible only for creating Context objects, and
16
+ # loop over the created set of contexts elsewhere.
17
+ #
16
18
  # @api private
17
19
  class TreeWalker
18
- def initialize(smell_repository = Smells::SmellRepository.new)
20
+ def initialize(smell_repository, exp)
19
21
  @smell_repository = smell_repository
20
- @element = Context::RootContext.new
22
+ @exp = exp
23
+ @element = Context::RootContext.new(exp)
21
24
  end
22
25
 
26
+ def walk
27
+ @result ||= process(@exp)
28
+ @result.each do |element|
29
+ @smell_repository.examine(element)
30
+ end
31
+ end
32
+
33
+ private
34
+
23
35
  def process(exp)
24
36
  context_processor = "process_#{exp.type}"
25
37
  if context_processor_exists?(context_processor)
@@ -30,12 +42,6 @@ module Reek
30
42
  @element
31
43
  end
32
44
 
33
- def process_default(exp)
34
- exp.children.each do |child|
35
- process(child) if child.is_a? AST::Node
36
- end
37
- end
38
-
39
45
  def process_module(exp)
40
46
  inside_new_context(Context::ModuleContext, exp) do
41
47
  process_default(exp)
@@ -44,6 +50,14 @@ module Reek
44
50
 
45
51
  alias_method :process_class, :process_module
46
52
 
53
+ def process_casgn(exp)
54
+ if exp.defines_module?
55
+ process_module(exp)
56
+ else
57
+ process_default(exp)
58
+ end
59
+ end
60
+
47
61
  def process_def(exp)
48
62
  inside_new_context(Context::MethodContext, exp) do
49
63
  count_clause(exp.body)
@@ -58,6 +72,12 @@ module Reek
58
72
  end
59
73
  end
60
74
 
75
+ def process_default(exp)
76
+ exp.children.each do |child|
77
+ process(child) if child.is_a? AST::Node
78
+ end
79
+ end
80
+
61
81
  def process_args(_) end
62
82
 
63
83
  #
@@ -65,12 +85,19 @@ module Reek
65
85
  #
66
86
 
67
87
  def process_send(exp)
88
+ if visibility_modifier? exp
89
+ @element.track_visibility(exp.method_name, exp.arg_names)
90
+ end
68
91
  @element.record_call_to(exp)
69
92
  process_default(exp)
70
93
  end
71
94
 
72
- alias_method :process_attrasgn, :process_send
73
- alias_method :process_op_asgn, :process_send
95
+ def process_attrasgn(exp)
96
+ @element.record_call_to(exp)
97
+ process_default(exp)
98
+ end
99
+
100
+ alias_method :process_op_asgn, :process_attrasgn
74
101
 
75
102
  def process_ivar(exp)
76
103
  @element.record_use_of_self
@@ -145,10 +172,8 @@ module Reek
145
172
  process_default(exp)
146
173
  end
147
174
 
148
- private
149
-
150
175
  def context_processor_exists?(name)
151
- respond_to?(name)
176
+ self.class.private_method_defined?(name)
152
177
  end
153
178
 
154
179
  def count_clause(sexp)
@@ -161,22 +186,25 @@ module Reek
161
186
 
162
187
  def inside_new_context(klass, exp)
163
188
  scope = klass.new(@element, exp)
189
+ @element.append_child_context(scope)
164
190
  push(scope) do
165
191
  yield
166
- check_smells(exp.type)
167
192
  end
168
193
  scope
169
194
  end
170
195
 
171
- def check_smells(type)
172
- @smell_repository.examine(@element, type)
173
- end
174
-
175
196
  def push(scope)
176
197
  orig = @element
177
198
  @element = scope
178
199
  yield
179
200
  @element = orig
180
201
  end
202
+
203
+ # FIXME: Move to SendNode?
204
+ def visibility_modifier?(call_node)
205
+ VISIBILITY_MODIFIERS.include?(call_node.method_name)
206
+ end
207
+
208
+ VISIBILITY_MODIFIERS = [:private, :public, :protected, :module_function]
181
209
  end
182
210
  end
data/lib/reek/version.rb CHANGED
@@ -3,6 +3,6 @@ module Reek
3
3
  # This module holds the Reek version informations
4
4
  #
5
5
  module Version
6
- STRING = '3.0.4'
6
+ STRING = '3.1'
7
7
  end
8
8
  end
@@ -309,15 +309,11 @@ RSpec.describe Reek::AST::SexpExtensions::ModuleNode do
309
309
  end
310
310
 
311
311
  it 'has the correct #name' do
312
- expect(subject.name).to eq :Fred
312
+ expect(subject.name).to eq 'Fred'
313
313
  end
314
314
 
315
315
  it 'has the correct #simple_name' do
316
- expect(subject.simple_name).to eq :Fred
317
- end
318
-
319
- it 'has the correct #text_name' do
320
- expect(subject.text_name).to eq 'Fred'
316
+ expect(subject.simple_name).to eq 'Fred'
321
317
  end
322
318
 
323
319
  it 'has a simple full_name' do
@@ -335,15 +331,11 @@ RSpec.describe Reek::AST::SexpExtensions::ModuleNode do
335
331
  end
336
332
 
337
333
  it 'has the correct #name' do
338
- expect(subject.name).to eq s(:const, s(:const, nil, :Foo), :Bar)
334
+ expect(subject.name).to eq 'Foo::Bar'
339
335
  end
340
336
 
341
337
  it 'has the correct #simple_name' do
342
- expect(subject.simple_name).to eq :Bar
343
- end
344
-
345
- it 'has the correct #text_name' do
346
- expect(subject.text_name).to eq 'Foo::Bar'
338
+ expect(subject.simple_name).to eq 'Bar'
347
339
  end
348
340
 
349
341
  it 'has a simple full_name' do
@@ -1,78 +1,106 @@
1
+ require 'pathname'
1
2
  require_relative '../../spec_helper'
2
3
  require_relative '../../../lib/reek/configuration/app_configuration'
3
4
  require_relative '../../../lib/reek/smells/smell_repository'
5
+ require_relative '../../../lib/reek/source/source_code'
4
6
 
5
7
  RSpec.describe Reek::Configuration::AppConfiguration do
6
- let(:sample_configuration_path) { 'spec/samples/configuration/simple_configuration.reek' }
7
- let(:sample_configuration_loaded) do
8
- {
9
- 'UncommunicativeVariableName' => { 'enabled' => false },
10
- 'UncommunicativeMethodName' => { 'enabled' => false }
11
- }
12
- end
13
- let(:default_configuration) { Hash.new }
14
-
15
- after(:each) { described_class.reset }
8
+ describe '#initialize' do
9
+ let(:full_configuration_path) { SAMPLES_PATH.join('configuration/full_configuration.reek') }
10
+ let(:expected_exclude_paths) do
11
+ [SAMPLES_PATH.join('two_smelly_files'),
12
+ SAMPLES_PATH.join('source_with_non_ruby_files')]
13
+ end
14
+ let(:expected_default_directive) do
15
+ { Reek::Smells::IrresponsibleModule => { 'enabled' => false } }
16
+ end
17
+ let(:expected_directory_directives) do
18
+ { Pathname.new('spec/samples/three_clean_files') =>
19
+ { Reek::Smells::UtilityFunction => { 'enabled' => false } } }
20
+ end
16
21
 
17
- describe '.initialize_with' do
18
- it 'loads a configuration file if it can find one' do
22
+ it 'properly loads configuration and processes it' do
19
23
  finder = Reek::Configuration::ConfigurationFileFinder
20
- allow(finder).to receive(:find).and_return sample_configuration_path
21
- expect(described_class.configuration).to eq(default_configuration)
24
+ allow(finder).to receive(:find_by_cli).and_return full_configuration_path
22
25
 
23
- described_class.initialize_with(nil)
24
- expect(described_class.configuration).to eq(sample_configuration_loaded)
26
+ expect(subject.exclude_paths).to eq(expected_exclude_paths)
27
+ expect(subject.default_directive).to eq(expected_default_directive)
28
+ expect(subject.directory_directives).to eq(expected_directory_directives)
25
29
  end
30
+ end
31
+
32
+ describe '#directive_for' do
33
+ context 'our source is in a directory for which we have a directive' do
34
+ let(:baz_config) { { Reek::Smells::IrresponsibleModule => { enabled: false } } }
35
+ let(:bang_config) { { Reek::Smells::Attribute => { enabled: true } } }
26
36
 
27
- it 'leaves the default configuration untouched if it can\'t find one' do
28
- allow(Reek::Configuration::ConfigurationFileFinder).to receive(:find).and_return nil
29
- expect(described_class.configuration).to eq(default_configuration)
37
+ let(:directory_directives) do
38
+ {
39
+ Pathname.new('foo/bar/baz') => baz_config,
40
+ Pathname.new('foo/bar/bang') => bang_config
41
+ }
42
+ end
43
+ let(:source_via) { 'foo/bar/bang/dummy.rb' }
30
44
 
31
- described_class.initialize_with(nil)
32
- expect(described_class.configuration).to eq(default_configuration)
45
+ it 'returns the corresponding directive' do
46
+ allow(subject).to receive(:directory_directives).and_return directory_directives
47
+ expect(subject.directive_for(source_via)).to eq(bang_config)
48
+ end
33
49
  end
34
- end
35
50
 
36
- describe '.configure_smell_repository' do
37
- it 'should configure a given smell_repository' do
38
- described_class.load_from_file(sample_configuration_path)
39
- smell_repository = Reek::Smells::SmellRepository.new('def m; end')
40
- described_class.configure_smell_repository smell_repository
51
+ context 'our source is not in a directory for which we have a directive' do
52
+ let(:baz_config) { { Reek::Smells::IrresponsibleModule => { enabled: false } } }
53
+ let(:default_directive) do
54
+ { Reek::Smells::Attribute => { enabled: true } }
55
+ end
56
+ let(:directory_directives) do
57
+ { Pathname.new('foo/bar/baz') => baz_config }
58
+ end
59
+ let(:source_via) { 'foo/bar/bang/dummy.rb' }
41
60
 
42
- expect(smell_repository.detectors[Reek::Smells::DataClump]).to be_enabled
43
- expect(smell_repository.detectors[Reek::Smells::UncommunicativeVariableName]).
44
- not_to be_enabled
45
- expect(smell_repository.detectors[Reek::Smells::UncommunicativeMethodName]).not_to be_enabled
61
+ it 'returns the default directive' do
62
+ allow(subject).to receive(:directory_directives).and_return directory_directives
63
+ allow(subject).to receive(:default_directive).and_return default_directive
64
+ expect(subject.directive_for(source_via)).to eq(default_directive)
65
+ end
46
66
  end
47
67
  end
48
68
 
49
- describe '.exclude_paths' do
50
- let(:config_path) { 'spec/samples/configuration/with_excluded_paths.reek' }
69
+ describe '#best_directory_match_for' do
70
+ let(:directory_directives) do
71
+ {
72
+ Pathname.new('foo/bar/baz') => {},
73
+ Pathname.new('foo/bar') => {},
74
+ Pathname.new('bar/boo') => {}
75
+ }
76
+ end
51
77
 
52
- it 'should return all paths to exclude' do
53
- with_test_config(config_path) do
54
- expect(described_class.exclude_paths).to eq [
55
- 'spec/samples/source_with_exclude_paths/ignore_me',
56
- 'spec/samples/source_with_exclude_paths/nested/ignore_me_as_well'
57
- ]
58
- end
78
+ before do
79
+ allow(subject).to receive(:directory_directives).and_return directory_directives
59
80
  end
60
- end
61
81
 
62
- describe '.load_from_file' do
63
- it 'loads the configuration from given file' do
64
- described_class.load_from_file(sample_configuration_path)
65
- expect(described_class.configuration).to eq(sample_configuration_loaded)
82
+ it 'returns the corresponding directory when source_base_dir is a leaf' do
83
+ source_base_dir = 'foo/bar/baz/bang'
84
+ hit = subject.send :best_directory_match_for, source_base_dir
85
+ expect(hit.to_s).to eq('foo/bar/baz')
66
86
  end
67
- end
68
87
 
69
- describe '.reset' do
70
- it 'resets the configuration' do
71
- described_class.load_from_file(sample_configuration_path)
88
+ it 'returns the corresponding directory when source_base_dir is in the middle of the tree' do
89
+ source_base_dir = 'foo/bar'
90
+ hit = subject.send :best_directory_match_for, source_base_dir
91
+ expect(hit.to_s).to eq('foo/bar')
92
+ end
93
+
94
+ it 'returns nil we are on top at the top of the and all other directories are below' do
95
+ source_base_dir = 'foo'
96
+ hit = subject.send :best_directory_match_for, source_base_dir
97
+ expect(hit).to be_nil
98
+ end
72
99
 
73
- expect(described_class.configuration).to eq(sample_configuration_loaded)
74
- described_class.reset
75
- expect(described_class.configuration).to eq(default_configuration)
100
+ it 'returns nil when there source_base_dir is not part of any directory directive at all' do
101
+ source_base_dir = 'non/existent'
102
+ hit = subject.send :best_directory_match_for, source_base_dir
103
+ expect(hit).to be_nil
76
104
  end
77
105
  end
78
106
  end
@@ -16,36 +16,31 @@ RSpec.describe Reek::Configuration::ConfigurationFileFinder do
16
16
 
17
17
  it 'returns the file in current dir if config_file is nil' do
18
18
  options = double(config_file: nil)
19
- current = Pathname.new('spec/samples')
20
- found = described_class.find(options: options, current: current)
21
- expect(found).to eq(Pathname.new('spec/samples/exceptions.reek'))
19
+ found = described_class.find(options: options, current: SAMPLES_PATH)
20
+ expect(found).to eq(SAMPLES_PATH.join('exceptions.reek'))
22
21
  end
23
22
 
24
23
  it 'returns the file in current dir if options is nil' do
25
- current = Pathname.new('spec/samples')
26
- found = described_class.find(current: current)
27
- expect(found).to eq(Pathname.new('spec/samples/exceptions.reek'))
24
+ found = described_class.find(current: SAMPLES_PATH)
25
+ expect(found).to eq(SAMPLES_PATH.join('exceptions.reek'))
28
26
  end
29
27
 
30
28
  it 'returns the file in a parent dir if none in current dir' do
31
- current = Pathname.new('spec/samples/no_config_file')
32
- found = described_class.find(current: current)
33
- expect(found).to eq(Pathname.new('spec/samples/exceptions.reek'))
29
+ found = described_class.find(current: SAMPLES_PATH.join('no_config_file'))
30
+ expect(found).to eq(SAMPLES_PATH.join('exceptions.reek'))
34
31
  end
35
32
 
36
33
  it 'returns the file even if it’s just ‘.reek’' do
37
- current = Pathname.new('spec/samples/masked_by_dotfile')
38
- found = described_class.find(current: current)
39
- expect(found).to eq(Pathname.new('spec/samples/masked_by_dotfile/.reek'))
34
+ found = described_class.find(current: SAMPLES_PATH.join('masked_by_dotfile'))
35
+ expect(found).to eq(SAMPLES_PATH.join('masked_by_dotfile/.reek'))
40
36
  end
41
37
 
42
38
  it 'returns the file in home if traversing from the current dir fails' do
43
39
  skip_if_a_config_in_tempdir
44
40
  Dir.mktmpdir do |tempdir|
45
41
  current = Pathname.new(tempdir)
46
- home = Pathname.new('spec/samples')
47
- found = described_class.find(current: current, home: home)
48
- expect(found).to eq(Pathname.new('spec/samples/exceptions.reek'))
42
+ found = described_class.find(current: current, home: SAMPLES_PATH)
43
+ expect(found).to eq(SAMPLES_PATH.join('exceptions.reek'))
49
44
  end
50
45
  end
51
46
 
@@ -70,6 +65,23 @@ RSpec.describe Reek::Configuration::ConfigurationFileFinder do
70
65
  end
71
66
  end
72
67
 
68
+ describe '.load_from_file' do
69
+ let(:sample_configuration_path) do
70
+ SAMPLES_PATH.join('configuration/simple_configuration.reek')
71
+ end
72
+ let(:sample_configuration_loaded) do
73
+ {
74
+ 'UncommunicativeVariableName' => { 'enabled' => false },
75
+ 'UncommunicativeMethodName' => { 'enabled' => false }
76
+ }
77
+ end
78
+
79
+ it 'loads the configuration from given file' do
80
+ configuration = described_class.load_from_file(sample_configuration_path)
81
+ expect(configuration).to eq(sample_configuration_loaded)
82
+ end
83
+ end
84
+
73
85
  private
74
86
 
75
87
  def skip_if_a_config_in_tempdir