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
@@ -14,6 +14,7 @@ module Reek
14
14
  STATUS_SUCCESS = 0
15
15
  STATUS_ERROR = 1
16
16
  STATUS_SMELLS = 2
17
+ attr_reader :configuration
17
18
 
18
19
  def initialize(argv)
19
20
  @status = STATUS_SUCCESS
@@ -21,7 +22,7 @@ module Reek
21
22
  begin
22
23
  @options = options_parser.parse
23
24
  @command = ReekCommand.new(OptionInterpreter.new(@options))
24
- initialize_configuration
25
+ @configuration = Configuration::AppConfiguration.new @options
25
26
  rescue OptionParser::InvalidOption, Reek::Configuration::ConfigFileException => error
26
27
  $stderr.puts "Error: #{error}"
27
28
  @status = STATUS_ERROR
@@ -34,10 +35,6 @@ module Reek
34
35
  @status
35
36
  end
36
37
 
37
- def initialize_configuration
38
- Configuration::AppConfiguration.initialize_with @options
39
- end
40
-
41
38
  def output(text)
42
39
  print text
43
40
  end
@@ -11,7 +11,7 @@ module Reek
11
11
  class ReekCommand < Command
12
12
  def execute(app)
13
13
  @options.sources.each do |source|
14
- reporter.add_examiner Examiner.new(source, smell_names)
14
+ reporter.add_examiner Examiner.new(source, smell_names, configuration: app.configuration)
15
15
  end
16
16
  reporter.smells? ? app.report_smells : app.report_success
17
17
  reporter.show
@@ -1,90 +1,144 @@
1
+ require 'pathname'
1
2
  require_relative './configuration_file_finder'
2
3
 
3
4
  module Reek
4
5
  # @api private
5
6
  module Configuration
6
7
  # @api private
7
- class ConfigFileException < StandardError; end
8
8
  #
9
9
  # Reek's singleton configuration instance.
10
10
  #
11
11
  # @api private
12
- module AppConfiguration
13
- NON_SMELL_TYPE_KEYS = %w(exclude_paths)
14
- EXCLUDE_PATHS_KEY = 'exclude_paths'
15
- @configuration = {}
16
- @has_been_initialized = false
17
-
18
- class << self
19
- attr_reader :configuration
20
-
21
- def initialize_with(options)
22
- @has_been_initialized = true
23
- configuration_file_path = ConfigurationFileFinder.find(options: options)
24
- return unless configuration_file_path
25
- load_from_file configuration_file_path
26
- end
12
+ class AppConfiguration
13
+ EXCLUDE_PATHS_KEY = 'exclude_paths'
14
+ attr_reader :exclude_paths, :default_directive, :directory_directives
27
15
 
28
- def configure_smell_repository(smell_repository)
29
- # Let users call this method directly without having initialized AppConfiguration before
30
- # and if they do, initialize it without application context
31
- initialize_with(nil) unless @has_been_initialized
32
- for_smell_types.each do |klass_name, config|
33
- klass = load_smell_type(klass_name)
34
- smell_repository.configure(klass, config) if klass
35
- end
36
- end
16
+ # Given this configuration file:
17
+ #
18
+ # ---
19
+ # IrresponsibleModule:
20
+ # enabled: false
21
+ # "app/helpers":
22
+ # UtilityFunction:
23
+ # enabled: false
24
+ # exclude_paths:
25
+ # - "app/controllers"
26
+ #
27
+ # this would result in the following configuration:
28
+ #
29
+ # exclude_paths = [ Pathname('spec/samples/two_smelly_files') ]
30
+ # default_directive = { Reek::Smells::IrresponsibleModule => { "enabled" => false } }
31
+ # directory_directives = { Pathname("spec/samples/three_clean_files/") =>
32
+ # { Reek::Smells::UtilityFunction => { "enabled" => false } } }
33
+ #
34
+ #
35
+ # @param options [OpenStruct]
36
+ # e.g.: #<OpenStruct report_format =: text,
37
+ # location_format =: numbers,
38
+ # colored = true,
39
+ # smells_to_detect = [],
40
+ # config_file = #<Pathname:config/defaults.reek>,
41
+ # argv = [ "lib/reek/spec" ]>
42
+ def initialize(options = nil)
43
+ self.directory_directives = {}
44
+ self.default_directive = {}
45
+ self.exclude_paths = []
37
46
 
