rubocop-vibe 0.1.0 → 0.3.0

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.
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Shared helper methods for consecutive alignment cops.
7
+ #
8
+ # Provides common functionality for grouping consecutive statements
9
+ # and creating source ranges for autocorrection.
10
+ module AlignmentHelpers
11
+ private
12
+
13
+ # Extract statements from a body node.
14
+ #
15
+ # @param [RuboCop::AST::Node] body The body node.
16
+ # @return [Array<RuboCop::AST::Node>]
17
+ def extract_statements(body)
18
+ if body.begin_type?
19
+ body.children
20
+ else
21
+ [body]
22
+ end
23
+ end
24
+
25
+ # Group consecutive statements based on a predicate.
26
+ #
27
+ # Iterates through statements and groups consecutive ones where the block
28
+ # returns true. Groups are broken by non-matching statements or blank lines.
29
+ #
30
+ # @param [Array<RuboCop::AST::Node>] statements The statements.
31
+ # @yield [RuboCop::AST::Node] Block to determine if statement matches criteria.
32
+ # @return [Array<Array<RuboCop::AST::Node>>] Groups of consecutive matching statements.
33
+ def group_consecutive_statements(statements, &)
34
+ matching_with_indices = find_matching_statements(statements, &)
35
+
36
+ group_by_consecutive_lines(matching_with_indices, statements)
37
+ end
38
+
39
+ # Find statements matching the predicate with their indices.
40
+ #
41
+ # @param [Array<RuboCop::AST::Node>] statements The statements.
42
+ # @return [Array<Array>] Array of [index, statement] pairs.
43
+ def find_matching_statements(statements)
44
+ statements.each_with_index.filter_map do |stmt, idx|
45
+ [idx, stmt] if yield(stmt)
46
+ end
47
+ end
48
+
49
+ # Group matched statements that are on consecutive lines.
50
+ #
51
+ # @param [Array<Array>] matches Array of [index, statement] pairs.
52
+ # @param [Array<RuboCop::AST::Node>] statements Original statements for line lookups.
53
+ # @return [Array<Array<RuboCop::AST::Node>>] Groups with 2+ consecutive statements.
54
+ def group_by_consecutive_lines(matches, statements)
55
+ matches
56
+ .chunk_while { |a, b| consecutive?(a, b, statements) }
57
+ .filter_map { |group| group.map(&:last) if group.size > 1 }
58
+ end
59
+
60
+ # Check if two matched statements are consecutive (no gaps).
61
+ #
62
+ # @param [Array] first First [index, statement] pair.
63
+ # @param [Array] second Second [index, statement] pair.
64
+ # @param [Array<RuboCop::AST::Node>] statements Original statements.
65
+ # @return [Boolean]
66
+ def consecutive?(first, second, statements)
67
+ idx_a, stmt_a = first
68
+ idx_b, = second
69
+ idx_b == idx_a + 1 && no_blank_line_between?(stmt_a, statements[idx_b])
70
+ end
71
+
72
+ # Check if there's no blank line between two statements.
73
+ #
74
+ # @param [RuboCop::AST::Node] stmt_a First statement.
75
+ # @param [RuboCop::AST::Node] stmt_b Second statement.
76
+ # @return [Boolean]
77
+ def no_blank_line_between?(stmt_a, stmt_b)
78
+ stmt_b.loc.line - stmt_a.loc.last_line <= 1
79
+ end
80
+
81
+ # Create a source range between two positions.
82
+ #
83
+ # @param [Integer] start_pos The start position.
84
+ # @param [Integer] end_pos The end position.
85
+ # @return [Parser::Source::Range]
86
+ def range_between(start_pos, end_pos)
87
+ Parser::Source::Range.new(processed_source.buffer, start_pos, end_pos)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces that hash arguments in multiline method calls are on separate
7
+ # lines and alphabetically ordered by key name.
8
+ #
9
+ # This cop applies to method calls where the closing parenthesis is on
10
+ # its own line.
11
+ #
12
+ # Value alignment (table style) is handled separately by Layout/HashAlignment.
13
+ #
14
+ # @example
15
+ # # bad - multiple hash pairs on same line
16
+ # SomeService.call(
17
+ # website_id: website.id, data: data
18
+ # )
19
+ #
20
+ # # bad - hash pairs not alphabetically ordered
21
+ # SomeService.call(
22
+ # website_id: website.id,
23
+ # data: data
24
+ # )
25
+ #
26
+ # # good
27
+ # SomeService.call(
28
+ # data: data,
29
+ # website_id: website.id
30
+ # )
31
+ class MultilineHashArgumentStyle < Base
32
+ extend AutoCorrector
33
+
34
+ MSG = "Hash arguments in multiline calls should be one per line " \
35
+ "and alphabetically ordered."
36
+
37
+ # Check send nodes for hash arguments that need reformatting.
38
+ #
39
+ # @param [RuboCop::AST::Node] node The send node.
40
+ # @return [void]
41
+ def on_send(node)
42
+ return unless multiline_call_with_hash?(node)
43
+
44
+ hash_arg = find_hash_argument(node)
45
+
46
+ return unless hash_arg
47
+ return unless needs_correction?(hash_arg)
48
+
49
+ add_offense(hash_arg) do |corrector|
50
+ autocorrect(corrector, hash_arg)
51
+ end
52
+ end
53
+ alias on_csend on_send
54
+
55
+ private
56
+
57
+ # Check if this is a multiline call with closing paren on own line.
58
+ #
59
+ # @param [RuboCop::AST::Node] node The send node.
60
+ # @return [Boolean]
61
+ def multiline_call_with_hash?(node)
62
+ node.parenthesized? && closing_paren_on_own_line?(node)
63
+ end
64
+
65
+ # Check if closing paren is on its own line (after last argument).
66
+ #
67
+ # @param [RuboCop::AST::Node] node The send node.
68
+ # @return [Boolean]
69
+ def closing_paren_on_own_line?(node)
70
+ last_arg = node.last_argument
71
+
72
+ if last_arg
73
+ node.loc.end.line > last_arg.loc.last_line
74
+ else
75
+ false
76
+ end
77
+ end
78
+
79
+ # Find hash argument in the call (if any).
80
+ #
81
+ # @param [RuboCop::AST::Node] node The send node.
82
+ # @return [RuboCop::AST::Node, nil]
83
+ def find_hash_argument(node)
84
+ node.arguments.find(&:hash_type?)
85
+ end
86
+
87
+ # Check if hash needs correction (same line or unordered).
88
+ #
89
+ # @param [RuboCop::AST::Node] hash_arg The hash node.
90
+ # @return [Boolean]
91
+ def needs_correction?(hash_arg)
92
+ pairs = hash_arg.pairs
93
+
94
+ return false if pairs.size < 2
95
+
96
+ multiple_pairs_on_same_line?(pairs) || !pairs_alphabetically_ordered?(pairs)
97
+ end
98
+
99
+ # Check if multiple pairs are on the same line.
100
+ #
101
+ # @param [Array<RuboCop::AST::Node>] pairs The hash pairs.
102
+ # @return [Boolean]
103
+ def multiple_pairs_on_same_line?(pairs)
104
+ lines = pairs.map { |pair| pair.loc.line }
105
+
106
+ lines.size != lines.uniq.size
107
+ end
108
+
109
+ # Check if pairs are alphabetically ordered by key name.
110
+ #
111
+ # @param [Array<RuboCop::AST::Node>] pairs The hash pairs.
112
+ # @return [Boolean]
113
+ def pairs_alphabetically_ordered?(pairs)
114
+ key_names = pairs.map { |pair| extract_key_name(pair) }
115
+
116
+ key_names == key_names.sort
117
+ end
118
+
119
+ # Extract the key name as a string for sorting.
120
+ #
121
+ # @param [RuboCop::AST::Node] pair The hash pair node.
122
+ # @return [String]
123
+ def extract_key_name(pair)
124
+ key = pair.key
125
+
126
+ if key.type?(:sym, :str)
127
+ key.value.to_s
128
+ else
129
+ key.source
130
+ end
131
+ end
132
+
133
+ # Autocorrect by reordering and splitting pairs.
134
+ #
135
+ # @param [RuboCop::Cop::Corrector] corrector The corrector.
136
+ # @param [RuboCop::AST::Node] hash_arg The hash node.
137
+ # @return [void]
138
+ def autocorrect(corrector, hash_arg)
139
+ pairs = hash_arg.pairs
140
+ sorted = pairs.sort_by { |pair| extract_key_name(pair) }
141
+ indentation = calculate_indentation(pairs.first)
142
+ replacement = build_replacement(sorted, indentation)
143
+
144
+ corrector.replace(hash_arg, replacement)
145
+ end
146
+
147
+ # Calculate indentation for reformatted pairs.
148
+ #
149
+ # @param [RuboCop::AST::Node] first_pair The first pair node.
150
+ # @return [String]
151
+ def calculate_indentation(first_pair)
152
+ " " * first_pair.loc.column
153
+ end
154
+
155
+ # Build the replacement string with sorted pairs on separate lines.
156
+ #
157
+ # @param [Array<RuboCop::AST::Node>] sorted_pairs The sorted pairs.
158
+ # @param [String] indentation The indentation string.
159
+ # @return [String]
160
+ def build_replacement(sorted_pairs, indentation)
161
+ sorted_pairs.map.with_index do |pair, index|
162
+ prefix = index.zero? ? "" : indentation
163
+ suffix = index == sorted_pairs.size - 1 ? "" : ","
164
+
165
+ "#{prefix}#{pair.source}#{suffix}"
166
+ end.join("\n")
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces extracting compound boolean conditions into named methods.
7
+ #
8
+ # When a conditional has multiple boolean expressions joined by `&&` or
9
+ # `||`, the intent becomes unclear. Extracting to a descriptively-named
10
+ # method documents the business logic.
11
+ #
12
+ # This cop does NOT flag compound conditions that are:
13
+ # - The implied return value of a method (the extraction target)
14
+ #
15
+ # This cop DOES flag compound conditions that are:
16
+ # - In if/unless/while/until/when/ternary conditions
17
+ # - Inside an explicit return statement
18
+ #
19
+ # @example
20
+ # # bad - multiple conditions obscure intent
21
+ # if user.active? && user.verified?
22
+ # grant_access
23
+ # end
24
+ #
25
+ # # bad - mixing operators
26
+ # return if admin? || moderator?
27
+ #
28
+ # # bad - negation wrapping compound
29
+ # if !(order.paid? && order.shipped?)
30
+ # send_reminder
31
+ # end
32
+ #
33
+ # # bad - explicit return with compound
34
+ # def can_participate?
35
+ # return admin? || moderator? if override?
36
+ # check_other_conditions
37
+ # end
38
+ #
39
+ # # good - extracted to named method
40
+ # if user.can_participate?
41
+ # grant_access
42
+ # end
43
+ #
44
+ # # good - compound as implied method return value (this is the extraction)
45
+ # def can_participate?
46
+ # user.active? && user.verified?
47
+ # end
48
+ class NoCompoundConditions < Base
49
+ MSG = "Extract compound conditions into a named method."
50
+
51
+ # Check and/or nodes for conditional context.
52
+ #
53
+ # @param [RuboCop::AST::Node] node The and/or node.
54
+ # @return [void]
55
+ def on_and(node)
56
+ check_compound(node)
57
+ end
58
+ alias on_or on_and
59
+
60
+ private
61
+
62
+ # Check if a compound condition should be flagged.
63
+ #
64
+ # @param [RuboCop::AST::Node] node The and/or node.
65
+ # @return [void]
66
+ def check_compound(node)
67
+ if inside_return_statement?(node) || in_conditional_position?(node)
68
+ add_offense(node)
69
+ end
70
+ end
71
+
72
+ # Check if node is in a conditional position (if/unless/while/until/when condition).
73
+ #
74
+ # @param [RuboCop::AST::Node] node The and/or node.
75
+ # @return [Boolean]
76
+ def in_conditional_position?(node)
77
+ ancestor = conditional_ancestor(node)
78
+
79
+ if ancestor
80
+ condition_of_ancestor?(node, ancestor)
81
+ else
82
+ false
83
+ end
84
+ end
85
+
86
+ # Find the nearest conditional ancestor (if/while/until/when).
87
+ #
88
+ # @param [RuboCop::AST::Node] node The node.
89
+ # @return [RuboCop::AST::Node, nil]
90
+ def conditional_ancestor(node)
91
+ node.each_ancestor.find { |a| a.type?(:if, :while, :until, :when) }
92
+ end
93
+
94
+ # Check if node is (part of) the condition of the ancestor.
95
+ #
96
+ # @param [RuboCop::AST::Node] node The and/or node.
97
+ # @param [RuboCop::AST::Node] ancestor The if/while/until/when ancestor.
98
+ # @return [Boolean]
99
+ def condition_of_ancestor?(node, ancestor)
100
+ condition = condition_node(ancestor)
101
+
102
+ node_within_condition?(node, condition)
103
+ end
104
+
105
+ # Extract the condition node from a conditional ancestor.
106
+ #
107
+ # @param [RuboCop::AST::Node] ancestor The conditional ancestor.
108
+ # @return [RuboCop::AST::Node]
109
+ def condition_node(ancestor)
110
+ if ancestor.type?(:if, :while, :until)
111
+ ancestor.condition
112
+ else
113
+ ancestor
114
+ end
115
+ end
116
+
117
+ # Check if node is within the condition subtree.
118
+ #
119
+ # @param [RuboCop::AST::Node] node The node to find.
120
+ # @param [RuboCop::AST::Node] condition The condition root.
121
+ # @return [Boolean]
122
+ def node_within_condition?(node, condition)
123
+ return true if condition == node
124
+
125
+ condition.each_descendant.any?(node)
126
+ end
127
+
128
+ # Check if node is inside a return statement.
129
+ #
130
+ # @param [RuboCop::AST::Node] node The node.
131
+ # @return [Boolean]
132
+ def inside_return_statement?(node)
133
+ node.each_ancestor.any?(&:return_type?)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -9,30 +9,40 @@ module RuboCop
9
9
  # per-line basis. If a cop needs to be disabled, it should be configured
