From 2b903ff28919dcc2b59be55cce669d97f02821b3 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 29 May 2021 02:07:25 +0900 Subject: [PATCH] Add new `Style/InPatternThen` cop This PR adds new `Style/InPatternThen` cop. It checks for `in;` uses in `case` expressions. ```ruby # bad case expression in pattern_a; foo in pattern_b; bar end # good case expression in pattern_a then foo in pattern_b then bar end ``` This cop is similar to `Style/WhenThen`, but with different supported syntax and Ruby version (requires 2.7 or higher). And this PR use rubocop/rubocop-ast#186 feature, so it requires RuboCop AST 1.7.0 or higher. --- .../new_add_new_style_in_pattern_then_cop.md | 1 + config/default.yml | 6 ++ lib/rubocop.rb | 1 + lib/rubocop/cop/style/in_pattern_then.rb | 56 ++++++++++++ rubocop.gemspec | 2 +- .../rubocop/cop/style/in_pattern_then_spec.rb | 88 +++++++++++++++++++ 6 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 changelog/new_add_new_style_in_pattern_then_cop.md create mode 100644 lib/rubocop/cop/style/in_pattern_then.rb create mode 100644 spec/rubocop/cop/style/in_pattern_then_spec.rb diff --git a/changelog/new_add_new_style_in_pattern_then_cop.md b/changelog/new_add_new_style_in_pattern_then_cop.md new file mode 100644 index 00000000000..59a698fcee9 --- /dev/null +++ b/changelog/new_add_new_style_in_pattern_then_cop.md @@ -0,0 +1 @@ +* [#9833](https://github.com/rubocop/rubocop/pull/9833): Add new `Style/InPatternThen` cop. ([@koic][]) diff --git a/config/default.yml b/config/default.yml index 26dd618b50a..5fa6cf231cb 100644 --- a/config/default.yml +++ b/config/default.yml @@ -3608,6 +3608,12 @@ Style/ImplicitRuntimeError: Enabled: false VersionAdded: '0.41' +Style/InPatternThen: + Description: 'Checks for `in;` uses in `case` expressions.' + StyleGuide: '#no-in-pattern-semicolons' + Enabled: pending + VersionAdded: '<>' + Style/InfiniteLoop: Description: >- Use Kernel#loop for infinite loops. diff --git a/lib/rubocop.rb b/lib/rubocop.rb index a25dbc1d354..7bb1e3f6b54 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -492,6 +492,7 @@ require_relative 'rubocop/cop/style/if_with_boolean_literal_branches' require_relative 'rubocop/cop/style/if_with_semicolon' require_relative 'rubocop/cop/style/implicit_runtime_error' +require_relative 'rubocop/cop/style/in_pattern_then' require_relative 'rubocop/cop/style/infinite_loop' require_relative 'rubocop/cop/style/inverse_methods' require_relative 'rubocop/cop/style/inline_comment' diff --git a/lib/rubocop/cop/style/in_pattern_then.rb b/lib/rubocop/cop/style/in_pattern_then.rb new file mode 100644 index 00000000000..534dc77e8a1 --- /dev/null +++ b/lib/rubocop/cop/style/in_pattern_then.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Style + # This cop checks for `in;` uses in `case` expressions. + # + # @example + # # bad + # case expression + # in pattern_a; foo + # in pattern_b; bar + # end + # + # # good + # case expression + # in pattern_a then foo + # in pattern_b then bar + # end + # + class InPatternThen < Base + extend AutoCorrector + extend TargetRubyVersion + + minimum_target_ruby_version 2.7 + + MSG = 'Do not use `in %s;`. Use `in %s then` instead.' + + def on_in_pattern(node) + return if node.multiline? || node.then? || !node.body + + pattern = node.pattern + pattern_source = if pattern.match_alt_type? + alternative_pattern_source(pattern) + else + pattern.source + end + + add_offense(node.loc.begin, message: format(MSG, pattern: pattern_source)) do |corrector| + corrector.replace(node.loc.begin, ' then') + end + end + + private + + def alternative_pattern_source(pattern) + return pattern.children.map(&:source) unless pattern.children.first.match_alt_type? + + pattern_sources = alternative_pattern_source(pattern.children.first) + + (pattern_sources << pattern.children[1].source).join(' | ') + end + end + end + end +end diff --git a/rubocop.gemspec b/rubocop.gemspec index 49530d0bc48..032a9e4d571 100644 --- a/rubocop.gemspec +++ b/rubocop.gemspec @@ -35,7 +35,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency('rainbow', '>= 2.2.2', '< 4.0') s.add_runtime_dependency('regexp_parser', '>= 1.8', '< 3.0') s.add_runtime_dependency('rexml') - s.add_runtime_dependency('rubocop-ast', '>= 1.6.0', '< 2.0') + s.add_runtime_dependency('rubocop-ast', '>= 1.7.0', '< 2.0') s.add_runtime_dependency('ruby-progressbar', '~> 1.7') s.add_runtime_dependency('unicode-display_width', '>= 1.4.0', '< 3.0') diff --git a/spec/rubocop/cop/style/in_pattern_then_spec.rb b/spec/rubocop/cop/style/in_pattern_then_spec.rb new file mode 100644 index 00000000000..50c1e350335 --- /dev/null +++ b/spec/rubocop/cop/style/in_pattern_then_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Style::InPatternThen, :config do + context '>= Ruby 2.7', :ruby27 do + it 'registers an offense for `in b;`' do + expect_offense(<<~RUBY) + case a + in b; c + ^ Do not use `in b;`. Use `in b then` instead. + end + RUBY + + expect_correction(<<~RUBY) + case a + in b then c + end + RUBY + end + + it 'registers an offense for `in b, c, d;` (array pattern)' do + expect_offense(<<~RUBY) + case a + in b, c, d; e + ^ Do not use `in b, c, d;`. Use `in b, c, d then` instead. + end + RUBY + + expect_correction(<<~RUBY) + case a + in b, c, d then e + end + RUBY + end + + it 'registers an offense for `in b | c | d;` (alternative pattern)' do + expect_offense(<<~RUBY) + case a + in b | c | d; e + ^ Do not use `in b | c | d;`. Use `in b | c | d then` instead. + end + RUBY + + expect_correction(<<~RUBY) + case a + in b | c | d then e + end + RUBY + end + + it 'registers an offense for `in b, c | d;`' do + expect_offense(<<~RUBY) + case a + in b, c | d; e + ^ Do not use `in b, c | d;`. Use `in b, c | d then` instead. + end + RUBY + + expect_correction(<<~RUBY) + case a + in b, c | d then e + end + RUBY + end + + it 'accepts `;` separating statements in the body of `in`' do + expect_no_offenses(<<~RUBY) + case a + in b then c; d + end + + case e + in f + g; h + end + RUBY + end + + context 'when inspecting a case statement with an empty branch' do + it 'does not register an offense' do + expect_no_offenses(<<~RUBY) + case condition + in pattern + end + RUBY + end + end + end +end