38
- def load_from_file(path)
39
- if File.size(path) == 0
40
- report_problem('Empty file', path)
41
- return
42
- end
47
+ load options
48
+ end
43
49
 
44
- begin
45
- @configuration = YAML.load_file(path) || {}
46
- rescue => error
47
- raise_error(error.to_s, path)
48
- end
50
+ # @param source_via [String] - the source of the code inspected
51
+ # @return [Hash] the directory directive for the source or, if there is
52
+ # none, the default directive
53
+ def directive_for(source_via)
54
+ directory_directive_for_source(source_via) || default_directive
55
+ end
49
56
 
50
- raise_error('Not a hash', path) unless @configuration.is_a? Hash
51
- end
57
+ private
52
58
 
53
- def reset
54
- @configuration.clear
55
- end
59
+ attr_writer :exclude_paths, :default_directive, :directory_directives
56
60
 
57
- def exclude_paths
58
- @exclude_paths ||= @configuration.
59
- fetch(EXCLUDE_PATHS_KEY, []).
60
- map { |path| path.chomp('/') }
61
- end
61
+ # @param source_via [String] - the source of the code inspected
62
+ # Might be a string, STDIN or Filename / Pathname. We're only interested in the source
63
+ # when it's coming from file since a directory_directive doesn't make sense
64
+ # for anything else.
65
+ # @return [Hash | nil] the configuration for the source or nil
66
+ def directory_directive_for_source(source_via)
67
+ return unless source_via
68
+ source_base_dir = Pathname.new(source_via).dirname
69
+ hit = best_directory_match_for source_base_dir
70
+ directory_directives[hit]
71
+ end
62
72
 
63
- private
73
+ def load(options)
74
+ configuration_file = ConfigurationFileFinder.find_and_load(options: options)
64
75
 
65
- def for_smell_types
66
- @configuration.reject { |key, _value| NON_SMELL_TYPE_KEYS.include?(key) }
76
+ configuration_file.each do |key, value|
77
+ case
78
+ when key == EXCLUDE_PATHS_KEY
79
+ handle_exclude_paths(value)
80
+ when smell_type?(key)
81
+ handle_default_directive(key, value)
82
+ else
83
+ handle_directory_directive(key, value)
84
+ end
67
85
  end
86
+ end
68
87
 
69
- def load_smell_type(name)
70
- Reek::Smells.const_get(name)
71
- rescue NameError
72
- report_problem("\"#{name}\" is not a code smell")
73
- nil
88
+ def handle_exclude_paths(paths)
89
+ self.exclude_paths = paths.map do |path|
90
+ pathname = Pathname.new path.chomp('/')
91
+ raise ArgumentError, "Excluded directory #{path} does not exists" unless pathname.exist?
92
+ pathname
74
93
  end
94
+ end
75
95
 
76
- def report_problem(reason, path)
77
- $stderr.puts "Warning: #{message(reason, path)}"
78
- end
96
+ def handle_default_directive(key, config)
97
+ klass = Reek::Smells.const_get(key)
98
+ default_directive[klass] = config
99
+ end
79
100
 
80
- def raise_error(reason, path)
81
- raise ConfigFileException, message(reason, path)
82
- end
101
+ def handle_directory_directive(path, config)
102
+ pathname = Pathname.new path.chomp('/')
103
+ validate_directive pathname
83
104
 
84
- def message(reason, path)
85
- "Invalid configuration file \"#{File.basename(path)}\" -- #{reason}"
105
+ directory_directives[pathname] = config.each_with_object({}) do |(key, value), hash|
106
+ abort(error_message_for_invalid_smell_type(key)) unless smell_type?(key)
107
+ hash[Reek::Smells.const_get(key)] = value
86
108
  end
87
109
  end
