diff --git a/CHANGELOG.md b/CHANGELOG.md index 365261eec20..0c18d07b8e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### New features +* [#6704](https://github.com/rubocop-hq/rubocop/pull/6704): Add new `Rails/ReflectionClassName` cop. ([@Bhacaz][]) * [#6643](https://github.com/rubocop-hq/rubocop/pull/6643): Support `AllowParenthesesInCamelCaseMethod` option on `Style/MethodCallWithArgsParentheses` `omit_parentheses`. ([@dazuma][]) ### Bug fixes @@ -3787,3 +3788,4 @@ [@dazuma]: https://github.com/dazuma [@dischorde]: https://github.com/dischorde [@mhelmetag]: https://github.com/mhelmetag +[@Bhacaz]: https://github.com/bhacaz diff --git a/config/default.yml b/config/default.yml index bd2370ba520..87f4d74d821 100644 --- a/config/default.yml +++ b/config/default.yml @@ -2493,6 +2493,11 @@ Rails/RedundantReceiverInWithOptions: Enabled: true VersionAdded: '0.52' +Rails/ReflectionClassName: + Description: 'Use a string for `class_name` option value in the definition of a reflection.' + Enabled: true + VersionAdded: '0.64' + Rails/RefuteMethods: Description: 'Use `assert_not` methods instead of `refute` methods.' Enabled: true diff --git a/lib/rubocop.rb b/lib/rubocop.rb index 115b892f8db..fd64ff7a8fd 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -592,6 +592,7 @@ require_relative 'rubocop/cop/rails/present' require_relative 'rubocop/cop/rails/read_write_attribute' require_relative 'rubocop/cop/rails/redundant_receiver_in_with_options' +require_relative 'rubocop/cop/rails/reflection_class_name' require_relative 'rubocop/cop/rails/refute_methods' require_relative 'rubocop/cop/rails/relative_date_constant' require_relative 'rubocop/cop/rails/request_referer' diff --git a/lib/rubocop/cop/rails/reflection_class_name.rb b/lib/rubocop/cop/rails/reflection_class_name.rb new file mode 100644 index 00000000000..542a89f7ef3 --- /dev/null +++ b/lib/rubocop/cop/rails/reflection_class_name.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Rails + # This cop checks if the value of the option `class_name`, in + # the definition of a reflection is a string. + # + # @example + # # bad + # has_many :accounts, class_name: Account + # has_many :accounts, class_name: Account.name + # + # # good + # has_many :accounts, class_name: 'Account' + class ReflectionClassName < Cop + MSG = 'Use a string has value for a class_name.'.freeze + + def_node_matcher :association_with_options?, <<-PATTERN + (send nil? {:has_many :has_one :belongs_to} _ (hash $...)) + PATTERN + + def_node_search :reflection_class_name, <<-PATTERN + (pair (sym :class_name) !str) + PATTERN + + def on_send(node) + return unless association_with_options?(node) + + reflection_class_name = reflection_class_name(node).first + return unless reflection_class_name + + add_offense(node, location: reflection_class_name.loc.expression) + end + end + end + end +end diff --git a/manual/cops.md b/manual/cops.md index e07f644595a..2a6e756deb7 100644 --- a/manual/cops.md +++ b/manual/cops.md @@ -368,6 +368,7 @@ In the following section you find all available cops: * [Rails/Present](cops_rails.md#railspresent) * [Rails/ReadWriteAttribute](cops_rails.md#railsreadwriteattribute) * [Rails/RedundantReceiverInWithOptions](cops_rails.md#railsredundantreceiverinwithoptions) +* [Rails/ReflectionClassName](cops_rails.md#railsreflectionclassname) * [Rails/RefuteMethods](cops_rails.md#railsrefutemethods) * [Rails/RelativeDateConstant](cops_rails.md#railsrelativedateconstant) * [Rails/RequestReferer](cops_rails.md#railsrequestreferer) diff --git a/manual/cops_rails.md b/manual/cops_rails.md index d5a581f90ec..c11391f5c5a 100644 --- a/manual/cops_rails.md +++ b/manual/cops_rails.md @@ -1572,6 +1572,26 @@ with_options options: false do |merger| end ``` +## Rails/ReflectionClassName + +Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged +--- | --- | --- | --- | --- +Enabled | Yes | No | 0.64 | - + +This cop checks if the value of the option `class_name`, in +the definition of a reflection is a string. + +### Examples + +```ruby +# bad +has_many :accounts, class_name: Account +has_many :accounts, class_name: Account.name + +# good +has_many :accounts, class_name: 'Account' +``` + ## Rails/RefuteMethods Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged diff --git a/spec/rubocop/cop/rails/reflection_class_name_spec.rb b/spec/rubocop/cop/rails/reflection_class_name_spec.rb new file mode 100644 index 00000000000..05a91b4bb3b --- /dev/null +++ b/spec/rubocop/cop/rails/reflection_class_name_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Rails::ReflectionClassName do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + context "registers an offense when using `foreign_key: 'account_id'`" do + it 'has_many' do + expect_offense(<<-RUBY.strip_indent) + has_many :accounts, class_name: Account, foreign_key: :account_id + ^^^^^^^^^^^^^^^^^^^ Use a string has value for a class_name. + RUBY + end + + it '.name' do + expect_offense(<<-RUBY.strip_indent) + has_many :accounts, class_name: Account.name + ^^^^^^^^^^^^^^^^^^^^^^^^ Use a string has value for a class_name. + RUBY + end + + it 'has_one' do + expect_offense(<<-RUBY.strip_indent) + has_one :account, class_name: Account + ^^^^^^^^^^^^^^^^^^^ Use a string has value for a class_name. + RUBY + end + + it 'belongs_to' do + expect_offense(<<-RUBY.strip_indent) + belongs_to :account, class_name: Account + ^^^^^^^^^^^^^^^^^^^ Use a string has value for a class_name. + RUBY + end + end + + it 'does not register an offense when using `foreign_key :account_id`' do + expect_no_offenses(<<-RUBY.strip_indent) + has_many :accounts, class_name: 'Account', foreign_key: :account_id + has_one :account, class_name: 'Account' + belongs_to :account, class_name: 'Account' + RUBY + end +end