Define #hash alongside the eql?/== pairs that lacked one - #2836
Open
ericproulx wants to merge 1 commit into
Open
Define #hash alongside the eql?/== pairs that lacked one#2836ericproulx wants to merge 1 commit into
ericproulx wants to merge 1 commit into
Conversation
Five classes alias eql? to == without defining hash: Grape::Endpoint, Grape::Util::InheritableSetting, Grape::Middleware::Stack::Middleware, Grape::ServeStream::StreamResponse and Grape::ServeStream::FileBody. Ruby requires equal objects to hash alike, so each of them silently misbehaves in a Hash, a Set or under uniq -- equal objects land in different buckets and never dedup. Grape::Namespace and Grape::Util::MediaType already pair the two; this brings the rest in line. Each hash mirrors exactly what its == compares, and nothing more: - StreamResponse / FileBody key on stream / path alone. Their == does not check the class, so the class stays out of the hash. - Middleware keys on the wrapped class, which also makes it agree with the class itself -- something its == accepts. The superclass fallback cannot be honoured (a class and its superclass hash differently); it only ever serves #index / #include?, which compare with == rather than by hash. - Endpoint keys on config and inheritable_setting, omitting the class, since its == admits a subclass through is_a? and such a pair must agree. - InheritableSetting keys on the resolved state (#to_hash). Its == accepts two instances whose own stores differ as long as their chains resolve alike, so hashing own state would tell those apart. The same-parent fast path implies equal resolved state, so it agrees too. This is the cold path -- nothing in Grape uses a setting as a Hash key, and == keeps avoiding to_hash wherever it can. No caller in Grape relies on hash lookups for these, so this closes a latent hole rather than fixing an observed failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Danger ReportNo issues found. |
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.
Summary
Five classes alias
eql?to==without defininghash:==keys onGrape::Endpointconfig,inheritable_settingGrape::Util::InheritableSettingGrape::Middleware::Stack::MiddlewareGrape::ServeStream::StreamResponsestreamGrape::ServeStream::FileBodypathRuby requires equal objects to hash alike. Without
hash, each falls back to identity, so equal objects land in different buckets and never dedup in aHash, aSetor underuniq.Grape::NamespaceandGrape::Util::MediaTypealready pair the two — this brings the rest in line.Deriving each hash
Each
hashmirrors exactly what its==compares, and nothing more:StreamResponse/FileBodykey onstream/pathalone. Their==does not check the class, so the class stays out of the hash — including it would break pairs that==accepts.Middlewarekeys on the wrapped class, which also makes it agree with the class itself, something its==accepts. Thename.nil? && klass.superclass == otherfallback can't be honoured (a class and its superclass hash differently); it only ever serves#index/#include?, which compare with==rather than by hash.Endpointkeys onconfigandinheritable_setting, omitting the class: its==admits a subclass instance throughis_a?, and such a pair must hash alike. The block (#source) is in neither, matching the long-standing duplicate-route check inDSL::Routing#route.InheritableSettingkeys on the resolved state (#to_hash). This is the subtle one: its==accepts two instances whose own stores differ as long as their chains resolve alike, so hashing own state would tell those apart. The same-parent fast path implies equal resolved state, so it agrees too.This is the cold path — nothing in Grape uses a setting as a
Hashkey or in aSet, and==keeps avoidingto_hashwherever it can (Compare InheritableSetting without serializing both chains #2828 is untouched).Notes
InheritableSettings with different own stores that resolve to the same values are==and must hash alike.Setsize 2 instead of 1) and pass with it.Middleware's specs live in a newspec/grape/middleware/stack/middleware_spec.rbrather than nested instack_spec.rb, whosebefore { subject.use ... }hook anddescribed_classdon't apply to the inner class.bundle exec rubocopandbundle exec rspec(2526 examples) pass.🤖 Generated with Claude Code