110
+
111
+ def best_directory_match_for(source_base_dir)
112
+ directory_directives.
113
+ keys.
114
+ select { |pathname| source_base_dir.to_s =~ /#{pathname}/ }.
115
+ max_by { |pathname| pathname.to_s.length }
116
+ end
117
+
118
+ def smell_type?(key)
119
+ Reek::Smells.const_get key
120
+ rescue NameError
121
+ false
122
+ end
123
+
124
+ def error_message_for_invalid_smell_type(klass)
125
+ "You are trying to configure smell type #{klass} but we can't find one with that name.\n" \
126
+ "Please make sure you spelled it right (see 'config/defaults.reek' in the reek\n" \
127
+ 'repository for a list of all available smell types.'
128
+ end
129
+
130
+ def error_message_for_missing_directory(pathname)
131
+ "Configuration error: Directory `#{pathname}` does not exist"
132
+ end
133
+
134
+ def error_message_for_file_given(pathname)
135
+ "Configuration error: `#{pathname}` is supposed to be a directory but is a file"
136
+ end
137
+
138
+ def validate_directive(pathname)
139
+ abort(error_message_for_missing_directory(pathname)) unless pathname.exist?
140
+ abort(error_message_for_file_given(pathname)) unless pathname.directory?
141
+ end
88
142
  end
89
143
  end
90
144
  end
@@ -2,6 +2,7 @@ require 'pathname'
2
2
 
3
3
  module Reek
4
4
  module Configuration
5
+ class ConfigFileException < StandardError; end
5
6
  #
6
7
  # ConfigurationFileFinder is responsible for finding reek's configuration.
7
8
  #
@@ -17,6 +18,10 @@ module Reek
17
18
  module ConfigurationFileFinder
18
19
  module_function
19
20
 
21
+ def find_and_load(params = {})
22
+ load_from_file(find(params))
23
+ end
24
+
20
25
  # FIXME: switch to kwargs on upgrade to Ruby 2 and drop `params.fetch` calls:
21
26
  # def find(options: nil, current: Pathname.pwd, home: Pathname.new(Dir.home))
22
27
  def find(params = {})
@@ -37,6 +42,20 @@ module Reek
37
42
  return found if found
38
43
  end
39
44
  end
45
+
46
+ def load_from_file(path)
47
+ return {} unless path
48
+ begin
49
+ configuration = YAML.load_file(path) || {}
50
+ rescue => error
51
+ raise ConfigFileException, "Invalid configuration file #{path}, error is #{error}"
52
+ end
53
+
54
+ unless configuration.is_a? Hash
55
+ raise ConfigFileException, "Invalid configuration file \"#{path}\" -- Not a hash"
56
+ end
57
+ configuration
58
+ end
40
59
  end
41
60
  end
42
61
  end
@@ -1,6 +1,8 @@
1
1
  require_relative '../code_comment'
2
+ require_relative '../ast/object_refs'
2
3
 
3
4
  module Reek
5
+ # @api private
4
6
  module Context
5
7
  #
6
8
  # Superclass for all types of source code context. Each instance represents
@@ -11,23 +13,27 @@ module Reek
11
13
  # @api private
12
14
  class CodeContext
13
15
  attr_reader :exp
16
+ attr_reader :num_statements
17
+ attr_reader :children
18
+ attr_reader :visibility
14
19
 
15
20
  # Initializes a new CodeContext.
16
21
  #
17
- # context - *_context from the `core` namespace
18
- # exp - Reek::Source::ASTNode
22
+ # @param context [CodeContext, nil] The parent context
23
+ # @param exp [Reek::AST::Node] The code described by this context
19
24
  #
20
- # Examples:
25
+ # For example, given the following code:
21
26
  #
22
- # Given something like:
27
+ # class Omg
28
+ # def foo(x)
29
+ # puts x
30
+ # end
31
+ # end
23
32
  #
24
- # class Omg; def foo(x); puts x; end; end
33
+ # The {TreeWalker} object first instantiates a {RootContext}, which has no parent.
25
34
  #
26
- # the first time this is instantianted from TreeWalker `context` is a RootContext:
27
- #
28
- # #<Reek::Context::RootContext:0x00000002231098 @name="">
29
- #
30
- # and `exp` looks like this:
35
+ # Next, it instantiates a {ModuleContext}, with +context+ being the
36
+ # {RootContext} just created, and +exp+ looking like this:
31
37
  #
32
38
  # (class
33
39
  # (const nil :Omg) nil
@@ -37,20 +43,55 @@ module Reek
37
43
  # (send nil :puts
38
44
  # (lvar :x))))
39
45
  #
40
- # The next time we instantiate a CodeContext via TreeWalker `context` would be:
46
+ # Finally, {TreeWalker} will instantiate a {MethodContext}. This time,
47
+ # +context+ is the {ModuleContext} created above, and +exp+ is:
41
48
  #
42
- # Reek::Context::ModuleContext
49
+ # (def :foo
50
+ # (args
51
+ # (arg :x))
52
+ # (send nil :puts
53
+ # (lvar :x)))
54
+ def initialize(context, exp)
55
+ @context = context
56
+ @exp = exp
57
+ @visibility = :public
58
+ @children = []
59
+
60
+ @num_statements = 0
61
+ @refs = AST::ObjectRefs.new
62
+ end
63
+
64
+ # Register a child context. The child's parent context should be equal to
65
+ # the current context.
43
66
  #
44
- # and `exp` is:
67
+ # This makes the current context responsible for setting the child's
68
+ # visibility.
45
69
  #
46
- # (def :foo
47
- # (args
48
- # (arg :x))
49
- # (send nil :puts
50
- # (lvar :x)))
51
- def initialize(context, exp)
52
- @context = context
53
- @exp = exp
70
+ # @param child [CodeContext] the child context to register
71
+ def append_child_context(child)
72
+ child.visibility = tracked_visibility
73
+ @children << child
74
+ end
75
+
76
+ def count_statements(num)
77
+ @num_statements += num
78
+ end
79
+
80
+ def record_call_to(exp)
81
+ receiver = exp.receiver
82
+ type = receiver ? receiver.type : :self
83
+ case type
84
+ when :lvar, :lvasgn
85
+ unless exp.object_creation_call?
86
+ @refs.record_reference_to(receiver.name, line: exp.line)
87
+ end
88
+ when :self
89
+ @refs.record_reference_to(:self, line: exp.line)
90
+ end
91
+ end
92
+
93
+ def record_use_of_self
94
+ @refs.record_reference_to(:self)
54
95
  end
55
96
 
56
97
  def name
@@ -73,14 +114,6 @@ module Reek
73
114
  end
74
115
  end
75
116
 
76
- #
77
- # Bounces messages up the context tree to the first enclosing context
78
- # that knows how to deal with the request.
79
- #
80
- def method_missing(method, *args)
81
- @context.send(method, *args)
82
- end
83
-
84
117
  def num_methods
85
118
  0
86
119
  end
@@ -95,8 +128,48 @@ module Reek
95
128
  config[detector_class.smell_type] || {})
