Rails code review skill following Konvenit guidelines...
Perform thorough code reviews following Konvenit's Rails development standards.
This skill enforces the rules defined in ~/.claude/rules/. When reviewing, apply these:
| Rule File | Covers |
|---|---|
ruby.md |
Style, naming, conditionals, collections, strings |
architecture.md |
Service objects, presenters, finders, form objects |
controllers.md |
Thin controllers, strong params, REST conventions |
models.md |
ActiveRecord, enums, validations, callbacks, associations |
database.md |
Query optimization, N+1, transactions |
migrations.md |
Zero-downtime, constraints, rollback safety |
views.md |
HAML, I18n, partials, semantic HTML |
testing.md |
TDD philosophy, test types, real objects over mocks |
rspec.md |
RSpec syntax, subject/let, matchers, FactoryBot |
capybara.md |
Capybara system spec style, finders, matchers, actions, scoping |
api.md |
Versioning, Jbuilder, error formats |
jobs.md |
Idempotency, queue naming, retry strategies |
security.md |
Auth, CSRF, XSS, session management, mass assignment, file uploads |
design.md |
SOLID principles, Sandi Metz rules, Law of Demeter |
mailers.md |
Naming, HTML+text templates, background delivery |
performance.md |
filter_map, flat_map, match?, Struct over OpenStruct |
backend.md |
Error handling, caching |
frontend.md |
Hotwire/Turbo/Stimulus, minimal JS, CSS discipline |
hotwire.md |
Turbo Frames, Turbo Streams, morphing, Stimulus controller catalog |
caching.md |
HTTP caching, Russian doll, Solid Cache, counter caches |
delegated-types.md |
Delegated types vs STI, Contactable pattern |
webhooks.md |
SSRF protection, delivery lifecycle, signature verification |
activestorage.md |
Attachment removal, direct uploads, custom keys |
javascript.md |
JavaScript style (Airbnb), Hotwire/Turbo/Stimulus |
css.md |
CSS methodology, BEM, variables, specificity |
git.md |
Commit messages, branch naming, PR hygiene |
Understand why the change is necessary (fixes a bug, improves the user experience, refactors the existing code).
Then:
Communicate which ideas you feel strongly about and those you don't
Identify ways to simplify the code while still solving the problem
If discussions turn too philosophical or academic, move the discussion offline to a regular Friday afternoon technique discussion
Offer alternative implementations
Seek to understand the author's perspective
Approve the pull request
Remember that you are here to provide feedback, not to be a gatekeeper
When suggesting changes using the "Add a suggestion" feature:
PULL REQUESTS WITH FAILING SPECS ARE NOT APPROVABLE!!!
Approved with OLGTM (minor issues):
Approved with LGTM:
Rightly rejected:
"What do you think about ...?""Did you consider ...?""Can you clarify ...?"quote for manual escapingwhere("foo LIKE ?", "%#{arg}%")html_safe and raw are almost never requiredcontent_tag helpers instead"".html_safe and concatenate@var at class level)NOT NULL constraints make sense~> for patch updates)bundle audit)config/environments/Time.zone, not Time.now)en.controllers.users.create.success)en.yml, de.yml, fr.yml) — missing keys in any locale are a bug%() for strings with many quotesString#strip for cleaning whitespacesnake_case for variables, methods, file namesPascalCase for class namesSCREAMING_SNAKE_CASE for constantsunless sparingly — only for simple negative conditionsunless with else — use if instead%w[] and %i[] for word and symbol arraysHash#fetch when handling missing keys mattersHash.new with block for complex default valuesTime methodspresent? and blank? over nil? and empty?&.) for potentially nil objectsCheck code against all five SOLID principles during review.
# ❌ Bad: Multiple responsibilities in one class
class User < ApplicationRecord
validates :email, presence: true
def send_welcome_email # Notification concern
UserMailer.welcome_email(self).deliver_now
end
def generate_activity_report # Reporting concern
activities.map { |a| "#{a.created_at}: #{a.description}" }.join("\n")
end
def sync_to_crm # External integration concern
CrmApi.create_contact(email: email, name: name)
end
end
# ✅ Good: Each class has one responsibility
class User < ApplicationRecord
validates :email, presence: true
end
class UserNotifier
def initialize(user)
@user = user
end
def send_welcome_email
UserMailer.welcome_email(@user).deliver_now
end
end
class UserCrmSync
def initialize(user)
@user = user
end
def sync
CrmApi.create_contact(email: @user.email, name: @user.name)
end
end
case/if-elsif chains that grow with new types# ❌ Bad: Must modify existing code for each new format
class ReportGenerator
def generate(report_type, data)
case report_type
when :pdf then generate_pdf(data)
when :csv then generate_csv(data)
# Adding new format requires modifying this class
end
end
end
# ✅ Good: New formats added without modifying existing code
class ReportService
def initialize(generator)
@generator = generator
end
def create_report(data)
@generator.generate(data)
end
end
class PdfReportGenerator
def generate(data)
# PDF logic
end
end
# Adding XML doesn't touch existing code
class XmlReportGenerator
def generate(data)
# XML logic
end
end
is_a?, kind_of?) before calling methods# ❌ Bad: Subclass breaks the parent contract
class Bird
def fly
"Flying high!"
end
end
class Penguin < Bird
def fly
raise "Penguins can't fly!" # LSP violation
end
end
# ✅ Good: Better abstraction hierarchy
class Bird
def move
raise NotImplementedError
end
end
class FlyingBird < Bird
def move
"Flying high!"
end
end
class Penguin < Bird
def move
"Swimming fast!"
end
end
# ❌ Bad: Fat concern forces unrelated behavior
module Publishable
extend ActiveSupport::Concern
def publish; end
def unpublish; end
def schedule_publication(time); end
def generate_social_media_post; end # Not all publishable things need this
end
# ✅ Good: Segregated concerns — include only what you need
module Publishable
extend ActiveSupport::Concern
def publish
update!(published: true, published_at: Time.zone.now)
end
def unpublish
update!(published: false)
end
end
module Schedulable
extend ActiveSupport::Concern
def schedule_publication(time)
update!(scheduled_for: time)
end
end
class BlogPost < ApplicationRecord
include Publishable
include Schedulable
end
class Comment < ApplicationRecord
include Publishable
# No scheduling needed for comments
end
# ❌ Bad: Tightly coupled to concrete implementations
class OrderProcessor
def process(order)
payment = StripePaymentGateway.new
payment.charge(order.amount)
email = SmtpEmailService.new
email.send_confirmation(order.user.email)
end
end
# ✅ Good: Dependencies injected
class OrderProcessor
def initialize(payment_gateway:, email_service:)
@payment_gateway = payment_gateway
@email_service = email_service
end
def process(order)
@payment_gateway.charge(order.amount)
@email_service.send_confirmation(order.user.email)
end
end
# Easy to swap and test
processor = OrderProcessor.new(
payment_gateway: StripePaymentGateway.new,
email_service: SendgridEmailService.new
)
"Only talk to your immediate friends" — an object should only call methods on itself, its parameters, objects it creates, or its direct instance variables.
delegate or wrapper methods to hide navigation# ❌ Bad: Train wreck — reaching through objects
order.user.shipping_address.city.tax_rate
@post.author.profile.avatar_url
# ✅ Good: Delegation hides the chain
class Order
delegate :tax_rate, to: :user
end
class User
delegate :tax_rate, to: :shipping_address
end
# ✅ Good: Rails delegate with prefix
class Post < ApplicationRecord
delegate :avatar_url, to: :author, prefix: true
# Generates: post.author_avatar_url
end
before_action for common operations# ❌ Bad: Fat controller with business logic
def create
@user = User.new(user_params)
@user.status = "pending"
@user.activation_token = SecureRandom.hex(20)
UserMailer.welcome(@user).deliver_later if @user.save
# ... more logic
end
# ✅ Good: Delegate to service object
def create
@user = CreateUser.call(params: user_params)
end
app/services/CreateUser, ProcessPayment)call methodBaseService# ✅ Correct service object pattern
class CreateUser < BaseService
attr_accessor :params
def call
# Business logic here
end
end
When services return result objects, use a consistent pattern across the project:
# app/services/result.rb
class Result
attr_reader :value, :error
def initialize(success:, value: nil, error: nil)
@success = success
@value = value
@error = error
end
def success?
@success
end
def failure?
!@success
end
def self.success(value = nil)
new(success: true, value: value)
end
def self.failure(error)
new(success: false, error: error)
end
end
# ✅ Service using Result
class ProcessOrder < BaseService
attr_accessor :order_params
def call
order = Order.create(order_params)
return Result.failure(order.errors) unless order.persisted?
charge = PaymentService.charge(order)
return Result.failure(charge.error) if charge.failure?
Result.success(order)
end
end
# ✅ Controller usage
def create
result = ProcessOrder.call(order_params: order_params)
if result.success?
redirect_to result.value
else
@errors = result.error
render :new
end
end
app/presenters/Presenter format (UserPresenter, OrderPresenter)ApplicationPresentero. to access the underlying object# ✅ Correct presenter pattern
class UserPresenter < ApplicationPresenter
def full_name
"#{o.first_name} #{o.last_name}".strip
end
def formatted_created_at
o.created_at.strftime("%B %d, %Y")
end
def status_badge_class
o.active? ? "badge-success" : "badge-danger"
end
end
app/forms/ (if used)Form format (UserRegistrationForm)ActiveModel::Model or inherit from BaseForm# ✅ Form object pattern
class UserRegistrationForm
include ActiveModel::Model
attr_accessor :email, :password, :terms_accepted
validates :email, :password, presence: true
validates :terms_accepted, acceptance: true
def save
return false unless valid?
# Create user and related records
end
end
app/queries/ (if used)ActiveUsersQuery, RecentOrdersQuery)ActiveRecord::Relation for chaining.call or .all)# ✅ Query object pattern — chainable
class PostQuery
def initialize(relation = Post.all)
@relation = relation
end
def recent
@relation.where("created_at > ?", 1.week.ago)
end
def popular
@relation.where("views_count > ?", 1000)
end
def by_author(author)
@relation.where(author: author)
end
def trending
recent.popular.order(views_count: :desc)
end
end
# Usage
PostQuery.new.trending
PostQuery.new.by_author(current_user).recent
before_*, after_*) unless absolutely necessaryAcceptable callbacks (data normalization within the same record):
# ✅ OK: Normalizing data before validation
class User < ApplicationRecord
before_validation :normalize_email
private
def normalize_email
self.email = email.downcase.strip if email.present?
end
end
# ✅ OK: Setting calculated fields on the same record
class Post < ApplicationRecord
before_save :generate_slug
private
def generate_slug
self.slug = title.parameterize if slug.blank?
end
end
Unacceptable callbacks (side effects, external calls, other models):
# ❌ Bad: Email sending in callback — breaks seed scripts, bulk imports
class User < ApplicationRecord
after_create :send_welcome_email
end
# ❌ Bad: External API calls in callback — slows every save
class Order < ApplicationRecord
after_save :sync_to_crm
end
# ❌ Bad: Updating other models in callback — hidden dependencies
class Comment < ApplicationRecord
after_create :update_post_stats
end
# ✅ Good: Explicit orchestration in service
class CreateUser < BaseService
def call
user = User.create!(params)
UserMailer.welcome(user).deliver_later
DefaultSettings.create_for(user)
user
end
end
If you must use callbacks for side effects, prefer after_commit (transaction-aware, won't fire on rollback).
Archivable, Searchable)# ❌ Bad: Junk drawer concern — unrelated methods lumped together
module UserHelpers
extend ActiveSupport::Concern
def full_name; end
def send_notification; end
def calculate_discount; end
def export_to_csv; end
end
# ✅ Good: Focused concern, reused by multiple models
module Archivable
extend ActiveSupport::Concern
included do
scope :archived, -> { where.not(archived_at: nil) }
scope :active, -> { where(archived_at: nil) }
end
def archive!
update!(archived_at: Time.zone.now)
end
def archived?
archived_at.present?
end
end
resources over individual get/post definitionsnamespace :api do namespace :v1 do)only: or except: to limit generated routes to what's actually used# ❌ Bad: Deeply nested and custom routes
resources :companies do
resources :departments do
resources :employees do
member do
post :activate
post :deactivate
end
end
end
end
# ✅ Good: Shallow nesting, separate controllers for actions
resources :companies, only: %i[index show] do
resources :departments, only: %i[index show], shallow: true
end
resources :departments do
resources :employees, only: %i[index create], shallow: true
end
# Separate controller for activation
resources :employee_activations, only: %i[create destroy]
data-testid attributes for test selectors<nav>, <main>, <article>)has_secure_password, authenticate_by, generated Authentication concern)cancancan for authorizationapp/models/ability.rbload_and_authorize_resource or authorize! for every action# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Authentication # Rails 8 generated concern
before_action :require_authentication
end
# ✅ CanCanCan — load and authorize in one call
class PostsController < ApplicationController
load_and_authorize_resource
def index
# @posts already loaded and scoped by cancancan
end
end
# app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest
if user.admin?
can :manage, :all
else
can :read, :all
can :manage, Post, user_id: user.id
end
end
end
# ❌ Bad: Authorization only in view — server is unprotected
<% if current_user.admin? %>
<%= link_to "Delete", post_path(@post), method: :delete %>
<% end %>
# Controller has no check — anyone can send DELETE request
def destroy
@post = Post.find(params[:id])
@post.destroy
end
# ✅ Good: Server-side authorization
def destroy
@post = Post.find(params[:id])
authorize! :destroy, @post # cancancan check
@post.destroy
redirect_to posts_path
end
dependent: :destroy or dependent: :delete_all appropriately_prefix or _suffix options when enum names could clash# ❌ Bad: Array syntax — reordering will break existing data
enum status: [:draft, :published, :archived]
# ✅ Good: Explicit hash — values are stable
enum status: { draft: 0, published: 1, archived: 2 }
# ✅ Good: With prefix to avoid method name clashes
enum status: { active: 0, inactive: 1 }, _prefix: true
# Generates: status_active?, status_inactive?
presence: ↔ NOT NULL constraint in DBuniqueness: ↔ unique index in DB (note: validates_uniqueness_of alone is not race-condition safe — always back with a unique index)numericality:) matched by DB check constraints where critical# ✅ Good: Both layers
# Migration
add_column :users, :email, :string, null: false
add_index :users, :email, unique: true
# Model
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
end
class User < ApplicationRecord
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :age, numericality: { only_integer: true, greater_than: 0 }
validates :website, format: { with: /\Ahttps?:\/\// }, allow_blank: true
before_validation :sanitize_inputs
private
def sanitize_inputs
self.name = name.strip if name.present?
self.bio = ActionController::Base.helpers.sanitize(bio) if bio.present?
end
end
find_each / in_batches for bulk processing instead of .each on large sets.all without pagination on large tablespluck vs select trade-offs (pluck loads into memory immediately)limit on unbounded queriesupdate_all / delete_all without adequate where clausesexists? instead of present? or any? for existence checks (avoids loading records)# ❌ Bad: Loads all records into memory
User.all.each { |u| u.update(synced: true) }
# ✅ Good: Processes in batches
User.where(synced: false).find_each(batch_size: 500) do |user|
user.update(synced: true)
end
# ❌ Bad: Loads records just to check existence
if User.where(email: email).present?
# ✅ Good: SQL-level existence check
if User.where(email: email).exists?
# ❌ Bad: Unbounded delete
User.where(role: "guest").delete_all # Could delete millions
# ✅ Good: Scoped and controlled
User.where(role: "guest").where("created_at < ?", 1.year.ago).in_batches.delete_all
includes, preload, eager_load, and joins# ❌ RED FLAG: Accessing associations in loops
@posts.each do |post|
post.author.name # N+1 — queries author for EACH post
post.comments.count # N+1 — queries comments for EACH post
end
# ✅ includes — Rails picks preload or eager_load automatically
@posts = Post.includes(:author)
# ✅ preload — separate queries (better for large datasets)
Post.preload(:author, :comments)
# ✅ eager_load — LEFT OUTER JOIN (needed when filtering on association)
Post.eager_load(:author).where(authors: { active: true })
# ✅ joins — for filtering only, does NOT load association data
Post.joins(:author).where(authors: { country: "US" })
# Still need includes if you access the association after:
Post.joins(:author).includes(:author).where(authors: { country: "US" })
# ✅ Nested eager loading
Post.includes(comments: :author)
# ❌ Bad: Count query per post in a loop
@posts.each { |post| post.comments.count } # N+1
# ✅ Good: Counter cache — zero queries for count
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
end
# Migration
add_column :posts, :comments_count, :integer, default: 0
Post.find_each { |post| Post.reset_counters(post.id, :comments) }
# Now free:
@posts.each { |post| post.comments_count } # No query!
# Gemfile
gem "bullet", group: :development
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
end
includes, preload, joins) to avoid N+1Rails.cache.fetch with explicit expires_in for data caching# ✅ Good: Fragment caching with touch
# Model
class Comment < ApplicationRecord
belongs_to :post, touch: true
end
# View (HAML)
- cache @post do
= render @post.comments
# ✅ Good: Low-level caching
def expensive_stats
Rails.cache.fetch("user_stats:#{id}", expires_in: 1.hour) do
calculate_stats
end
end
app/jobs/ organized by domain# ❌ Bad: Passing full object — can be stale when executed
class ProcessPaymentJob < ApplicationJob
def perform(payment)
payment.process!
end
end
# ✅ Correct job pattern
class ProcessPaymentJob < ApplicationJob
queue_as :payments
sidekiq_options retry: 3
def perform(payment_id)
payment = Payment.find(payment_id)
PaymentProcessor.new(payment).call
rescue ActiveRecord::RecordNotFound
# Record deleted between enqueue and execution — safe to skip
Rails.logger.warn("Payment #{payment_id} not found, skipping")
end
end
/api/v1/)Api::V1::BaseController# ✅ Correct API controller
class Api::V1::BaseController < ApplicationController
respond_to :json
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
private
def not_found
render json: { error: "Resource not found" }, status: :not_found
end
end
Marshal.load on untrusted data (security risk — allows arbitrary code execution)YAML.safe_load instead of YAML.load (prevents object deserialization attacks)# ❌ CRITICAL: Remote code execution risk
data = Marshal.load(params[:data])
# ❌ CRITICAL: Arbitrary object instantiation
config = YAML.load(user_input)
# ✅ Good: Safe deserialization
config = YAML.safe_load(user_input, permitted_classes: [Date, Time])
# ✅ Good: Safe JSON parsing
begin
data = JSON.parse(raw_body)
rescue JSON::ParserError => e
render json: { error: "Invalid JSON" }, status: :bad_request
end
# Use Rack::Attack for rate limiting
class Rack::Attack
throttle("api/ip", limit: 300, period: 5.minutes) do |req|
req.ip if req.path.start_with?("/api/")
end
throttle("api/token", limit: 100, period: 1.minute) do |req|
req.env["HTTP_AUTHORIZATION"] if req.path.start_with?("/api/")
end
end
ApplicationMailertest/mailers/previews/)deliver_later)# ✅ Correct mailer pattern
class UserMailer < ApplicationMailer
def welcome(user)
@user = user
mail(
to: @user.email,
subject: I18n.t("mailers.user_mailer.welcome.subject")
)
end
end
# ❌ Bad: Permit all
params.require(:user).permit!
# ✅ Good: Explicit whitelist
params.require(:user).permit(:name, :email)
Look for these red flags during review:
# ❌ String interpolation in SQL
User.where("email = '#{params[:email]}'")
User.find_by_sql("SELECT * FROM users WHERE id = #{params[:id]}")
ActiveRecord::Base.connection.execute("DELETE FROM posts WHERE id = #{params[:id]}")
# ✅ Safe alternatives
User.where("email = ?", params[:email]) # Parameterized
User.where(email: params[:email]) # Hash conditions
User.where("email = :email", email: params[:email]) # Named placeholders
User.find_by_sql(["SELECT * FROM users WHERE email = ?", params[:email]])
# ❌ Dangerous
<%= params[:message].html_safe %>
<%= raw @comment.body %>
<script>var msg = "<%= @message %>";</script>
# ✅ Safe
<%= @user.bio %> # Rails auto-escapes
<%= sanitize @comment.body, tags: %w(strong em a) %> # Controlled allowlist
<script>var msg = <%= @message.to_json %>;</script> # JSON-escaped
# ✅ Use data attributes instead of inline JS
<div data-message="<%= @message %>"></div>
<script>
const message = document.querySelector("[data-message]").dataset.message;
</script>
# ❌ Red flags to catch
params.require(:user).permit! # Permits everything
User.create(params[:user]) # No strong params
current_user.attributes = params[:user] # Direct assignment
params[:product].each { |k, v| product.send("#{k}=", v) } # Dynamic assignment
# ✅ Conditional permissions for role-based access
def user_params
if current_user.admin?
params.require(:user).permit(:name, :email, :role, :status)
else
params.require(:user).permit(:name, :email)
end
end
protect_from_forgery enabled (default in Rails)# ❌ Bad: State change via GET
get "/users/:id/delete", to: "users#destroy"
# ✅ Good: Proper HTTP verb
delete "/users/:id", to: "users#destroy"
# ❌ Bad: Disabling CSRF without alternative auth
class ApiController < ApplicationController
skip_before_action :verify_authenticity_token # Only acceptable for token-authed APIs
end
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: "_app_session",
secure: Rails.env.production?, # HTTPS only in production
httponly: true, # Not accessible via JavaScript
same_site: :lax # CSRF protection
# Session fixation prevention
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
reset_session # Important: prevent session fixation
session[:user_id] = user.id
redirect_to root_path
end
end
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self, :https
policy.style_src :self, :https
end
# ❌ Bad: Trusts client-provided content type
if params[:file].content_type == "image/jpeg"
# Client can lie!
end
# ✅ Good: Validate extensions and content type, limit size
class AvatarUploader < CarrierWave::Uploader::Base
storage :fog # S3, not public/
def extension_whitelist
%w[jpg jpeg gif png]
end
def size_range
1..5.megabytes
end
end
data-testid attributes for stable selectorswebmock)shoulda-matchers for concise association/validation specs# ❌ Bad: Code change without specs
# PR contains only: app/services/create_user.rb
# ✅ Good: Code change with corresponding specs
# PR contains:
# app/services/create_user.rb
# spec/services/create_user_spec.rb
!important and global overridesMigrations must be backward-compatible with the currently running code. This is critical for zero-downtime deployments.
strong_migrations gem to automatically catch unsafe migration patternsalgorithm: :concurrently (Postgres)# ❌ Bad: Renaming a column in one step — breaks running code during deploy
class RenameUserNameToFullName < ActiveRecord::Migration[7.1]
def change
rename_column :users, :name, :full_name
end
end
# ✅ Good: Multi-step safe rename
# Step 1 (Deploy 1): Add new column
class AddFullNameToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :full_name, :string
end
end
# Step 2: Backfill data (rake task, not migration)
# Step 3 (Deploy 2): Update code to use full_name, write to both columns
# Step 4 (Deploy 3): Remove old column
class RemoveNameFromUsers < ActiveRecord::Migration[7.1]
def change
safety_assured { remove_column :users, :name }
end
end
If ActionCable is used:
connect method)# ✅ Correct ActionCable pattern
class ChatChannel < ApplicationCable::Channel
def subscribed
chat = Chat.find(params[:id])
reject unless current_user.can_access?(chat)
stream_for chat
end
def unsubscribed
stop_all_streams
end
end
Rails applications run in multi-threaded environments (Puma, Sidekiq). Thread-unsafe code can cause race conditions, data corruption, and intermittent bugs.
@variable at class level)@@variable) or class instance variables carefullythread_mattr_accessor or class_attribute for thread-safe class-level stateRequestStore or Current attributes for request-scoped data# ❌ CRITICAL: Not thread-safe - will cause race conditions
class UserService
@current_user = nil # Class instance variable - DANGEROUS
def self.process(user)
@current_user = user # Race condition! Multiple threads will overwrite this
# ... logic using @current_user
end
end
# ❌ CRITICAL: Not thread-safe - shared mutable state
class CacheManager
@@cache = {} # Class variable - shared across threads, not thread-safe
def self.store(key, value)
@@cache[key] = value # Race condition!
end
end
# ✅ Good: Use Rails thread-safe alternatives
class UserService
thread_mattr_accessor :current_user # Thread-safe storage
def self.process(user)
self.current_user = user
# ... logic
ensure
self.current_user = nil # Clean up
end
end
# ✅ Good: Use RequestStore for request-scoped data
class UserService
def self.process(user)
RequestStore.store[:current_user] = user
# ... logic
end
end
# ✅ Good: Use Rails Current attributes
class Current < ActiveSupport::CurrentAttributes
attribute :user, :request_id
end
class UserService
def self.process(user)
Current.user = user
# ... logic
end
end
# ✅ Good: Pass as parameter (best approach)
class UserService
def self.process(user)
new(user).call
end
def initialize(user)
@user = user # Instance variable - safe
end
def call
# ... logic using @user
end
end
# ❌ Bad: Race condition in memoization
def config
@config ||= load_config # Multiple threads can call load_config
end
# ✅ Good: Thread-safe memoization
def config
@config ||= Concurrent::LazyRegister.new { load_config }
end
# ✅ Good: Use Rails.cache for shared data
def config
Rails.cache.fetch("app_config", expires_in: 1.hour) do
load_config
end
end
# ❌ Bad: Shared array modified by multiple threads
class EventTracker
@@events = [] # Not thread-safe
def self.track(event)
@@events << event # Race condition!
end
end
# ✅ Good: Use thread-safe data structures
require "concurrent"
class EventTracker
@events = Concurrent::Array.new
def self.track(event)
@events << event # Thread-safe
end
end
# ✅ Better: Use proper logging/event system
class EventTracker
def self.track(event)
Rails.logger.info("Event: #{event}")
# or use proper event tracking service
end
end
# ❌ Bad: Not thread-safe initialization
class ApiClient
def self.instance
@instance ||= new # Race condition!
end
end
# ✅ Good: Use Rails' thread-safe class_attribute
class ApiClient
class_attribute :_instance
def self.instance
self._instance ||= new
end
end
# ✅ Better: Use proper singleton pattern
class ApiClient
include Singleton
def call
# ... API logic
end
end
# ❌ Bad: Modifying global/class state
class FeatureFlag
@@enabled_features = Set.new
def self.enable(feature)
@@enabled_features << feature # Not thread-safe
end
def self.enabled?(feature)
@@enabled_features.include?(feature)
end
end
# ✅ Good: Use database or Rails.cache
class FeatureFlag
def self.enable(feature)
Rails.cache.write("feature:#{feature}", true)
end
def self.enabled?(feature)
Rails.cache.read("feature:#{feature}") || false
end
end
@var at class level)@@var)thread_mattr_accessor or class_attribute for class-level storageCurrent attributes or RequestStore for request-scoped data# ✅ SAFE: Instance variables in instance methods
class UserService
def initialize(user)
@user = user # Safe - each instance has its own
end
end
# ✅ SAFE: Local variables
def process
user = User.find(params[:id]) # Safe - method scope
end
# ✅ SAFE: Constants
class Config
API_ENDPOINT = "https://api.example.com" # Safe - immutable
end
# ✅ SAFE: Database/cache for shared state
def config
Rails.cache.fetch("config") { load_config }
end
# ✅ SAFE: Thread-local storage
Thread.current[:user] = user
# ✅ SAFE: RequestStore (request-scoped)
RequestStore.store[:user] = user
# ✅ SAFE: Current attributes (request-scoped)
Current.user = user
## Code Review: [filename]
### Summary
Brief overview of code quality and main concerns.
### Metrics Summary
- Total issues: X
- Critical: X | Warnings: X | Suggestions: X
- Files reviewed: X
- Test coverage: X% (if available)
### Issues Found
#### 🔴 Critical
- **[Issue]**: Description
- Line: X
- Problem: Explanation
- Fix: Code suggestion
#### 🟡 Warnings
- **[Issue]**: Description
- Line: X
- Suggestion: How to improve
#### 🟢 Suggestions
- Minor improvements and style suggestions
### What's Good
- Highlight positive patterns found
### Dependencies Changed (if applicable)
- Added: [list]
- Updated: [list]
- Removed: [list]
- Security concerns: [list]
### Recommended Changes
Prioritized list of changes to make.
### Next Steps
1. [Prioritized action items]
2. ...
| Pattern | Status | Alternative |
|---|---|---|
| Code without specs | 🔴 | Add corresponding specs |
| Single quotes for strings | 🔴 | Use double quotes |
Time.now |
🟡 | Use Time.current |
| ERB templates | 🟡 | Prefer HAML |
| Business logic in controller | 🔴 | Use service object |
| Business logic in model | 🔴 | Use service object |
| View logic in controller | 🟡 | Use presenter |
| Callbacks with side effects | 🔴 | Explicit orchestration |
| Multiple instance variables | 🟡 | One per action |
unless with else |
🔴 | Use if |
| N+1 queries | 🟡 | Use includes / preload / eager_load |
| Missing indexes | 🔴 | Add index |
| Fat controller | 🟡 | Extract to service |
| Inline JS/complex frameworks | 🟡 | Use Hotwire |
| Swallowed exceptions | 🔴 | Handle explicitly |
permit! |
🔴 | Explicit whitelist |
| SQL string interpolation | 🔴 | Use parameterized queries |
html_safe / raw in views |
🔴 | Use content_tag / sanitize |
| Production credentials in code | 🔴 | Use env vars / credentials |
| Large data changes in migration | 🟡 | Move to rake task |
| Missing foreign keys | 🟡 | Add foreign key constraints |
| Unused columns | 🟡 | Remove or document |
| Personal preference changes | 🟡 | Create Rubocop issue instead |
| Hardcoded strings in views | 🟡 | Use I18n keys |
| I18n key missing in any locale | 🔴 | Add to all locale files |
| Typos in translation values | 🟡 | Spell-check locale files |
| God objects (>200 lines) | 🔴 | Split responsibilities |
| Methods >15 lines | 🟡 | Extract smaller methods |
| Logging sensitive data | 🔴 | Filter or exclude PII |
| Missing email plain text template | 🟡 | Add text version |
| Enums as integers in API | 🟡 | Serialize as strings |
| No audit trail for sensitive ops | 🟡 | Add logging/tracking |
| Deprecated Rails methods | 🔴 | Update to current API |
| Primitive obsession | 🟡 | Use value objects |
| Hardcoded URLs/domains | 🟡 | Use config/ENV vars |
| Environment-specific code outside config | 🔴 | Move to config/environments/ |
| Insecure gem versions | 🔴 | Update and run bundle audit |
Class-level instance variables (@var at class level) |
🔴 | Use thread_mattr_accessor, Current, or pass as parameter |
Unsynchronized class variables (@@var) |
🔴 | Use thread-safe alternatives or database |
| Shared mutable state | 🔴 | Use RequestStore, Current, or Rails.cache |
| Unsafe memoization in class methods | 🔴 | Use thread-safe memoization or cache |
| Global state modification | 🔴 | Use database or proper state management |
| Enum with array syntax | 🔴 | Use explicit hash syntax |
.each on large dataset |
🟡 | Use find_each / in_batches |
.present? for existence check |
🟡 | Use .exists? |
Marshal.load on untrusted data |
🔴 | Never — remote code execution risk |
YAML.load on user input |
🔴 | Use YAML.safe_load |
| Column rename in single migration | 🔴 | Multi-step safe rename |
| Full objects passed to jobs | 🟡 | Pass IDs (primitives) only |
| Junk drawer concerns | 🟡 | Split into focused concerns |
| Deeply nested routes (>1 level) | 🟡 | Flatten with shallow: or new controllers |
| TODOs/FIXMEs without linked issue | 🟡 | Link to ticket or remove |
| Commented-out dead code | 🟡 | Remove — use version control |
Unbounded delete_all / update_all |
🔴 | Add proper where scope |
Missing strong_migrations gem |
🟢 | Add for migration safety checks |
| Train wreck (chained dots) | 🟡 | Use delegate or wrapper methods (Law of Demeter) |
Long case/if-elsif by type |
🟡 | Use polymorphism (Open/Closed) |
| Type checking before method call | 🟡 | Fix abstraction (Liskov Substitution) |
| Hard-coded dependency instantiation | 🟡 | Inject dependencies (Dependency Inversion) |
| No-op methods to satisfy interface | 🟡 | Segregate interfaces (ISP) |
| Class with multiple responsibilities | 🔴 | Extract classes (Single Responsibility) |
| State-changing action via GET | 🔴 | Use POST/PUT/DELETE |
| Missing CSRF protection | 🔴 | Enable protect_from_forgery |
| No session fixation prevention | 🔴 | Call reset_session on login |
| File upload without type validation | 🔴 | Validate extensions and content |
| Missing API rate limiting | 🟡 | Add Rack::Attack |
| Missing Content Security Policy | 🟡 | Configure CSP headers |
| Client-side-only authorization | 🔴 | Add server-side checks |
| Missing counter cache for counts in loops | 🟡 | Add counter_cache: true |
| Missing Bullet gem in development | 🟢 | Add for N+1 detection |
Direct params[:model] without strong params |
🔴 | Use params.require().permit() |
Dynamic attribute assignment with send |
🔴 | Use strong parameters |
>4 method parameters |
🟡 | Introduce parameter object |
User input in <script> tags |
🔴 | Use to_json or data attributes |
| Trusting client content type for uploads | 🔴 | Validate actual file content |
| Leaking stack traces in error responses | 🟡 | Return generic error messages |