rubocop 1.6.1 → 1.9.1

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +4 -3
  4. data/config/default.yml +145 -19
  5. data/lib/rubocop.rb +16 -1
  6. data/lib/rubocop/cli/command/auto_genenerate_config.rb +5 -4
  7. data/lib/rubocop/comment_config.rb +6 -6
  8. data/lib/rubocop/config.rb +13 -7
  9. data/lib/rubocop/config_loader.rb +11 -14
  10. data/lib/rubocop/config_loader_resolver.rb +21 -4
  11. data/lib/rubocop/config_obsoletion.rb +5 -3
  12. data/lib/rubocop/config_store.rb +12 -1
  13. data/lib/rubocop/cop/base.rb +2 -1
  14. data/lib/rubocop/cop/exclude_limit.rb +26 -0
  15. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +3 -2
  16. data/lib/rubocop/cop/generator.rb +1 -3
  17. data/lib/rubocop/cop/internal_affairs.rb +6 -1
  18. data/lib/rubocop/cop/internal_affairs/empty_line_between_expect_offense_and_correction.rb +68 -0
  19. data/lib/rubocop/cop/internal_affairs/example_description.rb +89 -0
  20. data/lib/rubocop/cop/internal_affairs/redundant_described_class_as_subject.rb +61 -0
  21. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +64 -0
  22. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +145 -0
  23. data/lib/rubocop/cop/layout/class_structure.rb +7 -2
  24. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +56 -20
  25. data/lib/rubocop/cop/layout/first_argument_indentation.rb +16 -2
  26. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +14 -0
  27. data/lib/rubocop/cop/layout/line_length.rb +2 -1
  28. data/lib/rubocop/cop/layout/multiline_operation_indentation.rb +2 -2
  29. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +3 -10
  30. data/lib/rubocop/cop/layout/space_around_equals_in_parameter_default.rb +1 -0
  31. data/lib/rubocop/cop/layout/space_before_block_braces.rb +2 -0
  32. data/lib/rubocop/cop/layout/space_before_brackets.rb +67 -0
  33. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +13 -10
  34. data/lib/rubocop/cop/layout/space_inside_hash_literal_braces.rb +2 -2
  35. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +59 -0
  36. data/lib/rubocop/cop/lint/binary_operator_with_identical_operands.rb +7 -2
  37. data/lib/rubocop/cop/lint/deprecated_constants.rb +80 -0
  38. data/lib/rubocop/cop/lint/duplicate_branch.rb +64 -2
  39. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +44 -0
  40. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +10 -6
  41. data/lib/rubocop/cop/lint/number_conversion.rb +41 -6
  42. data/lib/rubocop/cop/lint/numbered_parameter_assignment.rb +47 -0
  43. data/lib/rubocop/cop/lint/or_assignment_to_constant.rb +39 -0
  44. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +2 -1
  45. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +50 -0
  46. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +50 -17
  47. data/lib/rubocop/cop/lint/shadowed_exception.rb +1 -11
  48. data/lib/rubocop/cop/lint/symbol_conversion.rb +103 -0
  49. data/lib/rubocop/cop/lint/triple_quotes.rb +71 -0
  50. data/lib/rubocop/cop/lint/unreachable_loop.rb +17 -0
  51. data/lib/rubocop/cop/message_annotator.rb +4 -1
  52. data/lib/rubocop/cop/metrics/block_nesting.rb +2 -2
  53. data/lib/rubocop/cop/metrics/parameter_lists.rb +5 -2
  54. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  55. data/lib/rubocop/cop/mixin/allowed_identifiers.rb +18 -0
  56. data/lib/rubocop/cop/mixin/check_line_breakable.rb +5 -0
  57. data/lib/rubocop/cop/mixin/code_length.rb +3 -1
  58. data/lib/rubocop/cop/mixin/comments_help.rb +1 -11
  59. data/lib/rubocop/cop/mixin/configurable_max.rb +1 -0
  60. data/lib/rubocop/cop/mixin/first_element_line_break.rb +1 -1
  61. data/lib/rubocop/cop/mixin/method_complexity.rb +3 -1
  62. data/lib/rubocop/cop/mixin/uncommunicative_name.rb +5 -1
  63. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +59 -5
  64. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +38 -5
  65. data/lib/rubocop/cop/naming/variable_name.rb +2 -0
  66. data/lib/rubocop/cop/naming/variable_number.rb +2 -9
  67. data/lib/rubocop/cop/registry.rb +10 -0
  68. data/lib/rubocop/cop/severity.rb +3 -3
  69. data/lib/rubocop/cop/style/access_modifier_declarations.rb +3 -1
  70. data/lib/rubocop/cop/style/ascii_comments.rb +1 -1
  71. data/lib/rubocop/cop/style/collection_methods.rb +14 -1
  72. data/lib/rubocop/cop/style/commented_keyword.rb +22 -5
  73. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +49 -9
  74. data/lib/rubocop/cop/style/empty_literal.rb +6 -2
  75. data/lib/rubocop/cop/style/endless_method.rb +102 -0
  76. data/lib/rubocop/cop/style/eval_with_location.rb +63 -34
  77. data/lib/rubocop/cop/style/explicit_block_argument.rb +10 -0
  78. data/lib/rubocop/cop/style/float_division.rb +3 -0
  79. data/lib/rubocop/cop/style/for.rb +2 -0
  80. data/lib/rubocop/cop/style/format_string_token.rb +18 -2
  81. data/lib/rubocop/cop/style/hash_except.rb +95 -0
  82. data/lib/rubocop/cop/style/hash_like_case.rb +2 -1
  83. data/lib/rubocop/cop/style/if_inside_else.rb +22 -10
  84. data/lib/rubocop/cop/style/if_with_boolean_literal_branches.rb +120 -0
  85. data/lib/rubocop/cop/style/keyword_parameters_order.rb +12 -2
  86. data/lib/rubocop/cop/style/lambda_call.rb +2 -1
  87. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +7 -0
  88. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +16 -6
  89. data/lib/rubocop/cop/style/method_def_parentheses.rb +7 -0
  90. data/lib/rubocop/cop/style/multiline_method_signature.rb +26 -1
  91. data/lib/rubocop/cop/style/multiline_when_then.rb +3 -1
  92. data/lib/rubocop/cop/style/mutable_constant.rb +13 -3
  93. data/lib/rubocop/cop/style/nested_parenthesized_calls.rb +4 -0
  94. data/lib/rubocop/cop/style/nil_comparison.rb +3 -0
  95. data/lib/rubocop/cop/style/non_nil_check.rb +23 -13
  96. data/lib/rubocop/cop/style/numeric_literals.rb +6 -9
  97. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  98. data/lib/rubocop/cop/style/raise_args.rb +5 -2
  99. data/lib/rubocop/cop/style/redundant_argument.rb +7 -1
  100. data/lib/rubocop/cop/style/redundant_freeze.rb +8 -4
  101. data/lib/rubocop/cop/style/redundant_return.rb +1 -1
  102. data/lib/rubocop/cop/style/single_line_methods.rb +36 -2
  103. data/lib/rubocop/cop/style/sole_nested_conditional.rb +29 -5
  104. data/lib/rubocop/cop/style/string_concatenation.rb +1 -1
  105. data/lib/rubocop/cop/style/symbol_proc.rb +5 -4
  106. data/lib/rubocop/cop/style/ternary_parentheses.rb +1 -1
  107. data/lib/rubocop/cop/style/while_until_modifier.rb +2 -4
  108. data/lib/rubocop/cop/util.rb +3 -1
  109. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +1 -0
  110. data/lib/rubocop/formatter/simple_text_formatter.rb +2 -1
  111. data/lib/rubocop/magic_comment.rb +30 -1
  112. data/lib/rubocop/options.rb +10 -10
  113. data/lib/rubocop/rspec/cop_helper.rb +0 -4
  114. data/lib/rubocop/rspec/expect_offense.rb +37 -22
  115. data/lib/rubocop/runner.rb +17 -1
  116. data/lib/rubocop/target_finder.rb +4 -2
  117. data/lib/rubocop/target_ruby.rb +47 -11
  118. data/lib/rubocop/util.rb +16 -0
  119. data/lib/rubocop/version.rb +8 -2
  120. metadata +27 -7
