Constants
ACCESSOR_NAME_RE = /\A(?:#{ILLUMINATE_ACCESSORS})\z/
ACTION_FILTER_RES = {"only" =>
Regex.new("->\\s*only\\s*\\((.*?)\\)",
Regex::Options::IGNORE_CASE |
Regex::Options::MULTILINE), "except" =>
Regex.new("->\\s*except\\s*\\((.*?)\\)",
Regex::Options::IGNORE_CASE |
Regex::Options::MULTILINE)}
ALIAS_ENTRY_RE = /['"]([\w.-]+)['"]\s*=>\s*(\\?[A-Za-z_][\w\\]*)::class/
ANY_CALL_RE = /([\\\w$]*)\s*\(/
ANY_REGEX = /Route::any\s*\(\s*#{PATH_ARG}\s*,/im
Route::any('/x', …): capture 1 is the path; the verbs are implied.
API_PREFIX_RE = /\bapiPrefix\s*:\s*['"]([^'"]*)['"]/
API_RESOURCE_ACTIONS = ["index", "store", "show", "update", "destroy"]
ARRAY_ENTRY_RE = /\A(?:(['"])([^'"$\\]*)\1\s*=>\s*)?(.+)\z/m
ARRAY_MERGE_RE = /\barray_merge\s*\(/
ARRAY_VALUE_RE = /\A(?:'[^'\\]*'|"[^"$\\]*"|\\?[A-Za-z_][\w\\]*::class|\[\s*\\?[A-Za-z_][\w\\]*::class\s*,\s*['"]\w+['"]\s*\])\z/
ASSIGNED_ARRAY_RE = /(?<![\w$>:])\$(\w+)\s*=\s*(\[|array\s*\()/
ASSIGNMENT_RE = /(?<![\w$>:])\$(\w+)\s*(?:\[[^\]]*\]\s*)*(?:[.+\-*\/%|&^]|\?\?|<<|>>)?=(?![=>])/
Every write to a variable: assignment (also through a subscript or a
compound operator), a foreach / destructuring target, a reference, a
mutating builtin or unset.
AUTH_ALIAS_RE = /\Aauth(?:\.basic)?(?::|\z)/
The built-in auth / auth:sanctum / auth.basic aliases.
BLOCK_REFUSAL_RE = /\A(?:abort\s*\(|throw\b|return\s+(?:redirect\b|Redirect::)|return\s+response\s*\([^;]*?\b40[13]\b|return\s+response\s*\(\s*\)\s*->\s*json\s*\([^;]*?\b40[13]\b)/
What an if (!user) block does to refuse: abort, throw, redirect, or
answer 401/403.
CHAIN_CALL_RE = /(?:->|::)\s*(\w+)\s*\(/
CHAINED_MATCH_REGEX = /Route::((?:\w+\s*\([^;]*?\)\s*->\s*)+)(?:match|addRoute)\s*\(\s*(\[[^\]]+\]|['"][^'"]+['"])\s*,\s*#{PATH_ARG}\s*,/im
The match/addRoute routes behind a fluent prelude. Capture 1 is the prelude,
capture 2 the verb specification, capture 3 the path.
CHAINED_STATIC_ROUTE_REGEX = /Route::((?:\w+\s*\([^;]*?\)\s*->\s*)+)(view|redirect|permanentRedirect)\s*\(\s*#{PATH_ARG}\s*,/im
The static routes behind a fluent prelude. Capture 1 is the prelude,
capture 2 the kind, capture 3 the path.
CHAINED_VERB_REGEX = /Route::((?:\w+\s*\([^;]*?\)\s*->\s*)+)(get|post|put|patch|delete|options|head|query)\s*\(\s*#{PATH_ARG}\s*,/im
The same, behind a fluent prelude — Route::middleware('auth')->prefix('v1')->get('/x', …).
Capture 1 is the whole prelude (a ->-separated chain of calls; the
inner [^;]*? keeps each call's argument list inside one statement),
capture 2 the verb, capture 3 the path.
CLASS_DECL_RE = /\b(?:abstract\s+|final\s+)*class\s+\w+[^{]*\{/
CLASS_EXTENDS_RE = /\bclass\s+\w+\s+extends\s+(\\?[A-Za-z_][\w\\]*)/
CLOSURE_ARG_RE = /\A\s*(?:static\s+)?(?:function|fn)\b/
COMPOUND_RE = /\A(?:if|else|try|foreach|for|while|switch|do|match)\b/
CONDITION_NOTE = "a condition decides whether it applies"
CONFIG_CALL_RE = /\bconfig\s*\(\s*['"]([\w-]+(?:\.[\w-]+)+)['"]\s*(?:,\s*(['"])([^'"]*)\2\s*)?\)/
config('portfolio.path') / config('x.y', 'default').
CONFIG_VALUE_RE = /\G\s*(?:(['"])([^'"\\$]*)\1|env\s*\(\s*['"][\w.-]+['"]\s*,\s*(['"])([^'"\\$]*)\3\s*\)|explode\s*\(\s*['"],['"]\s*,\s*env\s*\(\s*['"][\w.-]+['"]\s*,\s*(['"])([^'"\\$,]*)\5\s*\)\s*\))\s*[,\]]/
A config value the scan can read: a literal, or env('X', 'literal')
(what the app serves when the variable is unset).
explode(',', env('X', 'api')) (a one-name list's default) reads as that name.
ponytail: a comma-separated default stays unread (middleware names such as throttle:60,1 hold commas).
CREDENTIAL_CONDITION_RE = /\buser\s*\(|\bauth(?:::|\s*\()|->\s*(?:check|guest|tokenCan|can|cannot|hasValid\w*Signature)\s*\(|token|password|api_?key|signature|credential/i
CTOR_FILTER_CALL_RE = /->\s*(only|except)\s*\(/
CTOR_MIDDLEWARE_RE = /\$this\s*->\s*middleware\s*\(/
CTOR_OPTION_RE = /\A\[\s*['"](only|except)['"]\s*=>\s*(\[[^\[\]$]*\]|['"][^'"$]*['"])\s*,?\s*\]\z/
DESTRUCTURE_RE = /(?:\blist\s*\(|\[)([^\[\]();=]*)[\])]\s*=(?![=>])/
DYNAMIC_VARS_RE = /\$\$|\$\{|\b(?:extract|parse_str|compact)\s*\(|\$GLOBALS\b/
EMPTY_ARRAYS = {} of
String =>
LiteralArrayENV_CONDITION_RE = /\b(?:app\s*\(\s*\)\s*->|App::|\$this->app\s*->)\s*(?:environment|isLocal|isProduction|runningUnitTests|runningInConsole|isDownForMaintenance)\s*\(|\bconfig\s*\(|\bConfig::get\s*\(|\benv\s*\(/
FILTER_NOTE = "its only/except list is not readable, so it may not apply to this action; a condition decides whether it applies"
FOREACH_BIND_RE = /\bas\s+&?\s*\$(\w+)(?:\s*=>\s*&?\s*\$(\w+))?/
FOREACH_HEAD_RE = /\A\s*(\$\w+|\[.*\]|array\s*\(.*\))\s+as\s+(?:\$(\w+)\s*=>\s*)?\$(\w+)\s*\z/m
FOREACH_RE = /\bforeach\s*\(/
FORM_REQUEST_HINT_RE = /([A-Za-z_\\][\w\\]*)\s+\$/
Any class type hint: a FormRequest need not be named *Request
(login(LoginUser $request)), form_request_hint? decides.
GETTER_CALL_RE = /(?:\$this->|parent::|self::|static::)(?!\w*(?:Or|And)[A-Z])get[A-Z]\w*\(\s*\)/
GROUP_CONTROLLER_RE = /(?:(?:->|::)controller\s*\(\s*|['"]controller['"]\s*=>\s*)([A-Za-z_\\][\w\\]*)::class/i
Route::controller(X::class) in a group's chain, or the
'controller' => X::class group attribute.
GROUP_EDIT_RE = /->\s*(?:appendToGroup|prependToGroup|group)\s*\(\s*['"]([\w-]+)['"]\s*,\s*(\[[^\]]*\]|\\?[\w\\]+::class)/
GROUP_NAMESPACE_RE = /(?:(?:->|::)namespace\s*\(\s*|['"]namespace['"]\s*=>\s*)['"]([\w\\]+)['"]/i
Route::namespace('X') in a group's chain, or the 'namespace' => 'X' attribute.
GROUP_SHORTHAND_RE = /->\s*(web|api)\s*\(/
GUARD_ARG = (/(?:['"]\w+['"]|\$\w+)?/).source
HEALTH_ROUTE_RE = /\bhealth\s*:\s*['"]([^'"]+)['"]/
IDENTITY_REFUSAL_RE = /authenticate|authenticates|authentication|authenticated|authenticating|authenticator|\bguest\s*\(|\b
Auth\w*
Exception|
Unauthori[sz]ed
HttpException/i
A refusal that is about the caller's identity by its own words.
KNOWN_AUTH_MIDDLEWARE =
Set {"
Illuminate\\
Auth\\
Middleware\\
Authenticate", "
Illuminate\\
Auth\\
Middleware\\
AuthenticateWithBasicAuth"}
Framework classes whose definition lives in vendor/.
LOOKUP_ASSIGN_RE = /\A\$\w+\s*=(?!=)/
A lookup before the check ($post = Post::find($id);) answers nothing.
Only calls known to read count: anything else may have side effects.
LOOP_CONTROL_RE = /\b(?:continue|break|goto)\b/
LOOP_VAR_USE_RE = /\{\$(\w+)\}|\$(\w+)\b/
MATCH_REGEX = /Route::(?:match|addRoute)\s*\(\s*(\[[^\]]+\]|['"][^'"]+['"])\s*,\s*#{PATH_ARG}\s*,/im
Route::match(['get', 'post'], '/x', …) or Route::addRoute('QUERY', '/x', …):
capture 1 is the verb specification (array literal or string literal), capture 2 the path.
METHOD_DEF_RE = /(?:public|protected|private)\s+(?:static\s+)?function\s+([A-Za-z_]\w*)\s*\(/
MIDDLEWARE_ARG_RE = /(?:(?:->|::)middleware\s*\(\s*|['"]middleware['"]\s*=>\s*)(\[[^\]]*\]|#{MIDDLEWARE_NAME_ARG}(?:\s*[,.]\s*#{MIDDLEWARE_NAME_ARG})*)/
MIDDLEWARE_ENTRY_RE = /['"]([^'"]+)['"]|(\\?[A-Za-z_][\w\\]*)::class/
MIDDLEWARE_NAME_ARG = "(?:['\"][^'\"]+['\"]|\\\\?[A-Za-z_][\\w\\\\]*::class)"
The names a ->middleware(...) call or 'middleware' => attribute
gives: an array, or one or more strings / Foo::class arguments.
MIDDLEWARE_REFUSAL_RE = /\babort(?:_if|_unless)?\s*\([^;]*?\b40[13]\b|\bthrow\s+new\s+\\?(?:[\w\\]*\\)?\w*(?:Auth|Unauthori[sz]ed|AccessDenied|Forbidden)\w*\s*\(|\bthrow\s+new\s+[^;]*?\b40[13]\b|->\s*authenticate\s*\(|\bAuth::authenticate\s*\(|\bredirect\(\s*\)\s*->\s*guest\s*\(|\bRedirect::guest\s*\(|\broute\(\s*['"]login['"]|\bredirect\s*\(\s*['"]\/?login\b|\b(?:response\s*\(|->\s*json\s*\()[^;]*?\b40[13]\b|HTTP_(?:UNAUTHORIZED|FORBIDDEN)\b/
What a handle() that can refuse the caller does: abort / throw / answer
with 401 or 403, throw an auth exception, authenticate (which throws
AuthenticationException), or send the guest to the login page.
MIDDLEWARE_VAR_RE = /((?:->|::)middleware\s*\(\s*|['"]middleware['"]\s*=>\s*)\$(\w+)\b(?!\s*(?:\[|->|\(|::))/
->middleware($middleware) / 'middleware' => $middleware: a variable
handed whole as the list.
MUTATING_CALL_RE = /\b(?:array_(?:push|unshift|splice|pop|shift|walk)|[uk]?sort|r?sort|u?[ak]r?sort|shuffle|unset|settype|global)\b\s*\(?\s*\$(\w+)/
NAMESPACE_DECL_RE = /\bnamespace\s+([\w\\]+)\s*;/
NEXT_CALL_RE = /\$next\(/
NO_CTOR_NAMES = [] of CtorName
ORIGIN_CONDITION_RE = /['"](?:origin|referer|host)['"]|->\s*(?:isMethod|getMethod|method|getHost)\s*\(/i
A request-shape check (CORS origin, preflight method, host), not a
check on who the caller is.
PARAM_LIST_RE = /\b(?:function|fn)\s*&?\s*\w*\s*\(([^)]*)\)/
PARAMETERS_RE = /(?:->\s*parameters\s*\(\s*|['"]parameters['"]\s*=>\s*)\[([^\]]+)\]/im
PARENT_CTOR_RE = /\bparent::__construct\s*\(/
PATH_ARG = "(['\"][^'\"]*['\"](?:\\s*\\.\\s*(?:['\"][^'\"]*['\"]|\\$\\w+(?:->\\w+)*|[\\w\\\\]+(?:::\\w+)?\\s*\\((?:[^()'\"]++|['\"][^'\"]*['\"])*+\\)))*)"
A route's path argument, quotes included (laravel_path_arg folds it):
a string literal, or literals and simple expressions joined with .
('/a/' . config('x.y'), '/c/' . $prefix). One capture.
PATH_PIECE_RE = /\G\s*\.?\s*(?:(['"])([^'"]*)\1|\$(?:\w+->)*(\w+)|(?:[\w\\]+::|\\?(?:\w+\\)*)?(\w+)\s*\((?:[^()'"]++|['"][^'"]*['"])*+\))/
One piece of a PATH_ARG: a literal, a variable or a call.
POLICY_CHECK_RE = /\G(?:\$this\s*->\s*authorize|Gate::authorize)\s*\(/
$this->authorize('view', $post) / Gate::authorize(...): the gate
answers 403 for a signed-out caller unless the policy takes a nullable
user, so it stands in front of the handler like a user check, but a
policy can open it.
POLICY_NOTE = "the policy decides whether a guest passes; a condition decides whether it applies"
READ_BARE_CALLS =
Set {"array", "isset", "empty", "collect", "intval", "trim", "auth", "request", "route"}
READ_CALL_RE = /\A(?:find|findOrFail|findMany|first|firstOrFail|firstWhere|get|getKey|load|loadMissing|loadCount|where|whereKey|whereIn|whereNull|whereNotNull|with|withCount|withTrashed|onlyTrashed|query|all|latest|oldest|orderBy|pluck|count|input|route|string|integer|boolean|only|header|has|filled|user|exists|value)\z/
REDIRECT_RE = /\breturn\s+(?:redirect\b|Redirect::)/
REFERENCE_RE = /&\s*\$(\w+)/
RESOURCE_ACTIONS = ["index", "create", "store", "show", "edit", "update", "destroy"]
RESOURCE_CALL_RES = {"resource" =>
Regex.new("
Route::resource\\s*\\(\\s*['\"]([^'\"]+)['\"]",
Regex::Options::IGNORE_CASE |
Regex::Options::MULTILINE), "api
Resource" =>
Regex.new("
Route::api
Resource\\s*\\(\\s*['\"]([^'\"]+)['\"]",
Regex::Options::IGNORE_CASE |
Regex::Options::MULTILINE), "resources" => /
Route::(resources)\s*\(\s*(?:\[|array\s*\()/i, "api
Resources" => /
Route::(api
Resources)\s*\(\s*(?:\[|array\s*\()/i, "singleton" => /
Route::singleton\s*\(\s*['"]([^'"]+)['"]/i, "api
Singleton" => /
Route::api
Singleton\s*\(\s*['"]([^'"]+)['"]/i}
Hoisted patterns for extract_resource_route_calls /
extract_resource_action_filter — both call sites pass literal
names ("resource"/"apiResource", "only"/"except"), and the previous
Regex.new interpolation recompiled the pattern on every call
(per file, and per resource statement respectively).
RESOURCE_STRING_CONTROLLER_RE = /\A\w+::\w+\s*\(\s*['"][^'"]*['"]\s*,\s*['"]([A-Za-z_\\][\w\\]*)['"]/
Route::resource('photos', 'PhotoController'): the quoted controller
second argument, a string action like 'PhotoController@index'.
RESOURCES_ENTRY_RE = /['"]([^'"]+)['"]\s*=>\s*(\\?[A-Za-z_][\w\\]*::class|['"][^'"]*['"])/
ROUTE_FILE_REF_RE = /(['"])([^'"\n]*routes\/[^'"\n]*\.php)\1/
A route file's URL prefix comes from where the app loads it, not from
the file: Route::prefix('api')->group(base_path('routes/api.php')) in
a RouteServiceProvider (Laravel <= 10, including the older
Route::group(['prefix' => 'api'], function () { require ... })), or
->withRouting(api: __DIR__.'/../routes/api.php', apiPrefix: 'v2') in
bootstrap/app.php (Laravel 11+, apiPrefix defaulting to api),
whose then: closure registers further groups the same way.
ROUTE_REGISTRATION_RE = /Route::(?:\w+\s*\([^;]*?\)\s*->\s*)*\w+\s*\(\s*(?:(?:\[[^\]]*\]|['"][^'"]*['"])\s*,\s*)?#{PATH_ARG}\s*,/im
Any Route::…('path', … registration, whatever verb or chain precedes
it. Used only to locate handler-closure bodies up front; the six scans
above are what actually decide which registrations become endpoints.
RULES_METHOD_RE = /(?:public|protected|private)\s+function\s+rules\s*\(/
SCOPE_RE = /\b(?:function|class|trait|interface|enum)\b/
SETTING_NOTE = "an environment or config check decides; a setting can make it public"
Why a refusal is not unconditional: it only happens under an
environment or config check.
SHALLOW_RE = /->\s*shallow\s*\(/
SPREAD_RULES_RE = /\.\.\.\s*\$this\s*->\s*(\w+)\s*\(\s*\)\s*(?=[,\]])/
rules() composed from a helper's array, ...$this->folderRules() or
array_merge([...], $this->folderRules()): the helper's keys, from the
class or a trait it uses.
STATIC_REQUEST_CALL_RE = /(?<![\w\\$>])(\\?[A-Z][\w\\]*)::(\w+)\s*\(\s*\$request\s*[,)]/
Resolver::resolve($request): a static method of an app class (one
the project's PSR-4 autoload maps, never a vendor class) handed the
request as its first argument reads it under its own parameter name.
STATIC_ROUTE_REGEX = /Route::(view|redirect|permanentRedirect)\s*\(\s*#{PATH_ARG}\s*,/im
Route::view / redirect / permanentRedirect: capture 1 is the kind
and capture 2 the path. Laravel registers a view with match(['GET', 'HEAD']) and a redirect with any.
STRING_LITERAL_RE = /\A(['"])(.*)\1\z/m
THIS_CALL_RE = /\$this->(\w+)\s*\(/
THIS_LITERAL_CALL_RE = /\$this\s*->\s*(\w+)\s*\(\s*['"]([\w-]+)['"]/
THIS_READ_RE = /\$this\s*->\s*(#{ILLUMINATE_ACCESSORS})\s*\(\s*['"]([^'"]+)['"]/
A FormRequest is the request: $this->input('x') in its own methods
reads input, and so does $this->helper('x') when the helper (on the
class or a trait it uses) hands its first parameter to such a read.
TRAIT_ALIAS_RE = /(?:([\w\\]+)::)?(\w+)\s+as\s+(?:(?:public|protected|private)\s+)?(\w+)\s*;/
TRAIT_INSTEADOF_RE = /(?:([\w\\]+)::)?(\w+)\s+insteadof\s+[\w\\\s,]+;/
TRAIT_USE_RE = /(?:^|[;{}])\s*use\s+([\w\\]+(?:\s*,\s*[\w\\]+)*)\s*[;{]/m
UNRESOLVED_ROUTE_ARG_RE = /(?:\bRoute::|->)\s*\w+\s*\(\s*(?:[^;()"]*?,\s*)?"[^"]*\$[^"]*"/
A route call (or a call chained on one) whose string argument still
interpolates a variable after substitution: an inner loop that was not
expanded, or a value the body computes.
USER_ASSIGN_ANY_RE = /\$(\w+)\s*=\s*#{USER_SOURCE}\s*;/
USER_ASSIGN_RE = /\G\$(\w+)\s*=\s*#{USER_SOURCE}\s*;/
USER_CHECK_RE = /\G(?:abort_(if|unless)|if)\s*\(/
A handler that refuses an anonymous caller before doing anything: a
top-level abort_if(!$request->user(), 403) /
abort_unless(auth()->check(), 404), or if (!Auth::check()) { abort(403); } (also on a $user = $request->user() it read first),
before it does anything else.
USER_GUEST_RE = /\A(?:auth\s*\(\s*\)\s*->\s*guest|Auth::guest)\s*\(\s*\)\z/
USER_SOURCE = (/(?:\$request\s*->\s*user\s*\(\s*#{GUARD_ARG}\s*\)|(?:auth\s*\(\s*#{GUARD_ARG}\s*\)|Auth::guard\s*\(\s*#{GUARD_ARG}\s*\)|auth\s*\(\s*\)\s*->\s*guard\s*\(\s*#{GUARD_ARG}\s*\))\s*->\s*(?:user|check)\s*\(\s*\)|Auth::(?:user|check)\s*\(\s*\))/).source
USER_SOURCE_RE = /\A#{USER_SOURCE}\z/
USES_ACTION_RE = /\G(?:\[|array\s*\()[^\[\];]*?['"]uses['"]\s*=>\s*['"]([\w\\]+)@([A-Za-z_]\w*)['"]/i
VALIDATE_STATEMENT_RE = /\G(?:\$\w+\s*=\s*)?(?:\$this|\$request)\s*->\s*validate(?:WithBag)?\s*\(/
Validating the input first refuses rather than serves (422).
VERB_REGEX = /Route::(get|post|put|patch|delete|options|head|query)\s*\(\s*#{PATH_ARG}\s*,/im
Route::get('/x', …) and friends: the verb is capture 1, the path
capture 2.
VISIBILITY_WORDS = {"public", "protected", "private"}
VOLT_ROUTE_REGEX = /\bVolt::route\s*\(\s*['"]([^'"]+)['"]\s*,/im
Livewire Volt full-page components: Volt::route('/users', 'users.index')
registers a GET route. Capture 1 is the path.
WHERE_CALL_RE = /->\s*(where|where
Number|where
Alpha|where
AlphaNumeric)\s*\(/
->where('path', '.*') and its shorthands, chained after the route.
Laravel matches the placeholder against the regex, so .* lets one
placeholder span slashes and [0-9]+ makes /x/new fall through to the
next route; the URL keeps the constraint as {path:.*} (Laravel is in
URLPath::REGEX_PLACEHOLDER_TECHS).
WHERE_FIXED = {"where
Number" => "[0-9]+", "where
Alpha" => "[a-z
A-
Z]+", "where
AlphaNumeric" => "[a-z
A-
Z0-9]+"}
WHOLE_THIS_CALL_RE = /\A\$this\s*->\s*(\w+)\s*\(\s*\)\z/
WITH_ROUTING_API_RE = /\bapi\s*:\s*(?:\[[^\]]*\]|[^,)]*)/
WITH_ROUTING_WEB_RE = /\bweb\s*:\s*(?:\[[^\]]*\]|[^,)]*)/
WITHOUT_MIDDLEWARE_RE = /->\s*withoutMiddleware\s*\(\s*(\[[^\]]*\]|['"][^'"]+['"])?/