class

Analyzer::Ruby::Rails

Inherits Analyzer::Ruby::RubyEngine < Analyzer < FileHelper < Reference < Object

Constants

DEVISE_ROUTES = [{"GET", "/sign_in"}, {"POST", "/sign_in"}, {"DELETE", "/sign_out"}, {"POST", ""}, {"GET", "/sign_up"}, {"GET", "/edit"}, {"PUT", ""}, {"PATCH", ""}, {"DELETE", ""}, {"GET", "/password/new"}, {"POST", "/password"}, {"GET", "/password/edit"}, {"PUT", "/password"}, {"PATCH", "/password"}, {"GET", "/confirmation/new"}, {"POST", "/confirmation"}, {"GET", "/confirmation"}, {"GET", "/unlock/new"}, {"POST", "/unlock"}, {"GET", "/unlock"}]

Devise routes generated by devise_for :scope. Tuples are {METHOD, sub_path} appended to the scope's base path.

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.

INLINE_ROUTE_BLOCK = /^(member|collection|new)\s*\{\s*(.+?)\s*\}\s*$/

Rails commonly writes compact inline route scopes: collection { get "preview" } / member { post :use }. The parser is stack-based, so normalize these to the same do/end structure before route extraction.

LOOP_OPENER = /^%([wi])[\[\(\{]([^\]\)\}]*)[\]\)\}]\s*\.each(?:_with_index)?\s+do\s*\|\s*([a-z_]\w*)[^|]*\|\s*$/

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).

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).

MAX_LOOP_OUTPUT = 20000
MAX_LOOP_VALUES = 100
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|['\"][^'\"]+['\"]|:[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.

Class methods

tech_name
Source

Instance methods

analyze
Source
controller_to_endpoint(path : String, url : String, resource : String, path_prefix : String = "", only : Array(String) = [] of String, except : Array(String) = [] of String, singular : Bool = false)
Source
tech

Instance-side view of the same declaration. The per-file rescues live on this base class, which has no way to name the analyzer that is running inside them, so a skipped file could not be attributed to a tech. Deriving it from analyzer_for keeps the name written exactly once.

Source

Nested types