@@ -3,9 +3,14 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # This cop checks `eval` method usage. `eval` can receive source location
7
- # metadata, that are filename and line number. The metadata is used by
8
- # backtraces. This cop recommends to pass the metadata to `eval` method.
6
+ # This cop ensures that eval methods (`eval`, `instance_eval`, `class_eval`
7
+ # and `module_eval`) are given filename and line number values (`__FILE__`
8
+ # and `__LINE__`). This data is used to ensure that any errors raised
9
+ # within the evaluated code will be given the correct identification
10
+ # in a backtrace.
11
+ #
12
+ # The cop also checks that the line number given relative to `__LINE__` is
13
+ # correct.
9
14
  #
10
15
  # @example
11
16
  # # bad
@@ -32,27 +37,17 @@ module RuboCop
32
37
  # end
33
38
  # RUBY
34
39
  class EvalWithLocation < Base
35
- MSG = 'Pass `__FILE__` and `__LINE__` to `eval` method, ' \
36
- 'as they are used by backtraces.'
37
- MSG_INCORRECT_LINE = 'Use `%<expected>s` instead of `%<actual>s`, ' \
38
- 'as they are used by backtraces.'
40
+ MSG = 'Pass `__FILE__` and `__LINE__` to `%<method_name>s`.'
41
+ MSG_EVAL = 'Pass a binding, `__FILE__` and `__LINE__` to `eval`.'
42
+ MSG_INCORRECT_FILE = 'Incorrect file for `%<method_name>s`; ' \
43
+ 'use `%<expected>s` instead of `%<actual>s`.'
44
+ MSG_INCORRECT_LINE = 'Incorrect line number for `%<method_name>s`; ' \
45
+ 'use `%<expected>s` instead of `%<actual>s`.'
39
46
 