10
10
  # globally in `.rubocop.yml` with proper justification.
11
11
  #
12
- # @example Bad - inline disable directive on a line
13
- # def method
14
- # do_something # rubocop_disable Style/SomeCop (triggers offense)
15
- # end
12
+ # Specific cops can be allowed via the `AllowedCops` configuration option.
13
+ # If multiple cops are disabled in one comment, only the disallowed ones
14
+ # will be flagged.
16
15
  #
17
- # @example Bad - block disable directive
18
- # # rubocop_disable Style/SomeCop (triggers offense)
19
- # def method
20
- # do_something
21
- # end
22
- # # rubocop_enable Style/SomeCop
16
+ # @example Bad - inline disable directive
17
+ # do_something # rubocop_disable Style/SomeCop
18
+ #
19
+ # @example Bad - disable without specifying a cop
20
+ # # rubocop_disable all
21
+ # do_something
23
22
  #
24
23
  # @example Good - fix the issue instead
25
- # def method
26
- # do_something_correctly
27
- # end
24
+ # do_something_correctly
28
25
  #
29
26
  # @example Good - configure globally in .rubocop.yml
27
+ # # In .rubocop.yml:
30
28
  # # Style/SomeCop:
31
29
  # # Enabled: false
30
+ #
31
+ # @example AllowedCops: ['Rails/RakeEnvironment'] - allowed cop is not flagged
32
+ # # rubocop_disable Rails/RakeEnvironment
33
+ # task :my_task do
34
+ # # ...
35
+ # end
36
+ #
37
+ # NOTE: Examples use underscore (rubocop_disable) to prevent RuboCop from
38
+ # parsing them as actual directives. In real code, use colon (rubocop:disable).
32
39
  class NoRubocopDisable < Base
