reek 3.1 → 3.2

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