40
47
  RESTRICT_ON_SEND = %i[eval class_eval module_eval instance_eval].freeze
41
48
 
42
- def_node_matcher :eval_without_location?, <<~PATTERN
43
- {
44
- (send nil? :eval ${str dstr})
45
- (send nil? :eval ${str dstr} _)
46
- (send nil? :eval ${str dstr} _ #special_file_keyword?)
47
- (send nil? :eval ${str dstr} _ #special_file_keyword? _)
48
-
49
- (send _ {:class_eval :module_eval :instance_eval}
50
- ${str dstr})
51
- (send _ {:class_eval :module_eval :instance_eval}
52
- ${str dstr} #special_file_keyword?)
53
- (send _ {:class_eval :module_eval :instance_eval}
54
- ${str dstr} #special_file_keyword? _)
55
- }
49
+ def_node_matcher :valid_eval_receiver?, <<~PATTERN
50
+ { nil? (const {nil? cbase} :Kernel) }
56
51
  PATTERN
57
52
 
58
53
  def_node_matcher :line_with_offset?, <<~PATTERN
@@ -63,17 +58,31 @@ module RuboCop
63
58
  PATTERN
64
59
 
65
60
  def on_send(node)
66
- eval_without_location?(node) do |code|
67
- if with_lineno?(node)
68
- on_with_lineno(node, code)
69
- else
70
- add_offense(node)
71
- end
61
+ # Classes should not redefine eval, but in case one does, it shouldn't
62
+ # register an offense. Only `eval` without a receiver and `Kernel.eval`
63
+ # are considered.
64
+ return if node.method?(:eval) && !valid_eval_receiver?(node.receiver)
65
+
66
+ code = node.arguments.first
67
+ return unless code && (code.str_type? || code.dstr_type?)
68
+
69
+ file, line = file_and_line(node)
70
+
71
+ if line
72
+ check_file(node, file)
73
+ check_line(node, code)
74
+ else
75
+ register_offense(node)
72
76
  end
73
77
  end
74
78
 
75
79
  private
76
80
 
81
+ def register_offense(node)
82
+ msg = node.method?(:eval) ? MSG_EVAL : format(MSG, method_name: node.method_name)
83
+ add_offense(node, message: msg)
84
+ end
85
+
77
86
  def special_file_keyword?(node)
78
87
  node.str_type? &&
79
88
  node.source == '__FILE__'
@@ -84,6 +93,11 @@ module RuboCop
84
93
  node.source == '__LINE__'
85
94
  end
86
95
 
96
+ def file_and_line(node)
97
+ base = node.method?(:eval) ? 2 : 1
98
+ [node.arguments[base], node.arguments[base + 1]]
99
+ end
100
+
87
101
  # FIXME: It's a Style/ConditionalAssignment's false positive.
88
102
  # rubocop:disable Style/ConditionalAssignment
89
103
  def with_lineno?(node)
@@ -95,17 +109,32 @@ module RuboCop
95
109
  end
96
110
  # rubocop:enable Style/ConditionalAssignment
97
111
 
98
- def message_incorrect_line(actual, sign, line_diff)
112
+ def message_incorrect_line(method_name, actual, sign, line_diff)
99
113
  expected =
100
114
  if line_diff.zero?
101
115
  '__LINE__'
102
116
  else
103
117
  "__LINE__ #{sign} #{line_diff}"
104
118
  end
105
- format(MSG_INCORRECT_LINE, actual: actual.source, expected: expected)
119
+
120
+ format(MSG_INCORRECT_LINE,
121
+ method_name: method_name,
122
+ actual: actual.source,
123
+ expected: expected)
124
+ end
125
+
126
+ def check_file(node, file_node)
127
+ return true if special_file_keyword?(file_node)
128
+
129
+ message = format(MSG_INCORRECT_FILE,
130
+ method_name: node.method_name,
131
+ expected: '__FILE__',
132
+ actual: file_node.source)
133
+
134
+ add_offense(file_node, message: message)
106
135
  end
107
136
 
108
- def on_with_lineno(node, code)
137
+ def check_line(node, code)
109
138
  line_node = node.arguments.last
110
139
  lineno_range = line_node.loc.expression
111
140
  line_diff = string_first_line(code) - lineno_range.first_line
@@ -124,22 +153,22 @@ module RuboCop
124
153
  end
125
154
  end
126
155
 
127
- def add_offense_for_same_line(_node, line_node)
156
+ def add_offense_for_same_line(node, line_node)
128
157
  return if special_line_keyword?(line_node)
129
158
 
130
159
  add_offense(
131
160
  line_node.loc.expression,
132
- message: message_incorrect_line(line_node, nil, 0)
161
+ message: message_incorrect_line(node.method_name, line_node, nil, 0)
133
162
  )
134
163
  end
135
164
 
136
- def add_offense_for_different_line(_node, line_node, line_diff)
165
+ def add_offense_for_different_line(node, line_node, line_diff)
137
166
  sign = line_diff.positive? ? :+ : :-
138
167
  return if line_with_offset?(line_node, sign, line_diff.abs)
139
168
 
140
169
  add_offense(
141
170
  line_node.loc.expression,
142
- message: message_incorrect_line(line_node, sign, line_diff.abs)
171
+ message: message_incorrect_line(node.method_name, line_node, sign, line_diff.abs)
143
172
  )
144
173
  end
145
174
  end
@@ -6,6 +6,9 @@ module RuboCop
6
6
  # This cop enforces the use of explicit block argument to avoid writing
7
7
  # block literal that just passes its arguments to another block.
8
8
  #
9
+ # NOTE: This cop only registers an offense if the block args match the
10
+ # yield args exactly.
11
+ #
9
12
  # @example
10
13
  # # bad
11
14
  # def with_tmp_dir
@@ -75,7 +78,14 @@ module RuboCop
75
78
  private
76
79
 
77
80
  def yielding_arguments?(block_args, yield_args)
81
+ yield_args = yield_args.dup.fill(
82
+ nil,
83
+ yield_args.length, block_args.length - yield_args.length
84
+ )
85
+
78
86
  yield_args.zip(block_args).all? do |yield_arg, block_arg|
87
+ next false unless yield_arg && block_arg
88
+
79
89
  block_arg && yield_arg.children.first == block_arg.children.first
80
90
  end
81
91
  end
@@ -7,6 +7,9 @@ module RuboCop
7
7
  # It is recommended to either always use `fdiv` or coerce one side only.
8
8
  # This cop also provides other options for code consistency.
9
9
  #
10
+ # This cop is marked as unsafe, because if operand variable is a string object
11
+ # then `.to_f` will be removed and an error will occur.
12
+ #
10
13
  # @example EnforcedStyle: single_coerce (default)
11
14
  # # bad
12
15
  # a.to_f / b.to_f
@@ -51,6 +51,7 @@ module RuboCop
51
51
  if style == :each
52
52
  add_offense(node, message: PREFER_EACH) do |corrector|
53
53
  ForToEachCorrector.new(node).call(corrector)
54
+ opposite_style_detected
54
55
  end
55
56
  else
56
57
  correct_style_detected
@@ -63,6 +64,7 @@ module RuboCop
63
64
  if style == :for
64
65
  add_offense(node, message: PREFER_FOR) do |corrector|
65
66
  EachToForCorrector.new(node).call(corrector)
67
+ opposite_style_detected
66
68
  end
67
69
  else
68
70
  correct_style_detected
@@ -11,6 +11,8 @@ module RuboCop
11
11
  # The reason is that _unannotated_ format is very similar
12
12
  # to encoded URLs or Date/Time formatting strings.
13
13
  #
14
+ # This cop can be customized ignored methods with `IgnoredMethods`.
15
+ #
14
16
  # @example EnforcedStyle: annotated (default)
15
17
  #
16
18
  # # bad
@@ -58,12 +60,18 @@ module RuboCop
58
60
  #
59
61
  # # good
60
62
  # format('%06d', 10)
63
+ #
64
+ # @example IgnoredMethods: [redirect]
65
+ #
66
+ # # good
67
+ # redirect('foo/%{bar_id}')
68
+ #
61
69
  class FormatStringToken < Base
62
70
  include ConfigurableEnforcedStyle
71
+ include IgnoredMethods
63
72
 
64
73
  def on_str(node)
65
- return unless node.value.include?('%')
66
- return if node.each_ancestor(:xstr, :regexp).any?
74
+ return if format_string_token?(node) || use_ignored_method?(node)
67
75
 
68
76
  detections = collect_detections(node)
69
77
  return if detections.empty?
@@ -88,6 +96,14 @@ module RuboCop
88
96
  }
89
97
  PATTERN
90
98
 
99
+ def format_string_token?(node)
100
+ !node.value.include?('%') || node.each_ancestor(:xstr, :regexp).any?
101
+ end
102
+
103
+ def use_ignored_method?(node)
104
+ (parent = node.parent) && parent.send_type? && ignored_method?(parent.method_name)
105
+ end
106
+
91
107
  def unannotated_format?(node, detected_style)
92
108
  detected_style == :unannotated && !format_string_in_typical_context?(node)
93
109
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for usages of `Hash#reject`, `Hash#select`, and `Hash#filter` methods
7
+ # that can be replaced with `Hash#except` method.
8
+ #
9
+ # This cop should only be enabled on Ruby version 3.0 or higher.
10
+ # (`Hash#except` was added in Ruby 3.0.)
11
+ #
12
+ # For safe detection, it is limited to commonly used string and symbol comparisons
13
+ # when used `==`.
14
+ # And do not check `Hash#delete_if` and `Hash#keep_if` to change receiver object.
15
+ #
16
+ # @example
17
+ #
18
+ # # bad
19
+ # {foo: 1, bar: 2, baz: 3}.reject {|k, v| k == :bar }
20
+ # {foo: 1, bar: 2, baz: 3}.select {|k, v| k != :bar }
21
+ # {foo: 1, bar: 2, baz: 3}.filter {|k, v| k != :bar }
22
+ #
23
+ # # good
24
+ # {foo: 1, bar: 2, baz: 3}.except(:bar)
25
+ #
26
+ class HashExcept < Base
27
+ include RangeHelp
28
+ extend TargetRubyVersion
29
+ extend AutoCorrector
30
+
31
+ minimum_target_ruby_version 3.0
32
+
33
+ MSG = 'Use `%<prefer>s` instead.'
34
+ RESTRICT_ON_SEND = %i[reject select filter].freeze
35
+
36
+ def_node_matcher :bad_method?, <<~PATTERN
37
+ (block
38
+ (send _ _)
39
+ (args
40
+ (arg _)
41
+ (arg _))
42
+ (send
43
+ _ {:== :!= :eql?} _))
44
+ PATTERN
45
+
46
+ def on_send(node)
47
+ block = node.parent
48
+ return unless bad_method?(block) && semantically_except_method?(node, block)
49
+
50
+ except_key = except_key(block)
51
+ return unless safe_to_register_offense?(block, except_key)
52
+
53
+ range = offense_range(node)
54
+ preferred_method = "except(#{except_key.source})"
55
+
56
+ add_offense(range, message: format(MSG, prefer: preferred_method)) do |corrector|
57
+ corrector.replace(range, preferred_method)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def semantically_except_method?(send, block)
64
+ body = block.body
65
+
66
+ case send.method_name
67
+ when :reject
68
+ body.method?('==') || body.method?('eql?')
69
+ when :select, :filter
70
+ body.method?('!=')
71
+ else
72
+ false
73
+ end
74
+ end
75
+
76
+ def safe_to_register_offense?(block, except_key)
77
+ return true if block.body.method?('eql?')
78
+
79
+ except_key.sym_type? || except_key.str_type?
80
+ end
81
+
82
+ def except_key(node)
83
+ key_argument = node.argument_list.first
84
+ lhs, _method_name, rhs = *node.body
85
+
86
+ [lhs, rhs].find { |operand| operand.source != key_argument.source }
87
+ end
88
+
89
+ def offense_range(node)
90
+ range_between(node.loc.selector.begin_pos, node.parent.loc.end.end_pos)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -68,7 +68,8 @@ module RuboCop
68
68
  length = cop_config['MinBranchesCount'] || 3
69
69
  return length if length.is_a?(Integer) && length.positive?
70
70
 
71
- raise 'MinBranchesCount needs to be a positive integer!'
71
+ warn Rainbow('`MinBranchesCount` needs to be a positive integer!').red
72
+ exit!
72
73
  end
73
74
  end
74
75
  end
@@ -82,15 +82,14 @@ module RuboCop
82
82
  def autocorrect(corrector, node)
83
83
  if node.modifier_form?
84
84
  correct_to_elsif_from_modifier_form(corrector, node)
85
- end_range = node.parent.loc.end
86
85
  else
87
86
  correct_to_elsif_from_if_inside_else_form(corrector, node, node.condition)
88
- end_range = node.loc.end
89
87
  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
- )
88
+ corrector.remove(range_by_whole_lines(find_end_range(node), include_final_newline: true))
89
+ return unless (if_branch = node.if_branch)
90
+
91
+ range = range_by_whole_lines(if_branch.source_range, include_final_newline: true)
92
+ corrector.remove(range)
94
93
  end
95
94
 
96
95
  def correct_to_elsif_from_modifier_form(corrector, node)
@@ -103,13 +102,26 @@ module RuboCop
103
102
 
104
103
  def correct_to_elsif_from_if_inside_else_form(corrector, node, condition)
105
104
  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)
105
+ if_condition_range = if_condition_range(node, condition)
106
+ if (if_branch = node.if_branch)
107
+ corrector.replace(if_condition_range, if_branch.source)
108
+ else
109
+ corrector.remove(range_by_whole_lines(if_condition_range, include_final_newline: true))
110
+ end
110
111
  corrector.remove(condition)
111
112
  end
112
113
 
114
+ def find_end_range(node)
115
+ end_range = node.loc.end
116
+ return end_range if end_range
117
+
118
+ find_end_range(node.parent)
119
+ end
120
+
121
+ def if_condition_range(node, condition)
122
+ range_between(node.loc.keyword.begin_pos, condition.source_range.end_pos)
123
+ end
124
+
113
125
  def allow_if_modifier_in_else_branch?(else_branch)
114
126
  allow_if_modifier? && else_branch&.modifier_form?
115
127
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for redundant `if` with boolean literal branches.
7
+ # It checks only conditions to return boolean value (`true` or `false`) for safe detection.
8
+ # The conditions to be checked are comparison methods, predicate methods, and double negative.
9
+ # However, auto-correction is unsafe because there is no guarantee that all predicate methods
10
+ # will return boolean value. Those methods can be allowed with `AllowedMethods` config.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # if foo == bar
15
+ # true
16
+ # else
17
+ # false
18
+ # end
19
+ #
20
+ # # bad
21
+ # foo == bar ? true : false
22
+ #
23
+ # # good
24
+ # foo == bar
25
+ #
26
+ # @example AllowedMethods: ['nonzero?']
27
+ # # good
28
+ # num.nonzero? ? true : false
29
+ #
30
+ class IfWithBooleanLiteralBranches < Base
31
+ include AllowedMethods
32
+ extend AutoCorrector
33
+
34
+ MSG = 'Remove redundant %<keyword>s with boolean literal branches.'
35
+ MSG_FOR_ELSIF = 'Use `else` instead of redundant `elsif` with boolean literal branches.'
36
+
37
+ def_node_matcher :if_with_boolean_literal_branches?, <<~PATTERN
38
+ (if #return_boolean_value? {(true) (false) | (false) (true)})
39
+ PATTERN
40
+ def_node_matcher :double_negative?, '(send (send _ :!) :!)'
41
+
42
+ def on_if(node)
43
+ return unless if_with_boolean_literal_branches?(node)
44
+
45
+ condition = node.condition
46
+ range, keyword = offense_range_with_keyword(node, condition)
47
+
48
+ add_offense(range, message: message(node, keyword)) do |corrector|
49
+ replacement = replacement_condition(node, condition)
50
+
51
+ if node.elsif?
52
+ corrector.insert_before(node, "else\n")
53
+ corrector.replace(node, "#{indent(node.if_branch)}#{replacement}")
54
+ else
55
+ corrector.replace(node, replacement)
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def offense_range_with_keyword(node, condition)
63
+ if node.ternary?
64
+ range = condition.source_range.end.join(node.source_range.end)
65
+
66
+ [range, 'ternary operator']
67
+ else
68
+ keyword = node.loc.keyword
69
+
70
+ [keyword, "`#{keyword.source}`"]
71
+ end
72
+ end
73
+
74
+ def message(node, keyword)
75
+ message_template = node.elsif? ? MSG_FOR_ELSIF : MSG
76
+
77
+ format(message_template, keyword: keyword)
78
+ end
79
+
80
+ def return_boolean_value?(condition)
81
+ if condition.begin_type?
82
+ return_boolean_value?(condition.children.first)
83
+ elsif condition.or_type?
84
+ return_boolean_value?(condition.lhs) && return_boolean_value?(condition.rhs)
85
+ elsif condition.and_type?
86
+ return_boolean_value?(condition.rhs)
87
+ else
88
+ assume_boolean_value?(condition)
89
+ end
90
+ end
91
+
92
+ def assume_boolean_value?(condition)
93
+ return false unless condition.send_type?
94
+ return false if allowed_method?(condition.method_name)
95
+
96
+ condition.comparison_method? || condition.predicate_method? || double_negative?(condition)
97
+ end
98
+
99
+ def replacement_condition(node, condition)
100
+ bang = '!' if opposite_condition?(node)
101
+
102
+ if bang && require_parentheses?(condition)
103
+ "#{bang}(#{condition.source})"
104
+ else
105
+ "#{bang}#{condition.source}"
106
+ end
107
+ end
108
+
109
+ def opposite_condition?(node)
110
+ !node.unless? && node.if_branch.false_type? || node.unless? && node.if_branch.true_type?
111
+ end
112
+
113
+ def require_parentheses?(condition)
114
+ condition.and_type? || condition.or_type? ||
115
+ condition.send_type? && condition.comparison_method?
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end