Class: RuboCop::Cop::DevDoc::Auth::CurrentUserBranching

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/auth/current_user_branching.rb

Overview

Forbid branching on authentication state in page-specific code.

Rationale

In a Rails app using Pundit + Devise, current_user is guaranteed non-nil inside any controller action or view that requires auth — the policy has already denied anonymous visitors. Branching on if current_user or if user_signed_in? inside that code is therefore either dead code (the branch for nil can never fire) or a signal that the developer was unsure whether the page requires auth.

If the page genuinely serves both anonymous and signed-in visitors, the branching should be explicit and kept in shared/layout code, not sprinkled through action bodies and page views.

❌ Authenticated page branching on auth state (branch is dead code)
# app/views/posts/show.json.jbuilder
if current_user
json.actions [:edit, :delete]
end

✔️ Shared layout — branching here is the right place
# app/views/layouts/_nav_bar.html.erb
<% if user_signed_in? %>
<%= render 'profile_menu' %>
<% else %>
<%= link_to 'Log in', new_user_session_path %>
<% end %>

✔️ Genuinely dual-state page — suppress the cop with a reason comment
def new
# rubocop:disable DevDoc/Auth/CurrentUserBranching
# Reason: contact form is intentionally dual-state; pre-fills for signed-in users.
if current_user
  @form.name = current_user.full_name
end
# rubocop:enable DevDoc/Auth/CurrentUserBranching
end

Patterns flagged

  • if current_user / unless current_user (block or modifier) where the body is non-empty and is not a bare return (bare returns are the nil-guard pattern covered by LoadResourceCurrentUserGuard).
  • if user_signed_in? / unless user_signed_in? (any form).
  • if current_user&.foo and other safe-nav uses of current_user as a condition. The &. is itself a confession that the dev expects current_user to be nil sometimes — i.e., it's the same anti-pattern as if current_user, just in disguise: when current_user is nil the csend returns nil → branch is dead for anonymous visitors, exactly what the cop prevents.
  • Ternaries: current_user ? a : b, user_signed_in? ? a : b.
  • Hash/argument values: authenticated: user_signed_in?.

Allowed paths (Exclude:)

By default the cop is silent in:

app/policies/**/*.rb          ← auth-dependent checks belong here
app/helpers/**/*.rb
app/controllers/concerns/**/*.rb
app/views/layouts/**/*        ← display branching (nav bars, etc.)

Override via Exclude: in your .rubocop.yml. Note: literal file paths (e.g. app/controllers/application_controller.rb) in .rubocop.yml Exclude: lists are flagged by the DevDoc::Test::Lints::NoFileExcludes lint — they hide the suppression from readers of that file. If ApplicationController needs an exception, use an inline # rubocop:disable at the specific line with a reason.

Before disabling inline, consider the Policy

The Policy exclusion is not accidental — it's the canonical Rails home for auth-dependent branching. If the flagged line is an authorization check (return if @record.accessible_by?(current_user), if current_user.admin?, etc.), the right move is almost always to push that check into a Pundit policy method, not disable inline. The controller becomes return unless policy(@record).access? — same behaviour, no current_user reference in the controller, and the auth logic is reusable + testable in isolation.

Even better: declare glib_authorize_resource at the controller level. glib-web runs the appropriate policy method before each action automatically, so the per-action verify_access / authorize @record line is usually not needed at all — the controller body no longer references current_user because the auth check has moved out of the action entirely. See best_practices/backend/en/05_controller.md item #7 for the canonical pattern.

❌ Auth check in the controller — flagged
def verify_access
return if @support_question.accessible_by?(current_user)
# ... token fallback ...
end

✔️ Same check in the Policy — silently allowed
# app/policies/support_question_policy.rb
def access?
user.present? && record.accessible_by?(user)
end

# in the controller:
def verify_access
return if policy(@support_question).access?
# ... token fallback ...
end

NOTE: The cop does not autocorrect — there is no mechanical fix. The right response depends on developer intent: drop the branch (if auth is required), restructure into a shared layout (if dual-state), or add an inline disable with a reason.

Examples:

# bad — inside an authenticated action
def show
  if current_user
    @data = current_user.private_data
  end
end

# bad — safe-nav predicate, same anti-pattern in disguise
def show
  if current_user&.admin?
    admin_thing
  end
end

# bad — inside a page view
# user_signed_in? ? render_private : render_public

# good — bare nil-guard return (LoadResourceCurrentUserGuard rule)
return unless current_user

# good — inside a shared layout file (excluded by default)
# app/views/layouts/_nav.html.erb
if user_signed_in?
  ...
end

Constant Summary collapse

MSG =
'Avoid branching on auth state in page code. ' \
'If this page serves both anonymous and signed-in users, ' \
'add an inline disable with a reason.'.freeze
AUTH_METHODS =
%i[current_user user_signed_in?].freeze

Instance Method Summary collapse

Instance Method Details

#on_if(node) ⇒ Object



148
149
150
151
152
153
# File 'lib/rubocop/cop/dev_doc/auth/current_user_branching.rb', line 148

def on_if(node)
  return unless auth_branch?(node)
  return if bare_return_guard?(node)

  add_offense(if_offense_location(node))
end

#on_send(node) ⇒ Object

value: user_signed_in? — condition passed as a value.



156
157
158
159
160
161
# File 'lib/rubocop/cop/dev_doc/auth/current_user_branching.rb', line 156

def on_send(node)
  return unless auth_method_call?(node)
  return unless used_as_value?(node)

  add_offense(node.loc.selector)
end