Repository navigation
Make a helper reach the routes declared above it in its scope - #3061
Merged
Merged
Conversation
ericproulx
force-pushed
the
fix/helpers-declared-after-routes
branch
from
November 7, 2026 13:11
a6c2d46 to
67f619d
Compare
Danger ReportNo issues found. |
ericproulx
force-pushed
the
fix/helpers-declared-after-routes
branch
from
November 7, 2026 13:21
67f619d to
dd6c2b4
Compare
dblock
approved these changes
Nov 8, 2026
ericproulx
force-pushed
the
fix/helpers-declared-after-routes
branch
2 times, most recently
from
November 8, 2026 20:15
938849a to
8bb530f
Compare
An endpoint is built from a point-in-time copy of its scope's settings. The copy keeps the scope's own helpers as they were at the route, but links to the enclosing scopes live. So a `helpers` declared further down the same scope never reached the route, and calling it raised NameError. One declared further down an enclosing scope, or after `mount`, did reach it. The endpoint now also keeps the live settings of its scope and reads the helpers from them when it compiles, by which time the whole class body has run. A helper declared in a sibling namespace still doesn't reach the route. A helper defined twice in one scope therefore answers with its last definition in every route of that scope, including those declared between the two definitions. UPGRADING describes that change. Since that change is silent, the endpoint warns about it when it compiles, until 5.0. Its settings copy still holds the helpers the route reached before, so it can tell exactly which methods a helper declared further down now answers in their place. It warns only for those: a helper that only adds methods doesn't warn, nor does a redefinition in an enclosing scope, which already reached the route. The warning lives in Endpoint::RedefinedHelpersWarning, to be deleted with it in 5.0, which also keeps Endpoint under its ClassLength limit. The README no longer lists `helpers` among the declarations that must come above the routes they cover. `use`, `rescue_from` and the callbacks are unchanged. The route endpoint gains a sixth ivar. Per-request copies hold eight, so their 80-byte slot is unchanged. Fixes #34 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ericproulx
force-pushed
the
fix/helpers-declared-after-routes
branch
from
November 8, 2026 20:31
8bb530f to
b196dbe
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #34.
Problem
Within one scope, a route declared above a
helpersblock couldn't call its helpers:The rule only held for the route's own scope, which made it look arbitrary. On master:
helpersdeclared…mountThe cause is that an endpoint is built from a point-in-time copy of its scope's settings (
point_in_time_copy_for_endpoint). The copy keeps the scope's own stackables, helpers included, as they were at the route, but its link to the enclosing scopes stays live.Fix
The endpoint also keeps the live settings of the scope it was declared in, and
build_helpersreads the helpers from them incompile!, by which time the whole class body has run. Nothing else reads them. Validations, callbacks,rescue_fromandusestill come from the copy, so they keep applying only to the routes declared below them.mountwork as before.Behaviour change
A helper method defined twice in one scope now answers with its last definition in every route of that scope, including routes declared between the two definitions. It's the usual Ruby rule that the last definition wins. No spec pinned the old behaviour, but it's a contract change, so it gets an UPGRADING entry (4.2.0), with the workarounds: different method names, or separate namespaces. The README no longer lists
helpersamong the declarations that have to come above the routes they cover.Warning, until 5.0
The change is silent at runtime, so the endpoint warns about it when it compiles. Its settings copy still holds the helpers the route reached before this PR, so it compares, method by method, the definition the route answered with before against the one it answers with now. It warns only where they differ:
The warning lives in
Grape::Endpoint::RedefinedHelpersWarning, so it goes away in 5.0 by deleting one file. That also keepsEndpointunder itsMetrics/ClassLengthlimit, which it went over once rebased onto #3054.These stay quiet:
Private helpers are covered too. The check runs at compile time only, never per request. It goes in 5.0.
params :namehelpers used withuseare untouched.useresolves them immediately, so they stay order-dependent (#2113, #1761).Memory
The route endpoint gains a sixth ivar (the comment on
build_prototypeis updated). Per-request copies hold eight, and measured withObjectSpace.memsize_ofthey stay at 80 bytes, with or without helpers.Tests
Five examples under
Grape::API .helpers:Mutation-checked:
bundle exec rake: 378 files with no offenses, 3189 examples, 0 failures.🤖 Generated with Claude Code