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
@@ -1,63 +1,17 @@
1
1
  require_relative 'code_context'
2
- require_relative '../ast/object_refs'
3
2
 
4
3
  module Reek
5
4
  module Context
6
- #
7
- # The parameters in a method's definition.
8
- #
9
- # @api private
10
- module MethodParameters
11
- def default_assignments
12
- result = []
13
- each do |exp|
14
- result << exp[1..2] if exp.optional_argument?
15
- end
16
- result
17
- end
18
- end
19
-
20
5
  #
21
6
  # A context wrapper for any method definition found in a syntax tree.
22
7
  #
23
8
  # @api private
24
9
  class MethodContext < CodeContext
25
- attr_reader :parameters
26
10
  attr_reader :refs
27
- attr_reader :num_statements
28
-
29
- def initialize(outer, exp)
30
- super
31
- @parameters = exp.parameters.dup
32
- @parameters.extend MethodParameters
33
- @num_statements = 0
34
- @refs = AST::ObjectRefs.new
35
- end
36
-
37
- def count_statements(num)
38
- @num_statements += num
39
- end
40
-
41
- def record_call_to(exp)
42
- receiver, meth = exp[1..2]
43
- receiver ||= [:self]
44
- case receiver[0]
45
- when :lvasgn
46
- @refs.record_reference_to(receiver.name, line: exp.line)
47
- when :lvar
48
- @refs.record_reference_to(receiver.name, line: exp.line) unless meth == :new
49
- when :self
50
- @refs.record_reference_to(:self, line: exp.line)
51
- end
52
- end
53
-
54
- def record_use_of_self
55
- @refs.record_reference_to(:self)
56
- end
57
11
 
58
12
  def envious_receivers
59
- return {} if @refs.self_is_max?
60
- @refs.most_popular
13
+ return {} if refs.self_is_max?
14
+ refs.most_popular
61
15
  end
62
16
 
63
17
  def references_self?
@@ -79,6 +33,15 @@ module Reek
79
33
  def uses_super_with_implicit_arguments?
80
34
  (body = exp.body) && body.contains_nested_node?(:zsuper)
81
35
  end
36
+
37
+ def default_assignments
38
+ @default_assignments ||=
39
+ exp.parameters.select(&:optional_argument?).map(&:children)
40
+ end
41
+
42
+ def singleton_method?
43
+ exp.singleton_method? || visibility == :module_function
44
+ end
82
45
  end
83
46
  end
84
47
  end
@@ -8,11 +8,6 @@ module Reek
8
8
  #
9
9
  # @api private
10
10
  class ModuleContext < CodeContext
11
- def initialize(outer, exp)
12
- super(outer, exp)
13
- @name = AST::SexpFormatter.format(exp.children.first)
14
- end
15
-
16
11
  def node_instance_methods
17
12
  local_nodes(:def)
18
13
  end
@@ -28,8 +23,9 @@ module Reek
28
23
  #
29
24
  # @return true if the module is a namespace module
30
25
  def namespace_module?
26
+ return false if exp.type == :casgn
31
27
  contents = exp.children.last
32
- contents && contents.find_nodes([:def, :defs], [:class, :module]).empty?
28
+ contents && contents.find_nodes([:def, :defs], [:casgn, :class, :module]).empty?
33
29
  end
34
30
  end
35
31
  end
@@ -1,25 +1,18 @@
1
+ require_relative 'code_context'
2
+
1
3
  module Reek
2
- # @api private
3
4
  module Context
4
5
  #
5
6
  # A context wrapper representing the root of an abstract syntax tree.
6
7
  #
7
8
  # @api private
8
- class RootContext
9
- def initialize
10
- @name = ''
11
- end
12
-
13
- def method_missing(_method, *_args)
14
- nil
15
- end
16
-
17
- def config_for(_)
18
- {}
9
+ class RootContext < CodeContext
10
+ def initialize(exp)
11
+ super(nil, exp)
19
12
  end
20
13
 
21
- def count_statements(_num)
22
- 0
14
+ def type
15
+ :root
23
16
  end
24
17
 
25
18
  def full_name
data/lib/reek/examiner.rb CHANGED
@@ -21,13 +21,16 @@ module Reek
21
21
  # If +source+ is a String it is assumed to be Ruby source code;
22
22
  # if it is a File or IO, it is opened and Ruby source code is read from it;
23
23
  #
