reek 3.1 → 3.2

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/{CHANGELOG → CHANGELOG.md} +150 -123
  4. data/README.md +61 -21
  5. data/Rakefile +2 -1
  6. data/bin/reek +1 -0
  7. data/config/defaults.reek +2 -2
  8. data/docs/Attribute.md +9 -13
  9. data/docs/Basic-Smell-Options.md +2 -2
  10. data/docs/Command-Line-Options.md +2 -2
  11. data/docs/Too-Many-Instance-Variables.md +1 -1
  12. data/features/samples.feature +22 -31
  13. data/features/step_definitions/sample_file_steps.rb +2 -2
  14. data/features/support/env.rb +1 -0
  15. data/lib/reek.rb +1 -0
  16. data/lib/reek/ast/ast_node_class_map.rb +5 -1
  17. data/lib/reek/ast/node.rb +4 -2
  18. data/lib/reek/ast/object_refs.rb +9 -5
  19. data/lib/reek/ast/reference_collector.rb +4 -2
  20. data/lib/reek/cli/application.rb +12 -9
  21. data/lib/reek/cli/command.rb +4 -0
  22. data/lib/reek/cli/input.rb +4 -4
  23. data/lib/reek/cli/option_interpreter.rb +11 -7
  24. data/lib/reek/cli/options.rb +42 -40
  25. data/lib/reek/cli/reek_command.rb +3 -3
  26. data/lib/reek/cli/warning_collector.rb +7 -3
  27. data/lib/reek/code_comment.rb +5 -1
  28. data/lib/reek/configuration/app_configuration.rb +4 -4
  29. data/lib/reek/context/code_context.rb +19 -17
  30. data/lib/reek/examiner.rb +8 -6
  31. data/lib/reek/rake/task.rb +13 -22
  32. data/lib/reek/report/formatter.rb +5 -1
  33. data/lib/reek/report/report.rb +46 -44
  34. data/lib/reek/smells/attribute.rb +42 -24
  35. data/lib/reek/smells/control_parameter.rb +21 -13
  36. data/lib/reek/smells/data_clump.rb +17 -9
  37. data/lib/reek/smells/duplicate_method_call.rb +12 -6
  38. data/lib/reek/smells/long_parameter_list.rb +2 -2
  39. data/lib/reek/smells/long_yield_list.rb +4 -4
  40. data/lib/reek/smells/nested_iterators.rb +4 -2
  41. data/lib/reek/smells/nil_check.rb +6 -2
  42. data/lib/reek/smells/repeated_conditional.rb +2 -2
  43. data/lib/reek/smells/smell_configuration.rb +15 -7
  44. data/lib/reek/smells/smell_detector.rb +23 -10
  45. data/lib/reek/smells/smell_repository.rb +9 -16
  46. data/lib/reek/smells/smell_warning.rb +6 -6
  47. data/lib/reek/smells/too_many_instance_variables.rb +4 -4
  48. data/lib/reek/smells/too_many_methods.rb +2 -2
  49. data/lib/reek/smells/too_many_statements.rb +4 -4
  50. data/lib/reek/smells/uncommunicative_method_name.rb +5 -5
  51. data/lib/reek/smells/uncommunicative_module_name.rb +5 -5
  52. data/lib/reek/smells/uncommunicative_parameter_name.rb +8 -4
  53. data/lib/reek/smells/uncommunicative_variable_name.rb +8 -4
  54. data/lib/reek/source/source_code.rb +6 -2
  55. data/lib/reek/source/source_locator.rb +4 -4
  56. data/lib/reek/spec/should_reek.rb +9 -4
  57. data/lib/reek/spec/should_reek_of.rb +8 -5
  58. data/lib/reek/spec/should_reek_only_of.rb +12 -8
  59. data/lib/reek/tree_dresser.rb +6 -2
  60. data/lib/reek/tree_walker.rb +28 -22
  61. data/lib/reek/version.rb +1 -1
  62. data/reek.gemspec +6 -5
  63. data/spec/gem/yard_spec.rb +6 -9
  64. data/spec/reek/code_comment_spec.rb +1 -1
  65. data/spec/reek/report/xml_report_spec.rb +11 -21
  66. data/spec/reek/smells/attribute_spec.rb +73 -57
  67. data/spec/reek/smells/too_many_instance_variables_spec.rb +26 -12
  68. data/spec/reek/source/source_locator_spec.rb +2 -2
  69. data/spec/samples/checkstyle.xml +12 -1
  70. data/spec/spec_helper.rb +1 -0
  71. metadata +20 -7
  72. data/spec/samples/unusual_syntax.rb +0 -21
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'private_attr/everywhere'
3
4
  require 'rake'
