rubocop 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -10
  3. data/config/default.yml +59 -7
  4. data/lib/rubocop.rb +4 -0
  5. data/lib/rubocop/config_loader.rb +7 -6
  6. data/lib/rubocop/cop/bundler/duplicated_gem.rb +3 -3
  7. data/lib/rubocop/cop/bundler/gem_comment.rb +1 -1
  8. data/lib/rubocop/cop/commissioner.rb +1 -1
  9. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +3 -3
  10. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +4 -5
  11. data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +1 -1
  12. data/lib/rubocop/cop/generator.rb +1 -1
  13. data/lib/rubocop/cop/layout/block_alignment.rb +3 -4
  14. data/lib/rubocop/cop/layout/line_length.rb +8 -1
  15. data/lib/rubocop/cop/lint/constant_definition_in_block.rb +23 -2
  16. data/lib/rubocop/cop/lint/debugger.rb +17 -27
  17. data/lib/rubocop/cop/lint/duplicate_branch.rb +93 -0
  18. data/lib/rubocop/cop/lint/duplicate_case_condition.rb +2 -12
  19. data/lib/rubocop/cop/lint/empty_block.rb +23 -0
  20. data/lib/rubocop/cop/lint/empty_class.rb +93 -0
  21. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +21 -3
  22. data/lib/rubocop/cop/lint/loop.rb +4 -0
  23. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +19 -16
  24. data/lib/rubocop/cop/lint/shadowed_exception.rb +4 -5
  25. data/lib/rubocop/cop/lint/useless_method_definition.rb +2 -4
  26. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  27. data/lib/rubocop/cop/mixin/statement_modifier.rb +9 -4
  28. data/lib/rubocop/cop/naming/variable_number.rb +16 -0
  29. data/lib/rubocop/cop/style/and_or.rb +1 -3
  30. data/lib/rubocop/cop/style/collection_compact.rb +6 -0
  31. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +100 -5
  32. data/lib/rubocop/cop/style/identical_conditional_branches.rb +7 -2
  33. data/lib/rubocop/cop/style/if_inside_else.rb +37 -1
  34. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -3
  35. data/lib/rubocop/cop/style/infinite_loop.rb +4 -0
  36. data/lib/rubocop/cop/style/multiple_comparison.rb +3 -2
  37. data/lib/rubocop/cop/style/negated_if_else_condition.rb +7 -2
  38. data/lib/rubocop/cop/style/nil_lambda.rb +52 -0
  39. data/lib/rubocop/cop/style/static_class.rb +97 -0
  40. data/lib/rubocop/cop/style/while_until_modifier.rb +9 -0
  41. data/lib/rubocop/target_ruby.rb +57 -1
  42. data/lib/rubocop/version.rb +1 -1
  43. metadata +9 -5
@@ -140,11 +140,10 @@ module RuboCop
140
140
  rescued_groups.each_cons(2).all? do |x, y|
141
141
  if x.include?(Exception)
142
142
  false
143
- elsif y.include?(Exception)
144
- true
145
- elsif x.none? || y.none?
146
- # consider sorted if a group is empty or only contains
147
- # `nil`s
143
+ elsif y.include?(Exception) ||
144
+ # consider sorted if a group is empty or only contains
145
+ # `nil`s
146
+ x.none? || y.none?
148
147
  true
149
148
  else
150
149
  (x <=> y || 0) <= 0
@@ -54,11 +54,9 @@ module RuboCop
54
54
  end
55
55
 
56
56
  def delegating?(node, def_node)
57
- if node.nil?
58
- false
59
- elsif node.zsuper_type?
57
+ if node&.zsuper_type?
60
58
  true
61
- elsif node.super_type?
59
+ elsif node&.super_type?
62
60
  node.arguments.map(&:source) == def_node.arguments.map(&:source)
63
61
  else
64
62
  false
@@ -103,7 +103,7 @@ module RuboCop
103
103
  # hashes wrapped in a set of curly braces like {foo: 1}.
104
104
  # That is, not a kwargs hash. For method calls, this ensures
105
105
  # the method call is made with parens.
106
- starts_with_bracket = node.loc.begin
106
+ starts_with_bracket = !node.hash_type? || node.loc.begin
107
107
 
108
108
  # If the call has a second argument, we can insert a line
