Skip to content

Instantly share code, notes, and snippets.

@SameedAtif
Created September 30, 2025 09:32
Show Gist options
  • Select an option

  • Save SameedAtif/596a93520f1c1d08026d273b97bf4a7e to your computer and use it in GitHub Desktop.

Select an option

Save SameedAtif/596a93520f1c1d08026d273b97bf4a7e to your computer and use it in GitHub Desktop.
Custom RuboCop to check `unscoped` positioning
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