4
5
  require 'rake/tasklib'
5
6
  require 'pathname'
@@ -38,17 +39,17 @@ module Reek
38
39
 
39
40
  # Path to reek's config file.
40
41
  # Setting the REEK_CFG environment variable overrides this.
41
- attr_writer :config_file
42
+ attr_accessor :config_file
42
43
 
43
44
  # Glob pattern to match source files.
44
45
  # Setting the REEK_SRC environment variable overrides this.
45
46
  # Defaults to 'lib/**/*.rb'.
46
- attr_writer :source_files
47
+ attr_accessor :source_files
47
48
 
48
49
  # String containing commandline options to be passed to Reek.
49
50
  # Setting the REEK_OPTS environment variable overrides this value.
50
51
  # Defaults to ''.
51
- attr_writer :reek_opts
52
+ attr_accessor :reek_opts
52
53
 
53
54
  # Whether or not to fail Rake when an error occurs (typically when smells are found).
54
55
  # Defaults to true.
@@ -59,10 +60,11 @@ module Reek
59
60
  attr_writer :verbose
60
61
 
61
62
  def initialize(name = :reek)
63
+ @config_file = ENV['REEK_CFG']
62
64
  @name = name
63
- @reek_opts = ''
65
+ @reek_opts = ENV['REEK_OPTS'] || ''
64
66
  @fail_on_error = true
65
- @source_files = 'lib/**/*.rb'
67
+ @source_files = ENV['REEK_SRC'] || 'lib/**/*.rb'
66
68
  @verbose = false
67
69
 
68
70
  yield self if block_given?
@@ -71,15 +73,17 @@ module Reek
71
73
 
72
74
  private
73
75
 
76
+ private_attr_reader :fail_on_error, :name, :verbose
77
+
74
78
  def define_task
75
79
  desc 'Check for code smells'
76
- task(@name) { run_task }
80
+ task(name) { run_task }
77
81
  end
78
82
 
79
83
  def run_task
80
- puts "\n\n!!! Running 'reek' rake command: #{command}\n\n" if @verbose
84
+ puts "\n\n!!! Running 'reek' rake command: #{command}\n\n" if verbose
81
85
  system(*command)
82
- abort("\n\n!!! `reek` has found smells - exiting!") if sys_call_failed? && @fail_on_error
86
+ abort("\n\n!!! `reek` has found smells - exiting!") if sys_call_failed? && fail_on_error
83
87
  end
84
88
 
85
89
  def command
@@ -88,25 +92,12 @@ module Reek
88
92
  reject(&:empty?)
89
93
  end
90
94
 
91
- def source_files
92
- FileList[ENV['REEK_SRC'] || @source_files]
93
- end
94
-
95
- def reek_opts
96
- ENV['REEK_OPTS'] || @reek_opts
97
- end
98
-
99
- def config_file
100
- ENV['REEK_CFG'] || @config_file
101
- end
102
-
103
95
  def sys_call_failed?
104
96
  !$CHILD_STATUS.success?
105
97
  end
106
98
 
107
99
  def config_file_as_argument
108
- return [] unless @config_file
109
- ['-c', @config_file]
100
+ config_file ? ['-c', config_file] : []
110
101
  end
111
102
 
112
103
  def reek_opts_as_arguments
@@ -35,7 +35,7 @@ module Reek
35
35
  end
36
36
 
37
37
  def format(warning)
38
- "#{@location_formatter.format(warning)}#{base_format(warning)}"
38
+ "#{location_formatter.format(warning)}#{base_format(warning)}"
39
39
  end
40
40
 
41
41
  private
@@ -43,6 +43,10 @@ module Reek
43
43
  def base_format(warning)
44
44
  "#{warning.context} #{warning.message} (#{warning.smell_type})"
45
45
  end
46
+
47
+ private
48
+
49
+ private_attr_reader :location_formatter
46
50
  end
47
51
 
48
52
  #
@@ -30,8 +30,8 @@ module Reek
30
30
  #
31
31
  # @param [Reek::Examiner] examiner object to report on
32
32
  def add_examiner(examiner)
33
- @total_smell_count += examiner.smells_count
34
- @examiners << examiner
33
+ self.total_smell_count += examiner.smells_count
34
+ examiners << examiner
35
35
  self
36
36
  end
37
37
 
@@ -42,13 +42,22 @@ module Reek
42
42
 
43
43
  # @api private
44
44
  def smells?
