rubocop-appdev
RuboCop configuration and custom cops for firstdraft classroom codebases. Keeps starter and solution code written in the verbose, explicit style taught in our courses — hash rockets, explicit parens, string param keys, where(...).first over find, and so on — so students never encounter polished Rails idioms they haven't been taught yet.
Built on top of StandardRB and the standard-performance / standard-rails extensions.
Installation
# Gemfile
group :development, :test do
gem "rubocop-appdev"
end
Then pick one of the three configurations in your project's .rubocop.yml:
# For starter/solution codebases authored by instructors
inherit_gem:
rubocop-appdev: config/author.yml
# For student project templates
inherit_gem:
rubocop-appdev: config/student.yml
The three configurations
| Config | Intended audience | Hash syntax | Parens on method calls | Custom Appdev cops |
|---|---|---|---|---|
config/shared.yml |
Don't inherit directly — used by the two below. | — | — | Defined (but governed by the specific config). |
config/student.yml |
Student homework. Permissive. | Any style allowed. | Any style allowed. | Disabled. |
config/author.yml |
Starter / solution codebases authored by instructors. Strict. | Hash rockets required ({ :a => 1 }). |
Required on every method call, including Rails macros (belongs_to(:user, ...)). |
All enabled. |
Both student and author configs share:
- Linting scoped to
app/**/*.rbandconfig/routes.rb. - StandardRB as the base (so Lint cops like
UselessAssignment,Void,DuplicateHashKeyare on). - The "make it more idiomatic" cluster is disabled (
GuardClause,IfUnlessModifier,NumericPredicate,SafeNavigation,SymbolProc,RedundantReturn,RedundantSelf,ConditionalAssignment,RedundantBegin,Next). These would push code toward syntax students haven't learned. - Force-verbose overrides: no
%i[...], no%w[...], nodef foo; endone-liners. Layout/LineLengthdisabled — authors write descriptive messages.
Custom cops (author config only)
All namespaced under Appdev/ and enabled by default in author.yml.
Appdev/PreferWhereOverFind
Forbids Model.find, Model.find_by, Model.find_by!. Autocorrects to .where(...).first! / .first — behavior-preserving across the rewrite since find and .first! both raise ActiveRecord::RecordNotFound.
# bad
Movie.find(id)
Movie.find_by(:title => "x")
Movie.find_by!(:title => "x")
# good
Movie.where({ :id => id }).first!
Movie.where({ :title => "x" }).first
Movie.where({ :title => "x" }).first!
Appdev/NoResourcefulRoutes
Forbids resources and resource in config/routes.rb. Students write explicit verb-mapped routes. root, namespace, scope, mount, match, and the HTTP verb methods (get, post, put, patch, delete) are all allowed.
# bad
resources :movies
# good
get("/movies", { :controller => "movies", :action => "index" })
post("/insert_movie", { :controller => "movies", :action => "create" })
Appdev/NoMassAssignment
Forbids mass-assignment ActiveRecord APIs so authors demonstrate attribute-by-attribute assignment. Scoped to app/controllers/**/*.rb and app/models/**/*.rb.
Uses a receiver-shape heuristic to dodge false positives: class-method rules (.new / .create / .create!) only fire when the receiver is a constant not in the NonARConstants allowlist; instance-method rules (.update / .update! / .assign_attributes / .attributes=) fire on instance variables and locals whose names don't hint at a hash (hash, map, dict, params).
# bad
Movie.new(:title => "x")
@movie.update({ :title => "x" })
@movie.assign_attributes(h)
# good
the_movie = Movie.new
the_movie.title = params.fetch("query_title")
the_movie.save
Appdev/StringParamKeys
Requires params.fetch("string_key"). Flags three variants and autocorrects to the blessed form:
# bad
params[:path_id] # -> params.fetch("path_id")
params["path_id"] # -> params.fetch("path_id")
params.fetch(:path_id) # -> params.fetch("path_id")
# good
params.fetch("path_id")
Reinforces two pedagogical points at once: string keys (so students see the underlying hash) and .fetch (so missing keys raise instead of silently returning nil).
Appdev/ExplicitRenderAndRedirect
Requires the hash form for render, a string literal for redirect_to, and a string fallback_location for redirect_back.
# bad
render :new
render "movies/index"
redirect_to @movie
redirect_to places_url
redirect_back(:fallback_location => request.referer)
# good
render({ :template => "movies/index" })
render({ :partial => "form" })
redirect_to("/movies")
redirect_to("/movies/#{movie.id}", { :notice => "Created." })
redirect_back(:fallback_location => "/movies")
Appdev/ExplicitAssociationOptions
Requires explicit options on ActiveRecord associations so students see the column and class being wired up, rather than relying on Rails' pluralization magic.
belongs_to,has_one,has_many(without:through) — must pass:class_nameand:foreign_key.has_many :throughandhas_one :through— must pass:class_name,:source, and:through.has_and_belongs_to_many— banned outright; usehas_many :throughinstead.
# bad
belongs_to :user
has_many :reviews
has_and_belongs_to_many :tags
# good
belongs_to(:user, { :class_name => "User", :foreign_key => "user_id" })
has_many(:reviews, { :class_name => "Review", :foreign_key => "place_id" })
has_many(:reviewers, { :through => :reviews, :source => :reviewer, :class_name => "User" })
Development
bundle install
bundle exec rspec
Specs use RuboCop's expect_offense helper — one file per cop under spec/rubocop/cop/appdev/.
License
MIT. See LICENSE.txt.