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