33
- MSG = "Do not use `# rubocop:disable`. Fix the issue or configure globally in `.rubocop.yml`."
40
+ MSG = "Do not disable `%<cops>s`. Fix the issue or configure globally in `.rubocop.yml`."
41
+ MSG_NO_COP = "Do not use `# rubocop:disable`. Fix the issue or configure globally in `.rubocop.yml`."
34
42
 
35
- DISABLE_PATTERN = /\A#\s*rubocop\s*:\s*disable\b/i
43
+ DISABLE_PATTERN = /\A#\s*rubocop\s*:\s*disable\b/i
44
+ COP_NAME_PATTERN = %r{[A-Za-z]+/[A-Za-z0-9]+}
45
+ ALL_PATTERN = /\brubocop\s*:\s*disable\s+all\b/i
36
46
 
37
47
  # Check for rubocop:disable comments.
38
48
  #
@@ -41,7 +51,7 @@ module RuboCop
41
51
  processed_source.comments.each do |comment|
42
52
  next unless disable_comment?(comment)
43
53
 
44
- add_offense(comment)
54
+ check_disabled_cops(comment)
45
55
  end
46
56
  end
47
57
 
@@ -54,6 +64,46 @@ module RuboCop
54
64
  def disable_comment?(comment)
55
65
  DISABLE_PATTERN.match?(comment.text)
