From d4a9bf524d642b0e0b5d94d1d07434a498ed0d5e Mon Sep 17 00:00:00 2001 From: Saurabh Maurya Date: Mon, 13 Apr 2020 18:19:59 +0530 Subject: [PATCH] [Fix #7826] Add new `Layout/SpaceAroundMethodCallOperator` cop (#7857) Co-authored-by: Saurabh Maurya --- CHANGELOG.md | 2 + config/default.yml | 5 + lib/rubocop.rb | 1 + .../ast/node/mixin/method_dispatch_node.rb | 8 + .../space_around_method_call_operator.rb | 131 +++++ manual/cops.md | 1 + manual/cops_layout.md | 40 ++ spec/rubocop/cli/cli_options_spec.rb | 2 + .../space_around_method_call_operator_spec.rb | 482 ++++++++++++++++++ 9 files changed, 672 insertions(+) create mode 100644 lib/rubocop/cop/layout/space_around_method_call_operator.rb create mode 100644 spec/rubocop/cop/layout/space_around_method_call_operator_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb8515e416..457e1e063b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ * [#7816](https://github.com/rubocop-hq/rubocop/pull/7816): Support Ruby 2.7's numbered parameter for `Style/Lambda`. ([@koic][]) * [#7829](https://github.com/rubocop-hq/rubocop/issues/7829): Fix an error for `Style/OneLineConditional` when one of the branches contains `next` keyword. ([@koic][]) * [#7384](https://github.com/rubocop-hq/rubocop/pull/7384): Add new `Style/DisableCopsWithinSourceCodeDirective` cop. ([@egze][]) +* [#7826](https://github.com/rubocop-hq/rubocop/issues/7826): Add new `Layout/SpaceAroundMethodCallOperator` cop. ([@saurabhmaurya15][]) ### Bug fixes @@ -4449,3 +4450,4 @@ [@egze]: https://github.com/egze [@rafaelfranca]: https://github.com/rafaelfranca [@knu]: https://github.com/knu +[@saurabhmaurya15]: https://github.com/saurabhmaurya15 diff --git a/config/default.yml b/config/default.yml index 0d6ff3d0906..549a38a8203 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1088,6 +1088,11 @@ Layout/SpaceAroundKeyword: Enabled: true VersionAdded: '0.49' +Layout/SpaceAroundMethodCallOperator: + Description: 'Checks method call operators to not have spaces around them.' + Enabled: pending + VersionAdded: '0.81' + Layout/SpaceAroundOperators: Description: 'Use a single space around operators.' StyleGuide: '#spaces-operators' diff --git a/lib/rubocop.rb b/lib/rubocop.rb index dc7ff847b1b..e8a3b43d52b 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -261,6 +261,7 @@ require_relative 'rubocop/cop/layout/space_around_block_parameters' require_relative 'rubocop/cop/layout/space_around_equals_in_parameter_default' require_relative 'rubocop/cop/layout/space_around_keyword' +require_relative 'rubocop/cop/layout/space_around_method_call_operator' require_relative 'rubocop/cop/layout/space_around_operators' require_relative 'rubocop/cop/layout/space_before_block_braces' require_relative 'rubocop/cop/layout/space_before_comma' diff --git a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb index 9592d04b275..bf2bd3b4f73 100644 --- a/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +++ b/lib/rubocop/ast/node/mixin/method_dispatch_node.rb @@ -121,6 +121,14 @@ def double_colon? loc.respond_to?(:dot) && loc.dot && loc.dot.is?('::') end + # Checks whether the dispatched method uses a safe navigation operator to + # connect the receiver and the method name. + # + # @return [Boolean] whether the method was called with a connecting dot + def safe_navigation? + loc.respond_to?(:dot) && loc.dot && loc.dot.is?('&.') + end + # Checks whether the *explicit* receiver of this method dispatch is # `self`. # diff --git a/lib/rubocop/cop/layout/space_around_method_call_operator.rb b/lib/rubocop/cop/layout/space_around_method_call_operator.rb new file mode 100644 index 00000000000..78b9b473fac --- /dev/null +++ b/lib/rubocop/cop/layout/space_around_method_call_operator.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Layout + # Checks method call operators to not have spaces around them. + # + # @example + # # bad + # foo. bar + # foo .bar + # foo . bar + # foo. bar .buzz + # foo + # . bar + # . buzz + # foo&. bar + # foo &.bar + # foo &. bar + # foo &. bar&. buzz + # RuboCop:: Cop + # RuboCop:: Cop:: Cop + # :: RuboCop::Cop + # + # # good + # foo.bar + # foo.bar.buzz + # foo + # .bar + # .buzz + # foo&.bar + # foo&.bar&.buzz + # RuboCop::Cop + # RuboCop::Cop::Cop + # ::RuboCop::Cop + # + class SpaceAroundMethodCallOperator < Cop + include SurroundingSpace + + MSG = 'Avoid using spaces around a method call operator.' + + def on_send(node) + return unless dot_or_safe_navigation_operator?(node) + + check_and_add_offense(node) + end + + def on_const(node) + return unless node.loc.double_colon + + check_and_add_offense(node, false) + end + + def autocorrect(node) + operator = operator_token(node) + left = left_token_for_auto_correction(node, operator) + right = right_token_for_auto_correction(operator) + + lambda do |corrector| + SpaceCorrector.remove_space( + processed_source, corrector, left, right + ) + end + end + + alias on_csend on_send + + private + + def check_and_add_offense(node, add_left_offense = true) + operator = operator_token(node) + left = previous_token(operator) + right = next_token(operator) + + if valid_right_token?(right, operator) + no_space_offenses(node, operator, right, MSG) + end + return unless valid_left_token?(left, operator) + + no_space_offenses(node, left, operator, MSG) if add_left_offense + end + + def operator_token(node) + operator_location = + node.const_type? ? node.loc.double_colon : node.loc.dot + + processed_source.find_token do |token| + token.pos == operator_location + end + end + + def previous_token(current_token) + index = processed_source.tokens.index(current_token) + index.zero? ? nil : processed_source.tokens[index - 1] + end + + def next_token(current_token) + index = processed_source.tokens.index(current_token) + processed_source.tokens[index + 1] + end + + def dot_or_safe_navigation_operator?(node) + node.dot? || node.safe_navigation? + end + + def valid_left_token?(left, operator) + left && left.line == operator.line + end + + def valid_right_token?(right, operator) + right && right.line == operator.line + end + + def left_token_for_auto_correction(node, operator) + left_token = previous_token(operator) + return operator if node.const_type? + return left_token if valid_left_token?(left_token, operator) + + operator + end + + def right_token_for_auto_correction(operator) + right_token = next_token(operator) + return right_token if valid_right_token?(right_token, operator) + + operator + end + end + end + end +end diff --git a/manual/cops.md b/manual/cops.md index a8f56fb4fb6..ce789d7984a 100644 --- a/manual/cops.md +++ b/manual/cops.md @@ -154,6 +154,7 @@ In the following section you find all available cops: * [Layout/SpaceAroundBlockParameters](cops_layout.md#layoutspacearoundblockparameters) * [Layout/SpaceAroundEqualsInParameterDefault](cops_layout.md#layoutspacearoundequalsinparameterdefault) * [Layout/SpaceAroundKeyword](cops_layout.md#layoutspacearoundkeyword) +* [Layout/SpaceAroundMethodCallOperator](cops_layout.md#layoutspacearoundmethodcalloperator) * [Layout/SpaceAroundOperators](cops_layout.md#layoutspacearoundoperators) * [Layout/SpaceBeforeBlockBraces](cops_layout.md#layoutspacebeforeblockbraces) * [Layout/SpaceBeforeComma](cops_layout.md#layoutspacebeforecomma) diff --git a/manual/cops_layout.md b/manual/cops_layout.md index 2cccc572a34..f902b13d1ba 100644 --- a/manual/cops_layout.md +++ b/manual/cops_layout.md @@ -4075,6 +4075,46 @@ end something = 123 if test ``` +## Layout/SpaceAroundMethodCallOperator + +Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged +--- | --- | --- | --- | --- +Pending | Yes | Yes | 0.81 | - + +Checks method call operators to not have spaces around them. + +### Examples + +```ruby +# bad +foo. bar +foo .bar +foo . bar +foo. bar .buzz +foo + . bar + . buzz +foo&. bar +foo &.bar +foo &. bar +foo &. bar&. buzz +RuboCop:: Cop +RuboCop:: Cop:: Cop +:: RuboCop::Cop + +# good +foo.bar +foo.bar.buzz +foo + .bar + .buzz +foo&.bar +foo&.bar&.buzz +RuboCop::Cop +RuboCop::Cop::Cop +::RuboCop::Cop +``` + ## Layout/SpaceAroundOperators Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged diff --git a/spec/rubocop/cli/cli_options_spec.rb b/spec/rubocop/cli/cli_options_spec.rb index ce4a895bb7c..807f754c397 100644 --- a/spec/rubocop/cli/cli_options_spec.rb +++ b/spec/rubocop/cli/cli_options_spec.rb @@ -364,6 +364,8 @@ class SomeCop < Cop Enabled: false Style: Enabled: false + Layout: + Enabled: false Style/SomeCop: Description: Something diff --git a/spec/rubocop/cop/layout/space_around_method_call_operator_spec.rb b/spec/rubocop/cop/layout/space_around_method_call_operator_spec.rb new file mode 100644 index 00000000000..970cc6b6961 --- /dev/null +++ b/spec/rubocop/cop/layout/space_around_method_call_operator_spec.rb @@ -0,0 +1,482 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Layout::SpaceAroundMethodCallOperator do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + shared_examples 'offense' do |name, code, offense, correction| + it "registers an offense when #{name}" do + expect_offense(offense) + end + + it "autocorrects offense when #{name}" do + corrected = autocorrect_source(code) + + expect(corrected).to eq(correction) + end + end + + context 'dot operator' do + include_examples 'offense', 'space after method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo. bar + CODE + foo. bar + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar + CORRECTION + + include_examples 'offense', 'space before method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo .bar + CODE + foo .bar + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar + CORRECTION + + include_examples 'offense', 'spaces before method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo .bar + CODE + foo .bar + ^^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar + CORRECTION + + include_examples 'offense', 'spaces after method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo. bar + CODE + foo. bar + ^^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar + CORRECTION + + include_examples 'offense', 'spaces around method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo . bar + CODE + foo . bar + ^ Avoid using spaces around a method call operator. + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar + CORRECTION + + context 'when multi line method call' do + include_examples 'offense', 'space before method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo + . bar + CODE + foo + . bar + ^ Avoid using spaces around a method call operator. + OFFENSE + foo + .bar + CORRECTION + + include_examples 'offense', 'space before method call in suffix chaining', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo . + bar + CODE + foo . + ^ Avoid using spaces around a method call operator. + bar + OFFENSE + foo. + bar + CORRECTION + + it 'does not register an offense when no space after the `.`' do + expect_no_offenses(<<~RUBY) + foo + .bar + RUBY + end + end + + it 'does not register an offense when no space around method call' do + expect_no_offenses(<<~RUBY) + 'foo'.bar + RUBY + end + + include_examples 'offense', 'space after last method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo.bar. buzz + CODE + foo.bar. buzz + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar.buzz + CORRECTION + + include_examples 'offense', 'space after first method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo. bar.buzz + CODE + foo. bar.buzz + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar.buzz + CORRECTION + + include_examples 'offense', 'space before first method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo .bar.buzz + CODE + foo .bar.buzz + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar.buzz + CORRECTION + + include_examples 'offense', 'space before last method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo.bar .buzz + CODE + foo.bar .buzz + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar.buzz + CORRECTION + + include_examples 'offense', + 'space around intermediate method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo.bar .buzz.bat + CODE + foo.bar .buzz.bat + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar.buzz.bat + CORRECTION + + include_examples 'offense', 'space around multiple method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo. bar. buzz.bat + CODE + foo. bar. buzz.bat + ^ Avoid using spaces around a method call operator. + ^ Avoid using spaces around a method call operator. + OFFENSE + foo.bar.buzz.bat + CORRECTION + + it 'does not register an offense when no space around any `.` operators' do + expect_no_offenses(<<~RUBY) + foo.bar.buzz + RUBY + end + end + + context 'safe navigation operator' do + include_examples 'offense', 'space after method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo&. bar + CODE + foo&. bar + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar + CORRECTION + + include_examples 'offense', 'space before method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo &.bar + CODE + foo &.bar + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar + CORRECTION + + include_examples 'offense', 'spaces before method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo &.bar + CODE + foo &.bar + ^^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar + CORRECTION + + include_examples 'offense', 'spaces after method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo&. bar + CODE + foo&. bar + ^^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar + CORRECTION + + include_examples 'offense', 'spaces around method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo &. bar + CODE + foo &. bar + ^ Avoid using spaces around a method call operator. + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar + CORRECTION + + context 'when multi line method call' do + include_examples 'offense', 'space before method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo + &. bar + CODE + foo + &. bar + ^ Avoid using spaces around a method call operator. + OFFENSE + foo + &.bar + CORRECTION + + include_examples 'offense', 'space before method call in suffix chaining', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo &. + bar + CODE + foo &. + ^ Avoid using spaces around a method call operator. + bar + OFFENSE + foo&. + bar + CORRECTION + + it 'does not register an offense when no space after the `&.`' do + expect_no_offenses(<<~RUBY) + foo + &.bar + RUBY + end + end + + it 'does not register an offense when no space around method call' do + expect_no_offenses(<<~RUBY) + foo&.bar + RUBY + end + + include_examples 'offense', 'space after last method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo&.bar&. buzz + CODE + foo&.bar&. buzz + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar&.buzz + CORRECTION + + include_examples 'offense', 'space after first method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo&. bar&.buzz + CODE + foo&. bar&.buzz + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar&.buzz + CORRECTION + + include_examples 'offense', 'space before first method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo &.bar&.buzz + CODE + foo &.bar&.buzz + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar&.buzz + CORRECTION + + include_examples 'offense', 'space before last method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo&.bar &.buzz + CODE + foo&.bar &.buzz + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar&.buzz + CORRECTION + + include_examples 'offense', + 'space around intermediate method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo&.bar &.buzz&.bat + CODE + foo&.bar &.buzz&.bat + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar&.buzz&.bat + CORRECTION + + include_examples 'offense', 'space around multiple method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + foo&. bar&. buzz&.bat + CODE + foo&. bar&. buzz&.bat + ^ Avoid using spaces around a method call operator. + ^ Avoid using spaces around a method call operator. + OFFENSE + foo&.bar&.buzz&.bat + CORRECTION + + it 'does not register an offense when no space around any `.` operators' do + expect_no_offenses(<<~RUBY) + foo&.bar&.buzz + RUBY + end + end + + context ':: operator' do + include_examples 'offense', 'space after method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + RuboCop:: Cop + CODE + RuboCop:: Cop + ^ Avoid using spaces around a method call operator. + OFFENSE + RuboCop::Cop + CORRECTION + + include_examples 'offense', 'spaces after method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + RuboCop:: Cop + CODE + RuboCop:: Cop + ^^ Avoid using spaces around a method call operator. + OFFENSE + RuboCop::Cop + CORRECTION + + context 'when multi line method call' do + include_examples 'offense', 'space before method call', + <<-CODE, <<-OFFENSE, <<-CORRECTION + RuboCop + :: Cop + CODE + RuboCop + :: Cop + ^ Avoid using spaces around a method call operator. + OFFENSE + RuboCop + ::Cop + CORRECTION + + it 'does not register an offense when no space after the `::`' do + expect_no_offenses(<<~RUBY) + RuboCop + ::Cop + RUBY + end + end + + it 'does not register an offense when no space around method call' do + expect_no_offenses(<<~RUBY) + RuboCop::Cop + RUBY + end + + include_examples 'offense', 'space after last method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + RuboCop::Cop:: Cop + CODE + RuboCop::Cop:: Cop + ^ Avoid using spaces around a method call operator. + OFFENSE + RuboCop::Cop::Cop + CORRECTION + + include_examples 'offense', + 'space around intermediate method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + RuboCop::Cop:: Cop::Cop + CODE + RuboCop::Cop:: Cop::Cop + ^ Avoid using spaces around a method call operator. + OFFENSE + RuboCop::Cop::Cop::Cop + CORRECTION + + include_examples 'offense', 'space around multiple method call operator', + <<-CODE, <<-OFFENSE, <<-CORRECTION + :: RuboCop:: Cop:: Cop::Cop + CODE + :: RuboCop:: Cop:: Cop::Cop + ^ Avoid using spaces around a method call operator. + ^ Avoid using spaces around a method call operator. + ^ Avoid using spaces around a method call operator. + OFFENSE + ::RuboCop::Cop::Cop::Cop + CORRECTION + + include_examples 'offense', 'space after first operator with assignment', + <<-CODE, <<-OFFENSE, <<-CORRECTION + klass = :: RuboCop::Cop + CODE + klass = :: RuboCop::Cop + ^ Avoid using spaces around a method call operator. + OFFENSE + klass = ::RuboCop::Cop + CORRECTION + + it 'does not register an offense when no space around any `.` operators' do + expect_no_offenses(<<~RUBY) + RuboCop::Cop::Cop + RUBY + end + + it 'does not register an offense if no space before `::` + operator with assignment' do + expect_no_offenses(<<~RUBY) + klass = ::RuboCop::Cop + RUBY + end + + it 'does not register an offense if no space before `::` + operator with inheritance' do + expect_no_offenses(<<~RUBY) + class Test < ::RuboCop::Cop + end + RUBY + end + + it 'does not register an offense if no space with + conditionals' do + expect_no_offenses(<<~RUBY) + ::RuboCop::Cop || ::RuboCop + RUBY + end + + include_examples 'offense', 'multiple spaces with assignment', + <<-CODE, <<-OFFENSE, <<-CORRECTION + :: RuboCop:: Cop || :: RuboCop + CODE + :: RuboCop:: Cop || :: RuboCop + ^ Avoid using spaces around a method call operator. + ^ Avoid using spaces around a method call operator. + ^ Avoid using spaces around a method call operator. + OFFENSE + ::RuboCop::Cop || ::RuboCop + CORRECTION + end + + it 'does not register an offense when no method call operator' do + expect_no_offenses(<<~RUBY) + 'foo' + 'bar' + RUBY + end +end