0.0
No release in over 3 years
Enforces the verbose, beginner-friendly Ruby style taught in firstdraft classroom materials. Ships three inheritable configurations (shared, student, author) plus six custom cops that keep polished Rails idioms out of lesson code.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

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/**/*.rb and config/routes.rb.
  • StandardRB as the base (so Lint cops like UselessAssignment, Void, DuplicateHashKey are 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[...], no def foo; end one-liners.
  • Layout/LineLength disabled — 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_name and :foreign_key.
  • has_many :through and has_one :through — must pass :class_name, :source, and :through.
  • has_and_belongs_to_many — banned outright; use has_many :through instead.
# 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.