109
109
  # break before the second argument and the rest of the
@@ -19,7 +19,8 @@ module RuboCop
19
19
  def non_eligible_node?(node)
20
20
  node.modifier_form? ||
21
21
  node.nonempty_line_count > 3 ||
22
- processed_source.line_with_comment?(node.loc.last_line)
22
+ processed_source.line_with_comment?(node.loc.last_line) ||
23
+ (first_line_comment(node) && code_after(node))
23
24
  end
24
25
 
25
26
  def non_eligible_body?(body)
@@ -41,11 +42,9 @@ module RuboCop
41
42
 
42
43
  def length_in_modifier_form(node)
43
44
  keyword_element = node.loc.keyword
44
- end_element = node.loc.end
45
45
  code_before = keyword_element.source_line[0...keyword_element.column]
46
- code_after = end_element.source_line[end_element.last_column..-1]
47
46
  expression = to_modifier_form(node)
48
- line_length("#{code_before}#{expression}#{code_after}")
47
+ line_length("#{code_before}#{expression}#{code_after(node)}")
49
48
  end
50
49
 
51
50
  def to_modifier_form(node)
@@ -64,6 +63,12 @@ module RuboCop
64
63
  comment_source unless comment_disables_cop?(comment_source)
65
64
  end
66
65
 
66
+ def code_after(node)
67
+ end_element = node.loc.end
68
+ code = end_element.source_line[end_element.last_column..-1]
69
+ code unless code.empty?
70
+ end
71
+
67
72
  def parenthesize?(node)
68
73
  # Parenthesize corrected expression if changing to modifier-if form
69
74
  # would change the meaning of the parent expression
@@ -92,6 +92,10 @@ module RuboCop
92
92
  # # good
93
93
  # :some_sym_1
94
94
  #
95
+ # @example AllowedIdentifier: [capture3]
96
+ # # good
97
+ # expect(Open3).to receive(:capture3)
98
+ #
95
99
  class VariableNumber < Base
96
100
  include ConfigurableNumbering
97
101
 
@@ -108,12 +112,16 @@ module RuboCop
108
112
 
109
113
  def on_def(node)
110
114
  @node = node
115
+ return if allowed_identifier?(node.method_name)
116
+
111
117
  check_name(node, node.method_name, node.loc.name) if cop_config['CheckMethodNames']
112
118
  end
113
119
  alias on_defs on_def
114
120
 
115
121
  def on_sym(node)
116
122
  @node = node
123
+ return if allowed_identifier?(node.value)
124
+
117
125
  check_name(node, node.value, node) if cop_config['CheckSymbols']
118
126
  end
119
127
 
@@ -129,6 +137,14 @@ module RuboCop
129
137
 
130
138
  format(MSG, style: style, identifier_type: identifier_type)
131
139
  end
140
+
141
+ def allowed_identifier?(name)
142
+ allowed_identifiers.include?(name.to_s)
143
+ end
144
+
145
+ def allowed_identifiers
146
+ cop_config.fetch('AllowedIdentifiers', [])
147
+ end
132
148
  end
133
149
  end
134
150
  end
@@ -66,9 +66,7 @@ module RuboCop
66
66
  node.each_child_node do |expr|
67
67
  if expr.send_type?
68
68
  correct_send(expr, corrector)
69
- elsif expr.return_type?
70
- correct_other(expr, corrector)
71
- elsif expr.assignment?
69
+ elsif expr.return_type? || expr.assignment?
72
70
  correct_other(expr, corrector)
73
71
  end
74
72
  end
@@ -6,6 +6,12 @@ module RuboCop
6
6
  # This cop checks for places where custom logic on rejection nils from arrays
7
7
  # and hashes can be replaced with `{Array,Hash}#{compact,compact!}`.
8
8
  #
9
+ # It is marked as unsafe by default because false positives may occur in the
10
+ # nil check of block arguments to the receiver object.
11
+ # For example, `[[1, 2], [3, nil]].reject { |first, second| second.nil? }`
12
+ # and `[[1, 2], [3, nil]].compact` are not compatible. This will work fine
13
+ # when the receiver is a hash object.
14
+ #
9
15
  # @example
10
16
  # # bad
11
17
  # array.reject { |e| e.nil? }
@@ -25,7 +25,7 @@ module RuboCop
25
25
  # end