45
- @total_smell_count > 0
45
+ total_smell_count > 0
46
46
  end
47
47
 
48
48
  # @api private
49
49
  def smells
50
- @examiners.map(&:smells).flatten
50
+ examiners.map(&:smells).flatten
51
51
  end
52
+
53
+ protected
54
+
55
+ attr_accessor :total_smell_count
56
+
57
+ private
58
+
59
+ private_attr_reader :examiners, :options, :report_formatter,
60
+ :sort_by_issue_count, :warning_formatter
52
61
  end
53
62
 
54
63
  #
@@ -64,7 +73,7 @@ module Reek
64
73
  private
65
74
 
66
75
  def smell_summaries
67
- @examiners.map { |ex| summarize_single_examiner(ex) }.reject(&:empty?)
76
+ examiners.map { |ex| summarize_single_examiner(ex) }.reject(&:empty?)
68
77
  end
69
78
 
70
79
  def display_summary
@@ -72,33 +81,33 @@ module Reek
72
81
  end
73
82
 
74
83
  def display_total_smell_count
75
- return unless @examiners.size > 1
84
+ return unless examiners.size > 1
76
85
  print total_smell_count_message
77
86
  end
78
87
 
79
88
  def summarize_single_examiner(examiner)
80
89
  result = heading_formatter.header(examiner)
81
90
  if examiner.smelly?
82
- formatted_list = @report_formatter.format_list(examiner.smells,
83
- @warning_formatter)
91
+ formatted_list = report_formatter.format_list(examiner.smells,
92
+ warning_formatter)
84
93
  result += ":\n#{formatted_list}"
85
94
  end
86
95
  result
87
96
  end
88
97
 
89
98
  def sort_examiners
90
- @examiners.sort_by!(&:smells_count).reverse! if @sort_by_issue_count
99
+ examiners.sort_by!(&:smells_count).reverse! if sort_by_issue_count
91
100
  end
92
101
 
93
102
  def total_smell_count_message
94
103
  colour = smells? ? WARNINGS_COLOR : NO_WARNINGS_COLOR
95
- s = @total_smell_count == 1 ? '' : 's'
96
- Rainbow("#{@total_smell_count} total warning#{s}\n").color(colour)
104
+ s = total_smell_count == 1 ? '' : 's'
105
+ Rainbow("#{total_smell_count} total warning#{s}\n").color(colour)
97
106
  end
98
107
 
99
108
  def heading_formatter
100
109
  @heading_formatter ||=
101
- @options.fetch(:heading_formatter, HeadingFormatter::Quiet).new(@report_formatter)
110
+ options.fetch(:heading_formatter, HeadingFormatter::Quiet).new(report_formatter)
102
111
  end
103
112
  end
104
113
 
@@ -118,7 +127,7 @@ module Reek
118
127
  def show
119
128
  print ::JSON.generate(
120
129
  smells.map do |smell|
121
- smell.yaml_hash(@warning_formatter)
130
+ smell.yaml_hash(warning_formatter)
122
131
  end
123
132
  )
124
133
  end
@@ -144,53 +153,46 @@ module Reek
144
153
  require 'rexml/document'
145
154
 
146
155
  def show
147
- checkstyle = REXML::Element.new('checkstyle', document)
148
-
149
- smells.group_by(&:source).each do |file, file_smells|
150
- file_to_xml(file, file_smells, checkstyle)
151
- end
152
-
153
- print_xml(checkstyle.parent)
156
+ document.write output: $stdout, indent: 2
157
+ $stdout.puts
154
158
  end
155
159
 
156
160
  private
157
161
 
158
162
  def document
159
- REXML::Document.new.tap do |doc|
160
- doc << REXML::XMLDecl.new
163
+ REXML::Document.new.tap do |document|
164
+ document << REXML::XMLDecl.new << checkstyle
161
165
  end
162
166
  end
163
167
 
164
- def file_to_xml(file, file_smells, parent)
165
- REXML::Element.new('file', parent).tap do |element|
166
- element.attributes['name'] = File.realpath(file)
167
- smells_to_xml(file_smells, element)
168
+ def checkstyle
169
+ REXML::Element.new('checkstyle').tap do |checkstyle|
170
+ smells.group_by(&:source).each do |source, source_smells|
171
+ checkstyle << file(source, source_smells)
172
+ end
168
173
  end
169
174
  end
170
175
 
171
- def smells_to_xml(smells, parent)
172
- smells.each do |smell|
173
- smell_to_xml(smell, parent)
176
+ def file(name, smells)
177
+ REXML::Element.new('file').tap do |file|
178
+ file.add_attribute 'name', File.realpath(name)
179
+ smells.each do |smell|
180
+ smell.lines.each do |line|
181
+ file << error(smell, line)
182
+ end
183
+ end
174
184
  end
