Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,62 @@ REQUEST_ENV_MERGE:
- "request.env.merge!('HTTP_AUTHORIZATION' => token)"
no_match:
- "request.headers.merge!('HTTP_AUTHORIZATION' => token)"

DUPLICATE_ROOT_ROUTES:
# Pattern: "^\\s*root\\s+(to:|=>|['\"])"
# Rails 4 rejects a second `root`; flag every root declaration so a human can
# count them (a lone hit is noise, 2+ is the breaking case).
match:
# hash form with to:
- " root to: \"home#index\""
# legacy hash-rocket form
- " root => \"home#index\""
# Rails 4 double-quoted string shorthand
- " root \"home#index\""
# Rails 4 single-quoted string shorthand
- " root 'home#index'"
no_match:
# a root_path helper call is not a declaration
- " redirect_to root_path"
# a non-root route must not flag
- " get \"/\", to: \"home#index\""
# a commented-out root declaration is not live (line-anchored pattern)
- " # root to: \"home#index\""

ROUTES_CONTROLLER_NAMES:
# Pattern: "\\bto:\\s*['\"][^'\"]*(-|[A-Z])[^'\"]*#"
# Rails 4 rejects hyphen/CamelCase controller names in to:; both quote styles.
match:
# double-quoted hyphenated controller
- " get \"widgets\", to: \"my-pages#show\""
# double-quoted CamelCase controller
- " get \"widgets\", to: \"MyPages#show\""
# single-quoted hyphenated controller
- " get \"widgets\", to: 'my-pages#show'"
# single-quoted CamelCase controller
- " get \"widgets\", to: 'MyPages#show'"
# a trailing comment on a live route line still flags
- " get \"widgets\", to: \"MyPages#show\" # legacy alias"
no_match:
# hyphen only in the URL path, controller is fine
- " get \"my-widgets\", to: \"pages#show\""
# already snake_case controller
- " get \"widgets\", to: \"my_pages#show\""
# a commented-out route must not flag (excluded via ^\\s*#)
- " # get \"widgets\", to: \"MyPages#show\""

ROUTES_TOP_LEVEL_MODULE_AUTOLOAD:
# Pattern: "^\\s*module\\s+[A-Z]\\w*"
# Top-level module definitions inside config/routes/ break on reload.
match:
# the canonical constraints module definition
- " module Constraints"
# any single-segment module definition
- "module Api"
# compact-nested definition must also flag
- " module Constraints::Admin"
no_match:
# scope module: option is not a module definition
- " scope module: \"admin\" do"
# using a constant is not defining a module
- " constraints Constraints::Admin do"
30 changes: 30 additions & 0 deletions rails-upgrade/detection-scripts/patterns/rails-40-patterns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,36 @@ upgrade_findings:
fix: "Delete the config.whiny_nils = true line entirely. No replacement — Ruby's own nil-method errors are sufficient. False-positive watch: matches in comments documenting the removal; verify the hit is an actual assignment, not a comment"
variable_name: "WHINY_NILS"

- name: "Duplicate root route declarations across constraints"
kind: "breaking"
pattern: "^\\s*root\\s+(to:|=>|['\"])"
exclude: ""
search_paths:
- "config/"
explanation: "Only actionable when the routes file has 2+ `root` declarations (typically one per constraint block); a single root is fine, so treat a lone hit as noise. Rails 4 raises `ArgumentError: Invalid route name, already in use: 'root'` on the second and later `root` declarations. Rails 3.2 was lenient about duplicate names with different constraints. Matches all three forms: `root to:`, `root =>`, and the Rails 4 string shorthand `root \"home#index\"`"
fix: "Keep the FIRST `root` declaration as the default `:root` name; add `as: :<unique_name>` to the SECOND and subsequent declarations. Renaming all of them (giving the first an `as:` too) silently breaks every `root_path` / `root_url` call site because the `:root` name is no longer registered. Real apps have dozens to hundreds of these helper calls. Verification step: `grep -rnE \"\\b(root_path|root_url)\\b\" app/ config/ lib/` and confirm no caller targets a constraint-bound root that was renamed"
variable_name: "DUPLICATE_ROOT_ROUTES"

- name: "Hyphenated or CamelCase controller name in routes to: value"
kind: "breaking"
pattern: "\\bto:\\s*['\"][^'\"]*(-|[A-Z])[^'\"]*#"
exclude: "^\\s*#"
search_paths:
- "config/"
explanation: "Rails 4 validates controller names in `to:` clauses with strict rules: no hyphens, no CamelCase. Hyphenated values raise `ArgumentError: 'foo-bar' is not a supported controller name`; the CamelCase form raises the same error. Rails 3.2 was lenient. URL paths (the route path itself) can keep hyphens — only the controller class reference in `to:` needs to be snake_case. Detects both single- and double-quoted `to:` values; a hyphen in the URL path (before `to:`) does not trigger it. Unlike the root/module patterns this one is not line-anchored (a `to:` value sits mid-line), so the `exclude` skips whole-line comments (`^\\s*#`) to avoid flagging commented-out routes; a trailing comment on a live route line still flags"
fix: "Replace hyphens with underscores and CamelCase with snake_case in `to:` values. URL paths keep their original form. The snake_case form resolves to the same controller class on Rails 3.2, so the rewrite ships pre-bump and is backwards-compatible. Example: `to: \"my-pages#show\"` → `to: \"my_pages#show\"`; `to: \"MyPages#show\"` → `to: \"my_pages#show\"`"
variable_name: "ROUTES_CONTROLLER_NAMES"

- name: "Top-level module defined in a route file (autoload-on-reload boundary)"
kind: "breaking"
pattern: "^\\s*module\\s+[A-Z]\\w*"
exclude: ""
search_paths:
- "config/routes/"
explanation: "Rails 4's route loader does not reliably persist top-level module definitions across the routes-reload boundary. A pattern like `module Constraints; class Admin; ...; end; end` defined inside a `config/routes/*.rb` file may evaluate at boot but the `Constraints` constant does not survive subsequent reloads — downstream uses (e.g. `constraints Constraints::Admin do ... end`) raise `NameError: uninitialized constant Constraints`. Rails 3.2 was more lenient — modules defined in route files persisted across reload boundaries. Matches module definitions including the compact-nested `module Constraints::Admin` form; option lines like `scope module: \"admin\"` are not definitions and do not match"
fix: "Extract the inline module out of the route file into a conventional autoload path so the constant is loaded the same way on both Rails versions. This fix is version-agnostic — no `NextRails.next?` branch is needed, because an autoloaded constant survives the routes-reload boundary on 4.0 and loads identically on 3.2. Steps: (1) Move the definition into an autoload location that mirrors the constant name, e.g. `Constraints::Admin` → `app/constraints/constraints/admin.rb`. Every directory under `app/` is on the autoload path by default on both 3.2 and 4.0, so no config change is needed; only if you place the file under `lib/` do you add `config.autoload_paths += [Rails.root.join(\"lib\")]` in `config/application.rb`. (2) Delete the `module ... end` wrapper from the route file, leaving only the usage. BEFORE (config/routes/admin.rb): `module Constraints; class Admin; def matches?(req); ...; end; end; end` followed by the `Rails.application.routes.draw` block that calls `constraints Constraints::Admin.new do ... end`. AFTER: the `module`/`class` block lives in `app/constraints/constraints/admin.rb`; the route file keeps only the `constraints Constraints::Admin.new do ... end` usage and no longer defines the constant"
variable_name: "ROUTES_TOP_LEVEL_MODULE_AUTOLOAD"

medium_priority:
- name: "rescue_action method"
kind: "breaking"
Expand Down
Loading