56
66
  end
67
+
68
+ # Check disabled cops and flag disallowed ones.
69
+ #
70
+ # @param [Parser::Source::Comment] comment The comment to check.
71
+ # @return [void]
72
+ def check_disabled_cops(comment)
73
+ cops = extract_cop_names(comment.text)
74
+
75
+ if cops.empty?
76
+ add_offense(comment, message: MSG_NO_COP)
77
+ else
78
+ disallowed = cops.reject { |cop| allowed_cop?(cop) }
79
+ return if disallowed.empty?
80
+
81
+ add_offense(comment, message: format(MSG, cops: disallowed.join(", ")))
82
+ end
83
+ end
84
+
85
+ # Extract cop names from a rubocop:disable comment.
86
+ #
87
+ # @param [String] text The comment text.
88
+ # @return [Array<String>]
89
+ def extract_cop_names(text)
90
+ text.scan(COP_NAME_PATTERN)
91
+ end
92
+
93
+ # Check if a cop is in the allowed list.
94
+ #
95
+ # @param [String] cop The cop name.
96
+ # @return [Boolean]
97
+ def allowed_cop?(cop)
98
+ allowed_cops.include?(cop)
99
+ end
100
+
101
+ # Get the list of allowed cops from configuration.
102
+ #
103
+ # @return [Array<String>]
104
+ def allowed_cops
105
+ @allowed_cops ||= Array(cop_config.fetch("AllowedCops", []))
106
+ end
57
107
  end