175
185
  end
176
186
 
177
- def smell_to_xml(smell, parent)
178
- REXML::Element.new('error', parent).tap do |element|
179
- attributes = [
180
- ['line', smell.lines.first],
181
- ['column', 0],
182
- ['severity', 'warning'],
183
- ['message', smell.message],
184
- ['source', smell.smell_type]
185
- ]
186
- element.add_attributes(attributes)
187
+ def error(smell, line)
188
+ REXML::Element.new('error').tap do |error|
189
+ error.add_attributes 'column' => 0,
190
+ 'line' => line,
191
+ 'message' => smell.message,
192
+ 'severity' => 'warning',
193
+ 'source' => smell.smell_type
187
194
  end
188
195
  end
189
-
190
- def print_xml(document)
191
- formatter = REXML::Formatters::Default.new
192
- puts formatter.write(document, '')
193
- end
194
196
  end
195
197
  end
196
198
  end
@@ -9,22 +9,18 @@ module Reek
9
9
  # invites client classes to become too intimate with its inner workings,
10
10
  # and in particular with its representation of state.
11
11
  #
12
- # Currently this detector raises a warning for every +attr+,
13
- # +attr_reader+, +attr_writer+ and +attr_accessor+ -- including those
14
- # that are private.
12
+ # This detector raises a warning for every public +attr_writer+,
13
+ # +attr_accessor+, and +attr+ with the writable flag set to +true+.
15
14
  #
16
15
  # See {file:docs/Attribute.md} for details.
17
16
  # @api private
18
17
  #
19
18
  # TODO: Catch attributes declared "by hand"
20
19
  class Attribute < SmellDetector
21
- ATTR_DEFN_METHODS = [:attr, :attr_reader, :attr_writer, :attr_accessor]
20
+ ATTR_DEFN_METHODS = [:attr_writer, :attr_accessor]
22
21
  VISIBILITY_MODIFIERS = [:private, :public, :protected]
23
22
 
24
23
  def initialize(*args)
25
- @visiblity_tracker = {}
26
- @visiblity_mode = :public
27
- @result = Set.new
28
24
  super
29
25
  end
30
26
 
@@ -32,39 +28,61 @@ module Reek
32
28
  [:class, :module]
33
29
  end
34
30
 
35
- def self.default_config
36
- super.merge(SmellConfiguration::ENABLED_KEY => false)
37
- end
38
-
39
31
  #
40
32
  # Checks whether the given class declares any attributes.
41
33
  #
42
34
  # @return [Array<SmellWarning>]
43
35
  #
44
36
  def examine_context(ctx)
37
+ self.visiblity_tracker = {}
38
+ self.visiblity_mode = :public
45
39
  attributes_in(ctx).map do |attribute, line|
46
40
  SmellWarning.new self,
47
41
  context: ctx.full_name,
48
42
  lines: [line],
49
- message: "declares the attribute #{attribute}",
43
+ message: "declares the writable attribute #{attribute}",
50
44
  parameters: { name: attribute.to_s }
51
45
  end
52
46
  end
53
47
 
54
48
  private
55
49
 
50
+ private_attr_accessor :visiblity_mode, :visiblity_tracker
51
+ private_attr_reader :result
52
+
56
53
  def attributes_in(module_ctx)
54
+ attributes = Set.new
57
55
  module_ctx.local_nodes(:send) do |call_node|
58
- if visibility_modifier?(call_node)
59
- track_visibility(call_node)
60
- elsif ATTR_DEFN_METHODS.include?(call_node.method_name)
61
- call_node.arg_names.each do |arg|
62
- @visiblity_tracker[arg] = @visiblity_mode
63
- @result << [arg, call_node.line]
64
- end
65
- end
56
+ attributes += track_attributes(call_node)
66
57
  end
67
- @result.select { |args| recorded_public_methods.include?(args[0]) }
58
+ attributes.select { |name, _line| recorded_public_methods.include?(name) }
59
+ end
60
+
61
+ def track_attributes(call_node)
62
+ if attribute_writer? call_node
63
+ return track_arguments call_node.args, call_node.line
64
+ end
65
+ track_visibility call_node if visibility_modifier? call_node
66
+ []
67
+ end
68
+
69
+ def attribute_writer?(call_node)
70
+ ATTR_DEFN_METHODS.include?(call_node.method_name) ||
71
+ attr_with_writable_flag?(call_node)
72
+ end
73
+
74
+ def attr_with_writable_flag?(call_node)
75
+ call_node.method_name == :attr && call_node.args.last.type == :true
76
+ end
77
+
78
+ def track_arguments(args, line)
79
+ args.select { |arg| arg.type == :sym }.map { |arg| track_argument(arg, line) }
80
+ end
81
+
82
+ def track_argument(arg, line)
83
+ arg_name = arg.children.first
84
+ visiblity_tracker[arg_name] = visiblity_mode
85
+ [arg_name, line]
68
86
  end
