diff --git a/.rubocop.yml b/.rubocop.yml index ba382ebf0..de55003cf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -136,3 +136,5 @@ RSpec/Rails/AvoidSetupHook: Enabled: true RSpec/Rails/HaveHttpStatus: Enabled: true +RSpec/Rails/InferredSpecType: + Enabled: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 379f86202..7ab309cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Add `AllowedPatterns` configuration option to `RSpec/NoExpectationExample`. ([@ydah][]) * Improve `RSpec/NoExpectationExample` cop to ignore examples skipped or pending via metatada. ([@pirj][]) * Add `RSpec/FactoryBot/ConsistentParenthesesStyle` cop. ([@Liberatys][]) +* Add `RSpec/Rails/InferredSpecType` cop. ([@r7kamura][]) ## 2.13.2 (2022-09-23) diff --git a/config/default.yml b/config/default.yml index a274b117c..099f414f1 100644 --- a/config/default.yml +++ b/config/default.yml @@ -981,6 +981,29 @@ RSpec/Rails/HaveHttpStatus: VersionAdded: '2.12' Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/HaveHttpStatus +RSpec/Rails/InferredSpecType: + Description: Identifies redundant spec type. + Enabled: pending + Safe: false + VersionAdded: '2.14' + Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/InferredSpecType + Inferences: + channels: channel + controllers: controller + features: feature + generator: generator + helpers: helper + jobs: job + mailboxes: mailbox + mailers: mailer + models: model + requests: request + integration: request + api: request + routing: routing + system: system + views: view + RSpec/Rails/HttpStatus: Description: Enforces use of symbolic or numeric value to describe HTTP status. Enabled: true diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 367c78069..03fc17b41 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -113,5 +113,6 @@ * xref:cops_rspec_rails.adoc#rspecrails/avoidsetuphook[RSpec/Rails/AvoidSetupHook] * xref:cops_rspec_rails.adoc#rspecrails/havehttpstatus[RSpec/Rails/HaveHttpStatus] * xref:cops_rspec_rails.adoc#rspecrails/httpstatus[RSpec/Rails/HttpStatus] +* xref:cops_rspec_rails.adoc#rspecrails/inferredspectype[RSpec/Rails/InferredSpecType] // END_COP_LIST diff --git a/docs/modules/ROOT/pages/cops_rspec_rails.adoc b/docs/modules/ROOT/pages/cops_rspec_rails.adoc index e1ae03a76..26440468e 100644 --- a/docs/modules/ROOT/pages/cops_rspec_rails.adoc +++ b/docs/modules/ROOT/pages/cops_rspec_rails.adoc @@ -121,3 +121,86 @@ it { is_expected.to have_http_status :error } === References * https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/HttpStatus + +== RSpec/Rails/InferredSpecType + +|=== +| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed + +| Pending +| No +| Yes (Unsafe) +| 2.14 +| - +|=== + +Identifies redundant spec type. + +After setting up rspec-rails, you will have enabled +`config.infer_spec_type_from_file_location!` by default in +spec/rails_helper.rb. This cop works in conjunction with this config. +If you disable this config, disable this cop as well. + +=== Safety + +This cop is marked as unsafe because +`config.infer_spec_type_from_file_location!` may not be enabled. + +=== Examples + +[source,ruby] +---- +# bad +# spec/models/user_spec.rb +RSpec.describe User, type: :model do +end + +# good +# spec/models/user_spec.rb +RSpec.describe User do +end + +# good +# spec/models/user_spec.rb +RSpec.describe User, type: :common do +end +---- + +==== `Inferences` configuration + +[source,ruby] +---- +# .rubocop.yml +# RSpec/InferredSpecType: +# Inferences: +# services: service + +# bad +# spec/services/user_spec.rb +RSpec.describe User, type: :service do +end + +# good +# spec/services/user_spec.rb +RSpec.describe User do +end + +# good +# spec/services/user_spec.rb +RSpec.describe User, type: :common do +end +---- + +=== Configurable attributes + +|=== +| Name | Default value | Configurable values + +| Inferences +| `{"channels"=>"channel", "controllers"=>"controller", "features"=>"feature", "generator"=>"generator", "helpers"=>"helper", "jobs"=>"job", "mailboxes"=>"mailbox", "mailers"=>"mailer", "models"=>"model", "requests"=>"request", "integration"=>"request", "api"=>"request", "routing"=>"routing", "system"=>"system", "views"=>"view"}` +| +|=== + +=== References + +* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/InferredSpecType diff --git a/lib/rubocop/cop/rspec/rails/inferred_spec_type.rb b/lib/rubocop/cop/rspec/rails/inferred_spec_type.rb new file mode 100644 index 000000000..d86754bba --- /dev/null +++ b/lib/rubocop/cop/rspec/rails/inferred_spec_type.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + module Rails + # Identifies redundant spec type. + # + # After setting up rspec-rails, you will have enabled + # `config.infer_spec_type_from_file_location!` by default in + # spec/rails_helper.rb. This cop works in conjunction with this config. + # If you disable this config, disable this cop as well. + # + # @safety + # This cop is marked as unsafe because + # `config.infer_spec_type_from_file_location!` may not be enabled. + # + # @example + # # bad + # # spec/models/user_spec.rb + # RSpec.describe User, type: :model do + # end + # + # # good + # # spec/models/user_spec.rb + # RSpec.describe User do + # end + # + # # good + # # spec/models/user_spec.rb + # RSpec.describe User, type: :common do + # end + # + # @example `Inferences` configuration + # # .rubocop.yml + # # RSpec/InferredSpecType: + # # Inferences: + # # services: service + # + # # bad + # # spec/services/user_spec.rb + # RSpec.describe User, type: :service do + # end + # + # # good + # # spec/services/user_spec.rb + # RSpec.describe User do + # end + # + # # good + # # spec/services/user_spec.rb + # RSpec.describe User, type: :common do + # end + class InferredSpecType < Base + extend AutoCorrector + + MSG = 'Remove redundant spec type.' + + # @param [RuboCop::AST::BlockNode] node + def on_block(node) + return unless example_group?(node) + + pair_node = describe_with_type(node) + return unless pair_node + return unless inferred_type?(pair_node) + + removable_node = detect_removable_node(pair_node) + add_offense(removable_node) do |corrector| + autocorrect(corrector, removable_node) + end + end + alias on_numblock on_block + + private + + # @!method describe_with_type(node) + # @param [RuboCop::AST::BlockNode] node + # @return [RuboCop::AST::PairNode, nil] + def_node_matcher :describe_with_type, <<~PATTERN + (block + (send #rspec? #ExampleGroups.all + ... + (hash <$(pair (sym :type) sym) ...>) + ) + ... + ) + PATTERN + + # @param [RuboCop::AST::Corrector] corrector + # @param [RuboCop::AST::Node] node + def autocorrect(corrector, node) + corrector.remove( + node.location.expression.with( + begin_pos: node.left_sibling.location.expression.end_pos + ) + ) + end + + # @param [RuboCop::AST::PairNode] node + # @return [RuboCop::AST::Node] + def detect_removable_node(node) + if node.parent.pairs.size == 1 + node.parent + else + node + end + end + + # @return [String] + def file_path + processed_source.file_path + end + + # @param [RuboCop::AST::PairNode] node + # @return [Boolean] + def inferred_type?(node) + inferred_type_from_file_path.inspect == node.value.source + end + + # @return [Symbol, nil] + def inferred_type_from_file_path + inferences.find do |prefix, type| + break type.to_sym if file_path.include?("spec/#{prefix}/") + end + end + + # @return [Hash] + def inferences + cop_config['Inferences'] || {} + end + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec_cops.rb b/lib/rubocop/cop/rspec_cops.rb index 88187661e..884539bf8 100644 --- a/lib/rubocop/cop/rspec_cops.rb +++ b/lib/rubocop/cop/rspec_cops.rb @@ -20,6 +20,7 @@ rescue LoadError # Rails/HttpStatus cannot be loaded if rack/utils is unavailable. end +require_relative 'rspec/rails/inferred_spec_type' require_relative 'rspec/align_left_let_brace' require_relative 'rspec/align_right_let_brace' diff --git a/spec/rubocop/cop/rspec/rails/inferred_spec_type_spec.rb b/spec/rubocop/cop/rspec/rails/inferred_spec_type_spec.rb new file mode 100644 index 000000000..bcfaf3825 --- /dev/null +++ b/spec/rubocop/cop/rspec/rails/inferred_spec_type_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpec::Rails::InferredSpecType do + describe 'with necessary type in keyword arguments' do + it 'does not register any offense' do + expect_no_offenses(<<~RUBY) + RSpec.describe User, type: :model do + end + RUBY + end + end + + describe 'with redundant type in keyword arguments' do + it 'register and corrects an offense' do + expect_offense(<<~RUBY, '/path/to/project/spec/models/user_spec.rb') + RSpec.describe User, type: :model do + ^^^^^^^^^^^^ Remove redundant spec type. + end + RUBY + + expect_correction(<<~RUBY) + RSpec.describe User do + end + RUBY + end + end + + describe 'with redundant type in Hash arguments' do + it 'register and corrects an offense' do + expect_offense(<<~RUBY, '/path/to/project/spec/models/user_spec.rb') + RSpec.describe User, { type: :model } do + ^^^^^^^^^^^^^^^^ Remove redundant spec type. + end + RUBY + + expect_correction(<<~RUBY) + RSpec.describe User do + end + RUBY + end + end + + describe 'with redundant type and other Hash metadata' do + it 'register and corrects an offense' do + expect_offense(<<~RUBY, '/path/to/project/spec/models/user_spec.rb') + RSpec.describe User, other: true, type: :model do + ^^^^^^^^^^^^ Remove redundant spec type. + end + RUBY + + expect_correction(<<~RUBY) + RSpec.describe User, other: true do + end + RUBY + end + end + + describe 'with redundant type and other Symbol metadata' do + it 'register and corrects an offense' do + expect_offense(<<~RUBY, '/path/to/project/spec/models/user_spec.rb') + RSpec.describe User, :other, type: :model do + ^^^^^^^^^^^^ Remove redundant spec type. + end + RUBY + + expect_correction(<<~RUBY) + RSpec.describe User, :other do + end + RUBY + end + end + + describe 'with redundant type and receiver-less describe' do + it 'register and corrects an offense' do + expect_offense(<<~RUBY, '/path/to/project/spec/models/user_spec.rb') + describe User, type: :model do + ^^^^^^^^^^^^ Remove redundant spec type. + end + RUBY + + expect_correction(<<~RUBY) + describe User do + end + RUBY + end + end + + describe 'with redundant type in inner example group' do + it 'register and corrects an offense' do + expect_offense(<<~RUBY, '/path/to/project/spec/models/user_spec.rb') + RSpec.describe User do + describe 'inner', type: :model do + ^^^^^^^^^^^^ Remove redundant spec type. + end + end + RUBY + + expect_correction(<<~RUBY) + RSpec.describe User do + describe 'inner' do + end + end + RUBY + end + end + + describe 'with Inferences configuration' do + let(:cop_config) do + { + 'Inferences' => { + 'services' => 'service' + } + } + end + + it 'register and corrects an offense' do + expect_offense(<<~RUBY, '/path/to/project/spec/services/user_spec.rb') + RSpec.describe User, type: :service do + ^^^^^^^^^^^^^^ Remove redundant spec type. + end + RUBY + + expect_correction(<<~RUBY) + RSpec.describe User do + end + RUBY + end + end +end