Skip to content

Commit

Permalink
[Fix #11024] add require_single_line to Style/EndlessMethod
Browse files Browse the repository at this point in the history
Fixes #11024.

This PR adds `require_single_line` option to `Style/EndlessMethod.
It requires endless method definitions when they are a single line
and forbids endless methods having multiple lines.

```ruby
\# bad
def my_method
  x
end

\# good
def my_method
  x.foo
   .bar
   .baz
end

\# good
def my_method = x

\# bad
def my_method = x.foo
                 .bar
                 .baz
```
  • Loading branch information
jtannas authored and bbatsov committed Feb 26, 2025
1 parent 4b5609a commit 86690cc
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#11024](https://github.com/rubocop/rubocop/issues/11024): Add `require_single_line` option to `Style/EndlessMethod`. ([@jtannas][])
1 change: 1 addition & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3923,6 +3923,7 @@ Style/EndlessMethod:
- allow_single_line
- allow_always
- disallow
- require_single_line
- require_always

Style/EnvHome:
Expand Down
65 changes: 57 additions & 8 deletions lib/rubocop/cop/style/endless_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ module Cop
module Style
# Checks for endless methods.
#
# It can enforce endless method definitions whenever possible. It can also disallow multiline
# endless method definitions or all endless definitions.
# It can enforce endless method definitions whenever possible or with single line methods.
# It can also disallow multiline endless method definitions or all endless definitions.
#
# `require_single_line` style enforces endless method definitions for single line methods.
# `require_always` style enforces endless method definitions for single statement methods.
#
# Other method definition types are not considered by this cop.
Expand All @@ -17,6 +18,7 @@ module Style
# * allow_single_line (default) - only single line endless method definitions are allowed.
# * allow_always - all endless method definitions are allowed.
# * disallow - all endless method definitions are disallowed.
# * require_single_line - endless method definitions are required for single line methods.
# * require_always - all endless method definitions are required.
#
# NOTE: Incorrect endless method definitions will always be
Expand Down Expand Up @@ -85,6 +87,27 @@ module Style
# .baz
# end
#
# @example EnforcedStyle: require_single_line
# # bad
# def my_method
# x
# end
#
# # bad
# def my_method = x.foo
# .bar
# .baz
#
# # good
# def my_method = x
#
# # good
# def my_method
# x.foo
# .bar
# .baz
# end
#
# @example EnforcedStyle: require_always
# # bad
# def my_method
Expand Down Expand Up @@ -117,6 +140,7 @@ class EndlessMethod < Base
CORRECTION_STYLES = %w[multiline single_line].freeze
MSG = 'Avoid endless method definitions.'
MSG_MULTI_LINE = 'Avoid endless method definitions with multiple lines.'
MSG_REQUIRE_SINGLE = 'Use endless method definitions for single line methods.'
MSG_REQUIRE_ALWAYS = 'Use endless method definitions.'

def on_def(node)
Expand All @@ -125,6 +149,8 @@ def on_def(node)
handle_allow_style(node)
when :disallow
handle_disallow_style(node)
when :require_single_line
handle_require_single_line_style(node)
when :require_always
handle_require_always_style(node)
end
Expand All @@ -141,11 +167,26 @@ def handle_allow_style(node)
end
end

def handle_require_single_line_style(node)
if node.endless? && !node.single_line?
add_offense(node, message: MSG_MULTI_LINE) do |corrector|
correct_to_multiline(corrector, node)
end
elsif !node.endless? && can_be_made_endless?(node) && node.body.single_line?
return if too_long_when_made_endless?(node)

add_offense(node, message: MSG_REQUIRE_SINGLE) do |corrector|
corrector.replace(node, endless_replacement(node))
end
end
end

def handle_require_always_style(node)
return if node.endless? || !node.body || node.body.begin_type? || node.body.kwbegin_type?
return if node.endless? || !can_be_made_endless?(node)
return if too_long_when_made_endless?(node)

add_offense(node, message: MSG_REQUIRE_ALWAYS) do |corrector|
correct_to_endless_method(corrector, node)
corrector.replace(node, endless_replacement(node))
end
end

Expand All @@ -165,17 +206,25 @@ def #{node.method_name}#{arguments(node)}
corrector.replace(node, replacement)
end

def correct_to_endless_method(corrector, node)
replacement = <<~RUBY.strip
def endless_replacement(node)
<<~RUBY.strip
def #{node.method_name}#{arguments(node)} = #{node.body.source}
RUBY

corrector.replace(node, replacement)
end

def arguments(node, missing = '')
node.arguments.any? ? node.arguments.source : missing
end

def can_be_made_endless?(node)
node.body && !node.body.begin_type? && !node.body.kwbegin_type?
end

def too_long_when_made_endless?(node)
return false unless config.cop_enabled?('Layout/LineLength')

endless_replacement(node).length > config.for_cop('Layout/LineLength')['Max']
end
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/rubocop/cop/style/single_line_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ module Style
# Endless methods added in Ruby 3.0 are also accepted by this cop.
#
# If `Style/EndlessMethod` is enabled with `EnforcedStyle: allow_single_line`, `allow_always`,
# or `require_always`, single-line methods will be autocorrected to endless
# methods if there is only one statement in the body.
# `require_single_line`, or `require_always`, single-line methods will be autocorrected
# to endless methods if there is only one statement in the body.
#
# @example
# # bad
Expand Down
164 changes: 163 additions & 1 deletion spec/rubocop/cop/style/endless_method_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

RSpec.describe RuboCop::Cop::Style::EndlessMethod, :config do
context 'Ruby >= 3.0', :ruby30 do
let(:other_cops) do
{
'Layout/LineLength' => {
'Enabled' => line_length_enabled,
'Max' => 80
}
}
end
let(:line_length_enabled) { true }

context 'EnforcedStyle: disallow' do
let(:cop_config) { { 'EnforcedStyle' => 'disallow' } }

Expand Down Expand Up @@ -163,6 +173,133 @@ def my_method(a, b) = x.foo
end
end

context 'EnforcedStyle: require_single_line' do
let(:cop_config) { { 'EnforcedStyle' => 'require_single_line' } }

it 'does not register an offense for a single line endless method' do
expect_no_offenses(<<~RUBY)
def my_method() = x
RUBY
end

it 'does not register an offense for a single line endless method with arguments' do
expect_no_offenses(<<~RUBY)
def my_method(a, b) = x
RUBY
end

it 'registers an offense and corrects for a multiline endless method' do
expect_offense(<<~RUBY)
def my_method() = x.foo
^^^^^^^^^^^^^^^^^^^^^^^ Avoid endless method definitions with multiple lines.
.bar
.baz
RUBY

expect_correction(<<~RUBY)
def my_method
x.foo
.bar
.baz
end
RUBY
end

it 'does not register an offense for no statements method' do
expect_no_offenses(<<~RUBY)
def my_method
end
RUBY
end

it 'does not register an offense for multiple statements method' do
expect_no_offenses(<<~RUBY)
def my_method
x.foo
x.bar
end
RUBY
end

it 'does not register an offense for multiple statements method with `begin`' do
expect_no_offenses(<<~RUBY)
def my_method
begin
foo && bar
end
end
RUBY
end

it 'registers an offense and corrects for a single line method' do
expect_offense(<<~RUBY)
def my_method
^^^^^^^^^^^^^ Use endless method definitions for single line methods.
x
end
RUBY

expect_correction(<<~RUBY)
def my_method = x
RUBY
end

it 'does not register an offense when the endless version excess Metrics/MaxLineLength[Max]' do
expect_no_offenses(<<~RUBY)
def my_method
'this_string_ends_at_column_75_________________________________________'
end
RUBY
end

context 'when Metrics/MaxLineLength is disabled' do
let(:line_length_enabled) { false }

it 'registers an offense and corrects for a long single line method that is long' do
expect_offense(<<~RUBY)
def my_method
^^^^^^^^^^^^^ Use endless method definitions for single line methods.
'this_string_ends_at_column_75_________________________________________'
end
RUBY

expect_correction(<<~RUBY)
def my_method = 'this_string_ends_at_column_75_________________________________________'
RUBY
end
end

it 'registers an offense and corrects for a single line method with arguments' do
expect_offense(<<~RUBY)
def my_method(a, b)
^^^^^^^^^^^^^^^^^^^ Use endless method definitions for single line methods.
x
end
RUBY

expect_correction(<<~RUBY)
def my_method(a, b) = x
RUBY
end

it 'registers an offense and corrects for a multiline endless method with arguments' do
expect_offense(<<~RUBY)
def my_method(a, b) = x.foo
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid endless method definitions with multiple lines.
.bar
.baz
RUBY

expect_correction(<<~RUBY)
def my_method(a, b)
x.foo
.bar
.baz
end
RUBY
end
end

context 'EnforcedStyle: require_always' do
let(:cop_config) { { 'EnforcedStyle' => 'require_always' } }

Expand Down Expand Up @@ -202,7 +339,7 @@ def my_method
RUBY
end

it 'does not register an offenses for multiple statements method with `begin`' do
it 'does not register an offense for multiple statements method with `begin`' do
expect_no_offenses(<<~RUBY)
def my_method
begin
Expand Down Expand Up @@ -255,6 +392,31 @@ def my_method = x.foo
RUBY
end

it 'does not register an offense when the endless version excess Metrics/MaxLineLength[Max]' do
expect_no_offenses(<<~RUBY)
def my_method
'this_string_ends_at_column_75_________________________________________'
end
RUBY
end

context 'when Metrics/MaxLineLength is disabled' do
let(:line_length_enabled) { false }

it 'registers an offense and corrects for a long single line method that is long' do
expect_offense(<<~RUBY)
def my_method
^^^^^^^^^^^^^ Use endless method definitions.
'this_string_ends_at_column_75_________________________________________'
end
RUBY

expect_correction(<<~RUBY)
def my_method = 'this_string_ends_at_column_75_________________________________________'
RUBY
end
end

it 'registers an offense and corrects for a multiline method with arguments' do
expect_offense(<<~RUBY)
def my_method(a, b)
Expand Down
6 changes: 6 additions & 0 deletions spec/rubocop/cop/style/single_line_methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,12 @@ def some_method;#{trailing_whitespace}
it_behaves_like 'convert to endless method'
end

context 'with `require_single_line` style' do
let(:endless_method_config) { { 'EnforcedStyle' => 'require_single_line' } }

it_behaves_like 'convert to endless method'
end

context 'with `require_always` style' do
let(:endless_method_config) { { 'EnforcedStyle' => 'require_always' } }

Expand Down

0 comments on commit 86690cc

Please sign in to comment.