69
87
 
70
88
  def visibility_modifier?(call_node)
@@ -73,14 +91,14 @@ module Reek
73
91
 
74
92
  def track_visibility(call_node)
75
93
  if call_node.arg_names.any?
76
- call_node.arg_names.each { |arg| @visiblity_tracker[arg] = call_node.method_name }
94
+ call_node.arg_names.each { |arg| visiblity_tracker[arg] = call_node.method_name }
77
95
  else
78
- @visiblity_mode = call_node.method_name
96
+ self.visiblity_mode = call_node.method_name
79
97
  end
80
98
  end
81
99
 
82
100
  def recorded_public_methods
83
- @visiblity_tracker.select { |_, visbility| visbility == :public }
101
+ visiblity_tracker.select { |_, visbility| visbility == :public }
84
102
  end
85
103
  end
86
104
  end
@@ -73,16 +73,20 @@ module Reek
73
73
  end
74
74
 
75
75
  def smells?
76
- @occurences.any?
76
+ occurences.any?
77
77
  end
78
78
 
79
79
  def lines
80
- @occurences.map(&:line)
80
+ occurences.map(&:line)
81
81
  end
82
82
 
83
83
  def name
84
- @param.to_s
84
+ param.to_s
85
85
  end
86
+
87
+ private
88
+
89
+ private_attr_reader :occurences, :param
86
90
  end
87
91
 
88
92
  # Finds cases of ControlParameter in a particular node for a particular parameter
@@ -108,13 +112,15 @@ module Reek
108
112
 
109
113
  private
110
114
 
115
+ private_attr_reader :node, :param
116
+
111
117
  def conditional_nodes
112
- @node.body_nodes(CONDITIONAL_NODE_TYPES)
118
+ node.body_nodes(CONDITIONAL_NODE_TYPES)
113
119
  end
114
120
 
115
121
  def nested_finders
116
122
  @nested_finders ||= conditional_nodes.flat_map do |node|
117
- self.class.new(node, @param)
123
+ self.class.new(node, param)
118
124
  end
119
125
  end
120
126
 
@@ -129,12 +135,12 @@ module Reek
129
135
 
130
136
  def uses_of_param_in_condition
131
137
  return [] unless condition
132
- condition.each_node(:lvar).select { |inner| inner.var_name == @param }
138
+ condition.each_node(:lvar).select { |inner| inner.var_name == param }
133
139
  end
134
140
 
135
141
  def condition
136
- return nil unless CONDITIONAL_NODE_TYPES.include? @node.type
137
- @node.condition
142
+ return nil unless CONDITIONAL_NODE_TYPES.include? node.type
143
+ node.condition
138
144
  end
139
145
 
140
146
  def regular_call_involving_param?(call_node)
@@ -150,12 +156,12 @@ module Reek
150
156
  end
151
157
 
152
158
  def call_involving_param?(call_node)
153
- call_node.each_node(:lvar).any? { |it| it.var_name == @param }
159
+ call_node.each_node(:lvar).any? { |it| it.var_name == param }
154
160
  end
155
161
 
156
162
  def uses_param_in_body?
157
- nodes = @node.body_nodes([:lvar], [:if, :case, :and, :or])
158
- nodes.any? { |lvar_node| lvar_node.var_name == @param }
163
+ nodes = node.body_nodes([:lvar], [:if, :case, :and, :or])
164
+ nodes.any? { |lvar_node| lvar_node.var_name == param }
159
165
  end
160
166
  end
161
167
 
@@ -175,12 +181,14 @@ module Reek
175
181
 
176
182
  private
177
183
 
184
+ private_attr_reader :context
185
+
178
186
  def potential_parameters
179
- @context.exp.parameter_names
187
+ context.exp.parameter_names
180
188
  end
181
189
 
182
190
  def find_matches(param)
183
- ControlParameterFinder.new(@context.exp, param).find_matches
191
+ ControlParameterFinder.new(context.exp, param).find_matches
184
192
  end
185
193
  end
186
194
  end