26
26
  # end
27
27
  #
28
- # # good
28
+ # # good, inline comments in heredoc
29
29
  # UNSAFE_STRING_METHODS.each do |unsafe_method|
30
30
  # if 'String'.respond_to?(unsafe_method)
31
31
  # class_eval <<-EOT, __FILE__, __LINE__ + 1
@@ -41,25 +41,120 @@ module RuboCop
41
41
  # end
42
42
  # end
43
43
  #
44
+ # # good, block comments in heredoc
45
+ # class_eval <<-EOT, __FILE__, __LINE__ + 1
46
+ # # def capitalize!(*params)
47
+ # # @dirty = true
48
+ # # super
49
+ # # end
50
+ #
51
+ # def #{unsafe_method}!(*params)
52
+ # @dirty = true
53
+ # super
54
+ # end
55
+ # EOT
56
+ #
57
+ # # good, block comments before heredoc
58
+ # class_eval(
59
+ # # def capitalize!(*params)
60
+ # # @dirty = true
61
+ # # super
62
+ # # end
63
+ #
64
+ # <<-EOT, __FILE__, __LINE__ + 1
65
+ # def #{unsafe_method}!(*params)
66
+ # @dirty = true
67
+ # super
68
+ # end
69
+ # EOT
70
+ # )
44
71
  class DocumentDynamicEvalDefinition < Base
72
+ BLOCK_COMMENT_REGEXP = /^\s*#(?!{)/.freeze
73
+ COMMENT_REGEXP = /\s*#(?!{).*/.freeze
45
74
  MSG = 'Add a comment block showing its appearance if interpolated.'
46
75
 
47
76
  RESTRICT_ON_SEND = %i[eval class_eval module_eval instance_eval].freeze
48
77
 
49
78
  def on_send(node)
50
79
  arg_node = node.first_argument
51
- return unless arg_node&.dstr_type?
52
80
 
53
- add_offense(node.loc.selector) unless comment_docs?(arg_node)
81
+ return unless arg_node&.dstr_type? && interpolated?(arg_node)
82
+ return if inline_comment_docs?(arg_node) || comment_block_docs?(arg_node)
83
+
84
+ add_offense(node.loc.selector)
54
85
  end
55
86
 
56
87
  private
57
88
 
58
- def comment_docs?(node)
89
+ def interpolated?(arg_node)
90
+ arg_node.each_child_node(:begin).any?
91
+ end
92
+
93
+ def inline_comment_docs?(node)
59
94
  node.each_child_node(:begin).all? do |begin_node|
60
95
  source_line = processed_source.lines[begin_node.first_line - 1]
