diff --git a/changelog/new_add_new_cop_styleendlessmethod.md b/changelog/new_add_new_cop_styleendlessmethod.md new file mode 100644 index 00000000000..ca6ad4f049c --- /dev/null +++ b/changelog/new_add_new_cop_styleendlessmethod.md @@ -0,0 +1 @@ +* [#9281](https://github.com/rubocop-hq/rubocop/pull/9281): Add new cop `Style/EndlessMethod`. ([@dvandersluis][]) diff --git a/config/default.yml b/config/default.yml index 68cc8300ea9..89a7aa02c67 100644 --- a/config/default.yml +++ b/config/default.yml @@ -3206,6 +3206,17 @@ Style/EndBlock: VersionAdded: '0.9' VersionChanged: '0.81' +Style/EndlessMethod: + Description: 'Avoid the use of multi-lined endless method definitions.' + StyleGuide: '#endless-methods' + Enabled: pending + VersionAdded: <> + EnforcedStyle: allow_single_line + SupportedStyles: + - allow_single_line + - allow_always + - disallow + Style/EvalWithLocation: Description: 'Pass `__FILE__` and `__LINE__` to `eval` method, as they are used by backtraces.' Enabled: true diff --git a/lib/rubocop.rb b/lib/rubocop.rb index 8764cd07693..77ab959fb62 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -448,6 +448,7 @@ require_relative 'rubocop/cop/style/empty_lambda_parameter' require_relative 'rubocop/cop/style/empty_literal' require_relative 'rubocop/cop/style/empty_method' +require_relative 'rubocop/cop/style/endless_method' require_relative 'rubocop/cop/style/encoding' require_relative 'rubocop/cop/style/end_block' require_relative 'rubocop/cop/style/eval_with_location' diff --git a/lib/rubocop/cop/style/endless_method.rb b/lib/rubocop/cop/style/endless_method.rb new file mode 100644 index 00000000000..cc022a6f53a --- /dev/null +++ b/lib/rubocop/cop/style/endless_method.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Style + # This cop checks for endless methods. + # + # It can enforce either the use of endless methods definitions + # for single-lined method bodies, or disallow endless methods. + # + # Other method definition types are not considered by this cop. + # + # The supported styles are: + # * 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. + # + # NOTE: Incorrect endless method definitions will always be + # corrected to a multi-line definition. + # + # @example EnforcedStyle: allow_single_line (default) + # # good + # def my_method() = x + # + # # bad, multi-line endless method + # def my_method() = x.foo + # .bar + # .baz + # + # @example EnforcedStyle: allow_always + # # good + # def my_method() = x + # + # # good + # def my_method() = x.foo + # .bar + # .baz + # + # @example EnforcedStyle: disallow + # # bad + # def my_method; x end + # + # # bad + # def my_method() = x.foo + # .bar + # .baz + # + class EndlessMethod < Base + include ConfigurableEnforcedStyle + extend TargetRubyVersion + extend AutoCorrector + + minimum_target_ruby_version 3.0 + + CORRECTION_STYLES = %w[multiline single_line].freeze + MSG = 'Avoid endless method definitions.' + MSG_MULTI_LINE = 'Avoid endless method definitions with multiple lines.' + + def on_def(node) + if style == :disallow + handle_disallow_style(node) + else + handle_allow_style(node) + end + end + + private + + def handle_allow_style(node) + return unless node.endless? + return if node.single_line? || style == :allow_always + + add_offense(node, message: MSG_MULTI_LINE) do |corrector| + correct_to_multiline(corrector, node) + end + end + + def handle_disallow_style(node) + return unless node.endless? + + add_offense(node) do |corrector| + correct_to_multiline(corrector, node) + end + end + + def correct_to_multiline(corrector, node) + replacement = <<~RUBY.strip + def #{node.method_name}#{arguments(node)} + #{node.body.source} + end + RUBY + + corrector.replace(node, replacement) + end + + def arguments(node, missing = '') + node.arguments.any? ? node.arguments.source : missing + end + end + end + end +end diff --git a/spec/rubocop/cop/style/endless_method_spec.rb b/spec/rubocop/cop/style/endless_method_spec.rb new file mode 100644 index 00000000000..1f94acb8cee --- /dev/null +++ b/spec/rubocop/cop/style/endless_method_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Style::EndlessMethod, :config do + context 'Ruby >= 3.0', :ruby30 do + context 'EnforcedStyle: disallow' do + let(:cop_config) { { 'EnforcedStyle' => 'disallow' } } + + it 'registers an offense for an endless method' do + expect_offense(<<~RUBY) + def my_method() = x + ^^^^^^^^^^^^^^^^^^^ Avoid endless method definitions. + RUBY + + expect_correction(<<~RUBY) + def my_method + x + end + RUBY + end + + it 'registers an offense for an endless method with arguments' do + expect_offense(<<~RUBY) + def my_method(a, b) = x + ^^^^^^^^^^^^^^^^^^^^^^^ Avoid endless method definitions. + RUBY + + expect_correction(<<~RUBY) + def my_method(a, b) + x + end + RUBY + end + end + + context 'EnforcedStyle: allow_single_line' do + let(:cop_config) { { 'EnforcedStyle' => 'allow_single_line' } } + + it 'does not register an offense for an endless method' do + expect_no_offenses(<<~RUBY) + def my_method() = x + RUBY + end + + it 'does not register an offense for an 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 'registers an offense and corrects for a multiline endless method with begin' do + expect_offense(<<~RUBY) + def my_method() = begin + ^^^^^^^^^^^^^^^^^^^^^^^ Avoid endless method definitions with multiple lines. + foo && bar + end + RUBY + + expect_correction(<<~RUBY) + def my_method + begin + foo && bar + end + end + 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: allow_always' do + let(:cop_config) { { 'EnforcedStyle' => 'allow_always' } } + + it 'does not register an offense for an endless method' do + expect_no_offenses(<<~RUBY) + def my_method() = x + RUBY + end + + it 'does not register an offense for an 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_no_offenses(<<~RUBY) + def my_method() = x.foo + .bar + .baz + RUBY + end + + it 'registers an offense and corrects for a multiline endless method with begin' do + expect_no_offenses(<<~RUBY) + def my_method() = begin + foo && bar + end + RUBY + end + + it 'registers an offense and corrects for a multiline endless method with arguments' do + expect_no_offenses(<<~RUBY) + def my_method(a, b) = x.foo + .bar + .baz + RUBY + end + end + end +end