24
- # @param smell_types_to_filter_by [Array<String>]
24
+ # @param filter_by_smells [Array<String>]
25
25
  # List of smell types to filter by.
26
26
  #
27
- def initialize(source, smell_types_to_filter_by = [])
28
- @source = Source::SourceCode.from(source)
29
- @collector = CLI::WarningCollector.new
30
- @smell_types = eligible_smell_types(smell_types_to_filter_by)
27
+ def initialize(source,
28
+ filter_by_smells = [],
29
+ configuration: Configuration::AppConfiguration.new)
30
+ @source = Source::SourceCode.from(source)
31
+ @configuration = configuration
32
+ @collector = CLI::WarningCollector.new
33
+ @smell_types = eligible_smell_types(filter_by_smells)
31
34
 
32
35
  run
33
36
  end
@@ -63,16 +66,18 @@ module Reek
63
66
  private
64
67
 
65
68
  def run
66
- smell_repository = Smells::SmellRepository.new(description, @smell_types)
69
+ smell_repository = Smells::SmellRepository.new(source_description: description,
70
+ smell_types: @smell_types,
71
+ configuration: @configuration)
67
72
  syntax_tree = @source.syntax_tree
68
- TreeWalker.new(smell_repository).process(syntax_tree) if syntax_tree
73
+ TreeWalker.new(smell_repository, syntax_tree).walk if syntax_tree
69
74
  smell_repository.report_on(@collector)
70
75
  end
71
76
 
72
- def eligible_smell_types(smell_types_to_filter_by = [])
73
- if smell_types_to_filter_by.any?
77
+ def eligible_smell_types(filter_by_smells = [])
78
+ if filter_by_smells.any?
74
79
  Smells::SmellRepository.smell_types.select do |klass|
75
- smell_types_to_filter_by.include? klass.smell_type
80
+ filter_by_smells.include? klass.smell_type
76
81
  end
77
82
  else
78
83
  Smells::SmellRepository.smell_types
@@ -1,5 +1,6 @@
1
- require 'rainbow'
2
1
  require 'json'
2
+ require 'pathname'
3
+ require 'rainbow'
3
4
  require_relative 'formatter'
4
5
  require_relative 'heading_formatter'
5
6
 
@@ -129,9 +130,9 @@ module Reek
129
130
  class HTMLReport < Base
130
131
  require 'erb'
131
132
 
132
- def show(target_path = 'reek.html')
133
- template_path = File.expand_path('../html_report.html.erb', __FILE__)
134
- File.write target_path, ERB.new(File.read(template_path)).result(binding)
133
+ def show(target_path = Pathname.new('reek.html'))
134
+ template_path = Pathname.new("#{__dir__}/html_report.html.erb")
135
+ File.write target_path, ERB.new(template_path.read).result(binding)
135
136
  puts 'HTML file saved'
136
137
  end
137
138
  end
@@ -24,7 +24,7 @@ module Reek
24
24
  # @return [Array<SmellWarning>]
25
25
  #
26
26
  def examine_context(method_ctx)
27
- method_ctx.parameters.default_assignments.select do |_param, value|
27
+ method_ctx.default_assignments.select do |_param, value|
28
28
  [:true, :false].include?(value[0])
29
29
  end.map do |parameter, _value|
30
30
  SmellWarning.new self,
@@ -108,7 +108,7 @@ module Reek
108
108
 
109
109
  def collect_calls(result)
110
110
  context.each_node(:send, [:mlhs]) do |call_node|
111
- next if initializer_call? call_node
111
+ next if call_node.object_creation_call?
112
112
  next if simple_method_call? call_node
113
113
  result[call_node].record(call_node)
114
114
  end
@@ -125,10 +125,6 @@ module Reek
125
125
  !call_node.receiver && call_node.args.empty?
126
126
  end
127
127
 
128
- def initializer_call?(call_node)
129
- call_node.method_name == :new
130
- end
131
-
132
128
  def allow_calls?(method)
133
129
  @allow_calls.any? { |allow| /#{allow}/ =~ method }
134
130
  end
@@ -11,7 +11,7 @@ module Reek
11
11
  # @api private
12
12
  class IrresponsibleModule < SmellDetector
13
13
  def self.contexts
14
- [:class, :module]
14
+ [:casgn, :class, :module]
15
15
  end
16
16
 
17
17
  #
@@ -26,7 +26,7 @@ module Reek
26
26
  context: ctx.full_name,
27
27
  lines: [expression.line],
28
28
  message: 'has no descriptive comment',
29
- parameters: { name: expression.text_name })]
29
+ parameters: { name: expression.name })]
30
30
  end