58
108
  end
59
109
  end
@@ -105,6 +105,7 @@ module RuboCop
105
105
  # @return [void]
106
106
  def check_x_method(node)
107
107
  method_name = x_method_call?(node)
108
+
108
109
  if method_name
109
110
  add_offense(node, message: format(MSG_XMETHOD, method: method_name))
110
111
  end
@@ -98,6 +98,7 @@ module RuboCop
98
98
  def following_statements?(parent, node)
99
99
  siblings = parent.children
100
100
  node_index = siblings.index(node)
101
+
101
102
  siblings[(node_index + 1)..].any?
102
103
  end
103
104
 
@@ -151,6 +152,7 @@ module RuboCop
151
152
  def autocorrect(corrector, node)
152
153
  replacement = build_replacement(node)
153
154
  range = replacement_range(node)
155
+
154
156
  corrector.replace(range, replacement)
155
157
  end
156
158
 
@@ -212,6 +214,7 @@ module RuboCop
212
214
  def get_remaining_code(node)
213
215
  siblings = node.parent.children
214
216
  node_index = siblings.index(node)
217
+
215
218
  siblings[(node_index + 1)..].map(&:source).join("\n")
216
219
  end
217
220
 
@@ -223,6 +226,7 @@ module RuboCop
223
226
  siblings = node.parent.children
224
227
  start_pos = node.source_range.begin_pos
225
228
  end_pos = siblings.last.source_range.end_pos
229
+
226
230
  Parser::Source::Range.new(node.source_range.source_buffer, start_pos, end_pos)
227
231
  end
228
232
 
@@ -57,6 +57,7 @@ module RuboCop
57
57
  return unless single_statement?(node.body)
58
58
 
59
59
  expectation = extract_expectation(node.body)
60
+
60
61
  return unless simple_expectation?(expectation)