96
129
  end
97
130
 
131
+ # Handle the effects of a visibility modifier.
132
+ #
133
+ # @example Setting the current visibility
134
+ # track_visibility :public
135
+ #
136
+ # @example Modifying the visibility of existing children
137
+ # track_visibility :private, [:hide_me, :implementation_detail]
138
+ #
139
+ # @param visibility [Symbol]
140
+ # @param names [Array<Symbol>]
141
+ def track_visibility(visibility, names = [])
142
+ if names.any?
143
+ @children.each do |child|
144
+ child.visibility = visibility if names.include? child.name
145
+ end
146
+ else
147
+ @tracked_visibility = visibility
148
+ end
149
+ end
150
+
151
+ def type
152
+ @exp.type
153
+ end
154
+
155
+ # Iterate over +self+ and child contexts.
156
+ def each(&block)
157
+ yield self
158
+ @children.each do |child|
159
+ child.each(&block)
160
+ end
161
+ end
162
+
163
+ protected
164
+
165
+ attr_writer :visibility
166
+
98
167
  private
99
168
 
169
+ def tracked_visibility
170
+ @tracked_visibility ||= :public
171
+ end
172
+
100
173
  def config
101
174
  @config ||= if @exp
102
175
  CodeComment.new(@exp.full_comment || '').config