31
31
 
32
32
  private
@@ -9,45 +9,59 @@ module Reek
9
9
  #
10
10
  # @api private
11
11
  class SmellRepository
12
- attr_reader :detectors
13
-
14
12
  def self.smell_types
15
13
  Reek::Smells::SmellDetector.descendants.sort_by(&:name)
16
14
  end
17
15
 
18
- def initialize(source_description = nil, smell_types = self.class.smell_types)
19
- @typed_detectors = nil
20
- @detectors = {}
21
- smell_types.each do |klass|
22
- @detectors[klass] = klass.new(source_description)
16
+ def initialize(source_description: nil,
17
+ smell_types: self.class.smell_types,
18
+ configuration: Configuration::AppConfiguration.new)
19
+ self.source_via = source_description
20
+ self.typed_detectors = nil
21
+ self.configuration = configuration
22
+ self.smell_types = smell_types
23
+
24
+ configuration.directive_for(source_via).each do |klass, config|
25
+ configure klass, config
23
26
  end
24
- Configuration::AppConfiguration.configure_smell_repository self
25
27
  end
26
28
 
27
29
  def configure(klass, config)
28
- detector = @detectors[klass]
30
+ detector = detectors[klass]
29
31
  raise ArgumentError, "Unknown smell type #{klass} found in configuration" unless detector
30
32
  detector.configure_with(config)
31
33
  end
32
34
 
33
35
  def report_on(listener)
34
- @detectors.each_value { |detector| detector.report_on(listener) }
36
+ detectors.each_value { |detector| detector.report_on(listener) }
35
37
  end
36
38
 
37
- def examine(scope, node_type)
38
- smell_listeners[node_type].each do |detector|
39
+ def examine(scope)
40
+ smell_listeners[scope.type].each do |detector|
39
41
  detector.examine(scope)
40
42
  end
41
43
  end
42
44
 
45
+ def detectors
46
+ @initialized_detectors ||= begin
47
+ @detectors = {}
48
+ smell_types.each do |klass|
49
+ @detectors[klass] = klass.new(source_via)
50
+ end
51
+ @detectors
52
+ end
53
+ end
54
+
43
55
  private
44
56
 
57
+ attr_accessor :typed_detectors, :configuration, :source_via, :smell_types
58
+
45
59
  def smell_listeners
46
- unless @typed_detectors
47
- @typed_detectors = Hash.new { |hash, key| hash[key] = [] }
48
- @detectors.each_value { |detector| detector.register(@typed_detectors) }
60
+ unless typed_detectors
61
+ self.typed_detectors = Hash.new { |hash, key| hash[key] = [] }
62
+ detectors.each_value { |detector| detector.register(typed_detectors) }
49
63
  end
50
- @typed_detectors
64
+ typed_detectors
51
65
  end
52
66
  end
53
67
  end
@@ -54,7 +54,7 @@ module Reek
54
54
  # @return [Array<SmellWarning>]
55
55
  #
56
56
  def examine_context(method_ctx)
57
- return [] if method_ctx.exp.singleton_method?
57
+ return [] if method_ctx.singleton_method?
58
58
  return [] if method_ctx.num_statements == 0
59
59
  return [] if method_ctx.references_self?
60
60
  return [] if num_helper_methods(method_ctx).zero?
@@ -13,12 +13,15 @@ module Reek
13
13
  #
14
14
  # @api private
15
15
  class SourceCode
16
+ IO_IDENTIFIER = 'STDIN'
17
+ STRING_IDENTIFIER = 'string'
18
+
16
19
  attr_reader :description
17
20
 
18
21
  # Initializer.
19
22
  #
20
23
  # code - ruby code as String
21
- # description - in case of STDIN this is "STDIN" otherwise it's a filepath as String
24
+ # description - 'STDIN', 'string' or a filepath as String
22
25
  # parser - the parser to use for generating AST's out of the given source
23
26
  def initialize(code, description, parser = Parser::Ruby22)
24
27
  @source = code
@@ -27,8 +30,8 @@ module Reek
27
30
  end
28
31
 
29
32
  # Initializes an instance of SourceCode given a source.
30
- # This source can come via 3 different ways:
31
- # - from files a la `reek lib/reek/`
33
+ # This source can come via 4 different ways:
34
+ # - from Files or Pathnames a la `reek lib/reek/`
32
35
  # - from IO (STDIN) a la `echo "class Foo; end" | reek`
