reek 3.0.4 → 3.1

Sign up to get free protection for your applications and to get access to all the features.
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