class

Noir::RailsCallbackChain

Inherits Reference < Object

The before_action chain a Rails action runs. A controller's own class body is only half of it: before_action :authenticate_user! usually sits in ApplicationController, and rails generate authentication (Rails 8) installs before_action :require_authentication through an Authentication concern ApplicationController includes. Shared by the Rails analyzer (header/cookie reads in callbacks) and the auth tagger (whether a callback guards the action).

Constants

BLOCK_CALLBACK = /(?<!skip_)before_action\s*\(?\s*(?:->\s*(?:\([^)]*\))?|lambda\s*|proc\s*)?\{(?:\s*\|[^|]*\|)?([^{}]*)\}/

before_action -> { doorkeeper_authorize! :read } (also ->(c) {, lambda {, proc {, a bare { ... } block): the block body. Only LineReader text holds one; Reader writes a block callback as the one method it runs (see Reader#block_callback).

CALLBACK_LINE = /^(?:include|(?:prepend_|skip_)?before_action|allow_unauthenticated_access|(?:skip_)?(?:load_and_)?authorize_resource)\b/

Class-body lines that change the callback chain a subclass inherits.

CANCAN_LINE = /\A(?:skip_)?(?:load_and_)?authorize_resource\b/

CanCanCan's class macros, which register a before_action that runs authorize!(action, resource).

EXCEPT_OPTION = /\bexcept:|:except\s*=>/
GEM_CALLBACKS = {"Doorkeeper::AuthorizationsController" => ["before_action :authenticate_resource_owner!"], "Doorkeeper::AuthorizedApplicationsController" => ["before_action :authenticate_resource_owner!"], "Doorkeeper::ApplicationsController" => ["before_action :authenticate_admin!"]}

Gem controllers an app subclasses, which are not in the scan: the callback lines their class bodies register. Doorkeeper's authenticate_resource_owner! / authenticate_admin! run the app's resource_owner_authenticator / admin_authenticator blocks.

INCLUDE_ROOTS = ["lib", "app/helpers", "app/models/concerns"] of ::String

The roots outside app/controllers that Rails autoloads the modules a controller includes from (lib/, app/helpers/, ...).

LIFT_OPTION = /(?:\b(?:if|unless):|:(?:if|unless)\s*=>)\s*(.+)\z/

The if: / unless: a skip carries (skip_before_action :x, if: -> { preview? }, or :if =>), through to the end of the statement.

MACRO_MODIFIER = /\s+(if|unless)\s+(.+)\z/

The if cond / unless cond modifier Reader keeps on a macro call.

MAYBE_EXCEPTED = "except: with a splat it cannot read"

The lifted text of a callback whose except: splats something this reader cannot expand: the guard stays, with a note.

ONLY_OPTION = /\bonly:|:only\s*=>/

only: [...] or the hash-rocket :only => [...] older apps write.

SYMBOL_CALLBACKS = /(?<!skip_)before_action\s*\(?\s*((?::\w+[!?]?\s*,?\s*)+)/

before_action :a, :b — the symbol callbacks it registers.

Constructors

build(controllers : Enumerable(String), others : Enumerable(String), &block : String -> String | Nil) : RailsCallbackChain

A chain over the controllers files, plus the lib/ modules they (and the modules those pull in) include: a controller's concern kept outside app/controllers. others are the rest of the scan's Ruby files.

Source

Class methods

applies_to_action?(line : String, action : String | Nil) : Bool

An only: / except: naming its actions through a constant or a method (only: PROTECTED) lists actions this reader cannot see: a callback is taken to run for every action and a skip for none, which keeps the guard.

Source
before_action_callbacks(line : String) : Array(String)

The methods a before_action statement runs: its symbol callbacks, or the one method its block calls. The left operand of && / || always runs; a block under if / unless runs nothing unconditionally.

Source
callback_option(line : String, option : Regex) : Array(String) | Nil

The action names an only: / except: option lists, in any of its spellings: :show, [:show, :edit], %i[show edit], "show". Matching ":#{action}" against the raw line missed the %i[ new create ] the Rails generators write, and let :new match :new_session.

Source
conditional?(line : String) : Bool

before_action :x, if: :cond? runs x only when the condition holds, so a request can skip it: an opt-in such as if: :limited_federation_mode? leaves the action public by default. unless: is read as an exemption, the way except: is — unless: :devise_controller? guards every app controller.

Source
lifting?(line : String) : Bool

Whether a skip runs only on a condition, so the callback it names stays registered and that condition decides whether it runs.

Source
maybe_excepted?(line : String, action : String | Nil) : Bool

An except: that splats a method this reader cannot expand (*show_methods) may well list the action.

Source
opaque_option?(line : String) : Bool
Source
statement(lines : Array(String), idx : Int32) : String

The statement starting at lines[idx], joined across the lines a trailing comma or an unclosed [ / ( / { continues it onto: before_action :x, # note with its only: [...] on the next line, or only: [ closed lines later, applies to those actions only. Trailing comments are dropped. ponytail: a # inside a string on a callback line reads as a comment.

Source

Instance methods

add_file(path : String, source : String)

Indexes every class and module source declares, with its class-body callback statements.

Source
classes
Source
controller_in(path : String) : Klass | Nil

The controller class path declares. ponytail: the first *Controller class in the file; a file holding two controllers resolves both to the first.

Source
defs
Source
facts_for(path : String, source : String) : Facts | Nil

The file's Facts, read once per source: off the AST, or by LineReader when tree-sitter cannot place its classes.

Source
list_resolver

Resolves Const.method to the literal list the app defines it as (Discourse.filters); set by build over the files it was given.

Source
list_resolver=(list_resolver : Proc(String, Noir::RubyConstantList::Result | Nil) | Nil)

Resolves Const.method to the literal list the app defines it as (Discourse.filters); set by build over the files it was given.

Source
lookup(name : String, namespace : String, skip : Klass | Nil = nil) : Klass | Nil

Ruby resolves a bare constant from the innermost namespace outwards. Two definitions of one name (reopened, or the same name in two apps of a monorepo) are ambiguous and resolve to nothing. skip is the class whose superclass is being looked up: module Admin; class ApplicationController < ApplicationController names the top-level one, since the superclass is read before the new constant exists.

Source
macros
Source
missing_include_paths

The paths Rails autoloads the modules indexed classes include from, for each one no indexed file declares: include UploadHelpers in Admin::UploadsController is lib/admin/upload_helpers.rb or lib/upload_helpers.rb, nearest namespace first, as Zeitwerk looks.

Source
resolve(leaf : Klass, action : String | Nil) : Resolution

What leaf's parent chain, its concerns and its own class body leave active for action, honouring skip_before_action / allow_unauthenticated_access and their only: / except: lists. The leaf's own callbacks run last, so its skips remove what the parents registered. A parent outside the scan (Devise::SessionsController) ends the chain, after its GEM_CALLBACKS when it has any.

Source

Nested types