61
62
  return if complex_expectation?(node)
62
63
 
@@ -99,6 +100,7 @@ module RuboCop
99
100
  # @return [void]
100
101
  def autocorrect(corrector, node)
101
102
  expectation_source = node.body.source
103
+
102
104
  corrector.replace(node, "it { #{expectation_source} }")
103
105
  end
104
106
 
@@ -141,6 +143,7 @@ module RuboCop
141
143
  def find_expectation_in_chain(node)
142
144
  # Traverse up the chain looking for expect or is_expected.
143
145
  current = node
146
+
144
147
  while current&.send_type?
145
148
  return current if expectation_method?(current)
146
149
 
@@ -174,6 +177,7 @@ module RuboCop
174
177
  # @return [Boolean]
175
178
  def expect_subject?(node)
176
179
  argument = node.first_argument
180
+
177
181
  return false unless argument
178
182
  return false unless argument.send_type?
179
183
 
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces using `if/end` blocks instead of inline modifiers for `raise` statements.
7
+ #
8
+ # Inline `raise ... if/unless` statements can be harder to read because they
9
+ # place the condition after the action. This cop enforces converting them
10
+ # to `if/end` blocks for better readability.
11
+ #
12
+ # @example
13
+ # # bad - inline unless modifier
14
+ # raise ArgumentError, "Invalid column" unless COLUMNS.include?(column)
15
+ #
16
+ # # good - if block with negated condition
17
+ # if !COLUMNS.include?(column)
18
+ # raise ArgumentError, "Invalid column"
19
+ # end
20
+ #
21
+ # # bad - inline if modifier
22
+ # raise ArgumentError, "Invalid column" if column.nil?
23
+ #
24
+ # # good - if block
25
+ # if column.nil?
26
+ # raise ArgumentError, "Invalid column"
27
+ # end
28
+ #
29
+ # # good - raise without condition
30
+ # raise ArgumentError, "Invalid column"
31
+ class RaiseUnlessBlock < Base
32
+ extend AutoCorrector
33
+
34
+ MSG = "Use `if/end` block instead of inline modifier for `raise`."
35
+
36
+ # Check if nodes for raise with if/unless modifier.
37
+ #
38
+ # @param [RuboCop::AST::Node] node The if node.
39
+ # @return [void]
40
+ def on_if(node)
41
+ return unless node.modifier_form?
42
+ return unless raise_call?(node.body)
43
+
44
+ add_offense(node) do |corrector|
45
+ autocorrect(corrector, node)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # Check if the node is a raise method call.
52
+ #
53
+ # @param [RuboCop::AST::Node] node The node to check.
54
+ # @return [Boolean]
55
+ def raise_call?(node)
56
+ node.send_type? && node.method?(:raise)
57
+ end
58
+
59
+ # Autocorrect the offense by converting to if block.
60
+ #
61
+ # @param [RuboCop::Cop::Corrector] corrector The corrector.
62
+ # @param [RuboCop::AST::Node] node The if node.
63
+ # @return [void]
64
+ def autocorrect(corrector, node)
65
+ replacement = build_replacement(node)
66
+
67
+ corrector.replace(node, replacement)
68
+ end
69
+
70
+ # Build the replacement code for the raise statement.
71
+ #
72
+ # @param [RuboCop::AST::Node] node The if node.
73
+ # @return [String] The replacement code.
74
+ def build_replacement(node)
75
+ condition = build_condition(node)
76
+ raise_source = node.body.source
77
+ base_indent = " " * node.loc.column
78
+ inner_indent = "#{base_indent} "
79
+
80
+ [
81
+ "if #{condition}",
82
+ "#{inner_indent}#{raise_source}",
83
+ "#{base_indent}end"
84
+ ].join("\n")
85
+ end
86
+
87
+ # Build the condition for the if block.
88
+ # For unless, negate the condition. For if, keep it as is.
89
+ #
90
+ # @param [RuboCop::AST::Node] node The if node.
91
+ # @return [String] The condition source.
92
+ def build_condition(node)
93
+ if node.unless?
94
+ "!#{node.condition.source}"
95
+ else
96
+ node.condition.source
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end