diff --git a/config/default.yml b/config/default.yml index 8712b0991d4f..673827ed2f55 100644 --- a/config/default.yml +++ b/config/default.yml @@ -2557,6 +2557,11 @@ Style/DocumentationMethod: - 'test/**/*' RequireForNonPublicMethods: false +Style/DoubleCopDisableDirective: + Description: 'Checks for double rubocop:disable comments on a single line.' + Enabled: true + VersionAdded: '0.73' + Style/DoubleNegation: Description: 'Checks for uses of double negation (!!).' StyleGuide: '#no-bang-bang' diff --git a/lib/rubocop.rb b/lib/rubocop.rb index b316b4becd36..49dddd3ec10d 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -414,6 +414,7 @@ require_relative 'rubocop/cop/style/dir' require_relative 'rubocop/cop/style/documentation_method' require_relative 'rubocop/cop/style/documentation' +require_relative 'rubocop/cop/style/double_cop_disable_directive' require_relative 'rubocop/cop/style/double_negation' require_relative 'rubocop/cop/style/each_for_simple_loop' require_relative 'rubocop/cop/style/each_with_object' diff --git a/lib/rubocop/cop/style/double_cop_disable_directive.rb b/lib/rubocop/cop/style/double_cop_disable_directive.rb new file mode 100644 index 000000000000..97f8fda6c0ee --- /dev/null +++ b/lib/rubocop/cop/style/double_cop_disable_directive.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# rubocop:disable Lint/UnneededCopDisableDirective +# rubocop:disable Style/DoubleCopDisableDirective + +module RuboCop + module Cop + module Style + # Detects double disable comments on one line. This is mostly to catch + # automatically generated comments that need to be regenerated. + # + # @example + # # bad + # def f # rubocop:disable Style/For # rubocop:disable Metrics/AbcSize + # end + # + # # good + # # rubocop:disable Metrics/AbcSize + # def f # rubocop:disable Style/For + # end + # # rubocop:enable Metrics/AbcSize + # + # # if both fit on one line + # def f # rubocop:disable Style/For, Metrics/AbcSize + # end + # + class DoubleCopDisableDirective < Cop + # rubocop:enable Style/For, Style/DoubleCopDisableDirective + # rubocop:enable Lint/UnneededCopDisableDirective, Metrics/AbcSize + MSG = 'More than one disable comment on one line.' + + def investigate(processed_source) + processed_source.comments.each do |comment| + next unless comment.text.scan('# rubocop:disable').size > 1 + + add_offense(comment) + end + end + + def autocorrect(comment) + lambda do |corrector| + corrector.replace(comment.loc.expression, + comment.text[/# rubocop:disable \S+/]) + end + end + end + end + end +end diff --git a/manual/cops.md b/manual/cops.md index ca6df557e645..2d8dd66eb418 100644 --- a/manual/cops.md +++ b/manual/cops.md @@ -330,6 +330,7 @@ In the following section you find all available cops: * [Style/Dir](cops_style.md#styledir) * [Style/Documentation](cops_style.md#styledocumentation) * [Style/DocumentationMethod](cops_style.md#styledocumentationmethod) +* [Style/DoubleCopDisableDirective](cops_style.md#styledoublecopdisabledirective) * [Style/DoubleNegation](cops_style.md#styledoublenegation) * [Style/EachForSimpleLoop](cops_style.md#styleeachforsimpleloop) * [Style/EachWithObject](cops_style.md#styleeachwithobject) diff --git a/manual/cops_style.md b/manual/cops_style.md index e9dd281ad6c3..e4e045d21581 100644 --- a/manual/cops_style.md +++ b/manual/cops_style.md @@ -1437,6 +1437,33 @@ Name | Default value | Configurable values Exclude | `spec/**/*`, `test/**/*` | Array RequireForNonPublicMethods | `false` | Boolean +## Style/DoubleCopDisableDirective + +Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged +--- | --- | --- | --- | --- +Enabled | Yes | Yes | 0.73 | - + +Detects double disable comments on one line. This is mostly to catch +automatically generated comments that need to be regenerated. + +### Examples + +```ruby +# bad +def f # rubocop:disable Style/For # rubocop:disable Metrics/AbcSize +end + +# good +# rubocop:disable Metrics/AbcSize +def f # rubocop:disable Style/For +end +# rubocop:enable Metrics/AbcSize + +# if both fit on one line +def f # rubocop:disable Style/For, Metrics/AbcSize +end +``` + ## Style/DoubleNegation Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged diff --git a/spec/rubocop/cli/cli_disable_uncorrectable_spec.rb b/spec/rubocop/cli/cli_disable_uncorrectable_spec.rb index e9c777c7d0ce..fc5e11aa888e 100644 --- a/spec/rubocop/cli/cli_disable_uncorrectable_spec.rb +++ b/spec/rubocop/cli/cli_disable_uncorrectable_spec.rb @@ -18,7 +18,7 @@ == example.rb == C: 1: 1: [Corrected] Style/FrozenStringLiteralComment: Missing magic comment # frozen_string_literal: true. C: 1: 7: [Corrected] Layout/SpaceAroundOperators: Surrounding space missing for operator ==. - + 1 file inspected, 2 offenses detected, 2 offenses corrected OUTPUT expect(IO.read('example.rb')).to eq(<<-RUBY.strip_indent) @@ -28,7 +28,7 @@ RUBY end - context 'if a one-line disable statement fits' do + context 'if one one-line disable statement fits' do it 'adds it' do create_file('example.rb', <<-RUBY.strip_indent) def is_example @@ -79,6 +79,74 @@ def is_example # rubocop:disable Naming/PredicateName RUBY end end + + context "but there are more offenses on the line and they don't all " \ + 'fit' do + it 'adds both one-line and before-and-after disable statements' do + create_file('example.rb', <<-RUBY.strip_indent) + # Chess engine. + class Chess + def choose_move(who_to_move) + legal_moves = all_legal_moves_that_dont_put_me_in_check(who_to_move) + + return nil if legal_moves.empty? + + mating_move = checkmating_move(legal_moves) + return mating_move if mating_move + + best_moves = checking_moves(legal_moves) + best_moves = castling_moves(legal_moves) if best_moves.empty? + best_moves = taking_moves(legal_moves) if best_moves.empty? + best_moves = legal_moves if best_moves.empty? + best_moves = remove_dangerous_moves(best_moves, who_to_move) + best_moves = legal_moves if best_moves.empty? + best_moves.sample + end + end + RUBY + expect(exit_code).to eq(0) + expect($stderr.string).to eq('') + expect($stdout.string).to eq(<<-OUTPUT.strip_indent) + == example.rb == + C: 1: 1: [Corrected] Style/FrozenStringLiteralComment: Missing magic comment # frozen_string_literal: true. + C: 3: 3: [Corrected] Metrics/AbcSize: Assignment Branch Condition size for choose_move is too high. [15.62/15] + C: 3: 3: [Corrected] Metrics/CyclomaticComplexity: Cyclomatic complexity for choose_move is too high. [7/6] + C: 3: 3: [Corrected] Metrics/MethodLength: Method has too many lines. [11/10] + C: 5: 3: [Corrected] Metrics/AbcSize: Assignment Branch Condition size for choose_move is too high. [15.62/15] + C: 5: 3: [Corrected] Metrics/MethodLength: Method has too many lines. [11/10] + C: 5: 32: [Corrected] Style/DoubleCopDisableDirective: More than one disable comment on one line. + + 1 file inspected, 7 offenses detected, 7 offenses corrected + OUTPUT + expect(IO.read('example.rb')).to eq(<<-RUBY.strip_indent) + # frozen_string_literal: true + + # Chess engine. + class Chess + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize + def choose_move(who_to_move) # rubocop:disable Metrics/CyclomaticComplexity + legal_moves = all_legal_moves_that_dont_put_me_in_check(who_to_move) + + return nil if legal_moves.empty? + + mating_move = checkmating_move(legal_moves) + return mating_move if mating_move + + best_moves = checking_moves(legal_moves) + best_moves = castling_moves(legal_moves) if best_moves.empty? + best_moves = taking_moves(legal_moves) if best_moves.empty? + best_moves = legal_moves if best_moves.empty? + best_moves = remove_dangerous_moves(best_moves, who_to_move) + best_moves = legal_moves if best_moves.empty? + best_moves.sample + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + end + RUBY + end + end end context "if a one-line disable statement doesn't fit" do diff --git a/spec/rubocop/cop/style/double_cop_disable_directive_spec.rb b/spec/rubocop/cop/style/double_cop_disable_directive_spec.rb new file mode 100644 index 000000000000..7310d4e99951 --- /dev/null +++ b/spec/rubocop/cop/style/double_cop_disable_directive_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Style::DoubleCopDisableDirective do + subject(:cop) { described_class.new } + + it 'registers an offense when using `#bad_method`' do + expect_offense(<<~RUBY) + def choose_move(who_to_move) # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/MethodLength + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ More than one disable comment on one line. + end + RUBY + expect_correction(<<~RUBY) + def choose_move(who_to_move) # rubocop:disable Metrics/CyclomaticComplexity + end + RUBY + end + + it 'does not register an offense when using `#good_method`' do + expect_no_offenses(<<~RUBY) + def choose_move(who_to_move) # rubocop:disable Metrics/CyclomaticComplexity + end + RUBY + end +end