61
- source_line.match?(/\s*#[^{]+/)
96
+ source_line.match?(COMMENT_REGEXP)
97
+ end
98
+ end
99
+
100
+ def comment_block_docs?(arg_node)
101
+ comments = heredoc_comment_blocks(arg_node.loc.heredoc_body.line_span)
102
+ .concat(preceding_comment_blocks(arg_node.parent))
103
+
104
+ return if comments.none?
105
+
106
+ regexp = comment_regexp(arg_node)
107
+ comments.any? { |comment| regexp.match?(comment) } || regexp.match?(comments.join)
108
+ end
109
+
110
+ def preceding_comment_blocks(node)
111
+ # Collect comments in the method call, but outside the heredoc
112
+ comments = processed_source.each_comment_in_lines(node.loc.expression.line_span)
113
+
114
+ comments.each_with_object({}) do |comment, hash|
115
+ merge_adjacent_comments(comment.text, comment.loc.line, hash)
116
+ end.values
117
+ end
118
+
119
+ def heredoc_comment_blocks(heredoc_body)
120
+ # Collect comments inside the heredoc
121
+ line_range = (heredoc_body.begin - 1)..(heredoc_body.end - 1)
122
+ lines = processed_source.lines[line_range]
123
+
124
+ lines.each_with_object({}).with_index(line_range.begin) do |(line, hash), index|
125
+ merge_adjacent_comments(line, index, hash)
126
+ end.values
127
+ end
128
+
129
+ def merge_adjacent_comments(line, index, hash)
130
+ # Combine adjacent comment lines into a single string
131
+ return unless (line = line.dup.gsub!(BLOCK_COMMENT_REGEXP, ''))
132
+
133
+ hash[index] = if hash.keys.last == index - 1
134
+ [hash.delete(index - 1), line].join("\n")
135
+ else
136
+ line
137
+ end
138
+ end
139
+
140
+ def comment_regexp(arg_node)
141
+ # Replace the interpolations with wildcards
142
+ regexp_parts = arg_node.child_nodes.map do |n|
143
+ n.begin_type? ? /.+/ : source_to_regexp(n.source)
62
144
  end
145
+
146
+ Regexp.new(regexp_parts.join)
147
+ end
148
+
149
+ def source_to_regexp(source)
150
+ # Get the source in the heredoc being `eval`ed, without any comments
151
+ # and turn it into a regexp
152
+ return /\s+/ if source.blank?
153
+
154
+ source = source.gsub(COMMENT_REGEXP, '')
155
+ return if source.blank?
156
+
157
+ /\s*#{Regexp.escape(source.strip)}/
63
158
  end
64
159
  end
65
160
  end
@@ -3,8 +3,13 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # This cop checks for identical lines at the beginning or end of
7
- # each branch of a conditional statement.
6
+ # This cop checks for identical expressions at the beginning or end of
7
+ # each branch of a conditional expression. Such expressions should normally
8
+ # be placed outside the conditional expression - before or after it.
9
+ #
10
+ # NOTE: The cop is poorly named and some people might think that it actually
11
+ # checks for duplicated conditional branches. The name will probably be changed
12
+ # in a future major RuboCop release.
8
13
  #
9
14
  # @example
10
15
  # # bad
@@ -59,6 +59,9 @@ module RuboCop
59
59
  # end
60
60
  #
61
61
  class IfInsideElse < Base
62
+ include RangeHelp
63
+ extend AutoCorrector
64
+
62
65
  MSG = 'Convert `if` nested inside `else` to `elsif`.'
63
66
 
64
67
  def on_if(node)
@@ -69,11 +72,44 @@ module RuboCop
69
72
  return unless else_branch&.if_type? && else_branch&.if?
70
73
  return if allow_if_modifier_in_else_branch?(else_branch)
71
74
 
72
- add_offense(else_branch.loc.keyword)
75
+ add_offense(else_branch.loc.keyword) do |corrector|
76
+ autocorrect(corrector, else_branch)
77
+ end
73
78
  end
74
79
 
75
80
  private
76
81
 
82
+ def autocorrect(corrector, node)
83
+ if node.modifier_form?
84
+ correct_to_elsif_from_modifier_form(corrector, node)
85
+ end_range = node.parent.loc.end
86
+ else
87
+ correct_to_elsif_from_if_inside_else_form(corrector, node, node.condition)
88
+ end_range = node.loc.end
89
+ end
90
+ corrector.remove(range_by_whole_lines(end_range, include_final_newline: true))
91
+ corrector.remove(
92
+ range_by_whole_lines(node.if_branch.source_range, include_final_newline: true)
93
+ )
94
+ end
95
+
96
+ def correct_to_elsif_from_modifier_form(corrector, node)
97
+ corrector.replace(node.parent.loc.else, <<~RUBY.chop)
98
+ elsif #{node.condition.source}
99
+ #{indent(node.if_branch)}#{node.if_branch.source}
100
+ end
101
+ RUBY
102
+ end
103
+
104
+ def correct_to_elsif_from_if_inside_else_form(corrector, node, condition)
105
+ corrector.replace(node.parent.loc.else, "elsif #{condition.source}")
106
+ if_condition_range = range_between(
107
+ node.loc.keyword.begin_pos, condition.source_range.end_pos
108
+ )
109
+ corrector.replace(if_condition_range, node.if_branch.source)
110
+ corrector.remove(condition)
111
+ end
112
+
77
113
  def allow_if_modifier_in_else_branch?(else_branch)
78
114
  allow_if_modifier? && else_branch&.modifier_form?
79
115
  end
@@ -21,14 +21,18 @@ module RuboCop
21
21
  # Foo.do_something
22
22
  # end
23
23
  #
24
- # do_something_in_a_method_with_a_long_name(arg) if long_condition
24
+ # do_something_with_a_long_name(arg) if long_condition_that_prevents_code_fit_on_single_line
25
25
  #
26
26
  # # good
27
27
  # do_stuff(bar) if condition
28
28
  # Foo.do_something unless qux.empty?
29
29
  #
30
- # if long_condition
31
- # do_something_in_a_method_with_a_long_name(arg)
30
+ # if long_condition_that_prevents_code_fit_on_single_line
31
+ # do_something_with_a_long_name(arg)
32
+ # end
33
+ #
34
+ # if short_condition # a long comment that makes it too long if it were just a single line
35
+ # do_something
32
36
  # end
33
37
  class IfUnlessModifier < Base
34
38
  include StatementModifier
@@ -5,6 +5,10 @@ module RuboCop
5
5
  module Style
6
6
  # Use `Kernel#loop` for infinite loops.
7
7
  #
8
+ # This cop is marked as unsafe as the rule does not necessarily
9
+ # apply if the body might raise a `StopIteration` exception; contrary to
10
+ # other infinite loops, `Kernel#loop` silently rescues that and returns `nil`.
11
+ #
8
12
  # @example
9
13
  # # bad
10
14
  # while true
@@ -48,6 +48,7 @@ module RuboCop
48
48
 
49
49
  def on_new_investigation
50
50
  @compared_elements = []
51
+ @allowed_method_comparison = false
51
52
  end
52
53
 
53
54
  def on_or(node)
@@ -55,6 +56,7 @@ module RuboCop
55
56
 
56
57
  return unless node == root_of_or_node
57
58
  return unless nested_variable_comparison?(root_of_or_node)
59
+ return if @allowed_method_comparison
58
60
 
59
61
  add_offense(node) do |corrector|
60
62
  elements = @compared_elements.join(', ')
@@ -95,8 +97,7 @@ module RuboCop
95
97
  return [variable_name(var1), variable_name(var2)]
96
98
  end
97
99
  if (var, obj = simple_comparison_lhs?(node)) || (obj, var = simple_comparison_rhs?(node))
98
- return [] if allow_method_comparison? && obj.send_type?
99
-
100
+ @allowed_method_comparison = true if allow_method_comparison? && obj.send_type?
100
101
  @compared_elements << obj.source
101
102
  return [variable_name(var)]
102
103
  end
@@ -28,6 +28,7 @@ module RuboCop
28
28
  # x ? do_something_else : do_something
29
29
  #
30
30
  class NegatedIfElseCondition < Base
31
+ include RangeHelp
31
32
  extend AutoCorrector
32
33
 
33
34
  MSG = 'Invert the negated condition and swap the %<type>s branches.'
@@ -90,8 +91,12 @@ module RuboCop
90
91
  end
91
92
 
92
93
  def swap_branches(corrector, node)
93
- corrector.replace(node.if_branch, node.else_branch.source)
94
- corrector.replace(node.else_branch, node.if_branch.source)
94
+ if node.if_branch.nil?
95
+ corrector.remove(range_by_whole_lines(node.loc.else, include_final_newline: true))
96
+ else
97
+ corrector.replace(node.if_branch, node.else_branch.source)
98
+ corrector.replace(node.else_branch, node.if_branch.source)
99
+ end
95
100
  end
96
101
  end
97
102
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for lambdas that always return nil, which can be replaced
7
+ # with an empty lambda instead.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # -> { nil }
12
+ #
13
+ # lambda do
14
+ # next nil
15
+ # end
16
+ #
17
+ # # good
18
+ # -> {}
19
+ #
20
+ # lambda do
21
+ # end
22
+ #
23
+ # -> (x) { nil if x }
24
+ #
25
+ class NilLambda < Base
26
+ extend AutoCorrector
27
+ include RangeHelp
28
+
29
+ MSG = 'Use an empty lambda instead of always returning nil.'
30
+
31
+ def_node_matcher :nil_return?, <<~PATTERN
32
+ { ({return next break} nil) (nil) }
33
+ PATTERN
34
+
35
+ def on_block(node)
36
+ return unless node.lambda?
37
+ return unless nil_return?(node.body)
38
+
39
+ add_offense(node) do |corrector|
40
+ range = if node.single_line?
41
+ range_with_surrounding_space(range: node.body.loc.expression)
42
+ else
43
+ range_by_whole_lines(node.body.loc.expression, include_final_newline: true)
44
+ end
45
+
46
+ corrector.remove(range)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end