33
36
  # - from String via our rspec matchers a la `expect("class Foo; end").to reek`
34
37
  #
@@ -37,9 +40,10 @@ module Reek
37
40
  # @return an instance of SourceCode
38
41
  def self.from(source)
39
42
  case source
40
- when File then new(source.read, source.path)
41
- when IO then new(source.readlines.join, 'STDIN')
42
- when String then new(source, 'string')
43
+ when File then new(source.read, source.path)
44
+ when IO then new(source.readlines.join, IO_IDENTIFIER)
45
+ when Pathname then new(source.read, source.to_s)
46
+ when String then new(source, STRING_IDENTIFIER)
43
47
  end
44
48
  end
45
49
 
@@ -1,4 +1,5 @@
1
1
  require 'find'
2
+ require 'pathname'
2
3
 
3
4
  module Reek
4
5
  module Source
@@ -10,61 +11,61 @@ module Reek
10
11
  # Initialize with the paths we want to search.
11
12
  #
12
13
  # paths - a list of paths as Strings
13
- def initialize(paths)
14
- @pathnames = paths.
15
- map { |path| Pathname.new(path.chomp('/')) }.
16
- flat_map { |pathname| current_directory?(pathname) ? pathname.entries : pathname }
14
+ def initialize(paths, configuration: Configuration::AppConfiguration.new)
15
+ self.paths = paths.flat_map do |string|
16
+ path = Pathname.new(string)
17
+ current_directory?(path) ? path.entries : path
18
+ end
19
+ self.configuration = configuration
17
20
  end
18
21
 
19
22
  # Traverses all paths we initialized the SourceLocator with, finds
20
23
  # all relevant ruby files and returns them as a list.
21
24
  #
22
- # @return [Array<File>] - Ruby files found
25
+ # @return [Array<Pathname>] - Ruby paths found
23
26
  def sources
24
- source_paths.map { |pathname| File.new(pathname) }
27
+ source_paths
25
28
  end
26
29
 
27
30
  private
28
31
 
32
+ attr_accessor :paths, :configuration
33
+
29
34
  def source_paths
30
- @pathnames.each_with_object([]) do |given_pathname, relevant_paths|
31
- print_no_such_file_error(given_pathname) && next unless path_exists?(given_pathname)
32
- given_pathname.find do |pathname|
33
- if pathname.directory?
34
- ignore_path?(pathname) ? Find.prune : next
35
+ paths.each_with_object([]) do |given_path, relevant_paths|
36
+ print_no_such_file_error(given_path) && next unless given_path.exist?
37
+ given_path.find do |path|
38
+ if path.directory?
39
+ ignore_path?(path) ? Find.prune : next
35
40
  else
36
- relevant_paths << pathname if ruby_file?(pathname)
41
+ relevant_paths << path if ruby_file?(path)
37
42
  end
38
43
  end
39
44
  end
40
45
  end
41
46
 
42
- def path_excluded?(pathname)
43
- Configuration::AppConfiguration.exclude_paths.include? pathname.to_s
44
- end
45
-
46
- def path_exists?(path)
47
- Pathname.new(path).exist?
47
+ def path_excluded?(path)
48
+ configuration.exclude_paths.include?(path)
48
49
  end
49
50
 
50
51
  def print_no_such_file_error(path)
51
52
  $stderr.puts "Error: No such file - #{path}"
52
53
  end
53
54
 
54
- def hidden_directory?(pathname)
55
- pathname.basename.to_s.start_with? '.'
55
+ def hidden_directory?(path)
56
+ path.basename.to_s.start_with? '.'
56
57
  end
57
58
 
58
- def ignore_path?(pathname)
59
- path_excluded?(pathname) || hidden_directory?(pathname)
59
+ def ignore_path?(path)
60
+ path_excluded?(path) || hidden_directory?(path)
60
61
  end
61
62
 
62
- def ruby_file?(pathname)
63
- pathname.extname == '.rb'
63
+ def ruby_file?(path)
64
+ path.extname == '.rb'
64
65
  end
65
66
 
66
- def current_directory?(pathname)
67
- ['.', './'].include? pathname.to_s
67
+ def current_directory?(path)
68
+ [Pathname.new('.'), Pathname.new('./')].include?(path)
68
69
  end
69
70
  end
70
71
  end