Skip to content

Commit ce37499

Browse files
dvandersluisbbatsov
authored andcommitted
[Fix #12988] Add new Style/AmbiguousEndlessMethodDefinition cop.
1 parent bfc974d commit ce37499

File tree

7 files changed

+182
-14
lines changed

7 files changed

+182
-14
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#12988](https://github.com/rubocop/rubocop/issues/12988): Add new `Style/AmbiguousEndlessMethodDefinition` cop. ([@dvandersluis][])

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3143,6 +3143,11 @@ Style/Alias:
31433143
- prefer_alias
31443144
- prefer_alias_method
31453145

3146+
Style/AmbiguousEndlessMethodDefinition:
3147+
Description: 'Checks for endless methods inside operators of lower precedence.'
3148+
Enabled: pending
3149+
VersionAdded: '<<next>>'
3150+
31463151
Style/AndOr:
31473152
Description: 'Use &&/|| instead of and/or.'
31483153
StyleGuide: '#no-and-or-or'

lib/rubocop.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
require_relative 'rubocop/cop/mixin/empty_lines_around_body' # relies on range
9191
require_relative 'rubocop/cop/mixin/empty_parameter'
9292
require_relative 'rubocop/cop/mixin/end_keyword_alignment'
93+
require_relative 'rubocop/cop/mixin/endless_method_rewriter'
9394
require_relative 'rubocop/cop/mixin/enforce_superclass'
9495
require_relative 'rubocop/cop/mixin/first_element_line_break'
9596
require_relative 'rubocop/cop/mixin/frozen_string_literal'
@@ -460,6 +461,7 @@
460461
require_relative 'rubocop/cop/style/access_modifier_declarations'
461462
require_relative 'rubocop/cop/style/accessor_grouping'
462463
require_relative 'rubocop/cop/style/alias'
464+
require_relative 'rubocop/cop/style/ambiguous_endless_method_definition'
463465
require_relative 'rubocop/cop/style/and_or'
464466
require_relative 'rubocop/cop/style/arguments_forwarding'
465467
require_relative 'rubocop/cop/style/array_coercion'
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
# Common functionality for rewriting endless methods to normal method definitions
6+
module EndlessMethodRewriter
7+
def correct_to_multiline(corrector, node)
8+
replacement = <<~RUBY.strip
9+
def #{node.method_name}#{arguments(node)}
10+
#{node.body.source}
11+
end
12+
RUBY
13+
14+
corrector.replace(node, replacement)
15+
end
16+
17+
private
18+
19+
def arguments(node, missing = '')
20+
node.arguments.any? ? node.arguments.source : missing
21+
end
22+
end
23+
end
24+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Style
6+
# Looks for endless methods inside operations of lower precedence (`and`, `or`, and
7+
# modifier forms of `if`, `unless`, `while`, `until`) that are ambiguous due to
8+
# lack of parentheses. This may lead to unexpected behavior as the code may appear
9+
# to use these keywords as part of the method but in fact they modify
10+
# the method definition itself.
11+
#
12+
# In these cases, using a normal method definition is more clear.
13+
#
14+
# @example
15+
#
16+
# # bad
17+
# def foo = true if bar
18+
#
19+
# # good - using a non-endless method is more explicit
20+
# def foo
21+
# true
22+
# end if bar
23+
#
24+
# # ok - method body is explicit
25+
# def foo = (true if bar)
26+
#
27+
# # ok - method definition is explicit
28+
# (def foo = true) if bar
29+
class AmbiguousEndlessMethodDefinition < Base
30+
extend TargetRubyVersion
31+
extend AutoCorrector
32+
include EndlessMethodRewriter
33+
include RangeHelp
34+
35+
minimum_target_ruby_version 3.0
36+
37+
MSG = 'Avoid using `%<keyword>s` statements with endless methods.'
38+
39+
# @!method ambiguous_endless_method_body(node)
40+
def_node_matcher :ambiguous_endless_method_body, <<~PATTERN
41+
^${
42+
(if _ <def _>)
43+
({and or} def _)
44+
({while until} _ def)
45+
}
46+
PATTERN
47+
48+
def on_def(node)
49+
return unless node.endless?
50+
51+
operation = ambiguous_endless_method_body(node)
52+
return unless operation
53+
54+
return unless modifier_form?(operation)
55+
56+
add_offense(operation, message: format(MSG, keyword: keyword(operation))) do |corrector|
57+
correct_to_multiline(corrector, node)
58+
end
59+
end
60+
61+
private
62+
63+
def modifier_form?(operation)
64+
return true if operation.and_type? || operation.or_type?
65+
66+
operation.modifier_form?
67+
end
68+
69+
def keyword(operation)
70+
if operation.respond_to?(:keyword)
71+
operation.keyword
72+
else
73+
operation.operator
74+
end
75+
end
76+
end
77+
end
78+
end
79+
end

lib/rubocop/cop/style/endless_method.rb

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ module Style
4848
#
4949
class EndlessMethod < Base
5050
include ConfigurableEnforcedStyle
51+
include EndlessMethodRewriter
5152
extend TargetRubyVersion
5253
extend AutoCorrector
5354

@@ -81,20 +82,6 @@ def handle_disallow_style(node)
8182

8283
add_offense(node) { |corrector| correct_to_multiline(corrector, node) }
8384
end
84-
85-
def correct_to_multiline(corrector, node)
86-
replacement = <<~RUBY.strip
87-
def #{node.method_name}#{arguments(node)}
88-
#{node.body.source}
89-
end
90-
RUBY
91-
92-
corrector.replace(node, replacement)
93-
end
94-
95-
def arguments(node, missing = '')
96-
node.arguments.any? ? node.arguments.source : missing
97-
end
9885
end
9986
end
10087
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Style::AmbiguousEndlessMethodDefinition, :config do
4+
context 'Ruby >= 3.0', :ruby30 do
5+
it 'does not register an offense for a non endless method' do
6+
expect_no_offenses(<<~RUBY)
7+
def foo
8+
end
9+
RUBY
10+
end
11+
12+
%i[and or if unless while until].each do |operator|
13+
context "with #{operator}" do
14+
it "does not register an offense for a non endless method followed by `#{operator}`" do
15+
expect_no_offenses(<<~RUBY)
16+
def foo
17+
end #{operator} bar
18+
RUBY
19+
end
20+
21+
it 'does not register an offense when the operator is already wrapped in parens' do
22+
expect_no_offenses(<<~RUBY)
23+
def foo = (true #{operator} bar)
24+
RUBY
25+
end
26+
27+
it 'does not register an offense when the method definition is already wrapped in parens' do
28+
expect_no_offenses(<<~RUBY)
29+
(def foo = true) #{operator} bar
30+
RUBY
31+
end
32+
33+
unless %i[and or].include?(operator)
34+
it "does not register an offense for non-modifier `#{operator}`" do
35+
expect_no_offenses(<<~RUBY)
36+
#{operator} bar
37+
def foo = true
38+
end
39+
RUBY
40+
end
41+
end
42+
43+
it "registers and offense and corrects an endless method followed by `#{operator}`" do
44+
expect_offense(<<~RUBY, operator: operator)
45+
def foo = true #{operator} bar
46+
^^^^^^^^^^^^^^^^{operator}^^^^ Avoid using `#{operator}` statements with endless methods.
47+
RUBY
48+
49+
expect_correction(<<~RUBY)
50+
def foo
51+
true
52+
end #{operator} bar
53+
RUBY
54+
end
55+
end
56+
end
57+
58+
it 'does not register an offense for `&&`' do
59+
expect_no_offenses(<<~RUBY)
60+
def foo = true && bar
61+
RUBY
62+
end
63+
64+
it 'does not register an offense for `||`' do
65+
expect_no_offenses(<<~RUBY)
66+
def foo = true || bar
67+
RUBY
68+
end
69+
end
70+
end

0 commit comments

Comments
 (0)