Created
September 30, 2025 09:32
-
-
Save SameedAtif/596a93520f1c1d08026d273b97bf4a7e to your computer and use it in GitHub Desktop.
Custom RuboCop to check `unscoped` positioning
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| RuboCop: | |
| ``` | |
| # lib/rubocop/cop/style | |
| # frozen_string_literal: true | |
| module RuboCop | |
| module Cop | |
| module Style | |
| # `Client.unscoped.where(...)` is better than `Client.where(...).unscoped` | |
| # | |
| # @example | |
| # # bad | |
| # Client.where(...).unscoped | |
| # | |
| # # good | |
| # Client.unscoped.where(...) | |
| # | |
| class UnscopedPositioning < Base | |
| MSG = 'Use `.unscoped` immediately after the model name.' | |
| # We only care about method calls to `unscoped` | |
| RESTRICT_ON_SEND = %i[unscoped].freeze | |
| def on_send(node) | |
| return unless node.method?(:unscoped) | |
| receiver = node.receiver | |
| return unless receiver&.send_type? | |
| # Get the full chain of calls | |
| chain = build_method_chain(node) | |
| # We're only interested if `unscoped` is not the first call after the model | |
| # i.e., the model name should be the base of the chain, and `unscoped` should be next | |
| return unless chain.size > 1 && chain[0]&.const_type? && chain[1]&.method_name != :unscoped | |
| add_offense(node) | |
| end | |
| private | |
| # Builds an array of nodes representing the method chain | |
| # For `Client.where(...).order(...).unscoped`, returns: | |
| # [Client, where, order, unscoped] | |
| def build_method_chain(node) | |
| chain = [] | |
| while node | |
| chain.unshift(node) | |
| node = node.receiver if node.respond_to?(:receiver) | |
| end | |
| chain | |
| end | |
| end | |
| end | |
| end | |
| end | |
| ``` | |
| RSepc: | |
| ``` | |
| # spec/rubocop/cop/style | |
| # frozen_string_literal: true | |
| require 'rubocop' | |
| require 'rubocop/rspec/support' | |
| require_relative '../../../../lib/rubocop/cop/style/unscoped_positioning' | |
| RSpec.describe RuboCop::Cop::Style::UnscopedPositioning, :rubocop do | |
| subject(:cop) { described_class.new } | |
| context 'when unscoped comes after another query method' do | |
| it 'registers an offense for .where followed by .unscoped' do | |
| expect_offense(<<~RUBY) | |
| Client.where(status: :active).unscoped | |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Style/UnscopedPositioning: Use `.unscoped` immediately after the model name. | |
| RUBY | |
| end | |
| it 'registers an offense for .joins followed by .unscoped' do | |
| expect_offense(<<~RUBY) | |
| Client.joins(:posts).unscoped | |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Style/UnscopedPositioning: Use `.unscoped` immediately after the model name. | |
| RUBY | |
| end | |
| end | |
| context 'when unscoped is the first method call on the model' do | |
| it 'does not register an offense' do | |
| expect_no_offenses(<<~RUBY) | |
| Client.unscoped.where(status: :active) | |
| RUBY | |
| end | |
| it 'does not register an offense for complex chain starting with unscoped' do | |
| expect_no_offenses(<<~RUBY) | |
| Client.unscoped.joins(:posts).where(status: :active) | |
| RUBY | |
| end | |
| end | |
| context 'when no unscoped is used' do | |
| it 'does not register an offense' do | |
| expect_no_offenses(<<~RUBY) | |
| Client.where(status: :active) | |
| RUBY | |
| end | |
| end | |
| end | |
| ``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment