Constants
APP_CONFIG_REF = /\b(?:Rails\.configuration|Rails\.application\.config|[A-Z][A-Za-z0-9_]*::Application\.config)\.([a-z_][a-z0-9_]*)\b(?![\w(\[.=])/
BARE_VAR_ARG = /\A((?:scope|namespace|get|post|put|patch|delete|options|head|match|resources?)\s*\(?\s*)([a-z_][a-z0-9_]*)(?=\s*(?:,|=>|\)|\bdo\b|\z))/
CALLBACK_PARAM_TAG =
Tag.new("rails_callback_param", "", "rails")
CONFIG_ASSIGN = /^\s*config\.([a-z_][a-z0-9_]*)\s*(=(?![=~>])|\|\|=|&&=|\+=|<<)(.*)$/
CONFIG_LITERAL = /\A\s*["']([^"'#]*)["']\s*(?:#.*)?\z/
CONTAINER_USE = /\
A\s*&?\.\s*(?:(?:is_a\?|kind_of\?|instance_of\?)\s*\(?\s*(?:
Hash|
ActionController::Parameters|
Array)\b|key\?|has_key\?|keys\b|each\b|to_unsafe_h\b|to_h\b|slice\b|except\b)/
A read that treats the value as a hash rather than a field.
is_a?(String) asks whether the value is a scalar, which is a field.
DEVISE_ACTION_PARAMS = { {"sessions", "create"} => {"sign_in_params", ["email", "password"] of ::
String}, {"registrations", "create"} => {"sign_up_params", ["email", "password", "password_confirmation"] of ::
String}, {"registrations", "update"} => {"account_update_params", ["email", "password", "password_confirmation", "current_password"] of ::
String}, {"passwords", "create"} => {"resource_params", ["email"] of ::
String}, {"passwords", "update"} => {"resource_params", ["reset_password_token", "password", "password_confirmation"] of ::
String}, {"confirmations", "create"} => {"resource_params", ["email"] of ::
String}, {"unlocks", "create"} => {"resource_params", ["email"] of ::
String}}
Parameters Devise's built-in controllers read per {group, action},
paired with the helper method that sanitizes them (user[...] nesting
dropped, matching permit extraction). Attached when the action is
handled by Devise itself or by a subclass that does not redefine that
helper.
DEVISE_DEFAULT_MODULES = ["database_authenticatable", "registerable", "recoverable", "confirmable", "lockable"] of ::
StringModules assumed when app/models/<scope>.rb is not in the scan.
:omniauthable is opt-in and only emits routes when declared on the model.
DEVISE_MODULE_ROUTES = {"database_authenticatable" => DEVISE_SESSION_ROUTES, "two_factor_authenticatable" => DEVISE_SESSION_ROUTES, "registerable" => [{"GET", "%{registration}/%{cancel}", "registrations", "cancel"}, {"GET", "%{registration}/%{sign_up}", "registrations", "new"}, {"GET", "%{registration}/%{edit}", "registrations", "edit"}, {"PATCH", "%{registration}", "registrations", "update"}, {"PUT", "%{registration}", "registrations", "update"}, {"DELETE", "%{registration}", "registrations", "destroy"}, {"POST", "%{registration}", "registrations", "create"}], "recoverable" => [{"GET", "%{password}/new", "passwords", "new"}, {"POST", "%{password}", "passwords", "create"}, {"GET", "%{password}/edit", "passwords", "edit"}, {"PATCH", "%{password}", "passwords", "update"}, {"PUT", "%{password}", "passwords", "update"}], "confirmable" => [{"GET", "%{confirmation}/new", "confirmations", "new"}, {"POST", "%{confirmation}", "confirmations", "create"}, {"GET", "%{confirmation}", "confirmations", "show"}], "lockable" => [{"GET", "%{unlock}/new", "unlocks", "new"}, {"POST", "%{unlock}", "unlocks", "create"}, {"GET", "%{unlock}", "unlocks", "show"}], "omniauthable" => [{"GET", "auth/:provider", "omniauth_callbacks", "passthru"}, {"POST", "auth/:provider", "omniauth_callbacks", "passthru"}, {"GET", "auth/:provider/callback", "omniauth_callbacks", ":provider"}, {"POST", "auth/:provider/callback", "omniauth_callbacks", ":provider"}]}
DEVISE_PATH_NAMES = {"sign_in" => "sign_in", "sign_out" => "sign_out", "sign_up" => "sign_up", "password" => "password", "confirmation" => "confirmation", "unlock" => "unlock", "registration" => "", "edit" => "edit", "cancel" => "cancel"}
Devise's Mapping#default_path_names: every key names itself except
registration, which is empty (/users/sign_up, not /users/registration/sign_up).
DEVISE_SESSION_ROUTES = [{"GET", "%{sign_in}", "sessions", "new"}, {"POST", "%{sign_in}", "sessions", "create"}, {"DELETE", "%{sign_out}", "sessions", "destroy"}]
Routes devise_for :scope generates, keyed by the model module that
turns them on (devise :database_authenticatable, :registerable, ...
in app/models/<scope>.rb). Each tuple is {METHOD, sub_path template,
controller group, action}: the template's %{key} slots are the
path_names: keys Devise lets an app rename (sign_in: :login), and
the group is the controllers: key that swaps the implementing
controller (sessions: :sessions).
DIR_LOCAL_ASSIGN = /\
A([a-z_]\w*)\s*=\s*(?:__dir__|
File\.(?:expand_path|dirname)\(\s*(?:__dir__|__
FILE__)\s*\))\s*\z/
DOORKEEPER_ROUTES = [{"GET", "/oauth/authorize", "authorizations", "new"}, {"POST", "/oauth/authorize", "authorizations", "create"}, {"DELETE", "/oauth/authorize", "authorizations", "destroy"}, {"POST", "/oauth/token", "tokens", "create"}, {"POST", "/oauth/revoke", "tokens", "revoke"}, {"POST", "/oauth/introspect", "tokens", "introspect"}, {"GET", "/oauth/token/info", "token_info", "show"}, {"GET", "/oauth/applications", "applications", "index"}, {"POST", "/oauth/applications", "applications", "create"}, {"GET", "/oauth/applications/new", "applications", "new"}, {"GET", "/oauth/applications/:id", "applications", "show"}, {"GET", "/oauth/applications/:id/edit", "applications", "edit"}, {"PATCH", "/oauth/applications/:id", "applications", "update"}, {"PUT", "/oauth/applications/:id", "applications", "update"}, {"DELETE", "/oauth/applications/:id", "applications", "destroy"}, {"GET", "/oauth/authorized_applications", "authorized_applications", "index"}, {"DELETE", "/oauth/authorized_applications/:id", "authorized_applications", "destroy"}]
Routes generated by Doorkeeper's use_doorkeeper (the de-facto
OAuth2 provider for Rails — Mastodon, GitLab, Discourse plugins,
…). Each tuple is {METHOD, sub_path, group, action}. group
matches the skip_controllers symbols so a use_doorkeeper do skip_controllers :applications end block drops the right rows,
and indexes the controllers foo: 'bar' remap used to resolve
the implementing controller for params/callees. Paths are
appended to /oauth under the scope where use_doorkeeper runs.
HELPER_TOKEN = /(?:(?<=self\.)|(?<![:\w@.]))([A-Za-z_]\w*[!?=]?)(?:\s*\[\s*:(\w+)\s*\])?/
A call to a method of this controller: a bare or self. identifier,
never @ivar (@note is not the note callback) or a method on
another receiver (@contact.notes). A trailing [:key] is captured:
profile_params[:account_id] reads one field of what the helper
permits.
INLINE_DEVISE_SCOPE = /^devise_scope\b\s*\(?\s*(?::\w+|['"][^'"]+['"])\s*\)?\s*(?:\bdo\b\s*(.+?)\s*\bend|\{\s*(.+?)\s*\})\s*$/
INLINE_ROUTE_BLOCK = /^(member|collection|new)\s*\{\s*(.+?)\s*\}\s*$/
Rails commonly writes compact inline route scopes:
collection { get "preview" } / member { post :use } /
devise_scope :user do get "login", to: "devise/sessions#new" end.
The parser is stack-based, so normalize these to the same do/end
structure before route extraction.
LITERAL_ASSIGN = /^([a-z_][a-z0-9_]*)\s*=\s*["']([^"']+)["']\s*$/
LITERAL_CONCAT = /(["'])([^"'#]*)\1\s*\+\s*\1/
LOCAL_WRITE = /^([a-z_][a-z0-9_]*)\s*(?:=(?![=~>])|\|\|=|&&=|\+=|<<)/
LOOP_ONE_LINER = /^(#{LOOP_RECEIVER})\s*\.each(?:_with_index)?\s*\{\s*\|\s*([a-z_]\w*)[^|]*\|\s*(.+?)\s*\}\s*$/
LOOP_OPENER = /^(?:%[wi][\[\(\{]([^\]\)\}]*)[\]\)\}]|((?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\.[a-z_]\w*))\s*\.each(?:_with_index)?\s+do\s*\|\s*([a-z_]\w*)[^|]*\|\s*$/
LOOP_RECEIVER = /(?:%[wi][\[\(\{][^\]\)\}]*[\]\)\}]|(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\.[a-z_]\w*)/
A %w[...].each do |var| / %i[...].each_with_index do |var, i|
block over a STATIC literal list unrolls to one copy of its body per
element with the loop variable's #{var} interpolations substituted.
discourse alone wraps ~46 routes in %w[users u].each — without this,
get "#{root_path}/..." leaks {root_path} into ~130 endpoints (a
fabricated path param) AND the real /users + /u variants are lost.
Only literal %w/%i receivers are expanded: a dynamic
Model.scopes.each do |s| cannot be resolved to values at parse time,
so it is left for the normal parser (its block opens a neutral frame).
A Const.list receiver (Discourse.filters.each do |filter|) unrolls
too when the app defines it as a literal list (see constant_list).
The brace form RECV.each { |x| get "#{x}" } is the same block on one
line.
MAX_LOOP_DEPTH = 3
noir scans arbitrary untrusted repos, so loop unrolling must not be a DoS
vector — expansion is multiplicative (elements ^ nesting). A crafted
routes.rb with deep nesting or a huge %w[...] list could otherwise
blow up CPU/memory. Real apps use tiny shallow lists (discourse's biggest
is the 2-element %w[users u]), so modest caps preserve all genuine
recall while bounding the worst case. Over a limit, the block is passed
through unexpanded (the pre-unrolling behavior — a transparent frame).
NON_PRODUCTION_BLOCK = /\A#{NON_PRODUCTION_COND.source}/
NON_PRODUCTION_COND = /(?:if\s+(?:#{NON_PRODUCTION_ENV.source})|unless\s+Rails\.env\.production\?)\s*\z/
NON_PRODUCTION_ENV = /Rails\.env\.(?:test|development|local)\?(?:(?:\s*(?:\|\||\bor\b)\s*Rails\.env\.(?:test|development|local)\?)*|\s*(?:&&|\band\b)(?:(?!\|\||\bor\b).)*)|!\s*Rails\.env\.production\?/
A condition only a non-production Rails env satisfies: Rails.env.test?
(development?, local?, an || of them, or one && anything),
!Rails.env.production?, or unless Rails.env.production?.
NON_PRODUCTION_MODIFIER = /\s#{NON_PRODUCTION_COND.source}/
PARSE_OPTIONS_HASHROCKET_RE =
Regex.new(":(\\w+)\\s*=>\\s*(#{
PARSE_OPTIONS_VALUE_PATTERN})")
PARSE_OPTIONS_KEY_VALUE_RE =
Regex.new("(\\w+):\\s*(#{
PARSE_OPTIONS_VALUE_PATTERN})")
PARSE_OPTIONS_VALUE_PATTERN = "nil|true\\b|false\\b|['\"][^'\"]*['\"]|:[a-zA-Z_]\\w*|\\[[^\\]]*\\]|%i[\\[\\(][^\\]\\)]*[\\]\\)]|%w[\\[\\(][^\\]\\)]*[\\]\\)]"
parse_options runs on most DSL lines of every routes.rb file (scope,
namespace, resources, member/collection actions, verb routes, match) —
i.e. once per line in the main route-parsing hot loop. It used to build
two Regex.new("...#{value_pattern}...") regexes from scratch on every
call even though value_pattern is a fixed literal that never varies —
Crystal doesn't cache/memoize a Regex.new(String) call the way it
embeds a non-interpolated regex literal at compile time. Precompile
both patterns once at class load instead.
PATH_OPTION_VAR = /(\bpath:\s*|:path\s*=>\s*)([a-z_][a-z0-9_]*)\b(?!\s*[(.\[+:])/
PERMIT_OPEN = /(?<![\w.])params\.(?:expect\s*\(|(?:(?:require|fetch)\s*\([^)]*\)\s*\.)?permit\s*\()|(?<![\w.])permit\s*\(/
params.permit(, params.require(...).permit(, params.fetch(...).permit(,
or Rails 8's params.expect( (anchored to bare params).
PLAIN_LOCAL = /\A[a-z_]\w*\z/
PUNDIT_PERMITTED = /(?<![\w.])permitted_attributes\s*\(\s*(@\w+|[A-Z][\w:]*)\s*[,)]/
Pundit's permitted_attributes(@comment) / permitted_attributes(Comment).
RESOURCE_ACTION_VERBS = { {"index", "GET"}, {"show", "GET"}, {"create", "POST"}, {"update", "PUT"}, {"update", "PATCH"}, {"destroy", "DELETE"}, {"new", "GET"}, {"edit", "GET"} }
The RESTful actions resources routes, in the order their rows are
emitted; update answers both PUT and PATCH. new and edit serve
the forms at /posts/new and /posts/:id/edit.
ROUTE_FILE_LOAD = /(?:\
A|[\s{;|])(?:load|require)\b\s*\(?\s*[a-z_]\w*\s*\)?\s*(?:\}|;|\z)|instance_eval\s*\(?\s*
File\.read\b/
ROUTES_GLOB = /\b
Dir(?:\.glob\b|\s*\[).*?routes["']?\s*(?:,\s*["']|\/)(\*\*\/|["']\*\*["']\s*,\s*["'])?\*\.rb/
Dir.glob("#{__dir__}/routes/*.rb").each { |f| load f } (zammad) or
Dir[Rails.root.join("config/routes/**/*.rb")]: a routes file that
globs config/routes/ to load every file in it. Each file usually opens
its own routes.draw; parse them in glob (sorted) order.
ROUTES_GLOB_DIR = /\#\{\s*(__dir__|
File\.(?:expand_path|dirname)\(\s*(?:__dir__|__
FILE__)\s*\)|[a-z_]\w*)\s*\}\/routes\//
ROUTES_GLOB_ROOTED = /config\/routes\/|config["']\s*,\s*["']routes["']|config\/routes["']/
The glob must name config/routes: a literal config/routes path or,
from config/routes.rb itself, #{__dir__}/routes (or a local holding
__dir__).